diff --git a/mcp/integration-prompt.md b/mcp/integration-prompt.md index 6dfbe64..ec8f08b 100644 --- a/mcp/integration-prompt.md +++ b/mcp/integration-prompt.md @@ -2,18 +2,7 @@ You are integrating The Bridge into an Angular application. This adds authentication, tenant context, role and privilege access control, feature flags and billing. -## Decide first — which login surface? - -This decision shapes everything else. Make it before writing code; getting it wrong means rewriting the auth pages. - -| You want | Use | What you build | -|---|---|---| -| The fastest path; Bridge owns the login UI | **Hosted auth** (default) | Nothing — no login page | -| Login inside your app | **SDK auth** | Nothing either — `...bridgeAuthRoutes()` serves every sign-in page | - -**Setting `loginRoute` in `BridgeConfig` is the entire switch.** Without it you get hosted; with it you get in-app. If you are being redirected to a route you never built, that field is why. - -If the user has not said which they want, ask. +Sign-in happens inside the app: the login, signup and password pages render in the app's own layout, and `...bridgeAuthRoutes()` serves every one of them. For the sign-in pages in detail, matching them to the app's design and customising them, read `sdk-auth-prompt.md` (`get_integration_guide` topic `auth`, or `bridge guide angular sdk-auth`). ## Prerequisites @@ -44,8 +33,9 @@ import { routes } from './app.routes'; const bridgeConfig: BridgeConfig = { appId: environment.bridgeAppId, + // Where a signed-out visitor is sent: the app's own login page. + loginRoute: '/auth/login', // Defaults to production. Required for a stage / local / self-hosted app. - // The hosted sign-in pages follow it on Bridge's domains. apiBaseUrl: environment.bridgeApiBaseUrl, }; @@ -59,6 +49,8 @@ export const appConfig: ApplicationConfig = { }; ``` +(If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that: leave out `loginRoute`, sign in with `AuthService.login()` instead of a link to `/auth/login`, and log out with `AuthService.logout()`; a local or self-hosted Bridge also sets `hostedUrl`. Everything else stays the same.) + Configuration is code (Angular has no env-var convention): each option resolves as *explicit option > default*, and an empty string is unset. **An empty `appId` throws at startup, naming the option.** If `provideBridge()` itself is missing, nothing errors loudly: flags return their defaults and auth never resolves — check that first when something seems inert. ## Step 2b — Let Bridge serve its pages @@ -80,7 +72,7 @@ export const routes: Routes = [ ]; ``` -**Do not write an OAuth callback, a set-password page, or subscription pages.** These spreads own them, including the address in every signup verification email. The sign-in pages are public by construction; to replace one, `bridgeAuthRoutes({ overrides: { login: { component: MyLogin } } })`. +**Do not write a login page, a signup page, an OAuth callback, a set-password page, or subscription pages.** These spreads own them, including the address in every signup verification email. The sign-in pages are public by construction; to replace one, `bridgeAuthRoutes({ overrides: { login: { component: MyLogin } } })`. ## Step 3 — Guard routes declaratively @@ -123,34 +115,64 @@ export const routes: Routes = [ > **Gating a whole page? Use a rule, not a component-level `if`.** A rule redirects before the component ever renders; an `if` inside the component means the page mounts, fetches, and only then hides itself — which leaks both the route's existence and whatever the page loaded on the way. -## Step 4 — Read the user +## Step 4 — Read the user, sign in and out ```ts import { Component, inject } from '@angular/core'; +import { RouterLink } from '@angular/router'; import { AuthService } from '@nebulr-group/bridge-angular'; -@Component({ selector: 'app-header', standalone: true, template: `…` }) +@Component({ + selector: 'app-header', + standalone: true, + imports: [RouterLink], + template: ` + @if (auth.isAuthenticated()) { + {{ auth.profile()?.fullName ?? auth.profile()?.email }} + + } @else { + Log in + } + `, +}) export class HeaderComponent { - private auth = inject(AuthService); + protected auth = inject(AuthService); + + logout(): Promise { + return this.auth.getBridgeAuth().logout({ redirectTo: '/auth/login' }); + } } ``` +Always pass `redirectTo` to `logout()` so the user lands on the app's own login page. A signed-out visitor on a protected route is sent to `/auth/login` and brought back to that route afterwards; the link is only for pages that are public. + `ProfileService` exposes profile fields; `realtimeStatus` reports the live-channel state. +## Checklist + +- [ ] `@nebulr-group/bridge-angular` installed +- [ ] `provideBridge(bridgeConfig)` in `appConfig.providers`, with a non-empty `appId`, `loginRoute: '/auth/login'`, and `apiBaseUrl` for a stage or local app +- [ ] `...bridgeAuthRoutes()` in the routes; no hand-written login, signup, callback or set-password page +- [ ] The sign-in pages match the app's design (`sdk-auth-prompt.md`) +- [ ] Protected routes guarded with `bridgeAuthGuard()` and a `RouteGuardConfig` +- [ ] `logout()` passes `redirectTo: '/auth/login'` + ## Verify it works Do not stop at a clean build. Run it and confirm: -1. A protected route while signed out starts the login flow. -2. After login you land **inside the app**. +1. A protected route while signed out redirects to `/auth/login` **inside the app**. +2. After login you land back on that route, inside the app. 3. A reload keeps you signed in. -4. A flag-gated route redirects when the flag is off, and is reachable when on. +4. Log out: you land on the app's `/auth/login`. +5. A flag-gated route redirects when the flag is off, and is reachable when on. +6. The developer does steps 1, 2 and 4 themselves in a browser, and signs up once at `/auth/signup`; do not create test users. If a step fails, `bridge event auth-attempts --user ` names the reason. ## Where to go next | Goal | Guide | |---|---| -| Login/signup inside your app | `sdk-auth-prompt.md` | +| The sign-in pages, matching the app's design, customising them | `sdk-auth-prompt.md` | | Gate features behind a flag | `feature-flags-prompt.md` | | Plans, checkout, quotas | `billing-prompt.md` | | Team / workspace management UI | `team-prompt.md` | diff --git a/mcp/sdk-auth-prompt.md b/mcp/sdk-auth-prompt.md index 4dd0ae7..ee144b3 100644 --- a/mcp/sdk-auth-prompt.md +++ b/mcp/sdk-auth-prompt.md @@ -1,19 +1,6 @@ # Bridge Angular — Authentication -You are wiring authentication into an Angular application that uses The Bridge. - -## Decide first — hosted or in-app? - -| You want | Mode | What you build | Config | -|---|---|---|---| -| Bridge owns the login UI | **Hosted** (default) | Nothing — no login page | No `loginRoute` | -| Login inside your app | **SDK auth** | Nothing — `...bridgeAuthRoutes()` serves the pages | Set `loginRoute: '/auth/login'` | - -**One config field is the whole switch.** Adding `loginRoute` to `BridgeConfig` turns hosted mode off. If you are being redirected to a route you never built, that is why. - -If the user has not said which they want, ask. A wrong guess here means rewriting the auth pages. - -The rest of this guide covers **SDK auth**. +You are wiring authentication into an Angular application that uses The Bridge. Sign-in happens inside the app: the login, signup and password pages render in the app's own layout, and users never leave it. `...bridgeAuthRoutes()` serves every one of those pages; you write no login, signup, callback or set-password page. ## The components — reach for these, do not hand-roll @@ -28,7 +15,6 @@ The rest of this guide covers **SDK auth**. | MFA challenge / setup | ``, `` | | Workspace ("tenant") selection | ``, `` | | SSO button | `` | -| Hosted-login entry point | `` | > **`` is not just an email and password box.** It drives forgot-password, magic link, passkeys, MFA and workspace selection as inline steps, and it decides which methods to show from the app's own configuration — which the client cannot see. Rebuilding any of it means reimplementing a flow that exists and then keeping it in sync with settings you have no visibility of. > @@ -53,6 +39,8 @@ const bridgeConfig: BridgeConfig = { }; ``` +(If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that: leave out `loginRoute`, sign in with `AuthService.login()` instead of a link to `/auth/login`, and log out with `AuthService.logout()`; a local or self-hosted Bridge also sets `hostedUrl`. Everything else stays the same.) + ## Step 2 — Spread the auth routes ```ts @@ -67,7 +55,34 @@ export const routes: Routes = [ That serves login, signup, the OAuth callback, set password (the address in every signup verification and password-reset email), forgot password, magic link, passkey setup and workspace selection. They are public by construction — no route rule needed, and no redirect loop. -Customise only as far as needed: `--bridge-*` CSS tokens; `bridgeAuthRoutes({ frame: AuthLayoutComponent, heading: (page) => … })`; replace one page with `overrides: { login: { component: MyLoginComponent } }` (a page you own renders `` and navigates itself). +## Match the app's design — always + +The sign-in pages render inside the app, so they must look like the rest of it. Bridge's built-in look (indigo buttons, grey borders) is a fallback for an app with no design, never the finished result. Do this without asking: + +1. **Read the app's design.** The global stylesheet (`src/styles.css` / `styles.scss`, as listed under `styles` in `angular.json`) and its CSS variables, `tailwind.config.*` or the Tailwind `@theme` block, a component library's theme (Angular Material, PrimeNG, Taiga, …), the font the app loads, and one existing primary button and text input. +2. **Set the `--bridge-*` tokens from it** in that global stylesheet, pointing at the app's own variables rather than copying values, so dark mode and later theme changes carry over: + + ```css + :root { + --bridge-primary: var(--color-primary); /* the app's primary button background */ + --bridge-primary-hover: var(--color-primary-hover); + --bridge-primary-fg: var(--color-primary-foreground); + --bridge-border: var(--color-border); + --bridge-input-focus: var(--color-ring); + --bridge-border-radius: var(--radius); + --bridge-foreground: var(--color-foreground); + --bridge-muted: var(--color-muted-foreground); + --bridge-bg: var(--color-background); + --bridge-muted-bg: var(--color-muted); + } + ``` + + The names on the right are examples: use the variables the app actually defines, or its literal values when it has none. Map its error and success colours to the `--bridge-alert-*` tokens. The components inherit the page font; set nothing for it unless the app puts its font on something other than `body`. The full token list: `bridge guide mechanisms`. +3. **Give the pages the app's frame** when the app's own pages sit in a card, a panel or a split layout: pass `bridgeAuthRoutes({ frame: AuthLayoutComponent })` with a component that wraps `` in the app's own card or shell, built from the app's own classes or components, so sign-in is not the one page that looks different. + +Ask the developer for colours only when the project has no styles of its own. + +Customise further only as far as needed: `--bridge-*` CSS tokens (above); `bridgeAuthRoutes({ frame: AuthLayoutComponent, heading: (page) => … })`; replace one page with `overrides: { login: { component: MyLoginComponent } }` (a page you own renders `` and navigates itself). ## Step 3 — Guard the rest @@ -75,7 +90,7 @@ Use the declarative `RouteGuardConfig` + `bridgeAuthGuard` from `integration-pro ## Reading the user -Inject `AuthService` for auth state and `ProfileService` for profile fields. For the raw token — to call your own backend — take it from the auth service, send it as `Authorization: Bearer `, and verify it server-side with `@nebulr-group/bridge-nestjs` or `@nebulr-group/bridge-express`. +Inject `AuthService` for auth state and `ProfileService` for profile fields. To log out, call `getBridgeAuth().logout({ redirectTo: '/auth/login' })` on `AuthService` so the user lands on the app's own login page. For the raw token — to call your own backend — take it from the auth service, send it as `Authorization: Bearer `, and verify it server-side with `@nebulr-group/bridge-nestjs` or `@nebulr-group/bridge-express`. > **Never make an authorization decision on the client alone.** Hiding a control is UX. The check that matters happens on the server against the verified token. @@ -83,7 +98,8 @@ Inject `AuthService` for auth state and `ProfileService` for profile fields. For - **Hand-rolling a password form** instead of `` — loses magic link, passkeys, MFA and workspace selection, all configured server-side. - **Writing login/signup/callback pages by hand** — `bridgeAuthRoutes()` already serves them; a hand-written set usually forgets `set-password/:token`, which every signup email links to. -- **Setting `loginRoute` while expecting hosted login**, or the reverse. +- **Leaving out `loginRoute`.** The guard then sends signed-out visitors off the app instead of to `/auth/login`. +- **Logging out without `redirectTo`.** The user then leaves the app instead of landing on `/auth/login`. - **Trusting auth state for authorization.** It says a session exists, not what it may do. - **Expecting an NgModule.** These are standalone components; import them into `imports: []`.