diff --git a/Sprint-0-Plan.md b/Sprint-0-Plan.md index 9904de4..9e7c821 100644 --- a/Sprint-0-Plan.md +++ b/Sprint-0-Plan.md @@ -159,3 +159,239 @@ See § 8 for step-by-step. --- *Plan continues in the next section — backend extraction step-by-step.* + +--- + +## 4. Backend extraction — step-by-step + +### 4.1 W1 — Repo scaffolding + +**Goal:** Create the directory layout + skeleton `pom.xml` files + npm workspace structure. + +**Steps:** + +1. **W1-1** In the `plate-auth` repo root, create: + ``` + plate-auth/ + ├── pom.xml # parent POM, packaging=pom + ├── plate-auth-starter/ + │ ├── pom.xml # the published artifact + │ └── src/{main,test}/{java,resources}/ + ├── packages/ + │ └── auth/ + │ ├── package.json # @platesoft/auth + │ └── src/ + ├── package.json # workspace root + ├── pnpm-workspace.yaml # if pnpm; npm/yarn equivalent works + ├── .gitea/workflows/ # Gitea Actions + ├── CHANGELOG.md + └── README.md + ``` +2. **W1-2** Parent `pom.xml`: `de.platesoft`, `plate-auth-parent`, + `${revision}` (CI-injected), `pom`, + `plate-auth-starter`. Inherit from `spring-boot-starter-parent:4.1.0`. +3. **W1-3** `plate-auth-starter/pom.xml`: `plate-auth-starter`, + deps copied from InspectFlow `backend/pom.xml` minus app-specific (no ONNX, no openpdf, no pdfbox): + ``` + web, data-jpa, security, validation, actuator, mail, + hibernate-envers, postgresql, flyway, + jjwt-api/impl/jackson, + mapstruct, lombok, logstash-logback, + h2 (test scope), testcontainers (test scope) + ``` +4. **W1-4** Root `package.json` + `pnpm-workspace.yaml` configured to pick up `packages/*`. +5. **W1-5** `packages/auth/package.json`: `"name": "@platesoft/auth"`, `"version": "0.1.0"`, + peerDeps on `next@>=15`, `next-auth@^5.0.0-beta`, `react@>=19`. +6. **W1-6** Add `.gitignore`, `.editorconfig`, basic `README.md` linking to the wiki. +7. **W1-7** Commit + push to `main`. CI must pass on empty modules (just compiles). + +**Done when:** `mvn -B verify` and `pnpm -r build` both succeed on a fresh clone. + +### 4.2 W2-A — Backend: copy + rename classes + +**Goal:** Mechanical move from `de.platesoft.inspectflow.*` → `de.platesoft.auth.*` for every auth class. + +**Steps (per source class — repeat for the inventory in Sprint-0-Assessment § 1.1):** + +1. **W2-1** Copy class file from `inspectflow/backend/src/main/java/de/platesoft/inspectflow//.java` + to `plate-auth/plate-auth-starter/src/main/java/de/platesoft/auth//.java`. +2. **W2-2** Update `package` declaration: `package de.platesoft.auth.;`. +3. **W2-3** Update **internal imports** (i.e. imports of other classes we are also moving) to the new + package. Use search-replace, but verify each file compiles independently after. +4. **W2-4** **Leave behind**: any reference to `Company`, `OnboardingService`, `TenantAutoMapService` — + replace with an SPI call (see W4 below). DO NOT move these classes. + +**Class-by-class checklist:** + +- [ ] `filter/JwtAuthenticationFilter.java` — direct move, no rewrites +- [ ] `filter/OrgContextResolver.java` — direct move +- [ ] `config/SecurityConfig.java` — direct move, **but**: replace InspectFlow-specific permit-list + (`/api/companies/*/public-info`) with config-driven list from `PlateAuthProperties.cors.additionalPermitPaths` +- [ ] `service/JwtService.java` — direct move; rename `@Value("${jwt.secret}")` → `@Value("${plate.auth.jwt.secret}")` +- [ ] `service/OAuthService.java` — move + **swap** `tenantAutoMapService.maybeAutoMap(...)` call for + `onboardingHook.onFirstSignIn(...)` (SPI) +- [ ] `service/ExchangeService.java` — direct move +- [ ] `service/MembershipService.java` — move + **swap** `companyRepository.findById(...)` for + `orgValidator.exists(orgType, orgId)` / `orgDisplayNameResolver.displayName(...)` calls +- [ ] `service/InvitationService.java` — direct move (Sprint 14.3 already abstracted the mailer) +- [ ] `service/AccessRequestService.java` — direct move +- [ ] `service/LoginEventService.java` — direct move +- [ ] `service/AuthService.java` — direct move (password login/register) +- [ ] `controller/AuthController.java` — direct move +- [ ] `controller/OAuthController.java` — direct move +- [ ] `controller/InvitationController.java` — direct move +- [ ] `controller/AccessRequestController.java` — direct move +- [ ] `controller/AdminAuditController.java` — direct move +- [ ] `entity/User.java` — direct move +- [ ] `entity/UserIdentity.java` — direct move +- [ ] `entity/Membership.java` — direct move +- [ ] `entity/Invitation.java` — direct move +- [ ] `entity/AccessRequest.java` — direct move +- [ ] `entity/LoginEvent.java` — direct move +- [ ] `entity/RevInfo.java` (Envers revinfo with actor) — direct move +- [ ] `repository/*` — all auth-related repositories — direct move +- [ ] `dto/request/*` and `dto/response/*` — direct move +- [ ] `enums/*` — direct move (Role, OrgType, MembershipRole, MembershipStatus, InvitationStatus, + AccessRequestStatus, LoginProvider) + +**Done when:** `plate-auth-starter` compiles in isolation. No references to `de.platesoft.inspectflow.*` +remain in moved classes. + +### 4.3 W2-B — Configuration namespace + +**Goal:** Consolidate all `@Value("${...}")` injections into a single +`@ConfigurationProperties("plate.auth")` class. + +**Steps:** + +1. **W2-8** Create `de.platesoft.auth.PlateAuthProperties`: + ```java + @ConfigurationProperties(prefix = "plate.auth") + @Data + public class PlateAuthProperties { + private Jwt jwt = new Jwt(); + private Exchange exchange = new Exchange(); + private Registration registration = new Registration(); + private Cors cors = new Cors(); + private Providers providers = new Providers(); + + @Data public static class Jwt { + private String secret; + private Duration accessExpiration = Duration.ofMinutes(15); + private Duration refreshExpiration = Duration.ofDays(30); + private String issuer = "plate-auth"; + } + @Data public static class Exchange { + private String secret; + private Duration maxAge = Duration.ofSeconds(60); + private Duration nonceTtl = Duration.ofMinutes(5); + } + @Data public static class Registration { + private boolean enabled = false; + } + @Data public static class Cors { + private List allowedOrigins = new ArrayList<>(); + private List additionalPermitPaths = new ArrayList<>(); + } + @Data public static class Providers { + private ProviderToggle google = new ProviderToggle(true); + private ProviderToggle microsoft = new ProviderToggle(false); + private ProviderToggle emailMagicLink = new ProviderToggle(false); + } + @Data @AllArgsConstructor @NoArgsConstructor public static class ProviderToggle { + private boolean enabled; + } + } + ``` +2. **W2-9** Inject `PlateAuthProperties` (constructor injection) into every service that previously read + `@Value("${jwt....}")` etc. Replace `@Value` annotations. +3. **W2-10** Add `META-INF/spring-configuration-metadata.json` (or rely on `spring-boot-configuration-processor` + annotation processor — preferred — add it to the build). +4. **W2-11** Bean-validate critical fields: + ```java + @NotBlank @Size(min=32) private String secret; // both jwt.secret and exchange.secret + ``` + +**Done when:** No `@Value("${jwt....}")` or `@Value("${nextauth....}")` strings remain. All config flows +through `PlateAuthProperties`. App fails fast at startup if `secret` is missing or <32 chars. + +### 4.4 W2-C — Auto-configuration + +**Goal:** Make the starter "just work" when added as a dependency. + +**Steps:** + +1. **W2-12** Create `de.platesoft.auth.PlateAuthAutoConfiguration`: + ```java + @AutoConfiguration + @EnableConfigurationProperties(PlateAuthProperties.class) + @ComponentScan(basePackages = "de.platesoft.auth") + @EntityScan(basePackages = "de.platesoft.auth.entity") + @EnableJpaRepositories(basePackages = "de.platesoft.auth.repository") + @ConditionalOnProperty(prefix = "plate.auth", name = "enabled", havingValue = "true", matchIfMissing = true) + public class PlateAuthAutoConfiguration { + // Default SPI beans, ConditionalOnMissingBean — see W4 + } + ``` +2. **W2-13** Register the auto-config: + ``` + src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports + ``` + Contents: single line `de.platesoft.auth.PlateAuthAutoConfiguration`. +3. **W2-14** Verify that adding the starter to a minimal Spring Boot 4 app, setting `plate.auth.jwt.secret` + + `plate.auth.exchange.secret` env vars, and `spring.datasource.url` to a Postgres, makes the app boot + and serve `/api/auth/config`. + +**Done when:** Smoke test app (in `plate-auth-tests/`) boots and responds to `/api/auth/config`. + +### 4.5 W4 — SPI design + default implementations + +**Goal:** Five SPI interfaces with sensible defaults so consumers can override selectively. + +**Steps:** + +1. **W4-1** Create the 5 SPI interfaces under `de.platesoft.auth.spi`: + ```java + public interface OrgValidator { + boolean exists(OrgType type, UUID orgId); + } + public interface OrgDisplayNameResolver { + String displayName(OrgType type, UUID orgId); + } + public interface InvitationMailer { + void sendInvitation(Invitation invitation, String acceptUrl); + } + public interface AccessRequestMailer { + void notifyAdmins(AccessRequest request); + void notifyRequester(AccessRequest request); // on decision + } + public interface OnboardingHook { + void onFirstSignIn(User user, LoginProvider provider); + default void onSubsequentSignIn(User user, LoginProvider provider) { /* no-op */ } + } + ``` +2. **W4-2** Default implementations (annotated `@ConditionalOnMissingBean`, registered in + `PlateAuthAutoConfiguration`): + - `DefaultOrgValidator` — **does NOT exist as a default**; auto-config requires the consumer to + provide one if T2 endpoints are used. If absent, T2 endpoints fail-fast on first invocation with a + clear error: "Provide a `de.platesoft.auth.spi.OrgValidator` bean to use plate-auth multi-tenancy." + - `DefaultOrgDisplayNameResolver` — returns `type + ":" + orgId.toString()` + - `LoggingInvitationMailer` — logs the accept URL at INFO level + - `LoggingAccessRequestMailer` — logs notifications at INFO level + - `NoOpOnboardingHook` — no-op +3. **W4-3** Wire each service to its SPI dep via constructor injection: + - `MembershipService` ← `OrgValidator`, `OrgDisplayNameResolver` + - `InvitationService` ← `InvitationMailer`, `OrgDisplayNameResolver` + - `AccessRequestService` ← `AccessRequestMailer`, `OrgDisplayNameResolver` + - `OAuthService` ← `OnboardingHook` +4. **W4-4** Document each SPI with Javadoc including: + - When it is called + - What happens if the consumer doesn't provide one (which default kicks in) + - Migration: if a previous version's signature changed, link to CHANGELOG + +**Done when:** A consumer can register a single `OrgValidator` bean and have T2 fully functional. Default +mailers log instead of crashing. + +--- + +*Plan continues — frontend extraction, Flyway, publishing.*