plan(s0): chunk 3 - frontend extraction + Flyway + Gitea publishing
+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.*
|
||||||
|
|||||||
Reference in New Issue
Block a user