feat(mobile): bootstrap mobile app (PWA + Capacitor + backend auth/CORS)

Implements the §12 bootstrap from docs/mobile-app-plan.md.

Backend (§6):
- auth: optional returnToken flag on login returns the JWT in the body
  (data.authToken) for native Keychain/Keystore + Bearer; web cookie flow
  unchanged.
- main.ts: explicit CORS allowlist (APP_URL + CORS_ALLOWED_ORIGINS env +
  Capacitor WebView origins), credentials enabled, replaces open enableCors().
- optional OpenAPI/Swagger at /api/docs behind SWAGGER_ENABLED.
- env: CORS_ALLOWED_ORIGINS, SWAGGER_ENABLED, CAP_SERVER_URL.

PWA:
- manifest metadata, hand-rolled service worker (network-first nav, SWR
  assets, never intercepts /api,/socket.io,/collab), prod-only registration,
  apple-touch-icon.

Capacitor:
- capacitor.config.ts (webDir apps/client/dist; iOS via CAP_SERVER_URL to
  avoid bundling the AGPL client in the .ipa, see plan §9), cap:* scripts,
  deps, .gitignore for native dirs.
- docs/mobile-bootstrap.md documenting what is done and the remaining manual
  steps (cap add ios/android, APNs/FCM, stores).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-21 14:08:29 +03:00
committed by claude code agent 227
parent 7066ace635
commit 6571c0f122
14 changed files with 288 additions and 23 deletions

View File

@@ -95,6 +95,12 @@ export class AuthController {
} else if (mfaResult.authToken) {
// User doesn't have MFA and workspace doesn't require it
this.setAuthCookie(res, mfaResult.authToken);
// Opt-in body token for native clients (Bearer auth). The response is
// wrapped by TransformHttpResponseInterceptor, so clients read it at
// `data.authToken`. Web clients omit returnToken and keep the cookie.
if (loginInput.returnToken) {
return { authToken: mfaResult.authToken };
}
return;
}
}
@@ -102,6 +108,12 @@ export class AuthController {
const authToken = await this.authService.login(loginInput, workspace.id);
this.setAuthCookie(res, authToken);
// Opt-in body token for native clients (Bearer auth). The response is wrapped
// by TransformHttpResponseInterceptor, so clients read it at `data.authToken`.
// Web clients omit returnToken and keep using the httpOnly cookie only.
if (loginInput.returnToken) {
return { authToken };
}
}
@UseGuards(SetupGuard)