feat: migrate from username to email-only authentication #270

Closed
opened 2026-04-18 15:29:36 +02:00 by marcel · 8 comments
Owner

Email-Only Authentication Migration

Context

Currently users log in with a username. The goal is to replace username with email as the sole login identifier — simpler for family members who don't need to remember a separate handle.

Risk: Existing Users Without Email

The V44 migration sets email NOT NULL. It will fail if any users row has a null email. The migration should include a pre-check DO block that aborts with a clear message if null emails exist, forcing an admin cleanup first.


What Needs to Change

Database — V44 migration

-- Abort if any user has no email (run before ALTER)
DO $$
BEGIN
  IF EXISTS (SELECT 1 FROM users WHERE email IS NULL) THEN
    RAISE EXCEPTION 'Migration aborted: some users have no email address. Set emails for all users before running this migration.';
  END IF;
END $$;

ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users DROP COLUMN username;
-- UNIQUE constraint on email already exists (V7)

Backend

  1. AppUser entity — remove username field; email becomes the identity field
  2. UserRepository — replace findByUsername(String username) with findByEmail(String email)
  3. CustomUserDetailsServiceloadUserByUsername(String email) (interface method name is fixed by Spring Security); query by email; build the User object using email as the principal name
  4. SecurityConfig — add .usernameParameter("email") to form login config; HTTP Basic is unaffected (Spring passes whatever string is authenticated)
  5. CreateUserRequest DTO — remove username, make email required/non-null
  6. UserService — remove username handling from createUser() and updateFromRequest(); ensure email uniqueness is enforced via DataIntegrityViolationException catch (UNIQUE constraint already exists)

Frontend

  1. /login page — change input name="username"name="email"; update credential encoding: btoa("email:password") instead of btoa("username:password")
  2. Admin user create/edit forms — remove username field; email becomes a required field
  3. Any user.username display — replace with user.email or ${user.firstName} ${user.lastName} as appropriate

Registration (issue #269)

  1. RegisterRequest DTO — no username field; email is the login credential
  2. /register form — remove username input; email pre-filled from invite token becomes required

Verification

  1. Set emails for all existing users via admin UI
  2. Run migration: ./mvnw flyway:migrate — verify it succeeds
  3. Attempt migration with a user missing an email → verify abort with clear message
  4. Log in via /login with email + password → verify success
  5. Verify HTTP Basic auth still works: curl -u user@example.com:password /api/users/me
  6. Create a new user via admin UI — no username field present
  7. Run backend tests: cd backend && ./mvnw test
# Email-Only Authentication Migration ## Context Currently users log in with a username. The goal is to replace username with email as the sole login identifier — simpler for family members who don't need to remember a separate handle. ## Risk: Existing Users Without Email The V44 migration sets `email NOT NULL`. It will fail if any `users` row has a null email. The migration should include a pre-check `DO` block that aborts with a clear message if null emails exist, forcing an admin cleanup first. --- ## What Needs to Change ### Database — V44 migration ```sql -- Abort if any user has no email (run before ALTER) DO $$ BEGIN IF EXISTS (SELECT 1 FROM users WHERE email IS NULL) THEN RAISE EXCEPTION 'Migration aborted: some users have no email address. Set emails for all users before running this migration.'; END IF; END $$; ALTER TABLE users ALTER COLUMN email SET NOT NULL; ALTER TABLE users DROP COLUMN username; -- UNIQUE constraint on email already exists (V7) ``` ### Backend 1. **`AppUser` entity** — remove `username` field; email becomes the identity field 2. **`UserRepository`** — replace `findByUsername(String username)` with `findByEmail(String email)` 3. **`CustomUserDetailsService`** — `loadUserByUsername(String email)` (interface method name is fixed by Spring Security); query by email; build the `User` object using email as the principal name 4. **`SecurityConfig`** — add `.usernameParameter("email")` to form login config; HTTP Basic is unaffected (Spring passes whatever string is authenticated) 5. **`CreateUserRequest` DTO** — remove `username`, make `email` required/non-null 6. **`UserService`** — remove username handling from `createUser()` and `updateFromRequest()`; ensure email uniqueness is enforced via `DataIntegrityViolationException` catch (UNIQUE constraint already exists) ### Frontend 7. **`/login` page** — change input `name="username"` → `name="email"`; update credential encoding: `btoa("email:password")` instead of `btoa("username:password")` 8. **Admin user create/edit forms** — remove username field; email becomes a required field 9. **Any `user.username` display** — replace with `user.email` or `${user.firstName} ${user.lastName}` as appropriate ### Registration (issue #269) 10. **`RegisterRequest` DTO** — no `username` field; email is the login credential 11. **`/register` form** — remove username input; email pre-filled from invite token becomes required --- ## Verification 1. Set emails for all existing users via admin UI 2. Run migration: `./mvnw flyway:migrate` — verify it succeeds 3. Attempt migration with a user missing an email → verify abort with clear message 4. Log in via `/login` with email + password → verify success 5. Verify HTTP Basic auth still works: `curl -u user@example.com:password /api/users/me` 6. Create a new user via admin UI — no username field present 7. Run backend tests: `cd backend && ./mvnw test`
marcel added the featureuser labels 2026-04-18 15:29:40 +02:00
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • CustomUserDetailsServiceTest has 5 tests that mock findByUsername() and assert details.getUsername(). Every single one needs rewriting to use findByEmail() and assert the email as the principal name.
  • UserController calls findByUsername(authentication.getName()) in 3 endpoints (GET /me, PUT /me, POST /me/password). After migration authentication.getName() returns the email — these must switch to findByEmail().
  • AppUserRepository.searchByNameOrUsername — the JPQL query references LOWER(u.username). That column is being dropped; this query will throw at runtime if not updated before V44 ships.
  • UserService.createUserOrUpdate() — the upsert key is findByUsername(). Switching to findByEmail() as the upsert key changes upsert semantics and needs an explicit test covering "update an existing user by email."
  • AppUser.updateFromRequest() still sets this.username — that line must be removed.
  • NotificationRepositoryTest creates users with .username("userA") and no .email(). Once V44 enforces NOT NULL, any Testcontainers integration test that saves these users will blow up.

Recommendations

  • Write failing CustomUserDetailsServiceTest tests before touching CustomUserDetailsService — TDD red/green keeps the auth layer honest.
  • Fix NotificationRepositoryTest and any other integration test that creates bare AppUser builders in the same PR as the migration — do not let them slip to a follow-up.
  • searchByNameOrUsername should pivot to searching email, first_name, last_name — update the JPQL and its test in this issue's scope, not a separate one.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - `CustomUserDetailsServiceTest` has 5 tests that mock `findByUsername()` and assert `details.getUsername()`. Every single one needs rewriting to use `findByEmail()` and assert the email as the principal name. - `UserController` calls `findByUsername(authentication.getName())` in 3 endpoints (`GET /me`, `PUT /me`, `POST /me/password`). After migration `authentication.getName()` returns the email — these must switch to `findByEmail()`. - `AppUserRepository.searchByNameOrUsername` — the JPQL query references `LOWER(u.username)`. That column is being dropped; this query will throw at runtime if not updated before V44 ships. - `UserService.createUserOrUpdate()` — the upsert key is `findByUsername()`. Switching to `findByEmail()` as the upsert key changes upsert semantics and needs an explicit test covering "update an existing user by email." - `AppUser.updateFromRequest()` still sets `this.username` — that line must be removed. - `NotificationRepositoryTest` creates users with `.username("userA")` and no `.email()`. Once V44 enforces `NOT NULL`, any Testcontainers integration test that saves these users will blow up. ### Recommendations - Write failing `CustomUserDetailsServiceTest` tests **before** touching `CustomUserDetailsService` — TDD red/green keeps the auth layer honest. - Fix `NotificationRepositoryTest` and any other integration test that creates bare `AppUser` builders **in the same PR** as the migration — do not let them slip to a follow-up. - `searchByNameOrUsername` should pivot to searching `email`, `first_name`, `last_name` — update the JPQL and its test in this issue's scope, not a separate one.
Author
Owner

🏗️ Markus Keller — Senior Application Architect

Observations

  • Migration sequencing is critical: #270 (auth identity change) must merge and deploy before #269 (registration). The registration feature's RegisterRequest has no username field — that is only safe once the auth layer no longer expects one. Implementing them in the wrong order means registration ships with a broken auth flow.
  • The V44 pre-check DO block is exactly right — consistent with the project's constraint-first approach (see V30, V18). Do not weaken it.
  • updateProfile() currently allows setting email to null (user.setEmail(null)). Once V44 enforces NOT NULL at the DB layer, this produces a constraint violation at persist time instead of a clean DomainException. That gap must be closed before the migration runs — add an explicit null/blank check in UserService and throw DomainException.badRequest(...).
  • searchByNameOrUsername is a cross-concern: the JPQL references the username column being dropped. Updating it belongs in this issue's scope.

Recommendations

  • Label this issue as a prerequisite blocker for #269.
  • Add a @NotBlank / @Email validation on email in CreateUserRequest now — email is becoming the identity field, it should be validated at the DTO layer, not just the DB layer.
  • The updateProfile() null-email gap is the only pre-migration safety fix that can't be caught by the DO block — make it explicit in the implementation checklist.
## 🏗️ Markus Keller — Senior Application Architect ### Observations - **Migration sequencing is critical**: #270 (auth identity change) must merge and deploy **before** #269 (registration). The registration feature's `RegisterRequest` has no `username` field — that is only safe once the auth layer no longer expects one. Implementing them in the wrong order means registration ships with a broken auth flow. - The V44 pre-check `DO` block is exactly right — consistent with the project's constraint-first approach (see V30, V18). Do not weaken it. - `updateProfile()` currently allows setting `email` to `null` (`user.setEmail(null)`). Once V44 enforces `NOT NULL` at the DB layer, this produces a constraint violation at persist time instead of a clean `DomainException`. That gap must be closed **before** the migration runs — add an explicit null/blank check in `UserService` and throw `DomainException.badRequest(...)`. - `searchByNameOrUsername` is a cross-concern: the JPQL references the `username` column being dropped. Updating it belongs in this issue's scope. ### Recommendations - Label this issue as a prerequisite blocker for #269. - Add a `@NotBlank` / `@Email` validation on email in `CreateUserRequest` now — email is becoming the identity field, it should be validated at the DTO layer, not just the DB layer. - The `updateProfile()` null-email gap is the only pre-migration safety fix that can't be caught by the `DO` block — make it explicit in the implementation checklist.
Author
Owner

🔒 Nora Steiner — Security Engineer

Observations

  • Colon-in-email with HTTP Basic: RFC 7617 defines Basic auth credentials as userid ":" password, where userid must not contain a colon. An email like user:name@example.com would split incorrectly in btoa("email:password"). This is a real, exploitable parsing bug — not theoretical.
  • Existing session cookies: auth_token cookies store Basic base64(username:password). After V44 ships, any cached cookie encoding the old username will fail loadUserByUsername silently — the user will be redirected to login on next request. This is the correct and desired behavior, but the migration checklist should state it explicitly so it is not treated as a bug.
  • Log injection: CustomUserDetailsService logs in a warning path. Confirm the log statement uses SLF4J {} parameterized form (not string concatenation) — once it logs email addresses, concatenation would reopen a log-injection vector. Parameterized logging is fine.

Recommendations

  • Add a server-side validator that rejects email addresses containing a colon (@Pattern(regexp = "^[^:]+$")). Apply it to both CreateUserRequest and any profile update DTO. This is the only safe fix for the Basic Auth parsing issue.
  • Add "existing sessions are invalidated" to the verification checklist with an explicit test step: log in, deploy the migration, send the old cookie, verify 401 and redirect.
  • The rate-limiting in #269 (RateLimitInterceptor) should also cover the login endpoint — out of scope here, but log it as a follow-up.
## 🔒 Nora Steiner — Security Engineer ### Observations - **Colon-in-email with HTTP Basic**: RFC 7617 defines Basic auth credentials as `userid ":" password`, where `userid` must not contain a colon. An email like `user:name@example.com` would split incorrectly in `btoa("email:password")`. This is a real, exploitable parsing bug — not theoretical. - **Existing session cookies**: `auth_token` cookies store `Basic base64(username:password)`. After V44 ships, any cached cookie encoding the old username will fail `loadUserByUsername` silently — the user will be redirected to login on next request. This is the correct and desired behavior, but the migration checklist should state it explicitly so it is not treated as a bug. - **Log injection**: `CustomUserDetailsService` logs in a warning path. Confirm the log statement uses SLF4J `{}` parameterized form (not string concatenation) — once it logs email addresses, concatenation would reopen a log-injection vector. Parameterized logging is fine. ### Recommendations - Add a server-side validator that rejects email addresses containing a colon (`@Pattern(regexp = "^[^:]+$")`). Apply it to both `CreateUserRequest` and any profile update DTO. This is the only safe fix for the Basic Auth parsing issue. - Add "existing sessions are invalidated" to the verification checklist with an explicit test step: log in, deploy the migration, send the old cookie, verify 401 and redirect. - The rate-limiting in #269 (`RateLimitInterceptor`) should also cover the login endpoint — out of scope here, but log it as a follow-up.
Author
Owner

🧪 Sara Holt — QA Engineer

Observations

  • Missing acceptance criterion: The verification section has no step for "users with an old username-based cookie are gracefully rejected and redirected to login." This is the primary regression scenario.
  • updateProfile() null email: No test verifies that PUT /api/users/me with a null email body fails cleanly after V44. Without it, the constraint violation surfaces as a 500 instead of a 400.
  • NotificationRepositoryTest: Creates AppUser.builder().username("userA").password("pw").build() with no email. Once V44 runs on Testcontainers, this will throw a NOT NULL constraint violation. All integration test fixtures creating users must include .email("...").
  • UserControllerTest: Any assertion on jsonPath("$.username") will fail once the field is removed from the response. Tests must be updated to assert on email instead.
  • Admin create flow gap: No test covers the happy path of creating a user via the admin UI after migration (email required, no username field).

Recommendations

  • Add explicit AC to the issue: "Any active session cookie that encodes a username (not email) is rejected with 401 and the user is redirected to /login."
  • Add AC: "PUT /api/users/me with email: null returns 400 with a clear error message."
  • Add AC: "E2E: log in with email+password → GET /api/users/me → response contains email, no username field."
  • Fix all AppUser builder usages in Testcontainers tests in the same PR as the migration — this is not optional cleanup.
## 🧪 Sara Holt — QA Engineer ### Observations - **Missing acceptance criterion**: The verification section has no step for "users with an old username-based cookie are gracefully rejected and redirected to login." This is the primary regression scenario. - **`updateProfile()` null email**: No test verifies that `PUT /api/users/me` with a null email body fails cleanly after V44. Without it, the constraint violation surfaces as a 500 instead of a 400. - **`NotificationRepositoryTest`**: Creates `AppUser.builder().username("userA").password("pw").build()` with no email. Once V44 runs on Testcontainers, this will throw a `NOT NULL` constraint violation. All integration test fixtures creating users must include `.email("...")`. - **`UserControllerTest`**: Any assertion on `jsonPath("$.username")` will fail once the field is removed from the response. Tests must be updated to assert on `email` instead. - **Admin create flow gap**: No test covers the happy path of creating a user via the admin UI after migration (email required, no username field). ### Recommendations - Add explicit AC to the issue: _"Any active session cookie that encodes a username (not email) is rejected with 401 and the user is redirected to /login."_ - Add AC: _"PUT /api/users/me with email: null returns 400 with a clear error message."_ - Add AC: _"E2E: log in with email+password → GET /api/users/me → response contains `email`, no `username` field."_ - Fix all `AppUser` builder usages in Testcontainers tests in the same PR as the migration — this is not optional cleanup.
Author
Owner

⚙️ Tobias Wendt — DevOps Engineer

Observations

  • Zero-downtime risk: Dropping the username column means any running backend instance compiled against the old schema will fail immediately on any query that SELECTs username. A single-step migration + deploy has a brief window where the old pod and the new pod coexist, causing errors.
  • CI will fail on NotificationRepositoryTest and any Testcontainers test that creates users without .email() as soon as V44 is added — which is actually the correct behavior. But it means those tests must be fixed in the same PR, not after.
  • Rollback: Dropping a column is irreversible without a restore. The pre-check DO block prevents running against bad data, but there is no down-migration. A pg_dump must be taken and verified immediately before V44 runs in production.

Recommendations

  • Two-phase migration to eliminate the coexistence window: Phase 1 — make email NOT NULL, deploy new JAR; Phase 2 — drop username column once all instances run new code. For a single-node family app this may be overkill, but the option should be an explicit decision.
  • Add "take and verify pg_dump backup" as step 0 of the verification checklist — it is currently missing.
  • Document in the PR description that CI will fail until NotificationRepositoryTest and related tests are fixed in the same branch.
## ⚙️ Tobias Wendt — DevOps Engineer ### Observations - **Zero-downtime risk**: Dropping the `username` column means any running backend instance compiled against the old schema will fail immediately on any query that SELECTs `username`. A single-step migration + deploy has a brief window where the old pod and the new pod coexist, causing errors. - **CI will fail** on `NotificationRepositoryTest` and any Testcontainers test that creates users without `.email()` as soon as V44 is added — which is actually the correct behavior. But it means those tests must be fixed in the same PR, not after. - **Rollback**: Dropping a column is irreversible without a restore. The pre-check `DO` block prevents running against bad data, but there is no down-migration. A `pg_dump` must be taken and verified immediately before V44 runs in production. ### Recommendations - **Two-phase migration** to eliminate the coexistence window: Phase 1 — make `email NOT NULL`, deploy new JAR; Phase 2 — drop `username` column once all instances run new code. For a single-node family app this may be overkill, but the option should be an explicit decision. - Add "take and verify `pg_dump` backup" as **step 0** of the verification checklist — it is currently missing. - Document in the PR description that CI will fail until `NotificationRepositoryTest` and related tests are fixed in the same branch.
Author
Owner

🎨 Leonie Voss — UX & Accessibility Designer

Observations

  • <input type="text"><input type="email">: The login form and AccountSection.svelte currently use type="text" for the username field. Switching to type="email" gives mobile users the correct keyboard layout, enables browser email autofill, and provides basic format validation for free.
  • autocomplete attribute: The login form currently has autocomplete="username". Changing to autocomplete="email" is required — without it, password managers and browser autofill will not offer the correct credentials after migration.
  • i18n gap: m.login_label_username() and the hardcoded German string 'Bitte Benutzername und Passwort eingeben.' in +page.server.ts both reference "Benutzername." Both must become email-aware. The error string bypasses the Paraglide system entirely — it should use a translation key, not a hardcoded string.
  • Admin form label: m.admin_col_login() labels the username input in the admin user form. After the username field is removed, the email field label should be m.admin_label_email() — consistent with the rest of the form.

Recommendations

  • Change all username inputs to type="email" with autocomplete="email".
  • Add translation keys login_label_email and login_error_missing_credentials in de.json, en.json, es.json — and remove the hardcoded German string.
  • The login form placeholder should read "E-Mail-Adresse" (de), "Email address" (en), "Correo electrónico" (es).
## 🎨 Leonie Voss — UX & Accessibility Designer ### Observations - **`<input type="text">` → `<input type="email">`**: The login form and `AccountSection.svelte` currently use `type="text"` for the username field. Switching to `type="email"` gives mobile users the correct keyboard layout, enables browser email autofill, and provides basic format validation for free. - **`autocomplete` attribute**: The login form currently has `autocomplete="username"`. Changing to `autocomplete="email"` is required — without it, password managers and browser autofill will not offer the correct credentials after migration. - **i18n gap**: `m.login_label_username()` and the hardcoded German string `'Bitte Benutzername und Passwort eingeben.'` in `+page.server.ts` both reference "Benutzername." Both must become email-aware. The error string bypasses the Paraglide system entirely — it should use a translation key, not a hardcoded string. - **Admin form label**: `m.admin_col_login()` labels the username input in the admin user form. After the username field is removed, the email field label should be `m.admin_label_email()` — consistent with the rest of the form. ### Recommendations - Change all username inputs to `type="email"` with `autocomplete="email"`. - Add translation keys `login_label_email` and `login_error_missing_credentials` in `de.json`, `en.json`, `es.json` — and remove the hardcoded German string. - The login form placeholder should read "E-Mail-Adresse" (de), "Email address" (en), "Correo electrónico" (es).
Author
Owner

🗳️ Decision Queue — Action Required

4 decisions need your input before implementation starts.

Architecture

  • searchByNameOrUsername replacement — the JPQL query references the username column being dropped. Should it pivot to searching email only, or search email + first_name + last_name? The latter is more useful but changes the search semantics. (Raised by: Felix, Markus)
  • Two-phase vs. single-step migration — Phase 1: make email NOT NULL + deploy; Phase 2: drop username column. Eliminates the coexistence window at the cost of two deploys. For a single-node family app a single-step migration with a brief downtime window is probably fine — but make it an explicit call. (Raised by: Tobias)

Security

  • Colon-in-email validation — HTTP Basic Auth splits on the first colon. An email like a:b@example.com would break login. Add a @Pattern(regexp = "^[^:]+$") validator on all email fields. This is a hard recommendation given the auth mechanism — confirm it's in scope for this issue. (Raised by: Nora)

Backend

  • updateProfile() email nullabilityPUT /api/users/me currently allows setting email to null. Once V44 runs, this produces a DB constraint error (500) instead of a clean 400. Must be fixed before the migration. Confirm this belongs in this issue's PR scope. (Raised by: Markus, Sara)
## 🗳️ Decision Queue — Action Required _4 decisions need your input before implementation starts._ ### Architecture - **`searchByNameOrUsername` replacement** — the JPQL query references the `username` column being dropped. Should it pivot to searching `email` only, or search `email + first_name + last_name`? The latter is more useful but changes the search semantics. _(Raised by: Felix, Markus)_ - **Two-phase vs. single-step migration** — Phase 1: make email NOT NULL + deploy; Phase 2: drop username column. Eliminates the coexistence window at the cost of two deploys. For a single-node family app a single-step migration with a brief downtime window is probably fine — but make it an explicit call. _(Raised by: Tobias)_ ### Security - **Colon-in-email validation** — HTTP Basic Auth splits on the first colon. An email like `a:b@example.com` would break login. Add a `@Pattern(regexp = "^[^:]+$")` validator on all email fields. This is a hard recommendation given the auth mechanism — confirm it's in scope for this issue. _(Raised by: Nora)_ ### Backend - **`updateProfile()` email nullability** — `PUT /api/users/me` currently allows setting email to null. Once V44 runs, this produces a DB constraint error (500) instead of a clean 400. Must be fixed before the migration. Confirm this belongs in this issue's PR scope. _(Raised by: Markus, Sara)_
Author
Owner

🏗️ Markus Keller — Architect Review

Worked through all 4 open decisions from the Decision Queue. All resolved.

Resolved

  1. searchByNameOrUsername replacement — Pivot to searching email + firstName + lastName. Email-only is too narrow for an admin search; the added columns are already on the entity and the query is no more complex.

  2. Migration strategy — Single-step migration (make email NOT NULL + drop username in one V44 file). This app runs single-node Docker Compose — there is no rolling deploy coexistence window, only a brief maintenance window. Document this explicitly in the PR description as a conscious decision.

  3. Colon-in-email validation — In scope for this PR. Add @Pattern(regexp = "^[^:]+$") to all email input fields (CreateUserRequest, profile update DTO). Email is becoming the identity field; it must be validated correctly from day one. Closes the HTTP Basic Auth parsing bug Nora flagged.

  4. updateProfile() null email guard — In scope for this PR. Add an explicit null/blank check in UserService.updateProfile() throwing DomainException.badRequest(...) before the save. The V44 DO block pre-check cannot catch this runtime gap — it must land before the migration ships.

Overall read

The issue is well-scoped and the pre-check DO block is the right approach — consistent with how V30 and V18 handle constraint enforcement. The four decisions above are the only open items; once they are reflected in the implementation checklist, this is ready to implement.

## 🏗️ Markus Keller — Architect Review Worked through all 4 open decisions from the Decision Queue. All resolved. ### Resolved 1. **`searchByNameOrUsername` replacement** — Pivot to searching `email + firstName + lastName`. Email-only is too narrow for an admin search; the added columns are already on the entity and the query is no more complex. 2. **Migration strategy** — Single-step migration (make `email NOT NULL` + drop `username` in one V44 file). This app runs single-node Docker Compose — there is no rolling deploy coexistence window, only a brief maintenance window. Document this explicitly in the PR description as a conscious decision. 3. **Colon-in-email validation** — In scope for this PR. Add `@Pattern(regexp = "^[^:]+$")` to all email input fields (`CreateUserRequest`, profile update DTO). Email is becoming the identity field; it must be validated correctly from day one. Closes the HTTP Basic Auth parsing bug Nora flagged. 4. **`updateProfile()` null email guard** — In scope for this PR. Add an explicit null/blank check in `UserService.updateProfile()` throwing `DomainException.badRequest(...)` before the save. The V44 `DO` block pre-check cannot catch this runtime gap — it must land before the migration ships. ### Overall read The issue is well-scoped and the pre-check `DO` block is the right approach — consistent with how V30 and V18 handle constraint enforcement. The four decisions above are the only open items; once they are reflected in the implementation checklist, this is ready to implement.
Sign in to join this conversation.
No Label feature user
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#270