アカウント設定・プロフィール編集 仕様書
ステータス: Draft / 作成日: 2026-08-19 依存:
usersテーブル(スキーマ変更なし)
1. 概要
ログイン中のユーザーが、自分のプロフィールを自分で編集できるようにする。
これまで users の内容を変更できる API は管理者専用の PATCH /v1/admin/users/{id} だけで、
本人がユーザー名や自己紹介を直す手段が無かった。本仕様で PATCH /v1/auth/me を追加し、
/settings/profile から編集できるようにする。
編集できるのは users テーブルに既にある列のうち、本人が持ち主である 3 つに限る。
メールアドレス・管理者フラグ・凍結フラグ・2FA の有効状態はこの API では変更できない。
同名を許すことの影響
users.username に一意制約は無く、この API も重複を確認しない。したがって、既存の利用者と
同じ username に変更できる。登録 API(POST /v1/auth/register)も同様に重複を許す。
本人によるプロフィール更新は監査ログに記録し、ユーザー名の変更前後を追跡できる。
これはメンションの宛先に影響する。コメント本文の @ユーザー名 は
service::task_activities の resolve_mentions が username の完全一致で解決し、
プロジェクトに入っている該当者全員を通知先にする。同名の利用者が同じプロジェクトにいれば、
@alice は 2 人に届く。画面上でも、コメント・担当者・AvatarGroup の表示名が同じになる。
宛先を 1 人に確定させたくなった時点で、users.username に一意制約を入れる
(既存の重複行を先に解消する必要がある)か、メンションを ID 参照に変えるかの判断が要る。
本仕様ではどちらも行っていない。
2. 画面
URL と入口
テナントに紐づかない個人の設定なので、テナントスコープ(/{tenant}/...)の外に置く。
構成
左にアカウント設定のナビ、右にプロフィールのフォームを並べる。
ナビは 6 項目を表示するが、リンクとして機能するのは
プロフィール / セキュリティ / アクセストークン。
残りの 3 項目(環境設定 / 通知 / セッション)は、
対応する画面がまだ無いため aria-disabled の押せない項目として並べる。
ナビ項目は
apps/frontend/src/components/settings/AccountSettingsNav.vueのitemsに定義する。 画面を実装したら、その項目にhrefを足すだけで有効になる。
フォームの挙動
-
初期値は
GET /v1/auth/meの結果。 -
アバターは URL 入力欄とプレビュー。削除ボタンは入力欄を空にする(この時点では保存しない)。
-
保存すると
PATCH /v1/auth/meを送り、成功したら/v1/auth/meのクエリを無効化して サイドバーの表示名・アバターにも反映する。 -
入力チェックは送信前(
onSubmit)と項目からフォーカスが外れたとき(onBlur)の両方で走る。 条件は後述の API 側と同じものを持たせているので、正常な入力が 400 で跳ね返ることはない。
3. API
PATCH /v1/auth/me
ログイン中ユーザー自身のプロフィールを更新する。
リクエスト(UpdateProfileRequest)
文字数は UTF-16 コードユニットではなく Unicode コードポイント単位で数える。
avatar_url と clear_avatar_url: true は同時に指定できず、競合時は 400 Bad Request を返す。
レスポンス: 200 OK / UserResponse(更新後の値)
省略と消去の区別
送らなかったフィールドは変更しない。フィールドが 1 つも無い {} を送った場合も
200 OK を返し、値は変わらない(空の UPDATE は発行しない)。
avatar_url を消したいときだけ、値ではなくフラグで指定する。
{ "clear_avatar_url": true }
空文字("")では消せない。avatar_url は https:// で始まることを要求するため、
空文字は入力チェックで弾かれるからである。既存の PATCH .../tasks/{id}(UpdateTaskRequest)
が nullable な項目に clear_* を持たせているのと同じ形にした。
bio には clear_bio を用意しない。bio は空文字がそのまま有効な値で、
新規登録時も空文字が入る(NULL と "" をアプリ側で区別していない)ため、
消去は {"bio": ""} で足りる。
認可
エクストラクタは GET /v1/auth/me と同じ CurrentUser を使う。したがって次の拒否をそのまま引き継ぐ。
PAT で自分のプロフィールを書き換えられないことは、 個人アクセストークンの認可 の 「アカウント API は PAT 非対応」に沿う。
avatar_url のスキーム制限
avatar_url はフロントエンドで <img src> に流し込む。javascript: や data: を保存できると
そのまま描画経路に載るため、https:// 以外を保存段階で弾く
(payload::users::validate_avatar_url)。HTTP は mixed content で表示できないため、相対パスと同様に受け付けない。
同じ判定をフロントエンドの ProfileForm.vue にも置いている。片方だけ変えると、
画面では通るのに保存で 400 になる(またはその逆)ので、変更するときは両方を直すこと。
前後の空白は、画面側の判定・送信値のどちらでも取り除く。
なお avatar_url は本人以外の画面でも <img src> に載る(担当者一覧の AvatarGroup.vue など)。
指定された URL は閲覧者のブラウザが直接取りに行くため、その URL を用意した側は
アバターを見た人の IP アドレスと User-Agent を知り得る。URL を指定させる方式に
内在する性質で、ファイルのアップロードに対応する(§4)まで解消しない。
4. 未対応
モックアップにあるが、この仕様では実装していない項目と、その理由。
画面上部のパンくずは実データから組まれるようになった(breadcrumbs.md)。
設定系の画面は プロフィール のように一段で出る。Account settings に当たる中間の段は、
その名で開ける画面が無いため置いていない。
5. テスト
拒否を確認するテストには、対照として通るケースも置いている(過剰に拒否していないことの確認)。
6. アクセストークン画面
パーソナルアクセストークン(PAT)を発行・取り消しする画面。PAT 自体の認可設計は 個人アクセストークンの認可 を正とする。
画面の構成と挙動
- 一覧:
GET /v1/personal_tokens(この画面のために新設)。自分が発行した 取り消し前のトークンを名前の昇順で返す。各行にトークン名、末尾 4 文字だけの伏せ字 (pat_••••••7f3a)、スコープ数、有効期限、最終使用日時を表示する。 スコープの内訳は「スコープを表示」のドロップダウンで開く。 - 発行: 「トークンを発行」でフォームを開き、
POST /v1/personal_tokensを送る。- トークン名(1〜100 文字)
- テナント(自分がオーナーのテナントのみ。1 件ならフォームに出さず自動選択)
- 有効期限プリセット: 30日 / 90日(既定)/ 1年 / 無期限
- スコープ(backend の全 11 種。1 つ以上必須。説明付きチェックボックス)
- プロジェクトの絞り込み(
project_ids)は常にnull(テナント内全プロジェクト)で送る
- 平文トークンは発行直後の応答でしか取得できない。発行後にコピー付きで 1 度だけ表示し、 再表示できないことを明記する。
- 取り消し: 確認ダイアログを経て
DELETE /v1/personal_tokens/{id}。取り消したトークンは 一覧に出なくなる(行の削除ではなくrevokedフラグ。API は冪等)。
認可まわりの注意
-
PAT の発行はテナントオーナー限定(backend の
require_tenant_owner)。オーナーの テナントが無いユーザーには発行ボタンを無効にし、その旨を表示する。403 が返った場合も オーナー限定であることを伝える。 -
トークン管理 API はすべてセッション専用。PAT(Bearer)でこの画面の API は呼べない。
GET /v1/personal_tokens
この仕様で新設した一覧 API。
-
認可: セッション必須(PAT は 403)
-
返すもの: 自分(
user_id)のトークンのうちrevoked = falseのもの。 名前の昇順(同名はid順)。PersonalTokenResponseの配列で、平文トークン・ハッシュは含まない -
トークンを 1 件も持たない場合は空配列(404 にしない)
7. セキュリティ画面(認証方法)
パスワードと OAuth 連携を「サインインできる方法」としてまとめて管理する画面。 OAuth の認可フローと解除規則は OAuth ログイン を正とする。
1 枚のカードに「パスワード」「連携済み」「追加できる連携」を上から並べる。
パスワード
GET /v1/auth/me の has_password で出し分ける。
変更はすべてのセッションと PAT を失効させるため、フォームを開いた時点でその旨を出す。
成功したら /signin へフルページ遷移し、サインイン画面はなぜサインアウトされたのかを表示する。
クライアントルーティングにしないのは、失効済みのセッションで取ったキャッシュを
持ち越さないため。
入力チェックは lib/auth-methods.ts の validatePasswordForm。8 文字以上、確認との一致、
変更のときだけ「現在のパスワードが空でない」「現在と同じ値でない」。強度表示は
サインアップと同じ PasswordStrengthBar を使う。
OAuth 連携
-
連携済み:
GET /v1/auth/oauth/connections。プロバイダー名、provider_email、接続日時、 self-hosted のinstance_urlを出す。解除は確認を挟んでDELETE /v1/auth/oauth/connections/{provider}(self-hosted はinstance_urlをクエリに添える) -
追加できる連携:
GET /v1/auth/oauth/providersのうち未連携のもの。連携済みかどうかは 開始用 slug ではなくconnection_provider(連携一覧が返す識別子)で突き合わせる。 汎用 OIDC は開始用 slug がoidc、連携一覧の識別子がoidc:{issuer}で形が違い、 slug で比べると連携済みでも候補に残り続ける。ただしrequires_instance_urlのプロバイダー(GitLab セルフホスト)は、インスタンスが違えば 別の連携として足せるので、1 件連携済みでも候補に残す。連携済みと同じインスタンス URL を 入れた場合は開始しない(backend も(provider, instance_url)の重複を 409 で弾く)。 「連携する」で既存の OAuth 開始 URL へフルページ遷移する。redirect_afterにはこの画面の パスだけを渡す。プロバイダー側のエラーは backend が?oauth_error=を付けて同じ画面に戻す
開始 URL の組み立てとプロバイダー表示名は、サインイン画面の OAuthButtons と共通の
lib/oauth-providers.ts に置く。片方だけ直すと画面ごとに名前や戻り先が食い違うため。
成功通知の根拠
「連携しました」「パスワードを変更しました」は、URL のクエリではなく lib/one-time-notice.ts
の印(sessionStorage)を根拠に出す。クエリだけで判定すると、その URL を開かせるだけで
連携していない人に「連携しました」を、変更していない人に「失効しました」を読ませられる。
印は操作を始める側が置き、戻ってきた画面が 1 回だけ消費する。連携についてはさらに、
GET /v1/auth/oauth/connections にその接続が実際に入っていることまで確かめてから出す
(承認の途中で失敗してこの画面に戻らなかったとき、印だけが残って次の来訪で誤って出るのを防ぐ)。
印にはプロバイダー名だけでなく connection_provider とインスタンス URL も入れて、
開始したその接続が入ったことを確かめる。プロバイダー名だけで比べると、GitLab セルフホストの
インスタンス A を連携済みのまま B の承認を中断した人に、A を見て「連携しました」が出る。
最後の認証方法
利用できる認証方法が 0 件にならないよう、backend は「その連携が最後の 1 件 かつ
パスワード無し かつ パスキー無し」のとき解除を 403 oauth-last-auth-method で拒む。
画面はこの判定を先取りして注意書きを出すだけで、可否はサーバーの応答に従う
(画面が数えた後に別のタブで増減されうるため)。先取りの数え方(countAuthMethods)は
backend と揃えてパスキーも数えるので、この画面は件数を見る目的で GET /v1/auth/passkeys も呼ぶ。
UserResponse.has_password
パスワードの有無で表示を切り替えるために GET /v1/auth/me(UserResponse)へ
has_password: bool を追加した。ハッシュそのものは返さない。