plan: add Migration-InspectFlow.md (dep swap + Flyway baseline + rollback)

Patrick Plate
2026-06-24 14:32:48 +02:00
parent e6ba33b769
commit 35cb5e76c3
+563
@@ -0,0 +1,563 @@
# Migration Guide: InspectFlow → plate-auth
**Status:** Draft v1
**Date:** 2026-06-24
**Owner:** Patrick
**Audience:** InspectFlow maintainer performing the dependency swap from in-tree auth code to `plate-auth-starter`
**Target plate-auth version:** `0.1.0` (after `0.0.1` validation tag)
> This guide is **InspectFlow-specific**. For greenfield consumers, see [Integration-Guide.md](Integration-Guide.md).
---
## 1. What this migration does
Replaces InspectFlow's hand-rolled auth code with a dependency on `plate-auth-starter` + `@platesoft/auth`. After this migration:
- ~25 backend classes under `de.platesoft.inspectflow.{security,filter,service,controller,entity,repository,dto,enums}` are **deleted** and become a single Maven dependency.
- ~58 frontend files under `frontend/{auth.ts, app/api/[...path]/route.ts, app/api/auth/[...nextauth]/route.ts, lib/hmac.ts}` are **deleted** and become a single npm dependency + a 5-line `auth.ts`.
- Flyway migrations `V26..V31` are **kept** but marked as baseline rows in the new `flyway_schema_history_auth` table so plate-auth's `V1..V5` are not re-applied.
- InspectFlow keeps `Company` entity + `OrgValidator` SPI implementation + onboarding logic — **T3 stays in-house**.
**Goal:** zero behavioral change for end-users. All E2E tests stay green. ([Sprint-0-Testplan.md § 6](Sprint-0-Testplan.md))
---
## 2. Prerequisites
Before starting:
- [ ] InspectFlow on **Spring Boot 4.1.0** (bump from 4.0.7 if needed — Sprint 14.7 prerequisite, see [Open-Questions.md](Open-Questions.md) Q08)
- [ ] plate-auth `0.0.1` validation tag has been published and you can resolve it from Gitea
- [ ] All Sprint 14.6 work merged to `main` (clean baseline)
- [ ] Database backup taken
- [ ] Sparkboard has **already** consumed plate-auth `0.0.1` successfully (proves the library works against a real consumer)
- [ ] Feature branch: `feature/plate-auth-migration`
If any of these is false, **stop** and resolve first.
---
## 3. Migration overview
```mermaid
flowchart LR
S0[Sprint 14.6 baseline] --> A[Step A: Add deps]
A --> B[Step B: Delete moved backend classes]
B --> C[Step C: Rename config keys]
C --> D[Step D: Add SPI implementations]
D --> E[Step E: Flyway baseline rows]
E --> F[Step F: Frontend swap]
F --> G[Step G: Run E2E suite]
G -->|green| H[Step H: Merge + delete dead code]
G -->|red| R[Rollback]
```
Estimated effort: **12 days** for an experienced maintainer assuming the library works as advertised. Most time is verification, not coding.
---
## 4. Step A — Add dependencies
### 4.1 Backend `pom.xml`
```xml
<dependency>
<groupId>de.platesoft</groupId>
<artifactId>plate-auth-starter</artifactId>
<version>0.1.0</version>
</dependency>
```
Add Gitea Maven repo + credentials if not already present (see [Integration-Guide.md § 3.1](Integration-Guide.md)).
### 4.2 Frontend `package.json`
```bash
cd frontend
pnpm add @platesoft/auth@0.1.0
```
`.npmrc` must contain the `@platesoft:` scope mapping.
### 4.3 Verify build still passes
```bash
cd backend && mvn clean compile
cd frontend && pnpm install
```
No code changes yet — both halves should still build cleanly with the new deps idle alongside the existing in-tree code.
---
## 5. Step B — Delete moved backend classes
These classes are now provided by `plate-auth-starter`. Delete the InspectFlow-local copy after verifying the package and class name match exactly.
### 5.1 Classes to delete
| InspectFlow path | Replaced by |
|------------------|-------------|
| [`backend/src/main/java/de/platesoft/inspectflow/service/JwtService.java`](backend/src/main/java/de/platesoft/inspectflow/service/JwtService.java:1) | `de.platesoft.auth.service.JwtService` |
| [`backend/src/main/java/de/platesoft/inspectflow/filter/JwtAuthenticationFilter.java`](backend/src/main/java/de/platesoft/inspectflow/filter/JwtAuthenticationFilter.java:1) | `de.platesoft.auth.filter.JwtAuthenticationFilter` |
| [`backend/src/main/java/de/platesoft/inspectflow/config/SecurityConfig.java`](backend/src/main/java/de/platesoft/inspectflow/config/SecurityConfig.java:1) | Auto-config in `PlateAuthAutoConfiguration` (see § 5.4 below) |
| `…/inspectflow/service/ExchangeService.java` | `de.platesoft.auth.service.ExchangeService` |
| `…/inspectflow/service/HmacEnvelope.java` | `de.platesoft.auth.crypto.HmacEnvelope` |
| `…/inspectflow/service/MembershipService.java` | `de.platesoft.auth.service.MembershipService` |
| `…/inspectflow/service/InvitationService.java` | `de.platesoft.auth.service.InvitationService` |
| `…/inspectflow/service/AccessRequestService.java` | `de.platesoft.auth.service.AccessRequestService` |
| `…/inspectflow/service/AuthService.java` (login/signup/reset) | `de.platesoft.auth.service.AuthService` |
| `…/inspectflow/controller/AuthController.java` | `de.platesoft.auth.controller.AuthController` |
| `…/inspectflow/controller/MembershipController.java` | `de.platesoft.auth.controller.MembershipController` |
| `…/inspectflow/controller/InvitationController.java` | `de.platesoft.auth.controller.InvitationController` |
| `…/inspectflow/controller/AccessRequestController.java` | `de.platesoft.auth.controller.AccessRequestController` |
| `…/inspectflow/controller/AdminAuditController.java` | `de.platesoft.auth.controller.AdminAuditController` |
| `…/inspectflow/entity/User.java` | `de.platesoft.auth.entity.User` |
| `…/inspectflow/entity/UserIdentity.java` | `de.platesoft.auth.entity.UserIdentity` |
| `…/inspectflow/entity/Membership.java` | `de.platesoft.auth.entity.Membership` (with polymorphic FK — see § 6.3) |
| `…/inspectflow/entity/Invitation.java` | `de.platesoft.auth.entity.Invitation` |
| `…/inspectflow/entity/AccessRequest.java` | `de.platesoft.auth.entity.AccessRequest` |
| `…/inspectflow/entity/LoginEvent.java` | `de.platesoft.auth.entity.LoginEvent` |
| `…/inspectflow/repository/UserRepository.java` | `de.platesoft.auth.repository.UserRepository` |
| `…/inspectflow/repository/MembershipRepository.java` | `de.platesoft.auth.repository.MembershipRepository` |
| `…/inspectflow/repository/InvitationRepository.java` | `de.platesoft.auth.repository.InvitationRepository` |
| `…/inspectflow/repository/AccessRequestRepository.java` | `de.platesoft.auth.repository.AccessRequestRepository` |
| `…/inspectflow/repository/LoginEventRepository.java` | `de.platesoft.auth.repository.LoginEventRepository` |
| `…/inspectflow/dto/...` (auth-related) | `de.platesoft.auth.dto.*` |
### 5.2 Classes to KEEP (T3 — InspectFlow-specific)
These stay in InspectFlow because they encode the Company domain or onboarding logic:
| Path | Reason |
|------|--------|
| `de.platesoft.inspectflow.entity.Company` | Concrete org table — InspectFlow's domain |
| `de.platesoft.inspectflow.repository.CompanyRepository` | Same |
| `de.platesoft.inspectflow.service.CompanyService` | Same |
| `de.platesoft.inspectflow.service.OrgContextResolver` | Adapter — refactor to implement plate-auth's `OrgValidator` SPI |
| `de.platesoft.inspectflow.service.OnboardingService` | App-specific onboarding (create default machines, etc.) |
### 5.3 Find-and-fix imports
After deleting the classes in § 5.1, the rest of InspectFlow that references them (controllers, services that aren't auth-related) will fail to compile. Rewrite imports:
```bash
# Find all consumers of the deleted packages
cd backend
grep -rln "de.platesoft.inspectflow.service.JwtService" src/main/java/
grep -rln "de.platesoft.inspectflow.entity.User" src/main/java/
# … repeat per deleted class
# Replace package prefix
find src/main/java -name "*.java" -exec sed -i '' \
-e 's|de.platesoft.inspectflow.service.JwtService|de.platesoft.auth.service.JwtService|g' \
-e 's|de.platesoft.inspectflow.entity.User|de.platesoft.auth.entity.User|g' \
-e 's|de.platesoft.inspectflow.entity.Membership|de.platesoft.auth.entity.Membership|g' \
{} +
```
> **Take small steps.** Replace one package at a time and recompile. Big-bang sed will leave you with a sea of red.
### 5.4 SecurityConfig
The starter ships its own `SecurityFilterChain` bean named `plateAuthSecurityChain`. InspectFlow's current [`SecurityConfig.java`](backend/src/main/java/de/platesoft/inspectflow/config/SecurityConfig.java:21) declares its own beans for `passwordEncoder`, `authenticationManager`, and a `filterChain`. Three of these are auto-provided by the starter:
- `passwordEncoder``PlateAuthAutoConfiguration` provides `BCryptPasswordEncoder` with cost 10. Delete InspectFlow's.
- `authenticationManager` → same. Delete InspectFlow's.
- `filterChain` → starter's `plateAuthSecurityChain` covers `/auth/**` + JWT for the rest. **Keep** an InspectFlow-specific chain for paths that need custom rules (e.g. `/api/admin/**` requires `hasRole('ADMIN')`).
```java
// InspectFlow keeps a focused chain:
@Bean
@Order(0) // before plateAuthSecurityChain
public SecurityFilterChain inspectflowAdminChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/admin/**")
.authorizeHttpRequests(a -> a.anyRequest().hasRole("ADMIN"))
.csrf(c -> c.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}
```
---
## 6. Step C — Rename config keys
InspectFlow has historically scattered auth config across `inspectflow.*`, `jwt.*`, and `nextauth.*`. plate-auth consolidates everything under `plate.auth.*`.
### 6.1 Backend: `application.yml`
Before:
```yaml
jwt:
secret: ${JWT_SECRET}
access-expiration-minutes: 15
refresh-expiration-days: 30
nextauth:
exchange-secret: ${EXCHANGE_SECRET}
exchange-max-age-seconds: 60
inspectflow:
registration-enabled: false
cors:
allowed-origins: [...]
```
After:
```yaml
plate:
auth:
jwt:
secret: ${JWT_SECRET}
access-expiration: PT15M
refresh-expiration: P30D
issuer: inspectflow
exchange:
secret: ${EXCHANGE_SECRET}
max-age: PT60S
nonce-ttl: PT5M
registration:
enabled: false
cors:
allowed-origins: [...]
providers:
google:
enabled: true
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
```
**Format changes:**
- Numeric `*-minutes` / `*-seconds` / `*-days` → ISO-8601 `Duration` format (`PT15M`, `PT60S`, `P30D`). Spring Boot parses both `15m` and `PT15M`, but the starter validates the ISO form.
### 6.2 Frontend: `.env.local` / `.env.production`
| Old | New |
|-----|-----|
| `JWT_SECRET` | (unused — backend only now) |
| `EXCHANGE_SECRET` | `PLATE_AUTH_EXCHANGE_SECRET` |
| `BACKEND_URL` | `PLATE_AUTH_BACKEND_URL` |
| `NEXTAUTH_SECRET` | `NEXTAUTH_SECRET` (unchanged) |
| `NEXTAUTH_URL` | `NEXTAUTH_URL` (unchanged) |
| `GOOGLE_CLIENT_ID` | `GOOGLE_CLIENT_ID` (unchanged) |
| `GOOGLE_CLIENT_SECRET` | `GOOGLE_CLIENT_SECRET` (unchanged) |
Update `docker-compose.yml`, TrueNAS config, `.env.example`, and `.env.prod.example`.
### 6.3 Membership entity column changes
Old InspectFlow `Membership`:
```java
@Entity
public class Membership {
@ManyToOne private User user;
@ManyToOne private Company company; // ← direct FK
private Role role;
}
```
New plate-auth `Membership`:
```java
@Entity
public class Membership {
@ManyToOne private User user;
private String orgType; // "COMPANY"
private Long orgId; // company.id
private Role role;
}
```
**Migration:** the existing `memberships.company_id` column must be rewritten to `(org_type='COMPANY', org_id=company_id)`. Handled by the Flyway data-migration step in § 7.3.
---
## 7. Step D — Add SPI implementations
InspectFlow plugs back into plate-auth via SPI beans. Add a new config class:
```java
// backend/src/main/java/de/platesoft/inspectflow/auth/PlateAuthBindings.java
@Configuration
public class PlateAuthBindings {
@Bean
public OrgValidator orgValidator(CompanyRepository companies) {
return (orgType, orgId) ->
"COMPANY".equals(orgType) && companies.existsById(orgId);
}
@Bean
public OrgDisplayNameResolver orgDisplayNameResolver(CompanyRepository companies) {
return (orgType, orgId) -> companies.findById(orgId)
.map(Company::getName)
.orElse("Unbekannt");
}
@Bean
public InvitationMailer invitationMailer(InspectFlowMailService mail) {
return mail::sendInvitation; // delegate to existing mailer
}
@Bean
public AccessRequestMailer accessRequestMailer(InspectFlowMailService mail) {
return mail::sendAccessRequestNotification;
}
@Bean
public OnboardingHook onboardingHook(OnboardingService onboarding) {
return onboarding::onUserCreated; // delegate to existing onboarding
}
}
```
This replaces all five default SPI beans. The starter logs "User-provided OrgValidator detected, default disabled" at INFO on boot — verify this in your first run.
---
## 8. Step E — Flyway baseline rows
InspectFlow's existing `flyway_schema_history` contains rows for `V26..V31` (your existing auth migrations). plate-auth ships its own migrations `V1..V5` and runs them against `flyway_schema_history_auth` (a separate table). The schema already has the tables — running `V1..V5` would fail.
### 8.1 Migration approach
Add a new InspectFlow-local migration `V32__C_seed_plate_auth_history.sql`:
```sql
-- Pre-populate flyway_schema_history_auth so plate-auth Flyway sees the tables as already migrated.
-- The schema was created by InspectFlow's V26..V31; plate-auth's V1..V5 would otherwise re-apply them.
CREATE TABLE IF NOT EXISTS flyway_schema_history_auth (
installed_rank int NOT NULL,
version varchar(50),
description varchar(200) NOT NULL,
type varchar(20) NOT NULL,
script varchar(1000) NOT NULL,
checksum int,
installed_by varchar(100) NOT NULL,
installed_on timestamp NOT NULL DEFAULT now(),
execution_time int NOT NULL,
success boolean NOT NULL,
PRIMARY KEY (installed_rank)
);
INSERT INTO flyway_schema_history_auth
(installed_rank, version, description, type, script, checksum, installed_by, execution_time, success)
VALUES
(1, '1', 'Create users', 'SQL', 'V1__C_create_users.sql', NULL, 'inspectflow-migration', 0, true),
(2, '2', 'Create user_identities','SQL', 'V2__C_create_user_identities.sql', NULL, 'inspectflow-migration', 0, true),
(3, '3', 'Create memberships', 'SQL', 'V3__C_create_memberships.sql', NULL, 'inspectflow-migration', 0, true),
(4, '4', 'Create invitations', 'SQL', 'V4__C_create_invitations.sql', NULL, 'inspectflow-migration', 0, true),
(5, '5', 'Create login_events', 'SQL', 'V5__C_create_login_events.sql', NULL, 'inspectflow-migration', 0, true)
ON CONFLICT DO NOTHING;
```
> **Checksum caveat.** Setting `checksum = NULL` makes Flyway skip checksum validation. If you want strict mode, run plate-auth on a scratch DB once, copy the checksums from its `flyway_schema_history_auth`, and paste them here.
### 8.2 Data migration: memberships.company_id → (org_type, org_id)
Same migration file or separate `V33__C_rewrite_memberships_to_polymorphic.sql`:
```sql
-- Add new columns
ALTER TABLE memberships ADD COLUMN IF NOT EXISTS org_type varchar(64);
ALTER TABLE memberships ADD COLUMN IF NOT EXISTS org_id bigint;
-- Backfill from existing company_id
UPDATE memberships SET org_type = 'COMPANY', org_id = company_id WHERE company_id IS NOT NULL;
-- Index + non-null constraint
CREATE INDEX IF NOT EXISTS idx_memberships_org ON memberships(org_type, org_id);
ALTER TABLE memberships ALTER COLUMN org_type SET NOT NULL;
ALTER TABLE memberships ALTER COLUMN org_id SET NOT NULL;
-- Drop the old FK column (after verification!)
-- ALTER TABLE memberships DROP CONSTRAINT memberships_company_id_fkey;
-- ALTER TABLE memberships DROP COLUMN company_id;
```
> **Caution.** Do **not** drop `company_id` in the same migration. Keep it as a shadow column until the next minor release and verify no code still reads it.
---
## 9. Step F — Frontend swap
### 9.1 Delete files
- [`frontend/auth.ts`](frontend/auth.ts:1) → rewrite (5 lines, see § 9.3)
- `frontend/app/api/auth/[...nextauth]/route.ts` → keep, no change needed
- `frontend/app/api/[...path]/route.ts` → rewrite (3 lines, see § 9.4)
- `frontend/lib/hmac.ts` (if exists) → delete; replaced by `@platesoft/auth/edge`
- `frontend/lib/auth-config.ts` (if exists) → delete; replaced by `createAuthConfig` factory
### 9.2 Keep files
- `frontend/middleware.ts` — InspectFlow keeps its own (custom redirect logic for unauth users)
- `frontend/components/login-form.tsx` etc. if you have custom UI — leave them, ignore `@platesoft/auth/react`
### 9.3 New `frontend/auth.ts`
```typescript
import { createAuthConfig } from "@platesoft/auth/server";
import NextAuth from "next-auth";
export const { handlers, auth, signIn, signOut } = NextAuth(
createAuthConfig({
backendUrl: process.env.PLATE_AUTH_BACKEND_URL!,
exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!,
nextAuthSecret: process.env.NEXTAUTH_SECRET!,
providers: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
pages: {
signIn: "/login",
},
})
);
```
### 9.4 New `frontend/app/api/[...path]/route.ts`
```typescript
import { createProxyHandlers } from "@platesoft/auth/edge";
export const runtime = "edge";
export const { GET, POST, PUT, PATCH, DELETE } = createProxyHandlers({
backendUrl: process.env.PLATE_AUTH_BACKEND_URL!,
exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!,
});
```
That's it. Anything else InspectFlow needs (custom error pages, custom callbacks) is wired into NextAuth options that `createAuthConfig` exposes as overridable.
---
## 10. Step G — Run the E2E suite
```bash
cd frontend
pnpm exec playwright install --with-deps # if not already
pnpm exec playwright test
```
All 30+ scenarios in [`frontend/e2e/`](frontend/e2e/) must pass green. The critical ones for this migration:
- `auth-flow.unauth.spec.ts` — anonymous redirect, login form
- `companies.spec.ts` — membership reads work
- `admin-archive.spec.ts`, `admin-timeline.spec.ts`, `admin-migration.spec.ts` — admin role gate
- `command-palette.spec.ts` — JWT-protected ops
- Any "login as admin / login as user" setup script in `auth.setup.ts`
If any test fails, **stop**. Diagnose via:
1. Check backend logs for `OrgValidator` / SPI warnings.
2. Check `login_events` table for the failing user.
3. Diff the failing request between old + new code paths (proxy headers, exchange envelope shape).
---
## 11. Step H — Merge and clean up
After all tests pass:
1. Squash-merge `feature/plate-auth-migration` to `main`.
2. Tag InspectFlow as `vX.Y+1.0` (minor bump — internal refactor with config breaking change for ops).
3. Update `docs/` — add a section in [`docs/SPRINT-14-OVERVIEW.md`](docs/SPRINT-14-OVERVIEW.md) noting the carve-out + library version.
4. **One sprint later**, drop `memberships.company_id` column in a follow-up migration (give yourself time to revert if production reveals issues).
5. Subscribe to plate-auth `0.2.0` for the v0.2 feature wave (refresh rotation, magic-link, multi-key secrets).
---
## 12. Rollback procedure
If the migration goes sideways:
### Code rollback
```bash
git checkout main
git revert feature/plate-auth-migration # or just deploy the previous tag
```
### Database rollback
The data migration in § 8.2 is **additive** (`memberships.org_type`, `org_id` are new columns). The old `company_id` column is preserved. Rolling back the code means the old `Membership` entity reads `company_id` again and ignores the new columns — safe.
The Flyway baseline rows in `flyway_schema_history_auth` are harmless if the code is reverted — the table simply exists with 5 rows and no consumer.
### Secret rollback
If you have already rotated `PLATE_AUTH_EXCHANGE_SECRET` and `EXCHANGE_SECRET` was different in the old code, restore the old env vars when reverting the code.
### Total downtime
The migration is designed for **zero-downtime deploy**:
- New columns added before code switch (additive).
- New code reads new columns; old code reads old.
- Stop the old replica, start the new — one request is in flight, at worst.
If you cannot do zero-downtime, plan a 5-minute maintenance window.
---
## 13. What changes for end-users
**Nothing.** Logins, sessions, sign-ups, invitations, access requests, admin views — all behave identically. The change is internal: same APIs, same UX, different code path.
If a user reports a regression, treat it as a bug — file an InspectFlow ticket **and** a plate-auth ticket (since the regression is likely in the library, not in InspectFlow's residual code).
---
## 14. Common pitfalls (migration-specific)
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Boot fails with `Field 'orgType' of Membership is null` | Forgot the data migration in § 8.2 | Run V33 manually |
| Flyway: "checksum mismatch on V1" | Baseline rows in § 8.1 have wrong checksums | Set `flyway.validate-on-migrate=false` temporarily, or recompute checksums |
| `OrgValidator` always returns false | Wrong `orgType` literal — case sensitive (`"COMPANY"` not `"company"`) | Check § 7 SPI binding |
| Frontend cannot reach backend → 502 | `BACKEND_URL` rename to `PLATE_AUTH_BACKEND_URL` not propagated to all envs | Grep `BACKEND_URL` across repo + deploy configs |
| E2E `companies.spec.ts` fails | `OrgDisplayNameResolver` SPI not registered → display name shows `Unbekannt` | Add bean in § 7 |
| Admin chain order wrong → users get 403 on `/api/admin/...` | `@Order(0)` missing on `inspectflowAdminChain` | Add explicit order; default starter chain is `@Order(100)` |
---
## 15. Sign-off checklist
Before declaring migration complete:
- [ ] All Playwright E2E tests pass
- [ ] Manual smoke test: log in via Google, password, accept invite, request access, approve as admin
- [ ] `/admin/audit` shows `login_events` rows from both old and new code paths (timestamps span the migration)
- [ ] No `WARN`/`ERROR` log entries from `de.platesoft.auth.*` in first 24h post-deploy
- [ ] Database row counts before/after migration match (users, memberships, invitations, access_requests, login_events)
- [ ] `memberships.org_type` is non-null on all rows (use SQL audit)
- [ ] Deleted backend classes (§ 5.1) are actually deleted (no zombies)
- [ ] Deleted frontend files (§ 9.1) are actually deleted
---
## 16. Cross-references
- [Home.md](Home.md)
- [Vision.md](Vision.md)
- [Architecture.md](Architecture.md)
- [Roadmap.md](Roadmap.md)
- [Sprint-0-Assessment.md](Sprint-0-Assessment.md)
- [Sprint-0-Plan.md](Sprint-0-Plan.md)
- [Sprint-0-Testplan.md](Sprint-0-Testplan.md)
- [Open-Questions.md](Open-Questions.md)
- [Integration-Guide.md](Integration-Guide.md)
---
**End of Migration-InspectFlow.md (v1).**