2026-01-15 01:13:44 +00:00
|
|
|
<?php
|
|
|
|
|
|
refactor: settings redesign, Spanish translations, language system, strict_types
Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals
Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController
Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props
All 710 tests passing.
2026-03-30 03:20:43 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-01-15 01:13:44 +00:00
|
|
|
namespace App\Http\Controllers\Auth;
|
|
|
|
|
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
use App\Actions\SocialAccount\ToggleSocialAccount;
|
2026-05-02 15:22:42 +00:00
|
|
|
use App\Enums\PostPlatform\Status as PostPlatformStatus;
|
2026-01-17 17:44:37 +00:00
|
|
|
use App\Enums\SocialAccount\Platform as SocialPlatform;
|
|
|
|
|
use App\Enums\SocialAccount\Status;
|
2026-04-14 21:44:47 +00:00
|
|
|
use App\Features\SocialAccountLimit;
|
2026-01-15 01:13:44 +00:00
|
|
|
use App\Http\Controllers\Controller;
|
2026-05-02 17:15:41 +00:00
|
|
|
use App\Http\Resources\App\SocialAccountResource;
|
2026-01-15 01:13:44 +00:00
|
|
|
use App\Models\SocialAccount;
|
|
|
|
|
use App\Models\Workspace;
|
|
|
|
|
use Illuminate\Http\RedirectResponse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\Log;
|
2026-01-17 02:46:30 +00:00
|
|
|
use Illuminate\View\View;
|
2026-01-15 01:13:44 +00:00
|
|
|
use Inertia\Inertia;
|
|
|
|
|
use Inertia\Response;
|
2026-04-14 21:44:47 +00:00
|
|
|
use Laravel\Pennant\Feature;
|
2026-01-15 01:13:44 +00:00
|
|
|
use Laravel\Socialite\Facades\Socialite;
|
|
|
|
|
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
|
|
|
|
|
|
|
|
|
class SocialController extends Controller
|
|
|
|
|
{
|
2026-01-16 15:36:52 +00:00
|
|
|
protected SocialPlatform $platform;
|
|
|
|
|
|
|
|
|
|
protected function ensurePlatformEnabled(): void
|
|
|
|
|
{
|
|
|
|
|
if (isset($this->platform) && ! $this->platform->isEnabled()) {
|
2026-04-14 21:44:47 +00:00
|
|
|
abort(SymfonyResponse::HTTP_FORBIDDEN, 'This platform is currently unavailable.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function ensureSocialAccountLimit(Workspace $workspace): void
|
|
|
|
|
{
|
|
|
|
|
if (config('trypost.self_hosted')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 01:22:04 +00:00
|
|
|
$limit = Feature::for($workspace->account)->value(SocialAccountLimit::class);
|
2026-04-14 21:44:47 +00:00
|
|
|
|
|
|
|
|
if ($workspace->socialAccounts()->count() >= $limit) {
|
|
|
|
|
abort(SymfonyResponse::HTTP_FORBIDDEN, __('accounts.limit_reached'));
|
2026-01-16 15:36:52 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 02:46:30 +00:00
|
|
|
public function index(Request $request): Response|RedirectResponse
|
2026-01-15 01:13:44 +00:00
|
|
|
{
|
2026-01-17 02:46:30 +00:00
|
|
|
$workspace = $request->user()->currentWorkspace;
|
|
|
|
|
|
|
|
|
|
if (! $workspace) {
|
2026-03-29 22:24:28 +00:00
|
|
|
return redirect()->route('app.workspaces.create');
|
2026-01-17 02:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-15 01:13:44 +00:00
|
|
|
$this->authorize('view', $workspace);
|
|
|
|
|
|
2026-05-02 17:15:41 +00:00
|
|
|
$accounts = $workspace->socialAccounts()
|
|
|
|
|
->when(
|
|
|
|
|
$request->input('search'),
|
|
|
|
|
fn ($query, $search) => $query->where(function ($q) use ($search): void {
|
|
|
|
|
$q->where('display_name', 'ilike', "%{$search}%")
|
|
|
|
|
->orWhere('username', 'ilike', "%{$search}%")
|
|
|
|
|
->orWhere('platform', 'ilike', "%{$search}%");
|
|
|
|
|
}),
|
|
|
|
|
)
|
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
|
|
|
->orderBy('id')
|
2026-05-02 17:15:41 +00:00
|
|
|
->paginate(config('app.pagination.default'));
|
2026-01-15 01:13:44 +00:00
|
|
|
|
2026-04-14 22:48:35 +00:00
|
|
|
$platforms = collect(SocialPlatform::enabled())->map(fn ($platform) => [
|
|
|
|
|
'value' => $platform->value,
|
|
|
|
|
'label' => $platform->label(),
|
|
|
|
|
'color' => $platform->color(),
|
|
|
|
|
])->values();
|
2026-01-15 01:13:44 +00:00
|
|
|
|
|
|
|
|
return Inertia::render('accounts/Index', [
|
|
|
|
|
'workspace' => $workspace,
|
2026-05-02 17:15:41 +00:00
|
|
|
'accounts' => Inertia::scroll(fn () => SocialAccountResource::collection($accounts)),
|
2026-01-15 01:13:44 +00:00
|
|
|
'platforms' => $platforms,
|
2026-05-02 17:15:41 +00:00
|
|
|
'filters' => [
|
|
|
|
|
'search' => $request->input('search', ''),
|
|
|
|
|
],
|
2026-01-15 01:13:44 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 02:46:30 +00:00
|
|
|
public function disconnect(Request $request, SocialAccount $account): RedirectResponse
|
2026-01-15 01:13:44 +00:00
|
|
|
{
|
2026-01-17 02:46:30 +00:00
|
|
|
$workspace = $request->user()->currentWorkspace;
|
|
|
|
|
|
|
|
|
|
if (! $workspace) {
|
2026-03-29 22:24:28 +00:00
|
|
|
return redirect()->route('app.workspaces.create');
|
2026-01-17 02:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-15 17:24:39 +00:00
|
|
|
$this->authorize('manageAccounts', $workspace);
|
2026-01-15 01:13:44 +00:00
|
|
|
|
|
|
|
|
if ($account->workspace_id !== $workspace->id) {
|
|
|
|
|
abort(403);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 15:22:42 +00:00
|
|
|
// Drop pending platform rows from drafts/scheduled posts so the account
|
|
|
|
|
// disappears cleanly from their UI. Published/failed rows survive via the
|
|
|
|
|
// FK's nullOnDelete cascade and keep their snapshot fields for history.
|
|
|
|
|
$account->postPlatforms()
|
|
|
|
|
->where('status', PostPlatformStatus::Pending->value)
|
|
|
|
|
->delete();
|
|
|
|
|
|
2026-01-15 01:13:44 +00:00
|
|
|
$account->delete();
|
|
|
|
|
|
2026-01-22 01:08:18 +00:00
|
|
|
session()->flash('flash.banner', __('accounts.flash.disconnected'));
|
2026-01-15 17:24:39 +00:00
|
|
|
session()->flash('flash.bannerStyle', 'success');
|
|
|
|
|
|
|
|
|
|
return back();
|
2026-01-15 01:13:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 00:18:07 +00:00
|
|
|
public function toggleActive(Request $request, SocialAccount $account): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$workspace = $request->user()->currentWorkspace;
|
|
|
|
|
|
2026-03-31 03:40:18 +00:00
|
|
|
if (! $workspace) {
|
|
|
|
|
return redirect()->route('app.workspaces.create');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 00:18:07 +00:00
|
|
|
$this->authorize('manageAccounts', $workspace);
|
|
|
|
|
|
|
|
|
|
if ($account->workspace_id !== $workspace->id) {
|
|
|
|
|
abort(403);
|
|
|
|
|
}
|
|
|
|
|
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
ToggleSocialAccount::execute($account);
|
2026-03-31 00:18:07 +00:00
|
|
|
|
|
|
|
|
$status = $account->is_active ? 'activated' : 'deactivated';
|
|
|
|
|
session()->flash('flash.banner', __("accounts.flash.{$status}"));
|
|
|
|
|
session()->flash('flash.bannerStyle', 'success');
|
|
|
|
|
|
|
|
|
|
return back();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 02:46:30 +00:00
|
|
|
protected function redirectToProvider(Request $request, string $driver, array $scopes): SymfonyResponse
|
2026-01-15 01:13:44 +00:00
|
|
|
{
|
2026-01-17 02:46:30 +00:00
|
|
|
$workspace = $request->user()->currentWorkspace;
|
|
|
|
|
|
|
|
|
|
if (! $workspace) {
|
2026-03-29 22:24:28 +00:00
|
|
|
return redirect()->route('app.workspaces.create');
|
2026-01-17 02:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 21:44:47 +00:00
|
|
|
$this->ensureSocialAccountLimit($workspace);
|
|
|
|
|
|
2026-01-15 01:13:44 +00:00
|
|
|
session(['social_connect_workspace' => $workspace->id]);
|
2026-01-17 02:46:30 +00:00
|
|
|
session(['social_connect_onboarding' => $request->boolean('onboarding')]);
|
2026-01-15 01:13:44 +00:00
|
|
|
|
|
|
|
|
return Inertia::location(
|
|
|
|
|
Socialite::driver($driver)
|
|
|
|
|
->scopes($scopes)
|
|
|
|
|
->redirect()
|
|
|
|
|
->getTargetUrl()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function handleCallback(
|
|
|
|
|
Request $request,
|
|
|
|
|
SocialPlatform $platform,
|
|
|
|
|
string $driver
|
2026-01-17 02:46:30 +00:00
|
|
|
): View {
|
2026-01-15 01:13:44 +00:00
|
|
|
$workspaceId = session('social_connect_workspace');
|
|
|
|
|
|
|
|
|
|
if (! $workspaceId) {
|
2026-01-17 02:46:30 +00:00
|
|
|
return $this->popupCallback(false, 'Session expired. Please try again.', $platform->value);
|
2026-01-15 01:13:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$workspace = Workspace::find($workspaceId);
|
|
|
|
|
|
2026-01-15 17:24:39 +00:00
|
|
|
if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) {
|
2026-01-17 02:46:30 +00:00
|
|
|
return $this->popupCallback(false, 'Workspace not found.', $platform->value);
|
2026-01-15 01:13:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$socialUser = Socialite::driver($driver)->user();
|
2026-01-17 02:46:30 +00:00
|
|
|
|
2026-01-15 01:13:44 +00:00
|
|
|
$avatarPath = uploadFromUrl($socialUser->getAvatar());
|
|
|
|
|
|
2026-04-15 12:46:18 +00:00
|
|
|
$workspace->socialAccounts()->updateOrCreate(
|
|
|
|
|
[
|
|
|
|
|
'platform' => $platform->value,
|
|
|
|
|
'platform_user_id' => $socialUser->getId(),
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'username' => $socialUser->getNickname(),
|
|
|
|
|
'display_name' => $socialUser->getName(),
|
|
|
|
|
'avatar_url' => $avatarPath,
|
|
|
|
|
'access_token' => $socialUser->token,
|
|
|
|
|
'refresh_token' => $socialUser->refreshToken,
|
|
|
|
|
'token_expires_at' => $socialUser->expiresIn ? now()->addSeconds($socialUser->expiresIn) : null,
|
|
|
|
|
'scopes' => $socialUser->approvedScopes ?? null,
|
|
|
|
|
'status' => Status::Connected,
|
|
|
|
|
'error_message' => null,
|
|
|
|
|
'disconnected_at' => null,
|
|
|
|
|
],
|
|
|
|
|
);
|
2026-01-15 01:13:44 +00:00
|
|
|
|
2026-01-17 02:46:30 +00:00
|
|
|
return $this->popupCallback(true, 'Account connected!', $platform->value);
|
2026-01-15 01:13:44 +00:00
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
Log::error('Social OAuth Error', [
|
|
|
|
|
'platform' => $platform->value,
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
]);
|
|
|
|
|
|
2026-01-17 02:46:30 +00:00
|
|
|
return $this->popupCallback(false, 'Error connecting account. Please try again.', $platform->value);
|
2026-01-15 01:13:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 02:46:30 +00:00
|
|
|
|
|
|
|
|
protected function forgetSocialConnectSession(): void
|
|
|
|
|
{
|
|
|
|
|
session()->forget(['social_connect_workspace', 'social_connect_onboarding']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getRedirectRoute(): string
|
|
|
|
|
{
|
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale
Auth pages:
- Create AuthSplitLayout with animated feature slides (6 slides, 3 languages)
- All auth pages use split layout (form left, visual right)
- Add show/hide password toggle with tooltip on Register
- Legal footer only shown on Register via showLegal prop
Subscribe page:
- Redesign to match auth card pattern (centered, clean)
- Platform icons, feature checklist, dynamic trial days (trialDays - 1)
- Add "Switch workspace" link
- Full i18n (en, es, pt-BR)
Onboarding:
- Rename URLs: step1 -> role, step2 -> connect
- Add enforceStep() to prevent skipping/going back steps
- Redirect /onboarding to /onboarding/role
- Redesign Step2 with AuthSplitLayout and compact platform list
- 21 tests covering all step enforcement scenarios
Workspaces page:
- Redesign with AuthSplitLayout (list with avatars, current badge)
Language system:
- Move locale from DB to cookie (forever, unencrypted, session.domain)
- Create SetLocale middleware (sets cookie if missing, validates against config)
- Rename lang/pt-br to lang/pt-BR
- Add dayjs es locale
Other:
- Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard)
- ConfirmDeleteModal with text confirmation (sendkit pattern)
- i18n for ConfirmDeleteModal internal strings (common.php)
- EmptyState component for posts index
- Exact match for "All" posts in sidebar
- Posts breadcrumbs show current status filter
- DialogFooter buttons aligned left
- API Keys page redesign with Table, DropdownMenu, EmptyState
- Extract CreateApiKeyDialog and InviteMemberDialog to components
- Remove API Keys from sidebar
- DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
|
|
|
return session('social_connect_onboarding', false) ? 'onboarding.connect' : 'accounts';
|
2026-01-17 02:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return a view that closes the popup and notifies the parent window.
|
|
|
|
|
*/
|
|
|
|
|
protected function popupCallback(bool $success, string $message, ?string $platform = null): View
|
|
|
|
|
{
|
|
|
|
|
$this->forgetSocialConnectSession();
|
|
|
|
|
|
|
|
|
|
return view('auth.social-callback', [
|
|
|
|
|
'success' => $success,
|
|
|
|
|
'message' => $message,
|
|
|
|
|
'platform' => $platform,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-01-15 01:13:44 +00:00
|
|
|
}
|