plan(s0): chunk 2 - backend extraction step-by-step (W1, W2, W4)

Patrick Plate
2026-06-24 14:18:45 +02:00
parent c6ed37e6e0
commit 7b2a93f542
+236
@@ -159,3 +159,239 @@ See § 8 for step-by-step.
--- ---
*Plan continues in the next section — backend extraction 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`: `<groupId>de.platesoft</groupId>`, `<artifactId>plate-auth-parent</artifactId>`,
`<version>${revision}</version>` (CI-injected), `<packaging>pom</packaging>`,
`<modules><module>plate-auth-starter</module></modules>`. Inherit from `spring-boot-starter-parent:4.1.0`.
3. **W1-3** `plate-auth-starter/pom.xml`: `<artifactId>plate-auth-starter</artifactId>`,
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/<pkg>/<X>.java`
to `plate-auth/plate-auth-starter/src/main/java/de/platesoft/auth/<pkg>/<X>.java`.
2. **W2-2** Update `package` declaration: `package de.platesoft.auth.<pkg>;`.
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<String> allowedOrigins = new ArrayList<>();
private List<String> 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.*