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.*
|
||||
|
||||
---
|
||||
|
||||
## 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