diff --git a/Sprint-0-Plan.md b/Sprint-0-Plan.md index 9e7c821..fd3ffc7 100644 --- a/Sprint-0-Plan.md +++ b/Sprint-0-Plan.md @@ -395,3 +395,314 @@ mailers log instead of crashing. --- *Plan continues — frontend extraction, Flyway, publishing.* + +--- + +## 6. Frontend extraction — step-by-step + +### 6.1 W3-A — npm package skeleton + +**Goal:** A buildable, publishable `@platesoft/auth@0.1.0` with TypeScript + ESM/CJS dual build. + +**Steps:** + +1. **W3-1** Configure `packages/auth/package.json`: + ```json + { + "name": "@platesoft/auth", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { "import": "./dist/index.js", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" }, + "./config": { "import": "./dist/config/index.js", "types": "./dist/config/index.d.ts" }, + "./exchange": { "import": "./dist/exchange/index.js", "types": "./dist/exchange/index.d.ts" }, + "./proxy": { "import": "./dist/proxy/index.js", "types": "./dist/proxy/index.d.ts" }, + "./middleware":{ "import": "./dist/middleware/index.js","types": "./dist/middleware/index.d.ts" }, + "./client": { "import": "./dist/client/index.js", "types": "./dist/client/index.d.ts" } + }, + "peerDependencies": { + "next": ">=15.0.0", + "next-auth": "^5.0.0-beta", + "react": ">=19.0.0" + }, + "files": ["dist", "README.md", "LICENSE"] + } + ``` +2. **W3-2** Bundler choice: **`tsup`** (zero-config dual ESM/CJS, fast). Add `tsup.config.ts` + targeting Node 20 + Edge runtime. +3. **W3-3** TypeScript strict config, `"target": "ES2022"`, `"module": "ESNext"`, + `"moduleResolution": "Bundler"`, `"declaration": true`. +4. **W3-4** Add a `publishConfig` block pointing to the Gitea npm registry (set in W6). + +**Done when:** `pnpm -F @platesoft/auth build` produces `dist/` with ESM + CJS + `.d.ts` files. + +### 6.2 W3-B — Move + factor frontend code + +**Steps (per file from `inspectflow/frontend/`):** + +1. **W3-5** Copy [`frontend/lib/exchange.ts`](frontend/lib/exchange.ts) → `packages/auth/src/exchange/client.ts`. + - Replace `import { ... } from "@/lib/..."` patterns with relative imports inside the package. + - Extract the envelope-signing logic into `packages/auth/src/exchange/envelope.ts`: + ```ts + export interface ExchangeEnvelope { + provider: 'google' | 'microsoft' | 'email' | 'password'; + providerSubject: string; + email: string; + name?: string; + inviteToken?: string; + nonce: string; + iat: number; // unix seconds + } + export function signEnvelope(env: ExchangeEnvelope, secret: string): + { envelope: string; signature: string }; + export function makeNonce(): string; // crypto.randomUUID() + ``` + - Use Web Crypto API (`crypto.subtle.importKey` + `sign("HMAC", ...)`) so the code runs in the Edge + runtime as well as Node. +2. **W3-6** Copy [`frontend/lib/auth-config.ts`](frontend/lib/auth-config.ts) → `packages/auth/src/config/index.ts`. + - Refactor into a factory: + ```ts + export interface PlateAuthConfigOptions { + providers: { google?: GoogleOpts; microsoft?: MicrosoftOpts; email?: EmailOpts }; + exchange: { backendUrl: string; secret: string; appLabel?: string }; + session?: { strategy?: 'jwt'; maxAge?: number }; + callbacks?: { afterSignIn?: (user: PlateAuthUser) => Promise }; + trustHost?: boolean; + } + export function createAuthConfig(opts: PlateAuthConfigOptions): NextAuthConfig { + // builds provider list from opts.providers + // signIn callback calls exchangeWithBackend(envelope) using opts.exchange + // jwt callback persists access_token + memberships from backend response + // session callback exposes accessToken to client + } + ``` + - Provider modules under `packages/auth/src/config/providers/{google,microsoft,email}.ts` for clean tree-shaking. +3. **W3-7** Copy [`frontend/app/api/[...path]/route.ts`](frontend/app/api/[...path]/route.ts) + → `packages/auth/src/proxy/handlers.ts`. + - Refactor: + ```ts + export interface ProxyOptions { + backendUrl: string; + stripHeaders?: string[]; // default: hop-by-hop list + authHeaderName?: string; // default: 'Authorization' + } + export function createProxyHandlers(opts: ProxyOptions): { + GET: RouteHandler; POST: RouteHandler; PUT: RouteHandler; + PATCH: RouteHandler; DELETE: RouteHandler; OPTIONS: RouteHandler; + }; + ``` + - Must use NextAuth v5 `auth()` not `getToken()`. Body forwarding must include + `duplex: "half"` for streaming POST/PUT. +4. **W3-8** Copy [`frontend/middleware.ts`](frontend/middleware.ts) → `packages/auth/src/middleware/index.ts`. + - Factor as `createAuthMiddleware(opts?: { publicPaths?: string[] })` returning a `NextMiddleware`. +5. **W3-9** Move [`frontend/contexts/auth-context.tsx`](frontend/contexts/auth-context.tsx) logic into + `packages/auth/src/client/hooks.ts` — but **as hooks only**, no React Context wrapper. Consumers + build their own provider if needed. + - Expose: + ```ts + export function useAccessToken(): string | null; + export function useMemberships(): Membership[]; + export type { Membership, OrgType, MembershipRole, MembershipStatus }; + ``` +6. **W3-10** Re-export NextAuth client surface in `packages/auth/src/client/index.ts`: + ```ts + export { useSession, signIn, signOut, SessionProvider } from "next-auth/react"; + export * from "./hooks"; + ``` + +**Done when:** Library compiles, exports listed above resolve, and a sample Next.js app can +`import { createAuthConfig } from '@platesoft/auth/config'`. + +### 6.3 W3-C — Boilerplate Next.js route file + +Consumers need a one-line route file. We ship documentation, not the file itself +(it must live in *their* `app/api/auth/[...nextauth]/route.ts`): + +```ts +// app/api/auth/[...nextauth]/route.ts +import NextAuth from 'next-auth'; +import { createAuthConfig } from '@platesoft/auth/config'; + +const config = createAuthConfig({ + providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! } }, + exchange: { backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL!, secret: process.env.NEXTAUTH_EXCHANGE_SECRET! }, +}); +export const { handlers, auth, signIn, signOut } = NextAuth(config); +export const { GET, POST } = handlers; +``` + +Documented in [`Integration-Guide.md`](Integration-Guide.md). + +--- + +## 7. Flyway migration consolidation + +### 7.1 Strategy + +After Plan Reviewer feedback on Open-Questions Q03, finalize the strategy. The **recommended** approach +for v0.1 (subject to Plan Reviewer concurrence): + +> **Separate Flyway history table** for plate-auth migrations. +> +> - Consumer config: `spring.flyway.locations=classpath:db/migration,classpath:db/migration/auth` +> - plate-auth auto-configures a **second `Flyway` bean** named `plateAuthFlyway` with: +> - `locations = classpath:db/migration/auth` +> - `table = flyway_schema_history_auth` +> - Runs at startup *before* the application's own Flyway +> - Application's primary `Flyway` continues to manage `flyway_schema_history` for app migrations +> +> **Why:** plate-auth's `V1..V5` numbering is completely independent of any app's `V1..VN`. +> Both libraries can advance their own version space without collision. Consumers get a clean install +> from scratch, and InspectFlow's `Migration-InspectFlow.md` handles the in-place baseline. + +If Plan Reviewer rejects this and prefers numbered-tail approach (e.g. plate-auth ships V1..V5 and +relies on app migrations starting at V100), we revise to single-table strategy. Both approaches are +viable; the separate-table one is more isolating. + +### 7.2 W5 — Migration files + +**Steps:** + +1. **W5-1** Create `plate-auth-starter/src/main/resources/db/migration/auth/` directory. +2. **W5-2** Copy V26 → `V1__create_users_and_identities.sql`. Edit: + - Remove anything InspectFlow-specific (none expected) + - Verify Postgres compatibility (no H2-only syntax) +3. **W5-3** Copy V27 → `V2__create_memberships.sql`. **Drop the trigger** + `fn_membership_org_fk()` from the migration — that trigger references `companies` which is T3. + Consumers add their own trigger or rely solely on the `OrgValidator` SPI for validation. + - Document in [`Migration-InspectFlow.md`](Migration-InspectFlow.md): "InspectFlow's V27 trigger was + migrated; if you previously relied on it, keep it in your app's migration." +4. **W5-4** Copy V28 → `V3__create_invitations.sql`. +5. **W5-5** Copy V29 → `V4__create_access_requests.sql`. +6. **W5-6** Copy V31 → `V5__create_login_events_and_revinfo_actor.sql`. + (V30, `companies.microsoft_tenant_id`, stays in InspectFlow's migration set — T3.) +7. **W5-7** Add `MigrationContentTest` (integration test) that: + - Spins up Testcontainers Postgres + - Runs plate-auth Flyway against `flyway_schema_history_auth` + - Asserts all 5 versions applied successfully + - Asserts no SQL errors in clean install + +**Done when:** Migration test passes against Testcontainers Postgres in CI. + +### 7.3 W5 — Auto-config the second Flyway bean + +```java +@Configuration +@ConditionalOnClass(Flyway.class) +public class PlateAuthFlywayConfig { + + @Bean + public Flyway plateAuthFlyway(DataSource dataSource) { + Flyway fw = Flyway.configure() + .dataSource(dataSource) + .locations("classpath:db/migration/auth") + .table("flyway_schema_history_auth") + .baselineOnMigrate(true) // for fresh installs only + .load(); + fw.migrate(); + return fw; + } +} +``` + +Critical detail: this Bean's `migrate()` must run **before** any `@Entity` is touched. Spring Boot's +default Flyway runs as part of JPA initialization; we run ours explicitly in the bean factory method. +Integration tests verify ordering. + +--- + +## 8. Build + publish pipeline + +### 8.1 W6-A — Gitea Actions workflow + +**Steps:** + +1. **W6-1** Create `.gitea/workflows/ci.yml`: + ```yaml + name: CI + on: + push: { branches: [main] } + pull_request: { branches: [main] } + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: { java-version: '25', distribution: 'temurin' } + - uses: actions/setup-node@v4 + with: { node-version: '22' } + - run: npm install -g pnpm + - run: mvn -B verify + - run: pnpm install --frozen-lockfile + - run: pnpm -r build + - run: pnpm -r test + ``` +2. **W6-2** Create `.gitea/workflows/release.yml`: + ```yaml + name: Release + on: + push: { tags: ['v*'] } + jobs: + publish-maven: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: { java-version: '25', distribution: 'temurin' } + - name: Configure Maven for Gitea + run: | + mkdir -p ~/.m2 + cat > ~/.m2/settings.xml < + + gitea + ${{ secrets.GITEA_USER }} + ${{ secrets.GITEA_TOKEN }} + + + EOF + - run: mvn -B -Drevision=${GITHUB_REF_NAME#v} deploy + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://git.plate-software.de/api/packages/pplate/npm/' + - run: npm install -g pnpm + - run: pnpm install --frozen-lockfile + - name: Set version from tag + run: pnpm -F @platesoft/auth version ${GITHUB_REF_NAME#v} --no-git-tag-version + - run: pnpm -F @platesoft/auth build + - run: pnpm -F @platesoft/auth publish --no-git-checks + env: + NPM_CONFIG_TOKEN: ${{ secrets.GITEA_TOKEN }} + ``` +3. **W6-3** Add `distributionManagement` block to parent `pom.xml` pointing at the Gitea Maven endpoint + (`https://git.plate-software.de/api/packages/pplate/maven`). +4. **W6-4** Snapshot publishing on every push to `main`: + - Maven: `mvn -Drevision=0.1.0-SNAPSHOT deploy` (Gitea Package Registry allows SNAPSHOT-style for Maven) + - npm: skip on snapshots, or use `pnpm publish --tag snapshot` with `0.1.0-snapshot.` version + +**Done when:** Pushing tag `v0.0.1` publishes both `de.platesoft:plate-auth-starter:0.0.1` (Maven) and +`@platesoft/auth@0.0.1` (npm) to the Gitea Package Registry. Verified by `mvn dependency:get` + `npm view`. + +### 8.2 W6-B — Validation tag + +Before cutting `v0.1.0`, cut `v0.0.1` first: +- Verifies the publish pipeline end-to-end +- Lets InspectFlow team try `mvn dependency:get de.platesoft:plate-auth-starter:0.0.1` +- Forces us to fix all the inevitable "wrong settings.xml / missing token" issues *before* the real + release + +After `v0.0.1` lands cleanly and is consumed in a throwaway test app, cut `v0.1.0` from the same +commit. + +--- + +*Plan continues — security review, rollout, acceptance.*