Commit graph

10 commits

Author SHA1 Message Date
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
e72bf51c38 Make webhook host configurable via WEBHOOK_URL (sendkit-style domain group) 2026-06-14 09:31:00 -03:00
Paulo Castellano
b504243212 Document Telegram env vars in docker .env example 2026-06-13 22:02:02 -03:00
Paulo Castellano
2b75820544 chore: small improvements 2026-06-10 16:20:00 -03:00
Falconiere Barbosa
a2a7f44c9a
refactor(linkedin): drive OAuth scopes from LINKEDIN_SCOPES env via config
Replace the additive LINKEDIN_EXTRA_SCOPES approach with a single
full-override env var per flow, exploded into an array at the config
layer (config/trypost.php -> platforms.linkedin{,-page}.scopes). This
keeps env values as plain comma-separated strings, lets self-hosters
override the entire set in one place, and removes the controller-side
scope-merge logic.

- config/trypost.php: explode LINKEDIN_SCOPES / LINKEDIN_PAGE_SCOPES
  into the scopes arrays (deprecated r_basicprofile stays out of the
  personal default)
- LinkedInController: drop resolveScopes(), read config scopes directly
- LinkedInPageController: drop the hardcoded $scopes property, read
  config scopes at both call sites
- tests: drive the connect scope assertions from config overrides
- .env.example, docker/.env.docker.example: document LINKEDIN_SCOPES
  and LINKEDIN_PAGE_SCOPES
2026-06-10 15:04:57 -03:00
Falconiere Barbosa
410eb9612e fix(linkedin): drop deprecated r_basicprofile from default scopes
Make r_basicprofile opt-in via LINKEDIN_EXTRA_SCOPES so self-hosted users
unblock by default and ops with legacy/enterprise products keep working.

Why
---
LinkedIn rejects OAuth authorize requests with a generic "Bummer,
something went wrong" page when an app asks for a scope it can't grant.
`r_basicprofile` is a legacy scope deprecated in 2018; new LinkedIn dev
apps don't have it, so every self-hosted user hits the rejection
immediately on `/connect/linkedin`.

The two products LinkedIn actually grants to standard apps today are:

- Sign In with LinkedIn using OpenID Connect → `openid profile email`
- Share on LinkedIn                          → `w_member_social`

That set is enough for the connect flow. The only piece of data
`r_basicprofile` was buying us is `/v2/me`'s `vanityName` (pretty
`linkedin.com/in/<slug>`). `fetchVanityName()` already handles HTTP
failure gracefully (returns null), and the only downstream consumer —
`LinkedInPagePublisher`'s post-URL builder — already falls back to a
numeric `linkedin.com/feed/update/<id>` URL when `$account->username`
is null.

Backward compatibility
----------------------
Ops with legacy or enterprise LinkedIn products approved on their dev
app (so they DO have `r_basicprofile`) can opt back in via env:

    LINKEDIN_EXTRA_SCOPES=r_basicprofile

`LinkedInController::resolveScopes()` merges this comma-separated list
into the default scope array. The connect flow's `Socialite::scopes()`
call then includes the legacy scope, preserving the pre-PR behaviour
end-to-end (including `vanityName` lookup).

Net effect for users without `r_basicprofile`:
- Connect flow works (was previously rejected by LinkedIn).
- Posts publish exactly the same way.
- Generated post URLs use the numeric form instead of the vanity slug.

Tests
-----
- `linkedin connect requests the default scope set when LINKEDIN_EXTRA_SCOPES is unset`
- `linkedin connect appends LINKEDIN_EXTRA_SCOPES to the default scope set`
- Existing `splits comma-separated approvedScopes` fixture updated to
  match the new default set.
2026-06-10 13:51:07 -03:00
André Dantas
ef501dd385 fix(docker): add Reverb WebSocket proxy to nginx
Add /app/ (Pusher protocol) and /apps/ (REST API) location blocks
with Upgrade/Connection headers so browsers can connect to the
in-container Reverb server via the same port 80/443 endpoint.
2026-05-12 23:44:55 -03:00
André Dantas
1a74dfc60e fix(docker): production build/runtime fixes from homelab deploy
- Add libwebp-dev + libavif-dev and --with-webp --with-avif to GD
  configure (fixes imagewebp() undefined at runtime)
- Merge wayfinder-gen into asset-build stage: PHP + Node together
  so @laravel/vite-plugin-wayfinder can invoke artisan during build
- Pass VITE_* as build args and ENV so they bake into the bundle
- Stub APP_URL=http://localhost so artisan boot doesn't crash on null
- mkdir -p storage/framework/{cache/data,sessions,views} storage/logs
  bootstrap/cache before artisan runs (dirs stripped by .dockerignore)
- Production stage: rm vendor + bootstrap/cache/*.php before swapping
  to prod-only deps (prevents missing-provider errors from dev cache)
- Switch production chown from app:app to www-data:www-data to match
  Alpine php-fpm default pool user
2026-05-12 23:44:55 -03:00
André Dantas
7d3b8a478e fix(docker): drop stripe envs and bump upload limits to 1G
Two adjustments from the PR review.

Stripe placeholders (STRIPE_KEY, STRIPE_SECRET, STRIPE_WEBHOOK_SECRET,
and the eight price IDs) only matter when SELF_HOSTED=false. The
Docker stack defaults to SELF_HOSTED=true, so keeping the block in
.env.docker.example just adds noise for first-time contributors.

upload_max_filesize and post_max_size go from 100M to 1G in both
php.dev.ini and php.prod.ini to accept full-length video uploads.
Bumping nginx client_max_body_size to match — otherwise the proxy
would cut the request off before PHP ever sees it.
2026-05-12 23:44:55 -03:00
André Dantas
4ce5c9173f feat(docker): add multi-stage Dockerfile with dev and production targets
Introduces a self-contained Docker build under docker/ that boots the
full Laravel + Vite + Reverb + Horizon + scheduler stack inside a
single container.

The Dockerfile exposes two targets sharing a common system-base layer
(PHP 8.4-FPM Alpine + Postgres/Redis/intl/sockets/redis extensions):

  - dev: bind-mount source at runtime, runs Vite via supervisord, hot
    reloads PHP via opcache.validate_timestamps=1, UID/GID build args
    align container writes with the host user.

  - production: ships the prebuilt application — composer --no-dev,
    npm run build + build:ssr, wayfinder TS pre-generated, OpCache
    hardened, fixed UID 1000.

Sidecar configs (nginx, php.{dev,prod}.ini, supervisord.{dev,prod}.conf,
entrypoint, postgres-init for the test DB) live next to the Dockerfile
so the build context is self-describing. The entrypoint is idempotent
and handles APP_KEY generation, migrations, storage:link, Passport
keys, Wayfinder regen, and dependency reinstall on every boot.
2026-05-12 23:44:55 -03:00