Profile editing in account settings
Java · Spring Boot · intermediate · modification
Adds profile editing to the account settings page. `PATCH /api/me` takes a partial JSON object and merges it onto the current user with Jackson's `readerForUpdating`, so absent keys stay untouched without a null check per field. The merged profile is validated inside the transaction; an invalid value is a 400 and rolls the change back. `User` and `ProfileView` gain `jobTitle` for the new field on the page.
Workspace backend on Spring Boot 3.3 (Java 21) with Spring Web, Spring Data JPA (Hibernate) and Spring Security. The existing security configuration requires an authenticated session for every `/api/**` request, checks CSRF tokens (the SPA sends them) and stores an `AccountPrincipal(long userId, String email)` record as the principal, which `@AuthenticationPrincipal` resolves. `UserRepository` (existing, unchanged) extends `JpaRepository<User, Long>`. `Role` is an existing enum with `MEMBER` and `ADMIN`; the admin console (existing) calls `setRole`, and the verification link (existing) calls `markEmailVerified`. The `ObjectMapper` is Spring Boot's auto-configured one: no custom Jackson configuration, mixins or `@JsonIgnore` anywhere, so JSON keys that match no property are ignored. `GET /api/me` already exists.
Requirements
- `PATCH /api/me` lets the signed-in user edit three fields of their own profile: `displayName`, `timeZone` and the new `jobTitle`. The body is a JSON object holding any subset of those keys; an absent key leaves its field unchanged, and any other key in the body is ignored. A body that is not a JSON object is a 400. The response is the updated profile as `ProfileView`.
- Validation runs on the resulting profile: `displayName` is non-blank and at most 80 UTF-16 code units (`String.length()`, the same limit the settings page enforces); `timeZone` is an id that `ZoneId.of` accepts (`Europe/Berlin`, `UTC`, an offset); `jobTitle` is `null` (which clears it) or at most 100 code units. A violation is a 400 and nothing is saved. Existing rows already satisfy these rules, and the settings page sends these values as JSON strings.
- Every signed-in user can call the endpoint for their own account. `role` (`MEMBER` or `ADMIN`) is changed only by admins through the existing admin console, `emailVerified` only by the existing verification link, and `email` only through the existing change-email flow, which verifies the new address first. None of them may change through this endpoint.
- The settings page saves one form at a time; concurrent edits of one profile are out of scope (last write wins).
- The nullable `users.job_title` column is added by a migration that ships separately and is already applied.
Files touched
- src/main/java/com/acme/workspace/account/User.java
- src/main/java/com/acme/workspace/account/ProfileView.java
- src/main/java/com/acme/workspace/account/MeController.java
- src/main/java/com/acme/workspace/account/ProfileService.java
--- src/main/java/com/acme/workspace/account/User.java
private String timeZone = "UTC";
+ private String jobTitle;
+
@Enumerated(EnumType.STRING)
@Column(nullable = false)
public String getTimeZone() { return timeZone; }
public void setTimeZone(String timeZone) { this.timeZone = timeZone; }
+ public String getJobTitle() { return jobTitle; }
+ public void setJobTitle(String jobTitle) { this.jobTitle = jobTitle; }
public Role getRole() { return role; }
public void setRole(Role role) { this.role = role; }
--- src/main/java/com/acme/workspace/account/ProfileView.java
package com.acme.workspace.account;
-public record ProfileView(long id, String email, String displayName, String timeZone) {
+public record ProfileView(long id, String email, String displayName, String timeZone, String jobTitle) {
public static ProfileView from(User user) {
- return new ProfileView(user.getId(), user.getEmail(), user.getDisplayName(), user.getTimeZone());
+ return new ProfileView(user.getId(), user.getEmail(), user.getDisplayName(), user.getTimeZone(),
+ user.getJobTitle());
}
}
--- src/main/java/com/acme/workspace/account/MeController.java
package com.acme.workspace.account;
+import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.http.HttpStatus;