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