Commit graph

10 commits

Author SHA1 Message Date
Paulo Castellano
2ca5948309
Scope MCP OAuth tokens to user + workspace (#222) (#245)
* Scope MCP OAuth tokens to user + workspace

Bind authorization-code grants to the authorizing workspace (via auth codes),
inherit workspace on refresh, resolve MCP/API requests from the token instead
of current_workspace_id, backfill existing grants, and revoke workspace tokens
when a member is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add multi-workspace MCP OAuth coverage

Cover coexistence of the same client across workspaces, settings
list/disconnect scoped to the current workspace, and API key
controllers excluding workspace-bound MCP grants.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use constrained foreignUuid for oauth_auth_codes.workspace_id

Match the project's UUID foreign-key convention instead of a separate
foreign() call.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Localize the MCP OAuth authorize consent screen

Wire authorize.blade.php to mcp.* translation keys (including the
workspace scope copy) and cover pt-BR rendering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix invalid Mockery import in bind workspace test

CI treats the non-compound `use Mockery` as an ErrorException and
aborts the whole parallel suite.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Inline MCP OAuth workspace backfill into the migration

Move the one-shot backfill out of a dedicated Action and wrap it in an
explicit transaction so a failure rolls back partial binds/revokes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Nest MCP authorize i18n keys and test backfill rollback

Group consent-screen copy under mcp.authorize.*, and assert the
workspace backfill migration rolls back binds when it fails before
commit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Hardcode TryPost in the MCP authorize page title

Drop the config('app.name') interpolation from the consent screen title.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add workspace picker to MCP OAuth consent screen

Let users choose which workspace to bind at authorize time instead of
always using current_workspace_id; silent re-consent still falls back.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tighten MCP authorize workspace select spacing

Match NativeSelect styling and give the label, control, and helper text room to breathe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Convert MCP OAuth consent screen to Inertia Vue

Reuse AuthCardLayout, Button, and NativeSelect so the authorize page
matches the app UI. Keep native form posts so Passport's external
redirect still works for MCP client popups.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Polish MCP authorize layout with logo and workspace combobox

Drop the shield and AuthCardLayout double-logo, put TryPost branding
at the top, and reuse the app Combobox pattern for workspace search.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Align MCP OAuth workspace backfill with mcpOAuth scope

Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use
grants on non-PAT clients, matching the rest of the codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tighten MCP OAuth workspace backfill heuristics

Only touch connected MCP sessions, bind a sole membership or a valid
current workspace, and revoke ambiguous multi-workspace grants instead
of guessing the oldest workspace.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop Passport connection override from auth code migration

Always use the app default database connection from .env.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Bind MCP OAuth workspace in AccessTokenRepository

Replace the AccessTokenCreated listener with the same Passport repository
override pattern used for auth codes, so workspace_id is set at persist.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify AccessTokenRepository workspace binding

Drop redundant string casts and the oldest-workspace fallback; keep a
small ownedWorkspace/payloadId helper surface instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Extract Passport MCP authorization view from AppServiceProvider

Keep configurePassport thin by moving the Inertia consent props into an
invokable App\Passport\AuthorizationView class.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify AuthorizationView and cover it with direct tests

Use collection higher-order mapping for workspaces/scopes and add focused
tests for current-workspace selection and empty-user props.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Rename BindWorkspaceToAccessTokenTest after listener removal

The suite now covers AuthCodeRepository and AccessTokenRepository
workspace binding, not an AccessTokenCreated listener.

* Fail closed when auth code has no bindable workspace

Authorization-code grants no longer fall back to the user's current
workspace, so a token cannot be minted for a different tenant than consent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Retrigger CI after GitHub Actions infrastructure failures

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger CI

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden MCP OAuth workspace binding on refresh and backfill

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: always show MCP OAuth consent to pick a workspace

Disable Passport silent re-consent and require an explicit workspace_id
from the consent form, with Passport wiring moved to its own provider.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: sort MCP connected clients by last used

Show most recently used OAuth connections first on the workspace MCP settings page.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 21:59:34 -03:00
Paulo Castellano
4d8353d758
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 09:54:51 -03:00
Paulo Castellano
1af705f3fd
Localize calendar date pickers to the active UI locale (#234)
* Localize calendar date pickers to the active UI locale.

Pass the Inertia locale into Reka calendars, translate a11y labels, and use dayjs LL/LLL for DatePicker display text.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Localize remaining dayjs date displays across the UI.

Route list/calendar/preview timestamps through localized LL/LLL helpers so formats follow the active UI locale instead of English or Portuguese-locked patterns.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use scheduled-or-now timestamps in platform previews.

Drive preview labels from the post schedule when set, localize Discord without dayjs calendar(), and expose a single formatPreviewPostedAt helper.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix preview schedule wiring and Discord/Facebook timestamp labels.

Restore hasPickedTime from any saved scheduled_at, keep time on non-today Discord labels, and use a localized just-now fallback for unschedled Facebook previews.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix API key expiry day shift and tighten calendar range titles.

Format date-only expiry in UTC calendar days, and use compact dayjs titles for calendar day/week headers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Replace frontend date source greps with Inertia API key assertions.

Drop the Vue/TS file scanner and assert expiry calendar days through ApiKeyController props instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Localize calendar week titles with day-first dayjs formats.

Avoid English month-day order in week headers and chart axis labels so locales like pt-BR keep natural day-first dates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Split preview timestamp formatting into named platform helpers.

Replace the style-switch formatPreviewPostedAt with clear per-platform helpers so call sites read as formatDiscordPreview / formatXPreview / etc.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 21:02:34 -03:00
Paulo Castellano
20eeeaa493 feat: migrate custom API token implementation to Laravel Passport for authentication and token management 2026-05-03 18:38:17 -03:00
Paulo Castellano
d39124e90b refactor: reorganize settings UI, migrate post templates to a file-based registry, and remove legacy video generation features 2026-05-03 13:44:13 -03:00
Paulo Castellano
b3b59b4d13 refactor: remove onboarding flow, implement brand analysis services, and replace setup middleware with account readiness checks 2026-04-16 23:05:51 -03:00
Paulo Castellano
2da15df96c feat: introduce Account entity as billing owner and refactor architecture
- Create Account model as Cashier Billable entity (stripe, plan, subscription)
- Account owns workspaces and has an owner_id (User)
- User belongs to one Account via account_id
- Workspace belongs to Account via account_id, no longer has billing fields
- Remove Brand model entirely (workspaces serve as grouping)
- Rename brand_limit to workspace_limit in plans
- Workspace roles simplified: admin/member/viewer (owner via Account)
- Invites now belong to Account with workspaces JSON array
- Pennant features scope changed from Workspace to Account
- EnsureSubscribed middleware checks Account subscription
- All controllers updated: BillingController, OnboardingController,
  WorkspaceInviteController, SocialController, StripeEventListener
- Frontend: extract GoogleAuthButton component, create WorkspaceRole
  enum for type-safe role checks, fix all views for new architecture
- All 1101 tests passing
2026-04-14 22:22:04 -03:00
Paulo Castellano
c020419db1 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 01:42:39 -03:00
Paulo Castellano
74c6442728 refactor: code review fixes — policies, enums, data_get, tests
- Refactor WorkspacePolicy to use pivot role instead of workspace.user_id
- Add manageBilling policy (owner only) to BillingController
- Fix ApiKeyController authorization (view → manageTeam for store/destroy)
- Fix WorkspaceInviteController using workspace.user_id for owner checks
- Fix WorkspaceController settings is_owner using workspace.user_id
- Create PostAction enum for UpdatePost/PostController action strings
- Create ApiToken\Status enum
- Add User::SUBSCRIPTION_NAME constant, replace all hardcoded 'default'
- Convert wantsEmailFor to accept NotificationType enum
- Convert all $data[] to data_get() across publishers, controllers, jobs
- Fix SocialLoginController callback missing try/catch
- Fix SocialController::toggleActive missing workspace null check
- Fix UpdatePost NPE on meta merge when postPlatform not found
- Remove HTML5 required attributes from form inputs
- Convert function declarations to arrow functions in Vue components
- Replace hardcoded URLs with Wayfinder route helpers
- Replace new Date() with dayjs
- Add 16 new test files covering policies, authorization, publishing
2026-03-31 00:40:18 -03:00
Paulo Castellano
039ed02b42 feat: rate limiting, cleanup duplicates, ApiKey app tests, bootstrap hardening
- Add API rate limiting (60/min per workspace) in AppServiceProvider
- Add throttle:api middleware to all API routes
- Add rate limit exception rendering for JSON responses
- Add middleware priority list (AuthenticateApiToken before ThrottleRequests)
- Add ApiKeyControllerTest (6 tests for App dashboard CRUD)
- Remove duplicate test files (Controllers/ dir duplicates)
- Remove old controllers and routes/settings.php
- Add routes/mcp.php placeholder
- Add API Keys nav item to settings layout
- Fix LazilyRefreshDatabase conflict in API tests
- 698 tests passing
2026-03-29 20:14:23 -03:00