Skip to content

Latest commit

 

History

History
199 lines (148 loc) · 21.5 KB

File metadata and controls

199 lines (148 loc) · 21.5 KB

Web アプリ仕様

対象: apps/web

画面

パス 内容 要ログイン
/login ログイン / アカウント作成 (画面内で切り替える)。ログイン済みなら / に移動 いいえ
/ タスクのカンバンボード (追加、編集、列の移動と並べ替え、削除)。右上にメールアドレスとログアウト はい
/tasks/:id タスクの詳細 (全画面)。タイトル、ステータス、締め切り、リマインド、本文の編集、添付、共有、削除 はい
  • / はセッションがなければ /login に移動する。未ログインの / の HTML にタスク画面は含まれない。
  • 実装: src/routes/、純粋な関数は src/lib/。

認証

実装: src/server/auth.ts (better-auth)

  • メールアドレスとパスワードで登録・ログインする。パスワードは 8 文字以上。
  • セッションは Cookie で管理し、D1 の sessions テーブルに保存する。
  • エンドポイントは /api/auth/*。

設定

名前 内容
BETTER_AUTH_SECRET 必須。長いランダム文字列 (openssl rand -hex 32)
BETTER_AUTH_URL 任意。既定は http://localhost:8787

ローカルでは apps/web/.dev.vars に書く。本番は Cloudflare のシークレットに設定する。

GitHub でのログイン (#2)

実装: src/server/auth-config.ts、src/server/auth.ts、ログイン画面は src/routes/login.tsx。

  • GITHUB_CLIENT_ID と GITHUB_CLIENT_SECRET の両方がある環境でだけ有効になる。ない環境 (ローカルで書いていないとき、プレビューなど) では、ログイン画面に「GitHub でログイン」のボタンが出ない。環境の名前で分けるのではなく、設定の有無で分ける。
  • ボタンを出すかどうかは、サーバーの GET /api/config ({ "github": true | false }) で決める。キーやシークレットは返さない。
  • GitHub のアカウントのメールアドレス (確認済みのもの) で、アカウントを作る。
  • 同じメールアドレスのメール/パスワードのアカウントが、すでにある場合は、GitHub でのログインを断る。 better-auth は、既存のアカウントのメールアドレスが確認済みのときだけ、GitHub のアカウントを、ひとつにまとめる (requireLocalEmailVerified)。このアプリのメール/パスワードのアカウントは、メールの確認をしていないので、まとめない。まとめてしまうと、他人のメールアドレスで先に登録しておき、あとから本人が GitHub でログインしたときに、乗っ取れる (アカウントの事前乗っ取り)。断られた人には、ログイン画面に「このメールアドレスは、すでにメールとパスワードで登録されています」と出す (/login?error=unable_to_link_account)。メールの確認ができるようになったら (#8、#42)、まとめられるようにできる。
  • GitHub のアカウントにメールアドレスがない、または確認されていないときも、ログイン画面に理由を出す。
  • 共有の招待 (アカウントがないメールアドレスへの共有) は、GitHub でアカウントを作ったときにも、メール/パスワードのときと同じように反映される。
  • 取得する権限は、read:user と user:email (メールアドレスを知るため)。

GitHub 側の準備 (GitHub の画面での作業): ut-code の org (または個人のアカウント。あとから org に移せる) の Settings → Developer settings → OAuth Apps → New OAuth App で、アプリを 1 つ作る。GitHub の OAuth App は、2026 年 8 月から、戻り先 (Authorization callback URL) を最大 10 個まで登録できるので、本番とローカルを同じアプリに登録する。

項目 値
Homepage URL https://kanban-web.ut-code.workers.dev
Authorization callback URL (2 つ) https://kanban-web.ut-code.workers.dev/api/auth/callback/github、http://localhost:8787/api/auth/callback/github
Allow wildcard matching オフ (両方の URL)
Enable Device Flow オフ (CLI などの用。使わない)
Expire user access tokens オフでよい (ログインのときに 1 回、プロフィールを読むだけで、あとで GitHub の API を呼ばない)

できたクライアント ID と、「Generate a new client secret」で作ったシークレットを、環境に入れる。シークレットは、チャットやコミットに書かない。

# 本番 (ut-code のアカウント)
export CLOUDFLARE_ACCOUNT_ID=df6c3acd32f66bd1eb95e50607684297
pnpm --filter @todo/web exec wrangler secret put GITHUB_CLIENT_ID
pnpm --filter @todo/web exec wrangler secret put GITHUB_CLIENT_SECRET
# ローカル: apps/web/.dev.vars に GITHUB_CLIENT_ID と GITHUB_CLIENT_SECRET を書く

プレビューでは使えない: プレビューは、ブランチごとに URL が違う (<ブランチ名>-kanban-web.ut-code.workers.dev)。戻り先の「ワイルドカード」は、登録した URL のサブドメインを許すだけで、この URL は兄弟のホスト名なので合わない。全部を受け付けるには ut-code.workers.dev そのものを登録することになり、ut-code のアカウントのほかの Worker にも認可コードが渡りうるので、しない。ブランチごとのプレビューでは、メール/パスワードで確かめる。プレビューの設定には、キーを入れない (入れなければボタンが出ない)。better-auth の oAuthProxy で、本番を経由させる方法もあるが、本番とプレビューで暗号化のシークレットを共有するので、必要になるまで使わない。

予定

  • メールアドレスの確認とパスワードリセットは、現時点では扱わない。

タスク

データ

項目 内容
id UUID。サーバーが採番
userId 所有者。所有者と、共有された人だけがタスクを見られる
title 前後の空白を除いて 1〜200 文字
position 列の中の順番。小さいほど上。ほかの 2 枚の間に入れたときは、2 つの値の中間になる
description 本文。Markdown で保存する。20,000 文字まで。単体の取得 (GET /api/tasks/:id) でだけ返し、一覧には含めない
descriptionVersion 本文の版。本文を保存するたびに 1 つ増える。競合を見つけるために使う
status ボードの列。todo (未着手、既定)、doing (進行中)、done (完了)
dueAt 締め切り。任意。UTC の ISO 8601 で保存
remindBeforeMinutes 締め切りの何分前にメールでリマインドするか。null はリマインドなし (既定)
completedAt 完了日時。status が done のときだけ値が入る

入力のルール

実装: src/lib/task-input.ts

  • タイトルは前後の空白を取り除き、1〜200 文字でなければ 400 (タイトルは1〜200文字で入力してください。)。
  • 締め切りは日時として解釈できなければ 400 (締め切りの日時が正しくありません。)。空文字・未指定は「締め切りなし」(null)。タイムゾーン付きの値は UTC に直して保存する。
  • status を done にすると完了日時が現在時刻になり、todo / doing に戻すと null に戻る。done のまま done を指定しても、最初の完了日時は変わらない。
  • status は todo / doing / done のどれか。それ以外は 400。

リマインドの設定

  • 既定はオフ。オンにすると、締め切りの「1 日前」(1440 分前) が選ばれる。選択肢は 1 時間前、3 時間前、1 日前、2 日前、3 日前、1 週間前。API では 1〜43200 分 (30 日) の整数を指定できる。
  • 締め切りがないタスクには設定できない (400)。締め切りを消すと、リマインドもオフになる。
  • 設定できるのは所有者だけ (共有された人は 403)。
  • 送信の仕組みは ワーカー仕様 を参照。

カンバンボード

  • 「未着手」「進行中」「完了」の 3 列で、各カードがタスク 1 件。列の見出しに件数を出す。
  • カードを別の列にドラッグすると、status が変わる。変更は画面にすぐ反映し、サーバーへの保存に失敗したら一覧を取得し直して元に戻す。
  • ドラッグが使えない環境 (キーボード、タッチ) のために、カードにステータスの選択も付けている。
  • 画面が狭いとき (スマホ) は、3 列が縦に並ぶ。ページの幅は端末の幅に合わせ、横にスクロールしない。
  • 列の中は、各タスクの position の小さい順に並ぶ。新しく作ったタスクは「未着手」の先頭に入る。
  • カードを同じ列の別のカードの前後や、別の列のカードの前後にドラッグすると、その位置に入る (落とす位置の線が出る)。カードの上以外 (列の空いている所) に落としたときと、カードのステータスの選択で列を変えたときは、移動先の列の末尾に入る。
  • 順番は、共有されたタスクも含めて全員で共通。共有された人も並べ替えられる。
  • 共有された人のカードにも、同じように操作できる (削除と添付とリマインドと共有は所有者だけ)。

全画面の詳細ページ

  • カードの「開く」か、カードのタイトルから /tasks/:id を開く。URL を直接開いても同じページが表示される (ログインが必要)。
  • ボードのカードでできることは、すべてここでもできる。添付と共有は、カードのパネルではなくページに並ぶ。
  • 共有された人にも開ける。削除、リマインド、添付、共有の操作は所有者だけで、共有された人には「共有から外れる」が出る。
  • 他の人が編集、共有の解除、削除をしたら、このページにも反映する。見つからないタスクは「タスクが見つかりません」と表示する。

本文 (WYSIWYG エディタ)

  • 全画面の詳細ページに、本文の欄がある。見たまま編集できる (WYSIWYG)。見出し、太字・斜体・取り消し線、箇条書き、番号付き、チェックリスト、引用、コード、リンクが使える。Markdown の記法 (## で見出し、[ ] でチェックリストなど) でも書ける。実装: src/components/description-editor.tsx。
  • Markdown で保存する。エディタが扱える要素だけが残るので、生の HTML は保存されず、表示でもスクリプトは動かない。リンクは rel="noopener noreferrer nofollow" で別タブに開く。
  • 「本文を保存」で保存する。未保存の変更があるあいだは、その旨を表示する。
  • 本文は、後勝ちではなく、版で競合を見つける。保存するときに、読み込んだときの版 (descriptionVersion) を送り、サーバーの版と違えば 409 を返して、何も変えない。他の人の本文を黙って消さないため。
  • 他の人が本文を保存したら (ライブ通知で分かる)、自分に未保存の変更がなければ、その本文を自動で取り込む。未保存の変更があるときは取り込まずに警告を出し、「最新を読み込む」を押すと、自分の変更を捨てて最新の本文にする。
  • 自分で保存したときは、エディタを作り直さない (カーソルが動かない)。
  • 共有された人も、本文を編集できる。
  • カードには本文を出さない。
  • 文字単位のマージ (同時に同じ段落を書いても両方残す) はしない。

API

すべて要ログイン。未ログインは 401。

メソッド パス 内容 成功
GET /api/tasks 自分のタスクと、共有されたタスクの一覧 200
GET /api/tasks/:id タスク 1 件 (自分のタスクか、共有されたタスク) 200
POST /api/tasks 作成。body: { title, dueAt?, remindBeforeMinutes? } 201 (作成したタスク)
PATCH /api/tasks/:id 一部の項目を変更。body: { title?, dueAt?, remindBeforeMinutes?, status?, position?, description?, descriptionVersion? } (1 つ以上。description には descriptionVersion が必要) 204 (本文を保存したときは 200 と { descriptionVersion }、古い版からなら 409)
DELETE /api/tasks/:id 削除 (所有者のみ) 204
  • 自分のタスクでも、共有されたタスクでもないものを PATCH / DELETE すると 404。共有されたタスクの DELETE も 404 (所有者のみ)。
  • GET /api/tasks には、自分のタスクと共有されたタスクの両方が含まれる。
  • JSON でない body は 400、未対応のメソッドは 405、未知のパスは 404。

ファイル添付 (R2)

タスクにファイルを添付できる。実装: src/server/attachments/、制限や名前の処理は src/lib/attachment.ts。

  • 1 ファイル 5 MB まで、1 タスクにつき 5 件まで。空のファイルは不可。種類の制限はない。
  • 添付できるのは、タスクの所有者だけ。一覧、ダウンロード、削除も所有者だけができる (他のユーザーには 404)。
  • R2 のキーは <userId>/<taskId>/<attachmentId>。キーは API では返さない。
  • ダウンロードは常に Content-Disposition: attachment で、X-Content-Type-Options: nosniff と Cache-Control: private, no-store を付ける。ブラウザ上で添付ファイルの中身が実行されないようにするため。
  • タスクを削除すると、添付ファイルも R2 から削除する。
  • 画面では、各タスクの「添付」ボタンから一覧、追加、削除ができる。
メソッド パス 内容 成功
GET /api/tasks/:id/attachments 添付の一覧 200
POST /api/tasks/:id/attachments 追加。multipart/form-data の file 201
GET /api/attachments/:id ダウンロード 200
DELETE /api/attachments/:id 削除 204

共有と同時編集 (Durable Objects)

タスクを他のユーザーと共有して、同時に編集できる。実装: src/server/tasks/members-handler.ts、src/server/live.ts、src/durable-object.ts。

共有

  • 所有者が、相手のメールアドレスで共有する。自分自身とは共有できない。
  • アカウントがあるアドレスは、すぐに共有される。アカウントがないアドレスは「招待」として保存され、そのアドレスでアカウントを作った (メール/パスワードでも GitHub でも) ときに共有される。
  • 共有の API は、アドレスにアカウントがあるかどうかを答えない。 どちらの場合も同じ応答 (201 と共有先の一覧) を返し、一覧にも両者を区別せずにメールアドレスだけを並べる。ログイン済みなら誰でも、アカウントの有無を調べられる、という状態を避けるため。
  • 新しく共有したときだけ、相手にメールを送る (ワーカー仕様)。すでに共有している相手に共有し直しても、メールは送らない。
  • 共有された人は、タイトル・締め切り・ステータス・本文を編集し、カードを並べ替えられる。削除、リマインドの設定、添付ファイル、共有の操作は所有者だけ。リマインドは所有者にだけ届く。
  • 共有を解除できるのは、所有者 (誰でも) と、共有された本人 (自分から外れる)。招待中のアドレスも、所有者が解除できる。
  • 共有先の一覧は、所有者と共有された人が見られる。
メソッド パス 内容 成功
GET /api/tasks/:id/members 共有先の一覧 [{ email }] 200
POST /api/tasks/:id/members 共有する (所有者のみ)。body: { email } 201 (共有先の一覧)
DELETE /api/tasks/:id/members/:email 共有を解除 (所有者か本人)。:email は URL エンコード 204

同時編集の方針

  • 編集は項目ごとに保存する。同時に別の項目を編集しても、両方の変更が残る (例: 一方がタイトル、もう一方が締め切り)。
  • 同じ項目を同時に編集したときは、あとから保存したほうが勝つ (last write wins)。ただし本文は別で、版で競合を見つけて、他の人の変更を黙って消さない (上の「本文」)。
  • 文字単位のマージ (同じ段落を同時に書いても両方残す) はしない。

リアルタイムの反映

  • ブラウザは /api/live に WebSocket で接続する。接続は Durable Object (CollaborationRoom) の、ユーザーごとの部屋につながる。
  • タスクが変更されると (作成、編集、完了、削除、共有、共有の解除)、そのタスクを見られる全員 (所有者と共有された人。共有を解除された人も含む) の部屋に、{"type":"tasks.changed"} を送る。
  • 通知を受けた画面は、タスク一覧を取得し直す。通知が届かなくても、画面を開き直せば最新になる。
  • 接続が切れたら、1 秒から最大 30 秒の間隔で再接続し、再接続したら一覧を取得し直す。