背景・課題

現状 (Plan 1.5 で確立した claim 動線)

  1. 受付スタッフが事前に紙カルテからシステムへ User を登録 (provider: manual)
  2. 患者が LINE/Google でログイン → PendingClaimToken 発行
  3. /onboarding/claim で 3 要素 (電話・フリガナ・生年月日) を入力
  4. 一致したカルテに OAuth 紐付け、不一致なら 401

取りこぼし問題

アナログ管理から移行する際に受付負荷が極めて高い

運用上の前提

アナログカルテはそもそも電子カルテと紐付いていない (独立した紙運用)。
新システム導入の際、患者に「導入後から予約履歴を蓄積する」と周知すれば、過去予約の移行は不要。
LINE 公式アカウントから予約導線を流すケースが大半 → LINE 一本化が業務に適合。

設計方針

振る舞い (大幅シンプル化)

LINE ログイン → /onboarding でプロフィール入力 → 常に新規 User 作成 → Home

突合ロジック完全撤廃。受付運用とシステムを独立させる。

項目現状新仕様
ログイン手段LINE / GoogleLINE のみ
既存カルテとの突合3 要素一致 (phone / kana / dob)しない (常に新規 User 作成)
claim_failed 401あり廃止
possibly_existing_user 等の親切エラー(検討対象)不要
受付の事前登録要件必須不要 (任意・staff-web で続行可)

割り切り事項

項目影響
既存 provider: manual Userデータ残置。誰とも紐付かない「孤立カルテ」扱い。staff-web からは見える
「前回施術」カードLINE 連携後の初回予約後から表示 (2 回目以降)
患者から見える予約履歴LINE 連携後のみ
アナログ時代の予約参照紙カルテで対応、staff-web 上は別 User として表示

将来必要になれば 「LINE 連携 User と旧 manual User をマージする管理画面」Plan 4-D-2 として別途実装する。本仕様の MVP には含めない。

API 仕様

新規エンドポイント: POST /api/v1/auth/oauth/register

oauth/claimリネーム + 動作変更。Controller も OauthRegistrationController へ。

Request

POST /api/v1/auth/oauth/register
Content-Type: application/json

{
  "pending_claim_token": "<10分有効、LINE callback で発行>",
  "full_name": "山田太郎",
  "kana": "ヤマダタロウ",
  "phone": "09011112222",
  "date_of_birth": "1990-01-01"
}

Response

Statusbody説明
200 { user: { id, full_name, role: "user" } } 新規 User 作成成功 + JWT cookie 発行
401 { error: "claim_token_expired", message: "LINE ログインからやり直してください" } トークン期限切れ
422 { error: "validation_failed", details: { phone: ["..."], kana: ["..."] } } 入力 validation 失敗
429 { error: "rate_limited", message: "しばらく待ってから..." } レート制限

廃止される評価分岐

評価ロジック (擬似コード)

def create
  # Step 1: レート制限 (phone × ip_address)
  return rate_limited if OauthClaimRateLimiter.over_limit?(...)

  # Step 2: token decode
  payload = decode_claim_token  # → expired / invalid → 401

  # Step 3: validation (Zod / strong params で必須項目検証)
  attrs = registration_params  # → 不足/不正 → 422

  # Step 4: 新規 User 作成 + 紐付け
  user = User.create!(
    full_name: attrs[:full_name],
    full_name_kana: attrs[:kana],
    phone: attrs[:phone],
    date_of_birth: Date.parse(attrs[:date_of_birth]),
    provider: payload[:provider],  # "line"
    uid: payload[:uid],
    role: :user,
    onboarded: true
  )

  # Step 5: ログ + JWT 発行
  OauthLinkAttemptLogger.log!(result: "success_new", ...)
  set_jwt_cookie(encode_jwt(user))
  render json: { user: user_payload(user) }, status: :ok
end

データモデル

新規 User の固定値

カラム
role:user
onboardedtrue
provider:line
uidLINE userId
emailnil (LINE は email 取得しない)
patient_numbernil (来院時に受付が付与)
full_name / full_name_kana / phone / date_of_birth患者入力

enum 変更

enum :provider, { line: 0, google: 1, email: 2, manual: 3 }

残置 google: 1 は将来再導入余地、migration 不要

UI フロー

Before / After

Before: 本人確認 (claim)

← クリニック予約👤

本人確認

受付で登録された情報と一致するか確認します。
09012345678
ヤマダ タロウ
1990 / 01 / 01

既存患者前提・氏名なし・
不一致なら 401

After: ご利用登録

← クリニック予約👤

ご利用登録

ご予約のためにお名前と連絡先をご登録ください。
山田 太郎
ヤマダ タロウ
09012345678
1990 / 01 / 01

セルフ登録・氏名追加・
常に新規作成成功

エラーバナー

ケースメッセージ
validation_failed「入力内容に誤りがあります。ご確認ください」+ フィールド単位エラー
claim_token_expired「セッションが切れました [ログインへ]」
rate_limited「しばらく待ってから再度お試しください」
unknown「通信エラーが発生しました。再度お試しください」

ログイン画面 (/login) の文言変更

- 受付の方が登録した後、ご自身の LINE / Google で
-   ログインしてください。
+ LINE アカウントでログインしてください。
+   初めての方はそのままご登録いただけます。

LINE 一本化 (Google 動線撤去)

削除対象

ファイル内容
apps/user-web/src/components/auth/OAuthButtons.tsxGoogle ボタン
apps/api/config/routes.rbgoogle/callback ルート
apps/api/app/controllers/api/v1/auth/omniauth_callbacks_controller.rbdef google_oauth2 メソッド
apps/api/config/initializers/omniauth.rbprovider :google_oauth2, ...

残置

セキュリティ・運用

レート制限

既存 OauthClaimRateLimiter再利用: phone × ip_address ベース、24h で 5 回。新規登録もブルートフォース対象になり得るので維持。

監査ログ

既存 OauthLinkAttemptLogger再利用。新 result 種別 success_new を追加。受付スタッフが「LINE セルフ登録された患者」を staff-web 上でフィルタできる (Plan 4-D-2 で UI 化)。

なりすまし耐性

突合ロジック撤廃により 「他人の情報で誰かのカルテを覗く」経路は存在しない。LINE 連携 = 個人 1 アカウント、phone は自己申告。

テスト方針

API

Web

E2E

「LINE 連携 → 登録 → 初回予約 → 確定」のフルパス (mock または dev seed)

スコープ外 (別 plan)

関連参照

参照ファイル一覧