feat(phase-2): separate /profile (identity) from /career (master profile)

Phase 2 — Career/Profile separation. The master career profile is the source of
truth; identity and career data are now saved independently so neither wipes the
other. CV Builder deliberately not built yet.

Backend — PUT /auth/profile is now a partial update:
- null/omitted field -> unchanged; "" -> cleared; value -> set (trimmed).
- Email/UserName never cleared to empty (login identifiers).
This lets /profile save identity fields and /career save the master-profile
fields through the same endpoint without one nulling the other. 4 new tests
cover the data-integrity guarantees (identity save keeps the CV, career save
keeps identity, empty clears, null leaves).

Frontend:
- ProfilePage save payload is now scoped by careerOnly: /career sends only
  { profileCvText, profileCvStructureJson }, /profile sends only identity.
- CareerWorkspacePage: removed the inert "CV Builder" tab (careerView) — Phase 2
  establishes the master profile only; the builder is Phase 4.
- Dropped the dead careerView prop.
- Updated the CV-save test to render career mode and assert identity is excluded.

Source-of-truth flip (CareerProfileService authoritative) stays deferred to F5
per the branch design; CareerProfileService keeps mirroring via its dual-write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-17 19:59:38 +02:00
parent a28c47f515
commit 66cc6a7db4
5 changed files with 132 additions and 39 deletions
+22 -15
View File
@@ -363,21 +363,28 @@ public sealed class AuthController : ControllerBase
return StatusCode(501, "Profile updates are only supported for local username/password accounts.");
}
var email = TrimOrNull(request.Email);
var userName = TrimOrNull(request.UserName);
var firstName = TrimOrNull(request.FirstName);
var lastName = TrimOrNull(request.LastName);
var displayName = TrimOrNull(request.DisplayName);
var profileCvText = TrimOrNull(request.ProfileCvText);
var profileCvStructureJson = TrimOrNull(request.ProfileCvStructureJson);
if (email is not null) user.Email = email;
if (userName is not null) user.UserName = userName;
user.FirstName = firstName;
user.LastName = lastName;
user.DisplayName = displayName;
user.ProfileCvText = profileCvText;
user.ProfileCvStructureJson = profileCvStructureJson;
// Partial update. A field is only touched when the request actually carries it:
// - null / omitted -> leave unchanged (the caller isn't editing this field)
// - "" -> explicitly clear
// - "value" -> set (trimmed)
// This lets /profile save identity fields and /career save the master-profile fields
// through the same endpoint without one wiping the other. Email and UserName are the
// login identifiers and are never cleared to empty.
if (request.Email is not null)
{
var v = request.Email.Trim();
if (v.Length > 0) user.Email = v;
}
if (request.UserName is not null)
{
var v = request.UserName.Trim();
if (v.Length > 0) user.UserName = v;
}
if (request.FirstName is not null) user.FirstName = TrimOrNull(request.FirstName);
if (request.LastName is not null) user.LastName = TrimOrNull(request.LastName);
if (request.DisplayName is not null) user.DisplayName = TrimOrNull(request.DisplayName);
if (request.ProfileCvText is not null) user.ProfileCvText = TrimOrNull(request.ProfileCvText);
if (request.ProfileCvStructureJson is not null) user.ProfileCvStructureJson = TrimOrNull(request.ProfileCvStructureJson);
var res = await _users.UpdateAsync(user);
if (!res.Succeeded)