View and update the authenticated user’s own profile. All operations require a valid JWT access token. Includes sub-operations: get profile, update name/email/salutation, change password, and update locale.
- Email changes must be unique across all users.
- Password change requires the current password to be verified first.
newPasswordandnewPasswordConfirmationmust match and meet the minimum length requirement.- Locale change persists to DB and FE immediately switches
i18n.global.locale.value— no page reload required. salutationis the short form of the user’s name used by the UI and agents when addressing the user (e.g. Czech vocativeMíro,Honzo). It is optional (nullallowed) and has no character-set restriction.teamBriefingis a free-form text field where the user briefly describes themselves to the AI team (preferences, working style, what to know). Optional (nullallowed), maximum 2000 characters, persisted astext NULLinuserstable. The field is rendered as a multi-line textarea with a live character counter.- Achievement stats (
projectsShipped,daysActive,daysActiveStreak,totalSpend) are computed on the backend and returned read-only; they cannot be updated viaPUT /me.
sequenceDiagram
actor User
%% --- GET profile ---
User->>+FE: opens My Profile screen
FE->>+BE: GET /api/v1/users/me <br> Authorization: Bearer {accessToken}
BE->>BE: validate JWT access token
alt access token invalid or missing
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>DB: find user by id (from token)
BE->>DB: count projects shipped (status != DRAFT)
BE->>DB: count distinct activity days + consecutive streak
BE->>-FE: 200 OK <br> UserProfileResponse
FE->>-User: display profile data + achievement stats
%% --- PUT profile ---
User->>+FE: edits name / email and submits
FE->>FE: validate form
alt form is invalid
FE-->>User: show error messages <br> disable submit button
end
FE->>+BE: PUT /api/v1/users/me <br> Authorization: Bearer {accessToken} <br> UpdateProfileRequest
BE->>BE: validate JWT access token
alt access token invalid or missing
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>BE: validate request
alt request is invalid
BE-->>FE: 400 Bad Request <br> ErrorResponse
end
BE->>DB: check if new email is already taken (if email changed)
alt email already taken by another user
BE-->>FE: 409 Conflict <br> ErrorResponse
end
BE->>DB: update user record
BE->>-FE: 200 OK <br> UserProfileResponse
FE->>-User: show updated profile
%% --- PUT password ---
User->>+FE: fills Change Password form and submits
FE->>FE: validate form
alt form is invalid
FE-->>User: show error messages <br> disable submit button
end
FE->>+BE: PUT /api/v1/users/me/password <br> Authorization: Bearer {accessToken} <br> ChangePasswordRequest
BE->>BE: validate JWT access token
alt access token invalid or missing
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>BE: validate request
alt request is invalid
BE-->>FE: 400 Bad Request <br> ErrorResponse
end
BE->>DB: find user by id (from token)
BE->>BE: verify currentPassword against stored hash
alt currentPassword does not match
BE-->>FE: 400 Bad Request <br> ErrorResponse
end
BE->>BE: verify newPasswordConfirmation matches newPassword
alt passwords do not match
BE-->>FE: 400 Bad Request <br> ErrorResponse
end
BE->>DB: update password hash
BE->>-FE: 204 No Content
FE->>-User: show "Password changed successfully" message
%% --- PUT locale ---
User->>+FE: changes language in Language picker (auto-save)
FE->>+BE: PUT /api/v1/users/me/locale <br> Authorization: Bearer {accessToken} <br> UpdateLocaleRequest
BE->>BE: validate JWT access token
alt access token invalid or missing
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>BE: validate locale enum (cs|en)
alt locale is invalid
BE-->>FE: 400 Bad Request <br> ErrorResponse
end
BE->>DB: update user locale
BE->>-FE: 204 No Content
FE->>-User: switch i18n.global.locale.value — UI re-renders in selected language
UX Guidelines
Page Layout
The My Profile screen is a full-page settings hub.
Layout: grid-template-columns: 240px 1fr, max-width 1240px, centered. The sidebar occupies the left column; the content area the right.
TopBar:
- Left: back chevron + Logo (navigates back to Studio) + vertical divider +
"Account"label in mono 12px - Right:
Conciergeghost button (opens ConciergePanel) +ThemeToggle
Left sidebar (240px):
- Sticky, full-height,
border-right: 1px solid var(--line-1), padding32px 16px 32px 32px - Heading:
"Settings"— mono 11px, uppercase,var(--fg-3) - Nav buttons,
gap: 2px; each: icon (15px) + label (13px),padding: 8px 10px,border-radius: 8px - Active item:
background: var(--bg-3),font-weight: 500 - Danger zone item: always
color: var(--rose)(active and inactive) - Hover:
background: var(--bg-3), transition 0.12s
Sidebar nav items (in order):
- Profile
- Account
- Sounds (links to UC-01007)
- Billing & usage
- Notifications
- Team
- Connected accounts
- Danger zone
Right content area: padding: 32px 32px 64px
Each section opens inline in the content area — no page navigation, no modal.
Section: Profile (real API)
API status:
GET /api/v1/users/me+PUT /api/v1/users/me— fully implemented. Display name and email are editable. Cover image, avatar, handle, role, bio, website, social links, and “currently working with” tags are removed from MVP scope — no UI, no API.
Achievement stats row (3 cards, real data):
Cards use var(--bg-2), border-radius: 12px. Each card: mono 11px label (uppercase) + display 28px value + 11px subtitle.
| Card | Value source | Subtitle |
|---|---|---|
| Projects shipped | achievements.projectsShipped — COUNT of projects where status != 'DRAFT' | "since {createdAt month year}" e.g. "since March 2026" — formatted by FE |
| Days active | achievements.daysActive — COUNT DISTINCT dates with at least one activity entry; streak in parentheses: "184 days (12-day streak)" using achievements.daysActiveStreak | same subtitle format |
| Total spend | achievements.totalSpend — always 0.00 in MVP; displayed as "$0.00" | same subtitle format |
Form fields (FieldRow layout): grid-template-columns: 180px 1fr, border-bottom: 1px solid var(--line-1), padding: 20px 0
| Field | Input type | Note |
|---|---|---|
| Display name | text input | backed by PUT /me (name) |
| text input | backed by PUT /me (email) | |
| Salutation / Oslovení | text input | backed by PUT /me (salutation); hint: “How should the team address you?” / “Jak ti má tým říkat?”; placeholder: “e.g. Mike” / “např. Míro”; optional |
| Team briefing / Co má tým vědět | textarea (4-6 rows) + char counter | backed by PUT /me (teamBriefing); hint: “Briefly tell your AI team what they should know about you and your preferences.” / “Stručně shrň, co by tvůj AI tým měl vědět o tobě a tvých preferencích.”; placeholder: “e.g. I work in B2B SaaS, love TypeScript, hate comments that just repeat the code…” / “např. Pracuju v B2B SaaS, miluju TypeScript, nesnáším komentáře co jen opakují kód…”; optional; max 2000 chars; char counter {count}/2000 in --fg-3/--fg-4; counter turns red when count > 2000 |
Footer: Save changes (primary btn) + Cancel (ghost btn), right-aligned, margin-top: 24px
Uses ProfileForm.vue component.
Section: Account (partial real API)
API status: Email display is real (
GET /me). Password change usesPUT /me/password. Language picker usesPUT /me/locale(auto-save). Two-factor auth and active sessions are not yet implemented in MVP — kept as visual design only (HTML comment in FE).
| Row | Content | Status |
|---|---|---|
| Email address | text input (pre-filled, read-only display) + green ”✓ verified” mono badge | real |
| Password | ghost button “Change password” — opens ChangePasswordForm.vue inline | real |
| Language | <select> with English (en) / Čeština (cs); default en; auto-saves on change via PUT /me/locale; FE switches i18n.global.locale.value immediately on 204 | real |
| Two-factor auth | info text (“Currently disabled”) + primary “Enable 2FA” button | not yet implemented in MVP |
| Active sessions | list of session cards with device name, location, timestamp; “Sign out” button | not yet implemented in MVP |
Section: Billing & usage (partially real after Stopa C)
API status (after Stopa C): Payment method, billing email, invoices, and spending limit connect to real Stripe APIs. Estimated cost card (hosting hours breakdown) and Tax/VAT ID remain mockup. See UC-10 Stripe Billing for full API contracts.
- Estimated cost card: amber-tinted gradient (
var(--amber-soft)background), display 44px cost, next invoice date, “View detailed breakdown” ghost button — still mockup - Usage bars (inside the card): two
UsageBarrows — “Project hosting hours” and “Agent compute” — each with a 6px progress bar (var(--amber)fill) and rate label — still mockup; AI quota bars (5h + weekly rolling windows) are real via UC-08006 - FieldRows:
- Spending limit — number input, per-month suffix; backed by
GET/PUT /api/v1/users/me/billing/spending-limit(UC-10005);null= unlimited - Payment method — card brand + last4 + expiry from
GET /api/v1/users/me/billing/payment-method(UC-10002); “Change” triggers SetupIntent flow (UC-10001) - Billing email — editable, backed by
PUT /api/v1/users/me/billing/email(UC-10003) - Tax/VAT ID — still mockup, no backend
- Spending limit — number input, per-month suffix; backed by
- Invoices table: columns — invoice number (mono), date, amount, status pill, PDF download link; backed by
GET /api/v1/users/me/invoices(UC-10004);border-radius: 12px, each rowborder-bottom: 1px solid var(--line-1) - Soft alert banners: amber at 80 % of spending limit, rose at 100 % — rendered in
BillingSection.vuereactively fromSpendingLimitResponse
Section: Notifications (mockup, not implemented in MVP)
API status: Mockup. No backend notifications preference API in MVP.
Two groups of toggle rows, separated by a mono uppercase group heading:
Email group: Deploys, Errors & uptime, Weekly digest, Billing & invoices
In-app push group: Agent activity, @ mentions
Each row: label (13px, 500) + description (12px, var(--fg-3)) + Toggle component (36×20px, amber when on).
Digest frequency: segmented button control — Off / Daily / Weekly — background: var(--bg-2), active option uses var(--bg-3).
Section: Team (mockup, not implemented in MVP)
API status: Mockup. No team/collaborator API in MVP.
- Invite CTA card:
var(--bg-2)card with “Invite a collaborator” text + primary “Invite” button - Active collaborators list:
TAvatar(40px) + name + role description + project count + last active time + “Manage” button - Pending invites: dashed-border empty state — “No pending invites.”
Section: Connected accounts (mockup, not implemented in MVP)
API status: Mockup. No OAuth integration in MVP. GitHub login is configured via Spring Security OAuth2 at signup only; per-account management shown here is not implemented.
Four integration cards (var(--bg-2), border-radius: 12px): GitHub, Google, Stripe, Figma
Each card: service icon (40px square, var(--bg-3)) + name + description + connected account (mono) + “Connect” (primary) or “Disconnect” (ghost) button.
Section: Danger zone (mockup, not implemented in MVP)
API status: Mockup. Neither export nor account deletion is implemented in MVP.
Two action cards:
- Export data: ghost “Request export” button, neutral styling (
var(--bg-2)) - Delete account:
background: var(--rose-soft),border: 1px solid var(--rose-line); delete button is solid rose (background: var(--rose)); copy mentions that invoices are retained 7 years per Czech tax law
Get Profile
GET /api/v1/users/me (no request body)
200 OK UserProfileResponse:
{
"data": {
"id": 1,
"email": "jane@example.com",
"name": "Jane Doe",
"salutation": "Jane",
"teamBriefing": "I work in B2B SaaS, love TypeScript, hate comments that just repeat the code...",
"createdAt": "2026-01-15T10:30:00Z",
"preferences": {
"locale": "en",
"soundNewMessage": true,
"soundSendMessage": false,
"soundTaskCompleted": true,
"soundTaskError": true,
"soundMasterVolume": 50
},
"achievements": {
"projectsShipped": 7,
"daysActive": 184,
"daysActiveStreak": 12,
"totalSpend": 0.00
}
}
}
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
Update Profile
PUT /api/v1/users/me UpdateProfileRequest:
{
"name": "Jane Smith",
"email": "jane.smith@example.com",
"salutation": "Jane",
"teamBriefing": "I work in B2B SaaS, love TypeScript, hate comments that just repeat the code..."
}
200 OK UserProfileResponse:
{
"data": {
"id": 1,
"email": "jane.smith@example.com",
"name": "Jane Smith",
"salutation": "Jane",
"teamBriefing": "I work in B2B SaaS, love TypeScript, hate comments that just repeat the code...",
"createdAt": "2026-01-15T10:30:00Z",
"preferences": {
"locale": "en",
"soundNewMessage": true,
"soundSendMessage": false,
"soundTaskCompleted": true,
"soundTaskError": true,
"soundMasterVolume": 50
},
"achievements": {
"projectsShipped": 7,
"daysActive": 184,
"daysActiveStreak": 12,
"totalSpend": 0.00
}
}
}
400 Bad Request (validation) ErrorResponse:
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "must be a valid email address" }
]
}
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
409 Conflict (email already taken) ErrorResponse:
{
"status": 409,
"code": "CONFLICT_USER",
"message": "Email is already registered"
}
Change Password
PUT /api/v1/users/me/password ChangePasswordRequest:
{
"currentPassword": "oldSecret123",
"newPassword": "newSecret456",
"newPasswordConfirmation": "newSecret456"
}
204 No Content (success, no body)
400 Bad Request (validation or wrong current password) ErrorResponse:
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{ "field": "currentPassword", "message": "current password is incorrect" }
]
}
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
Update Locale
PUT /api/v1/users/me/locale UpdateLocaleRequest:
{
"locale": "cs"
}
204 No Content (success, no body — FE already holds the new value and switches i18n locale immediately)
400 Bad Request (invalid locale value) ErrorResponse:
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{ "field": "locale", "message": "must be one of: cs, en" }
]
}
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
Frontend
Update Profile — Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| name | not_blank | 1 - 100 | ||
| salutation | optional | 0 - 50 | nullable; any characters including diacritics allowed; send null to clear | |
| teamBriefing | optional | 0 - 2000 | nullable; multi-line text; send null to clear | |
| not_blank | ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ |
Change Password — Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| currentPassword | not_blank | |||
| newPassword | not_blank | 8 - 72 | ||
| newPasswordConfirmation | not_blank | must match newPassword (client-side check) |
Update Locale — Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| locale | not_blank | must be one of cs, en; validated before sending API request |
Backend
Update Profile — Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| name | not_blank | 1 - 100 | ||
| salutation | optional | 0 - 50 | nullable String; no pattern restriction; persisted as varchar(50) NULL in users table | |
| teamBriefing | optional | 0 - 2000 | nullable String; persisted as text NULL in users table | |
| not_blank, email |
Change Password — Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| currentPassword | not_blank | verified against stored hash (domain logic) | ||
| newPassword | not_blank | 8 - 72 | ||
| newPasswordConfirmation | not_blank | must equal newPassword (domain validation) |
Update Locale — Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| locale | not_blank | enum constraint: cs or en |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| authenticated user | GET /me is called | 200 OK with user profile (including salutation, teamBriefing), preferences, and achievements returned |
| no Authorization header | GET /me is called | 401 AUTHENTICATION_FAILED error response is returned |
| authenticated user, new unique email and valid name | PUT /me is called | 200 OK with updated profile returned |
| authenticated user, email unchanged | PUT /me is called | 200 OK with updated profile returned |
| authenticated user, salutation “Míro” provided | PUT /me is called | 200 OK with salutation persisted and returned in response |
| authenticated user, salutation null provided | PUT /me is called | 200 OK with salutation set to null in DB and response |
| salutation exceeds 50 characters | PUT /me is called | 400 VALIDATION_ERROR error response is returned |
| authenticated user, teamBriefing “I love TS” provided | PUT /me is called | 200 OK with teamBriefing persisted and returned in response |
| authenticated user, teamBriefing null provided | PUT /me is called | 200 OK with teamBriefing set to null in DB and response |
| teamBriefing exceeds 2000 characters | PUT /me is called | 400 VALIDATION_ERROR error response is returned |
| authenticated user, new email already taken | PUT /me is called | 409 CONFLICT_USER error response is returned |
| invalid email format | PUT /me is called | 400 VALIDATION_ERROR error response is returned |
| name is blank | PUT /me is called | 400 VALIDATION_ERROR error response is returned |
| no Authorization header | PUT /me is called | 401 AUTHENTICATION_FAILED error response is returned |
| authenticated user, correct currentPassword | PUT /me/password is called | 204 No Content, password hash updated in DB |
| currentPassword is incorrect | PUT /me/password is called | 400 VALIDATION_ERROR error response is returned |
| newPassword shorter than 8 characters | PUT /me/password is called | 400 VALIDATION_ERROR error response is returned |
| newPasswordConfirmation does not match newPassword | PUT /me/password is called | 400 VALIDATION_ERROR error response is returned |
| no Authorization header | PUT /me/password is called | 401 AUTHENTICATION_FAILED error response is returned |
| invalid request (empty body) | PUT /me/password is called | 400 VALIDATION_ERROR error response is returned |
| authenticated user, locale “cs” | PUT /me/locale is called | 204 No Content, locale persisted to DB |
| authenticated user, locale “en” | PUT /me/locale is called | 204 No Content, locale persisted to DB |
| locale value “de” (unsupported) | PUT /me/locale is called | 400 VALIDATION_ERROR error response is returned |
| locale field missing from request body | PUT /me/locale is called | 400 VALIDATION_ERROR error response is returned |
| no Authorization header | PUT /me/locale is called | 401 AUTHENTICATION_FAILED error response is returned |
| authenticated user refreshes page after locale save | GET /me is called | preferences.locale matches previously saved value |
Thanks for the feedback.