plan(s0): chunk 3 - frontend extraction + Flyway + Gitea publishing

Patrick Plate
2026-06-24 14:19:59 +02:00
parent 7b2a93f542
commit 75758699b2
+311
@@ -395,3 +395,314 @@ mailers log instead of crashing.
--- ---
*Plan continues — frontend extraction, Flyway, publishing.* *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<void> };
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 <<EOF
<settings>
<servers><server>
<id>gitea</id>
<username>${{ secrets.GITEA_USER }}</username>
<password>${{ secrets.GITEA_TOKEN }}</password>
</server></servers>
</settings>
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.<sha>` 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.*