Internal Documentation internal
TalkIDE internal documentation

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.
  • newPassword and newPasswordConfirmation must match and meet the minimum length requirement.
  • Locale change persists to DB and FE immediately switches i18n.global.locale.value — no page reload required.
  • salutation is the short form of the user’s name used by the UI and agents when addressing the user (e.g. Czech vocative Míro, Honzo). It is optional (null allowed) and has no character-set restriction.
  • teamBriefing is a free-form text field where the user briefly describes themselves to the AI team (preferences, working style, what to know). Optional (null allowed), maximum 2000 characters, persisted as text NULL in users table. 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 via PUT /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: Concierge ghost button (opens ConciergePanel) + ThemeToggle

Left sidebar (240px):

  • Sticky, full-height, border-right: 1px solid var(--line-1), padding 32px 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):

  1. Profile
  2. Account
  3. Sounds (links to UC-01007)
  4. Billing & usage
  5. Notifications
  6. Team
  7. Connected accounts
  8. 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.

CardValue sourceSubtitle
Projects shippedachievements.projectsShipped — COUNT of projects where status != 'DRAFT'"since {createdAt month year}" e.g. "since March 2026" — formatted by FE
Days activeachievements.daysActive — COUNT DISTINCT dates with at least one activity entry; streak in parentheses: "184 days (12-day streak)" using achievements.daysActiveStreaksame subtitle format
Total spendachievements.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

FieldInput typeNote
Display nametext inputbacked by PUT /me (name)
Emailtext inputbacked by PUT /me (email)
Salutation / Oslovenítext inputbacked 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ěttextarea (4-6 rows) + char counterbacked 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 uses PUT /me/password. Language picker uses PUT /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).

RowContentStatus
Email addresstext input (pre-filled, read-only display) + green ”✓ verified” mono badgereal
Passwordghost button “Change password” — opens ChangePasswordForm.vue inlinereal
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 204real
Two-factor authinfo text (“Currently disabled”) + primary “Enable 2FA” buttonnot yet implemented in MVP
Active sessionslist of session cards with device name, location, timestamp; “Sign out” buttonnot 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 UsageBar rows — “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 IDstill mockup, no backend
  • 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 row border-bottom: 1px solid var(--line-1)
  • Soft alert banners: amber at 80 % of spending limit, rose at 100 % — rendered in BillingSection.vue reactively from SpendingLimitResponse

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

FieldConstraintsSizePatternNote
namenot_blank1 - 100
salutationoptional0 - 50nullable; any characters including diacritics allowed; send null to clear
teamBriefingoptional0 - 2000nullable; multi-line text; send null to clear
emailnot_blank^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

Change Password — Validations

FieldConstraintsSizePatternNote
currentPasswordnot_blank
newPasswordnot_blank8 - 72
newPasswordConfirmationnot_blankmust match newPassword (client-side check)

Update Locale — Validations

FieldConstraintsSizePatternNote
localenot_blankmust be one of cs, en; validated before sending API request

Backend

Update Profile — Validations

FieldConstraintsSizePatternNote
namenot_blank1 - 100
salutationoptional0 - 50nullable String; no pattern restriction; persisted as varchar(50) NULL in users table
teamBriefingoptional0 - 2000nullable String; persisted as text NULL in users table
emailnot_blank, email

Change Password — Validations

FieldConstraintsSizePatternNote
currentPasswordnot_blankverified against stored hash (domain logic)
newPasswordnot_blank8 - 72
newPasswordConfirmationnot_blankmust equal newPassword (domain validation)

Update Locale — Validations

FieldConstraintsSizePatternNote
localenot_blankenum constraint: cs or en

Test Cases

GIVENWHENTHEN
authenticated userGET /me is called200 OK with user profile (including salutation, teamBriefing), preferences, and achievements returned
no Authorization headerGET /me is called401 AUTHENTICATION_FAILED error response is returned
authenticated user, new unique email and valid namePUT /me is called200 OK with updated profile returned
authenticated user, email unchangedPUT /me is called200 OK with updated profile returned
authenticated user, salutation “Míro” providedPUT /me is called200 OK with salutation persisted and returned in response
authenticated user, salutation null providedPUT /me is called200 OK with salutation set to null in DB and response
salutation exceeds 50 charactersPUT /me is called400 VALIDATION_ERROR error response is returned
authenticated user, teamBriefing “I love TS” providedPUT /me is called200 OK with teamBriefing persisted and returned in response
authenticated user, teamBriefing null providedPUT /me is called200 OK with teamBriefing set to null in DB and response
teamBriefing exceeds 2000 charactersPUT /me is called400 VALIDATION_ERROR error response is returned
authenticated user, new email already takenPUT /me is called409 CONFLICT_USER error response is returned
invalid email formatPUT /me is called400 VALIDATION_ERROR error response is returned
name is blankPUT /me is called400 VALIDATION_ERROR error response is returned
no Authorization headerPUT /me is called401 AUTHENTICATION_FAILED error response is returned
authenticated user, correct currentPasswordPUT /me/password is called204 No Content, password hash updated in DB
currentPassword is incorrectPUT /me/password is called400 VALIDATION_ERROR error response is returned
newPassword shorter than 8 charactersPUT /me/password is called400 VALIDATION_ERROR error response is returned
newPasswordConfirmation does not match newPasswordPUT /me/password is called400 VALIDATION_ERROR error response is returned
no Authorization headerPUT /me/password is called401 AUTHENTICATION_FAILED error response is returned
invalid request (empty body)PUT /me/password is called400 VALIDATION_ERROR error response is returned
authenticated user, locale “cs”PUT /me/locale is called204 No Content, locale persisted to DB
authenticated user, locale “en”PUT /me/locale is called204 No Content, locale persisted to DB
locale value “de” (unsupported)PUT /me/locale is called400 VALIDATION_ERROR error response is returned
locale field missing from request bodyPUT /me/locale is called400 VALIDATION_ERROR error response is returned
no Authorization headerPUT /me/locale is called401 AUTHENTICATION_FAILED error response is returned
authenticated user refreshes page after locale saveGET /me is calledpreferences.locale matches previously saved value

Was this page helpful?

Thanks for the feedback.