2026-03-29 22:24:28 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-04-02 20:57:06 +00:00
|
|
|
use App\Http\Controllers\App\AnalyticsController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\App\ApiKeyController;
|
2026-04-15 13:20:37 +00:00
|
|
|
use App\Http\Controllers\App\AssetController;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\Http\Controllers\App\AutomationController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\App\BillingController;
|
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
|
|
|
use App\Http\Controllers\App\DiscordController as AppDiscordController;
|
2026-04-15 14:02:10 +00:00
|
|
|
use App\Http\Controllers\App\GiphyController;
|
2026-07-17 17:32:40 +00:00
|
|
|
use App\Http\Controllers\App\LinkPreviewController;
|
MCP: workspace settings, viewer read access, and token access (#241)
* Add workspace MCP settings and token access controls.
Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring,
and workspace.token:mcp gating so assistants can connect without pulling
in welcome/onboarding from the parent epic.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Type MCP client config shapes instead of string checks.
Encode http/config-root on each advanced client and tighten primary
client ids so snippet generation does not branch on magic strings.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Polish MCP settings follow-ups from review.
Translate Ukrainian MCP copy, deep-link ChatGPT into connector
creation, drop an unused asset and revoke arg, and assert PATs are
rejected on the MCP endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden MCP connected clients, revoke scope, and OAuth consent.
List recoverable sessions with live refresh tokens, revoke only PATs,
throttle registration alone, and block viewers from authorizing MCP.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify MCP OAuth route throttling to a single middleware group.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Allow workspace viewers read-only MCP access with web policy writes.
Mirror the web app: MCP connects on view + OAuth mcp:use, write tools
enforce createPost/update/delete/manageAccounts/manageTeam, and demotion
to Viewer keeps grants. Cover role denials, consent, and disconnect.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden MCP tool authz with shared workspace helpers.
Route ApiKey tools through AuthorizesMcpTool, fail closed on null user
or policy argument, and resolve the current workspace before mutating.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop redundant string casts on validated request data.
Enum::from and validated() fields are already strings, so the casts
add noise without changing behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Show only the current user's MCP connections in settings.
Match API keys privacy: list and disconnect your own OAuth clients,
not teammates' across the account.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop redundant is_string guard before UpdatePostTool find.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined.
* Refresh MCP connected clients with Inertia usePoll.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Bump laravel/mcp to 0.9.1 and add the TryPost server icon.
Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop no-op ReflectionClass import in TryPostServerTest.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
|
|
|
use App\Http\Controllers\App\McpSettingsController;
|
feat: notification system with SendNotification job, dialog UI, tests
Backend:
- Create notifications table (user_id, workspace_id, type, channel,
title, body, data JSON, read_at, archived_at)
- Create Notification model with Type enum (post_failed,
account_disconnected, invite_received, member_joined, member_removed)
and Channel enum (email, in_app, both)
- Create SendNotification job: isolated from publish flow, handles
saving in-app notification and sending email independently
- NotificationController: index (excludes archived, scoped to workspace),
markAsRead, markAllAsRead, archiveAll
- Integrate with PublishToSocialPlatform (post failed/partial)
- Integrate with VerifyWorkspaceConnections (batch disconnection)
- Integrate with SocialAccount::markAsDisconnected (single disconnection)
- All use SendNotification::dispatch() instead of direct Mail::to()
Frontend:
- NotificationBell component in sidebar footer with unread badge
- Dialog with notification list, mark as read, mark all read, archive all
- Click navigates to relevant page (post edit, accounts)
- i18n for notifications UI (en, es, pt-BR)
Tests:
- 8 tests for NotificationController (auth, CRUD, workspace scoping)
- 4 tests for SendNotification job (channels, email, data storage)
All 745 tests passing.
2026-03-30 19:47:03 +00:00
|
|
|
use App\Http\Controllers\App\NotificationController;
|
2026-05-03 12:36:50 +00:00
|
|
|
use App\Http\Controllers\App\PostAiCreateController;
|
|
|
|
|
use App\Http\Controllers\App\PostAiGenerateController;
|
2026-05-21 19:27:55 +00:00
|
|
|
use App\Http\Controllers\App\PostAiRegenerateMediaController;
|
2026-05-03 12:36:50 +00:00
|
|
|
use App\Http\Controllers\App\PostAiReviewController;
|
2026-04-15 23:10:45 +00:00
|
|
|
use App\Http\Controllers\App\PostCommentController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\App\PostController;
|
2026-05-03 12:36:50 +00:00
|
|
|
use App\Http\Controllers\App\PostTemplateController;
|
feat: @mentions in comments, AI Action layer + MCP tools, settings tabs
Mentions in post comments
- @mention autocomplete (workspace members, current user excluded) with
marker syntax @[uuid] persisted, display names rendered via CommentBody
chips; live edit replaces markers with names and converts back on save.
- NotifyMentions action with workspace-scoped membership check, dedupes
same user, only newly-added mentions on update.
- Email + in-app via SendNotification job, respecting per-user
notification_preferences.mentioned_in_comment.
- Heartbeat-based presence (Cache, 60s TTL, 30s ping) so online recipients
get only the in-app notification — no email noise.
- Real-time bell on workspace.{id}.user.{id} private channel
(NotificationCreated event), scoped channel name avoids client-side
filtering and lays out a convention for future workspace channels.
- Mailable localized via lang/{en,es,pt-BR}/mail.php; Maizzle source
template for the email is committed and built into resources/views/mail.
AI generation refactor (Action layer + MCP)
- Extracted Actions/Ai/Generate{Image,Video} with QuotaExhaustedException
so agent tools and MCP tools share a single domain entry point.
- Mcp/Tools/Ai/Generate{Image,Video}Tool registered in TryPostServer; both
return MediaResource payloads.
- Orientation::imageApiSize maps non-OpenAI ratios to 1:1/2:3/3:2.
- config/ai.php is now the single source of truth driven by env, removing
the trypost.ai shim. Default text/image providers flipped to OpenAI.
Settings/UX
- /settings/workspace split into shadcn Tabs (Workspace / Brand / Users)
with three components.
- /assets and the in-editor MediaPicker open the ImagePreviewDialog
lightbox on image click while preserving action button behaviour.
- Comments tab landed via ?tab=comments&comment=<id> from notification
click (scroll-to + temporary highlight).
- Mention autocomplete popover flips above when near the viewport bottom.
- Real social platform PNGs replace Tabler brand glyphs in schedule
pills and post list, with hover tooltip carrying display_name + handle.
Bug fixes
- AcceptInvite: controller now passes workspace + role payload that the
Vue page expects; login/register CTAs preselect the invite email.
- WorkspaceInvite mailable: stopped referencing nonexistent
$invite->workspace and $invite->role; column added to the migration,
Invite model casts role to WorkspaceRole, CreateInvite persists it.
- PostCommentCreated: added broadcastAs so .PostCommentCreated actually
matches the Echo listener; payload now includes mentioned_users so
receivers render the chip correctly without a refetch.
- Preview components for X/Pinterest/Threads/Bluesky/LinkedIn/Mastodon/
TikTok/YouTube switched from item.type === 'image' to
!isVideoMedia(item) so media without a persisted type still renders.
- UpdatePostRequest now accepts media.*.{type,mime_type,size,...} so the
posts.media JSON keeps the metadata that the previews need.
- Removed throttle:6,1 from social connect routes (was 429ing legitimate
OAuth retries).
- Used MediaType enum cases instead of literal 'image'/'video' strings
when creating media rows.
Tests
- MentionParser unit tests, NotifyMentions feature tests including
online/offline channel selection and preference gating, MCP AI tool
happy paths, MentionedInComment mailable rendering, AcceptInvite +
search-members + index mentioned_users path. 1229 passing.
2026-05-01 23:59:03 +00:00
|
|
|
use App\Http\Controllers\App\PresenceController;
|
feat: redesign billing, onboarding, sidebar, and settings architecture
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
-> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
2026-04-15 03:33:38 +00:00
|
|
|
use App\Http\Controllers\App\Settings\AccountController;
|
2026-05-03 20:42:44 +00:00
|
|
|
use App\Http\Controllers\App\Settings\AuthenticationController;
|
refactor: notification preferences, header slots, calendar layout, UI polish
Notification preferences:
- Create notification_preferences table (post_published, post_failed,
account_disconnected booleans per user)
- NotificationPreferenceController with firstOrCreate on first visit
- SendNotification job respects email preferences before sending
- Settings page with toggle switches, i18n in 3 languages
- 8 new tests for preferences (controller + wantsEmailFor + job integration)
Post published notification:
- PostPublished mail + maizzle template
- Notify owner on successful publish via SendNotification job
- PostPublished type added to notification enum
Header & Layout:
- Rename AppSidebarHeader to AppHeader with left/center/right slots
- showSidebarTrigger prop to hide sidebar toggle
- Calendar: controls in header (left: nav, center: date, right: tabs + new post)
- Fixed header with scrollable content (flex h-screen pattern)
- fullWidth pages use overflow-y-auto (fixes month view scroll)
UI improvements:
- Action buttons moved to header-right: posts, hashtags, labels
- Settings breadcrumbs: "Settings > Profile" pattern
- Calendar: remove duplicate New Post button from day view
- Remove size="sm" from Schedule/Publish buttons
- Remove bg-background from header (inherits from SidebarInset)
- Add Cancel button to labels and hashtags create/edit dialogs
- Add common.cancel i18n key
- Clean up orphaned Calendar breadcrumbs
- Fix SocialAccountsGrid buttons to use shadcn Button ghost
All 753 tests passing.
2026-03-30 21:18:17 +00:00
|
|
|
use App\Http\Controllers\App\Settings\NotificationPreferenceController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\App\Settings\ProfileController;
|
2026-05-03 18:54:26 +00:00
|
|
|
use App\Http\Controllers\App\Settings\SettingsController;
|
feat: redesign billing, onboarding, sidebar, and settings architecture
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
-> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
2026-04-15 03:33:38 +00:00
|
|
|
use App\Http\Controllers\App\Settings\UsageController;
|
2026-04-15 13:20:37 +00:00
|
|
|
use App\Http\Controllers\App\UnsplashController;
|
2026-08-06 14:34:50 +00:00
|
|
|
use App\Http\Controllers\App\WelcomeController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\App\WorkspaceController;
|
|
|
|
|
use App\Http\Controllers\App\WorkspaceInviteController;
|
|
|
|
|
use App\Http\Controllers\App\WorkspaceLabelController;
|
2026-05-03 18:23:30 +00:00
|
|
|
use App\Http\Controllers\App\WorkspaceSignatureController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\Auth\BlueskyController;
|
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
|
|
|
use App\Http\Controllers\Auth\DiscordController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\Auth\FacebookController;
|
|
|
|
|
use App\Http\Controllers\Auth\InstagramController;
|
2026-04-02 20:57:06 +00:00
|
|
|
use App\Http\Controllers\Auth\InstagramFacebookController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\Auth\LinkedInController;
|
|
|
|
|
use App\Http\Controllers\Auth\MastodonController;
|
|
|
|
|
use App\Http\Controllers\Auth\PinterestController;
|
|
|
|
|
use App\Http\Controllers\Auth\SocialController;
|
Add Telegram connection flow (controller + webhook)
Connect a channel by issuing a one-time code the user posts as /connect <code>
in their channel. A secret-token-guarded webhook matches the code, links the
channel as a SocialAccount (chat_id in meta), and records it on the request so
the connect endpoint can poll for completion. Adds the TelegramConnectRequest
model + migration, the connect/status endpoints, the public webhook route (CSRF
exempt), a ConnectionVerifier branch (getChat liveness), and a telegram:set-webhook
command. Tests cover the code issue, webhook link, secret rejection, expired/
unknown codes, status polling, and the command.
2026-06-14 00:39:03 +00:00
|
|
|
use App\Http\Controllers\Auth\TelegramController;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Http\Controllers\Auth\ThreadsController;
|
|
|
|
|
use App\Http\Controllers\Auth\TikTokController;
|
|
|
|
|
use App\Http\Controllers\Auth\XController;
|
|
|
|
|
use App\Http\Controllers\Auth\YouTubeController;
|
2026-04-17 02:05:51 +00:00
|
|
|
use App\Http\Middleware\App\EnsureAccountReady;
|
2026-06-25 17:30:15 +00:00
|
|
|
use App\Http\Middleware\App\EnsureHasWorkspace;
|
2026-03-29 22:24:28 +00:00
|
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// Subscription selection (requires auth but not subscription)
|
2026-05-02 16:30:59 +00:00
|
|
|
Route::middleware(['auth'])->group(function () {
|
|
|
|
|
|
|
|
|
|
Route::get('/', function () {
|
|
|
|
|
return redirect()->route('app.calendar');
|
|
|
|
|
})->name('app.home');
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::get('subscribe', [BillingController::class, 'subscribe'])->name('app.subscribe');
|
2026-08-06 14:34:50 +00:00
|
|
|
Route::get('welcome', fn () => redirect()->route('app.welcome.persona'))->name('app.welcome');
|
|
|
|
|
Route::get('welcome/persona', [WelcomeController::class, 'persona'])->name('app.welcome.persona');
|
|
|
|
|
Route::post('welcome/persona', [WelcomeController::class, 'storePersona'])->name('app.welcome.persona.store');
|
|
|
|
|
Route::get('welcome/goals', [WelcomeController::class, 'goals'])->name('app.welcome.goals');
|
|
|
|
|
Route::post('welcome/goals', [WelcomeController::class, 'storeGoals'])->name('app.welcome.goals.store');
|
|
|
|
|
Route::get('welcome/referral-source', [WelcomeController::class, 'referralSource'])->name('app.welcome.referral-source');
|
|
|
|
|
Route::get('welcome/subscription-required', [WelcomeController::class, 'subscriptionRequired'])->name('app.welcome.subscription-required');
|
|
|
|
|
Route::post('welcome/referral-source', [WelcomeController::class, 'storeReferralSource'])
|
|
|
|
|
->middleware('throttle:6,1')
|
|
|
|
|
->name('app.welcome.referral-source.store');
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::get('billing/processing', [BillingController::class, 'processing'])->name('app.billing.processing');
|
|
|
|
|
|
2026-04-17 02:05:51 +00:00
|
|
|
Route::get('workspaces/create', [WorkspaceController::class, 'create'])->name('app.workspaces.create');
|
|
|
|
|
Route::post('workspaces', [WorkspaceController::class, 'store'])->name('app.workspaces.store');
|
|
|
|
|
Route::post('workspaces/autofill', [WorkspaceController::class, 'autofillBrand'])
|
feat: add brand autofill from website URL in onboarding
Users on the Brand step can type their website URL and click
'Preencher' (Portuguese) / 'Autofill' (English). The backend fetches
their homepage, parses standard meta tags, and returns:
- name ← og:site_name | title (suffix stripped at ' | ', ' - ')
- description ← meta[name=description] | og:description
- language ← <html lang> mapped to en / pt-BR / es
- logo ← apple-touch-icon | largest link[rel*=icon] | og:image
The logo is downloaded, validated (mime whitelist, 2MB max), and
attached to the workspace's 'logo' media collection so the avatar
updates immediately.
Zero LLM calls, zero external APIs. Uses symfony/dom-crawler +
symfony/css-selector (newly required) for meta extraction. Everything
else (Http client, workspace media, Intervention) was already in the
project.
Security:
- SSRF guardrail: resolves the host, rejects private / loopback /
link-local ranges, enforces http(s) scheme on both the initial
page fetch and the logo download.
- Rate limited at 10 req/min per user via the throttle middleware
alias on the route.
- Logo content-type must be one of the allowed image mimes; wrong
types are silently dropped so users never see broken images.
UX:
- 'Autofill' button next to the website input, disabled until there
is a URL; shows a spinner while running.
- If a logo was captured, a small preview appears below the input
so users can see what was pulled before saving.
- Success and error paths both surface as vue-sonner toasts, with
translations in en / pt-BR / es.
- Failures leave the form untouched — nothing is destructively
overwritten if parsing gave us nothing.
Tests (16 new): action-level coverage for happy path, title-suffix
fallback, language code normalization across 6 locales, scheme
rejection, private-range SSRF rejection, implicit https prefixing,
empty sites, upstream errors, and wrong-mime logo rejection. Plus
two controller-level tests for the autofill endpoint.
2026-04-16 14:13:54 +00:00
|
|
|
->middleware('throttle:10,1')
|
2026-04-17 02:05:51 +00:00
|
|
|
->name('app.workspaces.autofill');
|
feat: @mentions in comments, AI Action layer + MCP tools, settings tabs
Mentions in post comments
- @mention autocomplete (workspace members, current user excluded) with
marker syntax @[uuid] persisted, display names rendered via CommentBody
chips; live edit replaces markers with names and converts back on save.
- NotifyMentions action with workspace-scoped membership check, dedupes
same user, only newly-added mentions on update.
- Email + in-app via SendNotification job, respecting per-user
notification_preferences.mentioned_in_comment.
- Heartbeat-based presence (Cache, 60s TTL, 30s ping) so online recipients
get only the in-app notification — no email noise.
- Real-time bell on workspace.{id}.user.{id} private channel
(NotificationCreated event), scoped channel name avoids client-side
filtering and lays out a convention for future workspace channels.
- Mailable localized via lang/{en,es,pt-BR}/mail.php; Maizzle source
template for the email is committed and built into resources/views/mail.
AI generation refactor (Action layer + MCP)
- Extracted Actions/Ai/Generate{Image,Video} with QuotaExhaustedException
so agent tools and MCP tools share a single domain entry point.
- Mcp/Tools/Ai/Generate{Image,Video}Tool registered in TryPostServer; both
return MediaResource payloads.
- Orientation::imageApiSize maps non-OpenAI ratios to 1:1/2:3/3:2.
- config/ai.php is now the single source of truth driven by env, removing
the trypost.ai shim. Default text/image providers flipped to OpenAI.
Settings/UX
- /settings/workspace split into shadcn Tabs (Workspace / Brand / Users)
with three components.
- /assets and the in-editor MediaPicker open the ImagePreviewDialog
lightbox on image click while preserving action button behaviour.
- Comments tab landed via ?tab=comments&comment=<id> from notification
click (scroll-to + temporary highlight).
- Mention autocomplete popover flips above when near the viewport bottom.
- Real social platform PNGs replace Tabler brand glyphs in schedule
pills and post list, with hover tooltip carrying display_name + handle.
Bug fixes
- AcceptInvite: controller now passes workspace + role payload that the
Vue page expects; login/register CTAs preselect the invite email.
- WorkspaceInvite mailable: stopped referencing nonexistent
$invite->workspace and $invite->role; column added to the migration,
Invite model casts role to WorkspaceRole, CreateInvite persists it.
- PostCommentCreated: added broadcastAs so .PostCommentCreated actually
matches the Echo listener; payload now includes mentioned_users so
receivers render the chip correctly without a refetch.
- Preview components for X/Pinterest/Threads/Bluesky/LinkedIn/Mastodon/
TikTok/YouTube switched from item.type === 'image' to
!isVideoMedia(item) so media without a persisted type still renders.
- UpdatePostRequest now accepts media.*.{type,mime_type,size,...} so the
posts.media JSON keeps the metadata that the previews need.
- Removed throttle:6,1 from social connect routes (was 429ing legitimate
OAuth retries).
- Used MediaType enum cases instead of literal 'image'/'video' strings
when creating media rows.
Tests
- MentionParser unit tests, NotifyMentions feature tests including
online/offline channel selection and preference gating, MCP AI tool
happy paths, MentionedInComment mailable rendering, AcceptInvite +
search-members + index mentioned_users path. 1229 passing.
2026-05-01 23:59:03 +00:00
|
|
|
|
|
|
|
|
Route::get('workspace/members/search', [WorkspaceController::class, 'searchMembers'])
|
|
|
|
|
->middleware('throttle:60,1')
|
|
|
|
|
->name('app.workspace.members.search');
|
|
|
|
|
|
|
|
|
|
Route::post('presence/heartbeat', [PresenceController::class, 'heartbeat'])
|
|
|
|
|
->name('app.presence.heartbeat');
|
2026-03-31 14:35:49 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Social Connect routes
|
2026-06-25 18:34:03 +00:00
|
|
|
Route::middleware(['auth'])->group(function () {
|
|
|
|
|
// Starting a connection reads the user's current workspace, so these require
|
|
|
|
|
// one — during onboarding they redirect to workspace creation. Disconnecting
|
|
|
|
|
// lives here too (and not behind EnsureAccountReady) so it works before a
|
|
|
|
|
// subscription exists; the controller still authorizes workspace ownership.
|
|
|
|
|
Route::middleware(EnsureHasWorkspace::class)->group(function () {
|
|
|
|
|
Route::get('connect/linkedin', [LinkedInController::class, 'connect'])->name('app.social.linkedin.connect');
|
|
|
|
|
Route::get('connect/x', [XController::class, 'connect'])->name('app.social.x.connect');
|
|
|
|
|
Route::get('connect/tiktok', [TikTokController::class, 'connect'])->name('app.social.tiktok.connect');
|
|
|
|
|
Route::get('connect/youtube', [YouTubeController::class, 'connect'])->name('app.social.youtube.connect');
|
|
|
|
|
Route::get('connect/facebook', [FacebookController::class, 'connect'])->name('app.social.facebook.connect');
|
|
|
|
|
Route::get('connect/instagram', [InstagramController::class, 'connect'])->name('app.social.instagram.connect');
|
|
|
|
|
Route::get('connect/instagram-facebook', [InstagramFacebookController::class, 'connect'])->name('app.social.instagram-facebook.connect');
|
|
|
|
|
Route::get('connect/threads', [ThreadsController::class, 'connect'])->name('app.social.threads.connect');
|
|
|
|
|
Route::get('connect/pinterest', [PinterestController::class, 'connect'])->name('app.social.pinterest.connect');
|
|
|
|
|
Route::get('connect/bluesky', [BlueskyController::class, 'connect'])->name('app.social.bluesky.connect');
|
|
|
|
|
Route::post('connect/bluesky', [BlueskyController::class, 'store'])->name('app.social.bluesky.store');
|
|
|
|
|
Route::get('connect/mastodon', [MastodonController::class, 'connect'])->name('app.social.mastodon.connect');
|
|
|
|
|
Route::post('connect/mastodon', [MastodonController::class, 'authorizeInstance'])->name('app.social.mastodon.authorize');
|
|
|
|
|
Route::post('connect/telegram', [TelegramController::class, 'connect'])->name('app.social.telegram.connect');
|
|
|
|
|
Route::get('connect/discord', [DiscordController::class, 'connect'])->name('app.social.discord.connect');
|
|
|
|
|
|
|
|
|
|
Route::delete('accounts/{account}', [SocialController::class, 'disconnect'])->name('app.accounts.disconnect');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// OAuth callbacks and identity selection resolve their workspace from the
|
|
|
|
|
// session set when the flow started, then self-close the popup. They run
|
|
|
|
|
// without the current-workspace gate so a momentarily missing current
|
|
|
|
|
// workspace can't HTML-redirect the popup instead of closing it cleanly.
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::get('accounts/linkedin/callback', [LinkedInController::class, 'callback'])->name('app.social.linkedin.callback');
|
2026-06-25 00:05:09 +00:00
|
|
|
Route::get('accounts/linkedin/select', [LinkedInController::class, 'selectIdentity'])->name('app.social.linkedin.select-identity');
|
|
|
|
|
Route::post('accounts/linkedin/select', [LinkedInController::class, 'select'])->name('app.social.linkedin.select');
|
2026-03-31 14:35:49 +00:00
|
|
|
|
|
|
|
|
Route::get('accounts/x/callback', [XController::class, 'callback'])->name('app.social.x.callback');
|
|
|
|
|
|
|
|
|
|
Route::get('accounts/tiktok/callback', [TikTokController::class, 'callback'])->name('app.social.tiktok.callback');
|
|
|
|
|
|
|
|
|
|
Route::get('accounts/youtube/callback', [YouTubeController::class, 'callback'])->name('app.social.youtube.callback');
|
|
|
|
|
Route::get('accounts/youtube/select', [YouTubeController::class, 'selectChannel'])->name('app.social.youtube.select-channel');
|
|
|
|
|
Route::post('accounts/youtube/select', [YouTubeController::class, 'select'])->name('app.social.youtube.select');
|
|
|
|
|
|
|
|
|
|
Route::get('accounts/facebook/callback', [FacebookController::class, 'callback'])->name('app.social.facebook.callback');
|
|
|
|
|
Route::get('accounts/facebook/select', [FacebookController::class, 'selectPage'])->name('app.social.facebook.select-page');
|
|
|
|
|
Route::post('accounts/facebook/select', [FacebookController::class, 'select'])->name('app.social.facebook.select');
|
|
|
|
|
|
|
|
|
|
Route::get('accounts/instagram/callback', [InstagramController::class, 'callback'])->name('app.social.instagram.callback');
|
|
|
|
|
Route::get('accounts/instagram/select', [InstagramController::class, 'selectAccount'])->name('app.social.instagram.select-account');
|
|
|
|
|
Route::post('accounts/instagram/select', [InstagramController::class, 'select'])->name('app.social.instagram.select');
|
|
|
|
|
|
2026-04-02 20:57:06 +00:00
|
|
|
Route::get('accounts/instagram-facebook/callback', [InstagramFacebookController::class, 'callback'])->name('app.social.instagram-facebook.callback');
|
|
|
|
|
Route::get('accounts/instagram-facebook/select-page', [InstagramFacebookController::class, 'selectPage'])->name('app.social.instagram-facebook.select-page');
|
|
|
|
|
Route::post('accounts/instagram-facebook/select', [InstagramFacebookController::class, 'select'])->name('app.social.instagram-facebook.select');
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::get('accounts/threads/callback', [ThreadsController::class, 'callback'])->name('app.social.threads.callback');
|
|
|
|
|
|
|
|
|
|
Route::get('accounts/pinterest/callback', [PinterestController::class, 'callback'])->name('app.social.pinterest.callback');
|
|
|
|
|
|
|
|
|
|
Route::get('accounts/mastodon/callback', [MastodonController::class, 'callback'])->name('app.social.mastodon.callback');
|
Add Telegram connection flow (controller + webhook)
Connect a channel by issuing a one-time code the user posts as /connect <code>
in their channel. A secret-token-guarded webhook matches the code, links the
channel as a SocialAccount (chat_id in meta), and records it on the request so
the connect endpoint can poll for completion. Adds the TelegramConnectRequest
model + migration, the connect/status endpoints, the public webhook route (CSRF
exempt), a ConnectionVerifier branch (getChat liveness), and a telegram:set-webhook
command. Tests cover the code issue, webhook link, secret rejection, expired/
unknown codes, status polling, and the command.
2026-06-14 00:39:03 +00:00
|
|
|
|
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
|
|
|
Route::get('accounts/discord/callback', [DiscordController::class, 'callback'])->name('app.social.discord.callback');
|
2026-03-31 14:35:49 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Routes that require active subscription and completed onboarding
|
2026-06-25 17:30:15 +00:00
|
|
|
Route::middleware(['auth', EnsureAccountReady::class, EnsureHasWorkspace::class])->group(function () {
|
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
|
|
|
// Discord — live lookups for the composer (channel picker + mention autocomplete).
|
|
|
|
|
// Throttled because they proxy the shared bot's (rate-limited) Discord API.
|
|
|
|
|
Route::get('discord/accounts/{account}/channels', [AppDiscordController::class, 'channels'])
|
|
|
|
|
->middleware('throttle:60,1')
|
|
|
|
|
->name('app.discord.channels');
|
|
|
|
|
Route::get('discord/accounts/{account}/mentions', [AppDiscordController::class, 'mentions'])
|
|
|
|
|
->middleware('throttle:60,1')
|
|
|
|
|
->name('app.discord.mentions');
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// Workspaces
|
|
|
|
|
Route::get('workspaces', [WorkspaceController::class, 'index'])->name('app.workspaces.index');
|
|
|
|
|
Route::post('workspaces/{workspace}/switch', [WorkspaceController::class, 'switch'])->name('app.workspaces.switch');
|
|
|
|
|
Route::delete('workspaces/{workspace}', [WorkspaceController::class, 'destroy'])->name('app.workspaces.destroy');
|
|
|
|
|
|
|
|
|
|
// Workspace settings
|
|
|
|
|
Route::get('settings/workspace', [WorkspaceController::class, 'settings'])->name('app.workspace.settings');
|
|
|
|
|
Route::put('settings/workspace', [WorkspaceController::class, 'updateSettings'])->name('app.workspace.settings.update');
|
|
|
|
|
Route::post('settings/workspace/logo', [WorkspaceController::class, 'uploadLogo'])->name('app.workspace.upload-logo');
|
|
|
|
|
Route::delete('settings/workspace/logo', [WorkspaceController::class, 'deleteLogo'])->name('app.workspace.delete-logo');
|
|
|
|
|
|
2026-05-02 15:22:42 +00:00
|
|
|
// Brand settings
|
2026-05-03 16:44:13 +00:00
|
|
|
Route::get('settings/workspace/brand', [WorkspaceController::class, 'brandSettings'])->name('app.workspace.brand');
|
2026-05-02 15:22:42 +00:00
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// Social Accounts
|
|
|
|
|
Route::get('accounts', [SocialController::class, 'index'])->name('app.accounts');
|
|
|
|
|
Route::put('accounts/{account}/toggle', [SocialController::class, 'toggleActive'])->name('app.accounts.toggle');
|
|
|
|
|
|
2026-04-02 20:57:06 +00:00
|
|
|
// Analytics
|
|
|
|
|
Route::get('analytics', [AnalyticsController::class, 'index'])->name('app.analytics');
|
|
|
|
|
Route::get('analytics/{account}', [AnalyticsController::class, 'show'])->name('app.analytics.show');
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// Calendar
|
|
|
|
|
Route::get('calendar', [PostController::class, 'calendar'])->name('app.calendar');
|
|
|
|
|
|
|
|
|
|
// Posts
|
|
|
|
|
Route::get('posts/{status?}', [PostController::class, 'index'])->name('app.posts.index')->where('status', 'draft|scheduled|published');
|
2026-05-03 12:36:50 +00:00
|
|
|
Route::get('posts/create', [PostController::class, 'create'])->name('app.posts.create');
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::post('posts', [PostController::class, 'store'])->name('app.posts.store');
|
|
|
|
|
Route::get('posts/{post}/edit', [PostController::class, 'edit'])->name('app.posts.edit');
|
2026-05-02 15:22:42 +00:00
|
|
|
Route::get('posts/{post}', [PostController::class, 'show'])->name('app.posts.show');
|
|
|
|
|
Route::get('posts/{post}/platforms/{postPlatform}/metrics', [PostController::class, 'platformMetrics'])->name('app.posts.platforms.metrics');
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::put('posts/{post}', [PostController::class, 'update'])->name('app.posts.update');
|
|
|
|
|
Route::delete('posts/{post}', [PostController::class, 'destroy'])->name('app.posts.destroy');
|
2026-05-04 01:37:51 +00:00
|
|
|
Route::post('posts/{post}/duplicate', [PostController::class, 'duplicate'])->name('app.posts.duplicate');
|
2026-07-17 17:32:40 +00:00
|
|
|
Route::post('posts/link-preview', LinkPreviewController::class)
|
|
|
|
|
->middleware('throttle:30,1')
|
|
|
|
|
->name('app.posts.link-preview');
|
2026-03-31 14:35:49 +00:00
|
|
|
|
2026-05-03 12:36:50 +00:00
|
|
|
// Post Templates
|
|
|
|
|
Route::get('post-templates', [PostTemplateController::class, 'index'])->name('app.post-templates.index');
|
2026-05-03 16:44:13 +00:00
|
|
|
Route::post('post-templates/{slug}/apply', [PostTemplateController::class, 'apply'])->name('app.post-templates.apply');
|
2026-05-03 12:36:50 +00:00
|
|
|
|
|
|
|
|
// Post AI
|
|
|
|
|
Route::post('posts/{post}/ai/generate', [PostAiGenerateController::class, 'generate'])->name('app.posts.ai.generate');
|
2026-05-21 19:27:55 +00:00
|
|
|
Route::post('posts/{post}/media/{mediaId}/ai/regenerate', [PostAiRegenerateMediaController::class, 'regenerate'])->name('app.posts.ai.regenerate-media');
|
2026-05-03 12:36:50 +00:00
|
|
|
Route::post('posts/{post}/ai/review', [PostAiReviewController::class, 'review'])->name('app.posts.ai.review');
|
|
|
|
|
Route::post('posts/ai/create', [PostAiCreateController::class, 'start'])->name('app.posts.ai.create');
|
2026-05-08 21:30:11 +00:00
|
|
|
Route::get('posts/ai/{creationId}/loading', [PostAiCreateController::class, 'loading'])->name('app.posts.ai.loading')->whereUuid('creationId');
|
2026-05-03 12:36:50 +00:00
|
|
|
|
2026-04-15 23:10:45 +00:00
|
|
|
// Post Comments
|
|
|
|
|
Route::get('posts/{post}/comments', [PostCommentController::class, 'index'])->name('app.posts.comments.index');
|
|
|
|
|
Route::post('posts/{post}/comments', [PostCommentController::class, 'store'])->name('app.posts.comments.store');
|
|
|
|
|
Route::put('posts/{post}/comments/{comment}', [PostCommentController::class, 'update'])->name('app.posts.comments.update');
|
|
|
|
|
Route::delete('posts/{post}/comments/{comment}', [PostCommentController::class, 'destroy'])->name('app.posts.comments.destroy');
|
|
|
|
|
Route::post('posts/{post}/comments/{comment}/react', [PostCommentController::class, 'react'])->name('app.posts.comments.react');
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// Members
|
2026-05-03 16:44:13 +00:00
|
|
|
Route::get('settings/workspace/members', [WorkspaceInviteController::class, 'index'])->name('app.members');
|
|
|
|
|
Route::post('settings/workspace/members/invites', [WorkspaceInviteController::class, 'store'])->name('app.invites.store');
|
|
|
|
|
Route::delete('settings/workspace/members/invites/{invite}', [WorkspaceInviteController::class, 'destroy'])->name('app.invites.destroy');
|
|
|
|
|
Route::delete('settings/workspace/members/{user}', [WorkspaceInviteController::class, 'removeMember'])->name('app.members.remove');
|
|
|
|
|
Route::put('settings/workspace/members/{user}/role', [WorkspaceInviteController::class, 'updateRole'])->name('app.members.update-role');
|
2026-03-31 14:35:49 +00:00
|
|
|
|
2026-05-03 18:23:30 +00:00
|
|
|
// Signatures
|
|
|
|
|
Route::get('signatures', [WorkspaceSignatureController::class, 'index'])->name('app.signatures.index');
|
|
|
|
|
Route::post('signatures', [WorkspaceSignatureController::class, 'store'])->name('app.signatures.store');
|
|
|
|
|
Route::put('signatures/{signature}', [WorkspaceSignatureController::class, 'update'])->name('app.signatures.update');
|
|
|
|
|
Route::delete('signatures/{signature}', [WorkspaceSignatureController::class, 'destroy'])->name('app.signatures.destroy');
|
2026-03-31 14:35:49 +00:00
|
|
|
|
2026-04-15 13:20:37 +00:00
|
|
|
// Assets
|
|
|
|
|
Route::get('assets', [AssetController::class, 'index'])->name('app.assets.index');
|
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
|
|
|
Route::get('assets/search', [AssetController::class, 'search'])->name('app.assets.search');
|
2026-04-15 13:20:37 +00:00
|
|
|
Route::post('assets', [AssetController::class, 'store'])->name('app.assets.store');
|
|
|
|
|
Route::post('assets/chunked', [AssetController::class, 'storeChunked'])->name('app.assets.store-chunked');
|
|
|
|
|
Route::post('assets/from-url', [AssetController::class, 'storeFromUrl'])->name('app.assets.store-from-url');
|
|
|
|
|
Route::delete('assets/{media}', [AssetController::class, 'destroy'])->name('app.assets.destroy');
|
|
|
|
|
Route::get('assets/unsplash/search', [UnsplashController::class, 'search'])->name('app.assets.unsplash.search');
|
2026-04-15 14:02:10 +00:00
|
|
|
Route::get('assets/unsplash/trending', [UnsplashController::class, 'trending'])->name('app.assets.unsplash.trending');
|
|
|
|
|
Route::get('assets/giphy/search', [GiphyController::class, 'search'])->name('app.assets.giphy.search');
|
|
|
|
|
Route::get('assets/giphy/trending', [GiphyController::class, 'trending'])->name('app.assets.giphy.trending');
|
2026-04-15 13:20:37 +00:00
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// Labels
|
|
|
|
|
Route::get('labels', [WorkspaceLabelController::class, 'index'])->name('app.labels.index');
|
|
|
|
|
Route::post('labels', [WorkspaceLabelController::class, 'store'])->name('app.labels.store');
|
|
|
|
|
Route::put('labels/{label}', [WorkspaceLabelController::class, 'update'])->name('app.labels.update');
|
|
|
|
|
Route::delete('labels/{label}', [WorkspaceLabelController::class, 'destroy'])->name('app.labels.destroy');
|
|
|
|
|
|
2026-05-24 12:17:19 +00:00
|
|
|
// Automations
|
|
|
|
|
Route::get('automations', [AutomationController::class, 'index'])->name('app.automations.index');
|
|
|
|
|
Route::post('automations', [AutomationController::class, 'store'])->name('app.automations.store');
|
|
|
|
|
Route::get('automations/{automation}', [AutomationController::class, 'show'])->name('app.automations.show');
|
2026-06-12 22:19:46 +00:00
|
|
|
Route::get('automations/{automation}/workflow', [AutomationController::class, 'workflow'])->name('app.automations.workflow');
|
|
|
|
|
Route::get('automations/{automation}/invocations', [AutomationController::class, 'invocations'])->name('app.automations.invocations');
|
|
|
|
|
Route::get('automations/{automation}/metrics', [AutomationController::class, 'metrics'])->name('app.automations.metrics');
|
|
|
|
|
Route::get('automations/{automation}/settings', [AutomationController::class, 'settings'])->name('app.automations.settings');
|
2026-05-24 12:17:19 +00:00
|
|
|
Route::put('automations/{automation}', [AutomationController::class, 'update'])->name('app.automations.update');
|
|
|
|
|
Route::delete('automations/{automation}', [AutomationController::class, 'destroy'])->name('app.automations.destroy');
|
|
|
|
|
Route::post('automations/{automation}/activate', [AutomationController::class, 'activate'])->name('app.automations.activate');
|
|
|
|
|
Route::post('automations/{automation}/pause', [AutomationController::class, 'pause'])->name('app.automations.pause');
|
|
|
|
|
Route::post('automations/{automation}/runs/{run}/retry', [AutomationController::class, 'retryRun'])->name('app.automations.runs.retry');
|
|
|
|
|
Route::post('automations/{automation}/test', [AutomationController::class, 'test'])->name('app.automations.test');
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
Route::post('automations/{automation}/feed/inspect', [AutomationController::class, 'inspectFeed'])->name('app.automations.feed.inspect');
|
2026-05-24 12:17:19 +00:00
|
|
|
Route::get('automations/{automation}/runs/{run}', [AutomationController::class, 'showRun'])->name('app.automations.runs.show');
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// API Keys
|
2026-05-03 16:44:13 +00:00
|
|
|
Route::get('settings/workspace/api-keys', [ApiKeyController::class, 'index'])->name('app.api-keys.index');
|
|
|
|
|
Route::post('settings/workspace/api-keys', [ApiKeyController::class, 'store'])->name('app.api-keys.store');
|
2026-05-03 21:38:17 +00:00
|
|
|
Route::delete('settings/workspace/api-keys/{tokenId}', [ApiKeyController::class, 'destroy'])->name('app.api-keys.destroy');
|
2026-03-31 14:35:49 +00:00
|
|
|
|
MCP: workspace settings, viewer read access, and token access (#241)
* Add workspace MCP settings and token access controls.
Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring,
and workspace.token:mcp gating so assistants can connect without pulling
in welcome/onboarding from the parent epic.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Type MCP client config shapes instead of string checks.
Encode http/config-root on each advanced client and tighten primary
client ids so snippet generation does not branch on magic strings.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Polish MCP settings follow-ups from review.
Translate Ukrainian MCP copy, deep-link ChatGPT into connector
creation, drop an unused asset and revoke arg, and assert PATs are
rejected on the MCP endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden MCP connected clients, revoke scope, and OAuth consent.
List recoverable sessions with live refresh tokens, revoke only PATs,
throttle registration alone, and block viewers from authorizing MCP.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify MCP OAuth route throttling to a single middleware group.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Allow workspace viewers read-only MCP access with web policy writes.
Mirror the web app: MCP connects on view + OAuth mcp:use, write tools
enforce createPost/update/delete/manageAccounts/manageTeam, and demotion
to Viewer keeps grants. Cover role denials, consent, and disconnect.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden MCP tool authz with shared workspace helpers.
Route ApiKey tools through AuthorizesMcpTool, fail closed on null user
or policy argument, and resolve the current workspace before mutating.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop redundant string casts on validated request data.
Enum::from and validated() fields are already strings, so the casts
add noise without changing behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Show only the current user's MCP connections in settings.
Match API keys privacy: list and disconnect your own OAuth clients,
not teammates' across the account.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop redundant is_string guard before UpdatePostTool find.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined.
* Refresh MCP connected clients with Inertia usePoll.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Bump laravel/mcp to 0.9.1 and add the TryPost server icon.
Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop no-op ReflectionClass import in TryPostServerTest.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
|
|
|
// MCP
|
|
|
|
|
Route::get('settings/workspace/mcp', [McpSettingsController::class, 'index'])->name('app.mcp.index');
|
|
|
|
|
Route::delete('settings/workspace/mcp/{client}', [McpSettingsController::class, 'disconnect'])->name('app.mcp.disconnect');
|
|
|
|
|
|
feat: redesign billing, onboarding, sidebar, and settings architecture
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
-> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
2026-04-15 03:33:38 +00:00
|
|
|
// Account Settings
|
|
|
|
|
Route::get('settings/account', [AccountController::class, 'edit'])->name('app.account.edit');
|
|
|
|
|
Route::put('settings/account', [AccountController::class, 'update'])->name('app.account.update');
|
2026-05-03 16:44:13 +00:00
|
|
|
Route::get('settings/account/usage', [UsageController::class, 'index'])->name('app.usage.index');
|
feat: redesign billing, onboarding, sidebar, and settings architecture
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
-> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
2026-04-15 03:33:38 +00:00
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
// Billing
|
2026-05-03 16:44:13 +00:00
|
|
|
Route::get('settings/account/billing', [BillingController::class, 'index'])->name('app.billing.index');
|
|
|
|
|
Route::get('settings/account/billing/portal', [BillingController::class, 'portal'])->name('app.billing.portal');
|
feat: per-workspace pricing, onboarding, and billing overhaul
Pricing
- Bill per workspace ($12/mo or $120/yr each); Stripe quantity tracks the
workspace count and syncs on workspace create/delete.
- 2,500 AI credits per workspace, pooled at the account level; monthly reset
on the billing anniversary, annual granted upfront (no rollover).
- One social account per network per workspace; remove all count-based limits
(workspace/social/member) and the legacy plan tiers (single Workspace plan).
Onboarding (cloud only: SELF_HOSTED=false + PostHog)
- Replace the /subscribe plan picker with /onboarding persona selection
(Creator/Freelancer/Startup/Agency/Small business/Other), saved on the user
(users.persona) and mirrored to PostHog, then Stripe Checkout on the monthly
price. 8-day trial so Stripe displays 7.
Billing screen
- Remove the Change Plan dialog (dead with a single plan); add an annual-upgrade
banner for monthly subscribers (swapToYearly).
- Current-plan card shows the workspace count instead of the plan name.
System AI
- Brand analyzer / workspace autofill is always allowed and never debits credits
(system feature, not the user's usage).
Self-hosted (SELF_HOSTED=true) bypasses all billing, credit, limit, network,
and onboarding logic.
2026-06-21 23:40:03 +00:00
|
|
|
Route::post('settings/account/billing/swap-to-yearly', [BillingController::class, 'swapToYearly'])->name('app.billing.swap-to-yearly');
|
2026-03-31 14:35:49 +00:00
|
|
|
|
fix: Stripe webhook setup completion, notifications routing, billing UI
- Fix StripeEventListener: mark setup as Completed when subscription.created webhook fires
- Fix notifications routes: move outside subscribed middleware (non-critical feature)
- Fix NotificationBell: guard response.ok before parsing JSON
- Fix StripeEventListener: use data_get() for payload access
- Rewrite billing page to use SettingsLayout with download buttons on invoices
- Remove duplicate Unit/Listeners/StripeEventListenerTest (consolidated into Feature)
- Add comprehensive Stripe webhook tests (14 scenarios: happy path, setup transitions, edge cases, error handling)
- Update billing i18n: remove "no credit card" text, add status key
- Move auth layout logo to top-left corner
- Redesign onboarding Role page with compact list layout
- Add persona i18n translations (EN, PT-BR, ES)
- Update README with API, MCP, tech stack, quick start
2026-03-31 16:52:36 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Notifications (auth only, no subscription required)
|
|
|
|
|
Route::middleware(['auth'])->group(function () {
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::get('notifications', [NotificationController::class, 'index'])->name('app.notifications.index');
|
|
|
|
|
Route::put('notifications/{notification}/read', [NotificationController::class, 'markAsRead'])->name('app.notifications.read');
|
|
|
|
|
Route::post('notifications/read-all', [NotificationController::class, 'markAllAsRead'])->name('app.notifications.read-all');
|
|
|
|
|
Route::post('notifications/archive-all', [NotificationController::class, 'archiveAll'])->name('app.notifications.archive-all');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Settings (auth required)
|
|
|
|
|
Route::middleware(['auth'])->group(function () {
|
2026-05-03 18:54:26 +00:00
|
|
|
Route::get('settings', [SettingsController::class, 'index'])->name('app.settings');
|
|
|
|
|
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::get('settings/profile', [ProfileController::class, 'edit'])->name('app.profile.edit');
|
|
|
|
|
Route::put('settings/profile', [ProfileController::class, 'update'])->name('app.profile.update');
|
|
|
|
|
Route::post('settings/profile/photo', [ProfileController::class, 'uploadPhoto'])->name('app.profile.upload-photo');
|
|
|
|
|
Route::delete('settings/profile/photo', [ProfileController::class, 'deletePhoto'])->name('app.profile.delete-photo');
|
|
|
|
|
Route::put('settings/language', [ProfileController::class, 'updateLanguage'])->name('app.profile.language');
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-02 16:30:59 +00:00
|
|
|
Route::middleware(['auth'])->group(function () {
|
2026-03-31 14:35:49 +00:00
|
|
|
Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('app.profile.destroy');
|
2026-05-03 20:42:44 +00:00
|
|
|
|
|
|
|
|
Route::get('settings/authentication', [AuthenticationController::class, 'edit'])->name('app.authentication.edit');
|
|
|
|
|
Route::put('settings/authentication/password', [AuthenticationController::class, 'updatePassword'])
|
2026-03-31 14:35:49 +00:00
|
|
|
->middleware('throttle:6,1')
|
2026-05-03 20:42:44 +00:00
|
|
|
->name('app.authentication.update-password');
|
|
|
|
|
Route::delete('settings/authentication/sessions', [AuthenticationController::class, 'destroyOtherSessions'])
|
|
|
|
|
->name('app.authentication.destroy-other-sessions');
|
2026-05-04 22:22:09 +00:00
|
|
|
Route::get('settings/authentication/providers/{provider}/connect', [AuthenticationController::class, 'connectProvider'])
|
|
|
|
|
->name('app.authentication.connect-provider');
|
2026-05-03 20:42:44 +00:00
|
|
|
Route::delete('settings/authentication/providers/{provider}', [AuthenticationController::class, 'disconnectProvider'])
|
|
|
|
|
->name('app.authentication.disconnect-provider');
|
|
|
|
|
|
2026-05-03 16:44:13 +00:00
|
|
|
Route::get('settings/profile/notifications', [NotificationPreferenceController::class, 'edit'])->name('app.notifications.preferences');
|
|
|
|
|
Route::put('settings/profile/notifications', [NotificationPreferenceController::class, 'update'])->name('app.notifications.preferences.update');
|
2026-03-31 14:35:49 +00:00
|
|
|
});
|