Skip to content

Commit 70f0808

Browse files
Merge pull request #1094 from smalruby/feature/admin
feat(admin): 運営者向け管理 SPA — みんなの課題/クラス管理/バグ報告閲覧 (EPIC #1073)
2 parents b78d13f + 53216e9 commit 70f0808

52 files changed

Lines changed: 11106 additions & 29 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/infra/development.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ AWS CDK infrastructure projects live in `infra/`. Each project is independent fr
1818
| smalruby-classroom | `infra/smalruby-classroom/` | Classroom service (API Gateway + Lambda + DynamoDB + S3) |
1919
| smalruby-api | `infra/smalruby-api/` | Smalruby API endpoints (HTTP API v2 + Lambda): cors-proxy, mesh-domain, scratch-api-proxy/* |
2020
| smalruby-bug-report | `infra/smalruby-bug-report/` | Program bug report service (HTTP API v2 + Lambda + DynamoDB + S3): 作品添付つき不具合報告 + 管理者レジストリ |
21+
| smalruby-admin | `infra/smalruby-admin/` | Admin service (HTTP API v2 + Lambda + DynamoDB): 管理 SPA のバックエンド(deny-by-default 許可リスト + 監査ログ) |
2122

2223
See project-specific rules for details:
2324
- `.claude/rules/infra/smalruby-mesh-v2.md`
2425
- `.claude/rules/infra/smalruby-classroom.md`
2526
- `.claude/rules/infra/smalruby-rubytee-relay.md`
2627
- `.claude/rules/infra/smalruby-api.md`
2728
- `.claude/rules/infra/smalruby-bug-report.md`
29+
- `.claude/rules/infra/smalruby-admin.md`
2830

2931
## Docker Service
3032

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
paths:
3+
- "infra/smalruby-admin/"
4+
- "infra/smalruby-admin/**"
5+
- "infra/smalruby-admin/**/*"
6+
---
7+
8+
# smalruby-admin
9+
10+
CDK project for the Admin service (API Gateway + Lambda + DynamoDB) — 管理 SPA(`packages/admin`)のバックエンド。**最高権限の面**なので変更時は以下を厳守する。
11+
12+
## 不変条件(セキュリティ)
13+
14+
- **deny-by-default**: 認可は `SmalrubyAdmins{suffix}` テーブル(PK: `email`・RETAIN)への存在照合のみ。登録は AWS コンソール手動操作が唯一の経路 — **アプリ内に管理者管理 API/UI を作らない**(EPIC #1073 F4)
15+
- **sub 固定**: 初回ログインで Google `sub` を行に固定(`ConditionExpression: attribute_not_exists`)。以後 email 一致でも sub 不一致は 403。この防御を外さない
16+
- **admin 専用 Google Client ID**`ADMIN_GOOGLE_CLIENT_ID`)で `aud` 検証。エディタの `GOOGLE_CLIENT_ID` と共用しない。**prod は未設定だと stack が throw**(このガードを外さない)
17+
- `DEV_BYPASS_TOKEN` は stg のみ(prod 設定で throw、classroom/bug-report と同じガード)
18+
- **全変更操作に `audit()`**(構造化ログ)。prod の LogGroup retention は **ONE_YEAR**(艦隊標準の ONE_MONTH からの意図的逸脱 — 監査記録のため)
19+
- エラー規約は bug-report 準拠: 認証失敗 401 / 認可失敗 **403**(classroom の 401 とは異なる)
20+
21+
## DoS / コスト防御(ソース公開前提の脅威モデル)
22+
23+
ソースは公開されるため攻撃者は既知エンドポイント・ルート・env 変数名を把握している前提で設計する。
24+
25+
- **prod は API Gateway の JWT authorizer**(issuer `https://accounts.google.com` / audience = `ADMIN_GOOGLE_CLIENT_ID`)で、正当な署名トークンでない要求を **Lambda 到達前に 401** で弾く。認証なしフラッドで Lambda 起動・Lambda ログ ingestion 課金を発生させない。**stg は dev bypass(非 JWT)を使う E2E のため authorizer を付けない**(この stage 差を消さない)。Lambda 側の fail-closed 認可は両 stage で維持(多層)
26+
- **スロットルは単一運用者向けに絞る**(rate 5 / burst 10)。緩めない
27+
- **X-Ray(トレーシング)を有効化しない****API Gateway アクセスログを有効化しない**(コスト源。現状 Lambda は全て PassThrough)
28+
- Lambda ログは監査行と 500 のみ。**401/403/404/400 パスにアプリログを足さない**(DoS 時のログ ingestion 課金を防ぐ)
29+
- `dev-admin@example.com`**stg の allowlist のみ**。prod に登録しない
30+
31+
## cross-service アクセス
32+
33+
管理対象(classroom / shared-assignments のテーブル・バケット)へは**ステージ別の名前規約**で ARN を構築して grant する。**classroom スタック側に変更を加えない**(N2)。バグ報告ドメインは既存 bug-report API を SPA から直接呼ぶ(本スタックは関与しない)。
34+
35+
## 運用
36+
37+
- 管理者登録・監査ログ検索・デプロイ手順: `docs/admin/operations.md`
38+
- ステージ切替は `.env` symlink(`.claude/rules/infra/development.md` 共通規約)

.github/workflows/ci-cd.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ jobs:
277277
GTM_ID: ${{ github.ref == 'refs/heads/develop' && secrets.GTM_ID || '' }}
278278
GTM_ENV_AUTH: ${{ github.ref == 'refs/heads/develop' && secrets.GTM_ENV_AUTH || '' }}
279279
COMMIT_SHA: ${{ github.sha }}
280+
# Admin SPA (packages/admin — EPIC #1073). Register these in
281+
# Settings > Secrets and variables > Actions > Variables.
282+
ADMIN_API_ENDPOINT: ${{ vars.ADMIN_API_ENDPOINT }}
283+
ADMIN_GOOGLE_CLIENT_ID: ${{ vars.ADMIN_GOOGLE_CLIENT_ID }}
280284
run: npm run build
281285
- name: Generate version.json
282286
run: |
@@ -304,6 +308,20 @@ jobs:
304308
full_commit_message: "Build for ${{ github.sha }} ${{ github.event.head_commit.message }}"
305309
cname: smalruby.app
306310
external_repository: smalruby/smalruby.app
311+
# Admin SPA at smalruby.app/admin/ (EPIC #1073). Must run AFTER the
312+
# main deploy: that step replaces the gh-pages content, so the admin
313+
# subdirectory is re-published on every release (destination_dir +
314+
# keep_files is the same pattern the branch previews use).
315+
- name: Deploy admin SPA to smalruby.app/admin/
316+
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4
317+
if: github.ref == 'refs/heads/develop'
318+
with:
319+
deploy_key: ${{ secrets.SMALRUBY_APP_DEPLOY_KEY }}
320+
publish_dir: ./packages/admin/build
321+
destination_dir: admin
322+
keep_files: true
323+
full_commit_message: "Admin build for ${{ github.sha }}"
324+
external_repository: smalruby/smalruby.app
307325
- name: Rebuild for smalruby3-editor GitHub Pages
308326
if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
309327
env:

docs/admin/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# 管理 SPA(Smalruby Admin)
2+
3+
> **🆕 Smalruby 独自** — upstream に存在しない、運営者向けの管理コンソール(EPIC #1073)。
4+
5+
`https://smalruby.app/admin/` で提供される、**エディタとは完全に別の SPA**。みんなの課題のモデレーション、全ユーザーのクラス・課題の管理(期限切れクラスの復元を含む)、バグ報告の閲覧を 1 画面に集約する。
6+
7+
基本的に単一運営者(管理者はシステム外で AWS を直接操作して登録)で使う前提の、最小・最強権限の面。
8+
9+
## 全体像
10+
11+
| 構成要素 | 場所 | 役割 |
12+
|---|---|---|
13+
| SPA | `packages/admin/` | 独立 React アプリ(scratch-gui とは別ビルド)。GitHub Pages の `destination_dir: admin``smalruby.app/admin/` に配信 |
14+
| バックエンド | `infra/smalruby-admin/` | HTTP API v2 + Lambda + DynamoDB(`SmalrubyAdmins` 許可リスト)。`admin.api.smalruby.app`(stg は `stg.admin.api.smalruby.app`|
15+
| バグ報告(閲覧のみ) | 既存 `infra/smalruby-bug-report/` | Admin スタックは関与せず、SPA が既存 bug-report admin API の **read 系のみ**を直接呼ぶ |
16+
17+
### セクション(3 ドメイン)
18+
19+
1. **みんなの課題**(S3 #1083): 通報キュー(多い順)/ 全投稿一覧 / 詳細(ページ・画像・クレジット)/ 非公開⇄再公開(2 段階確認・audit)
20+
2. **クラス・課題**(S4 #1084): 全クラス検索(参加コード完全一致・名前部分一致)/ 詳細(参加・提出カウント)/ アーカイブ切替 / **期限切れクラスの復元**(ddb-archive スナップショット検索 → dry-run プラン → 実行。EPIC #1049 の CLI `bin/restore-classroom.ts` の UI 後継)
21+
3. **バグ報告**(S5 #1085 + 対応機能追加): 既存バグ報告の一覧・状態フィルタ・詳細・添付 presigned DL に加え、**状態の変更と進捗コメント(開発者からの返信)**を既存 bug-report admin API の PATCH で行える(2 段階確認・終端ステータスは自動削除 TTL の警告つき。返信は報告者の「私の不具合報告」に表示され、非表示にしていた報告も再表示される — サーバー側の既存挙動)。詳細には**状態に応じた Claude 連携プロンプト**`/bug-report` スキル向け・受付→Issue 化 / 改修 / 解決返信 / 再開)が表示され、ワンクリックでコピーして Claude Code に貼り付けられる
22+
23+
## 認証・認可モデル(要点)
24+
25+
- **admin 専用 Google OAuth Client ID**(エディタと共用しない・決定 B)。バグ報告 API へは同じトークンで到達できるよう、bug-report Lambda が `ADMIN_GOOGLE_CLIENT_ID` を追加 audience として受理する(決定 F。email レジストリのゲートは不変)
26+
- **deny-by-default 許可リスト**: `SmalrubyAdmins` テーブル(PK: email、RETAIN)への存在照合のみ。登録は AWS コンソール手動操作が唯一の経路(アプリ内に管理者管理 UI は無い・F4)
27+
- **sub 固定**: 初回ログインで Google `sub` を行に固定。以後 email 一致でも sub 不一致は 403(email 再利用防御)
28+
- トークンは**モジュールメモリのみ**(localStorage に保存しない・N1)。リロード時は再ログイン
29+
- **セッション切れ(約 1 時間)**: API が 401 を返すと全画面の再読み込みプロンプトを表示。表示中のセクションは **URL ハッシュ**`#/classrooms` 等)に保持しているため、再読み込み → 再ログイン後に元のセクションへ復帰する(localStorage 不使用)
30+
- すべての管理操作は `audit()` 構造化ログ(prod の CloudWatch 保持は **1 年**
31+
- バグ報告の閲覧・対応は bug-report 側の既存レジストリ `BugReportAdmins`(email)で認可される(両方への登録が必要)
32+
33+
詳細な登録手順・監査ログ検索・デプロイ手順は [operations.md](operations.md)
34+
35+
## 主要ファイル
36+
37+
### packages/admin(SPA)
38+
39+
| ファイル | 役割 |
40+
|---|---|
41+
| `src/components/app.jsx` | ルート(Google Sign-In → `/admin/me` 認可プローブ → セクションナビ + 各ビュー) |
42+
| `src/components/shared-assignments-view.jsx` | みんなの課題モデレーション |
43+
| `src/components/classrooms-view.jsx` | クラス・課題管理 + 期限切れ復元 |
44+
| `src/components/bug-reports-view.jsx` | バグ報告閲覧(read-only) |
45+
| `src/lib/admin-api.js` | admin API クライアント(トークンはモジュールメモリ) |
46+
| `src/lib/bug-report-api.js` | bug-report API クライアント(一覧・詳細 + 状態/返信の PATCH。既存 API のみ使用) |
47+
| `src/lib/bug-report-prompts.js` | 状態別の Claude 連携プロンプト生成(`/bug-report` スキルにワンクリックコピーで渡す) |
48+
| `src/lib/google-auth.js` | GIS ロード + `?devlogin=` バイパス(stg のみ) |
49+
| `webpack.config.js` | 独立ビルド(publicPath `/admin/`、port 8602、DefinePlugin で endpoint 埋め込み) |
50+
51+
### infra/smalruby-admin
52+
53+
| ファイル | 役割 |
54+
|---|---|
55+
| `lambda/handler.ts` | 認証(aud 検証)→ 認可(許可リスト + sub 固定)→ 各ドメインのハンドラ + `audit()` |
56+
| `lambda/restore-plan.ts` | 復元プランの純関数(classroom の `restore-lib.ts` と意図的に複製。形式変更時は両方を同期) |
57+
| `lib/admin-stack.ts` | SmalrubyAdmins(RETAIN)、Lambda、HTTP API、カスタムドメイン。classroom / shared のテーブル・バケットは**名前規約で import して grant**(classroom スタック不変・N2) |
58+
59+
## 設定
60+
61+
| 変数 | 場所 | 説明 |
62+
|---|---|---|
63+
| `ADMIN_GOOGLE_CLIENT_ID` | `infra/smalruby-admin/.env.*``infra/smalruby-bug-report/.env.*`・repo Variables | admin 専用 OAuth Client ID。**prod の admin スタックは未設定だとデプロイが落ちる** |
64+
| `ADMIN_API_ENDPOINT` | repo Variables(CI ビルド埋め込み) |`https://admin.api.smalruby.app` |
65+
| `BUG_REPORT_API_ENDPOINT` | root `.env` / repo Variables | 既存バグ報告 API |
66+
| `DEV_BYPASS_TOKEN` | `.env.stg` のみ | 自動テスト用(prod 設定は throw) |
67+
68+
## ローカル開発・E2E
69+
70+
```bash
71+
# SPA dev server(8602)。webpack は dotenv を読まないので env を明示
72+
cd packages/admin
73+
ADMIN_API_ENDPOINT=https://stg.admin.api.smalruby.app \
74+
BUG_REPORT_API_ENDPOINT=https://stg.bug-report.api.smalruby.app \
75+
npm start
76+
77+
# E2E(stg API・dev bypass。tools/playwright-verify/README.md 参照)
78+
cd tools/playwright-verify && node verify-admin.mjs
79+
```
80+
81+
- `http://localhost:8602/admin/?devlogin=<DEV_BYPASS_TOKEN>` で stg ログインをバイパス(バイパス identity `dev-admin@example.com` の allowlist 登録が前提)
82+
- ユニットテスト: `cd packages/admin && npm test`(eslint + jest)/ `cd infra/smalruby-admin && npm test`
83+
84+
## スクリーンショット
85+
86+
| ファイル | 内容 |
87+
|---|---|
88+
| `screenshots/0101-login.png` | ログイン画面(Google Sign-In) |
89+
| `screenshots/0102-shared-queue.png` | みんなの課題: 通報キュー |
90+
| `screenshots/0103-classrooms.png` | クラス・課題: 検索一覧 |
91+
| `screenshots/0104-restore-plan.png` | 期限切れ復元: dry-run プラン |
92+
| `screenshots/0105-bug-reports.png` | バグ報告: 閲覧一覧 |

docs/admin/operations.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 管理 SPA(Admin)運用手順書
2+
3+
> **🆕 Smalruby 独自** — 管理 SPA(EPIC #1073)の運用者向け手順。設計は EPIC #1073 / スパイク #1074 の Decision Log(A〜F)。
4+
5+
## 管理者の登録(唯一の方法 = AWS コンソール手動操作)
6+
7+
アプリ内に管理者管理 UI は**存在しない**(F4・deny-by-default)。登録・削除は AWS コンソールでの DynamoDB 直接操作のみ:
8+
9+
1. AWS コンソール → DynamoDB → テーブル `SmalrubyAdmins`(prod)/ `SmalrubyAdmins-stg`(stg)
10+
2. 「項目を作成」で以下を登録:
11+
- `email`(パーティションキー・文字列): 管理者の **Google アカウントの verified email**(小文字)
12+
- 他の属性は不要(`sub`**初回ログイン時に自動で固定**される)
13+
3. 削除 = 項目の削除(即時に全アクセスが 403 になる)
14+
15+
### sub 固定(email 再利用防御)の仕組み
16+
17+
- 初回ログイン成功時に Google の `sub` が項目へ自動追記される(`firstLoginAt` も記録)
18+
- 以後、**同じ email でも sub が異なる Google アカウントは 403**
19+
- 管理者が正当に Google アカウントを作り直した場合は、コンソールで項目の `sub` 属性を削除すれば次回ログインで再固定される
20+
21+
## 認証構成
22+
23+
- **admin 専用 Google OAuth Client ID**(決定 B): エディタの `GOOGLE_CLIENT_ID` とは別に GCP コンソールで作成し、`.env.prod` / `.env.stg``ADMIN_GOOGLE_CLIENT_ID` に設定する(**prod は未設定だとデプロイが落ちる**ガードあり)
24+
- 承認済み JavaScript 生成元: `https://smalruby.app`(+ stg 用に `http://localhost:8602`
25+
- stg のみ `DEV_BYPASS_TOKEN` による自動テスト用バイパスあり(prod 設定はデプロイ時に throw。バイパス identity `dev-admin@example.com`**allowlist 登録が必要**)。**`dev-admin@example.com` は stg の allowlist にのみ登録し、prod には絶対に登録しない**(prod ではバイパスが無効なので実害はないが、`example.com` は IANA 予約で verified email を取得できないため無意味かつ紛らわしい)
26+
27+
## セキュリティ・コスト方針(ソース公開前提の脅威モデル)
28+
29+
Admin を含む Smalruby のソースは公開されるため、攻撃者は既知のエンドポイント(`admin.api.smalruby.app`)とルート・env 変数名を把握して攻撃してくる前提で設計している。
30+
31+
- **多層の認可(fail-closed)**: すべてのルートで ① Google 署名 + `aud=ADMIN_GOOGLE_CLIENT_ID` 検証 → ② `SmalrubyAdmins` 許可リスト(deny-by-default)→ ③ sub 固定。空クライアント ID は全拒否、未登録 email は 403、email 一致でも sub 不一致は 403。DynamoDB / S3 は非公開(S3 Block Public Access、アクセスは Lambda の IAM ロールか短命 presigned URL のみ)
32+
- **prod のゲートレベル JWT authorizer**: prod は API Gateway の JWT authorizer(issuer `https://accounts.google.com` / audience = admin Client ID)で、**正当な署名トークンでない要求を Lambda 到達前に 401 で弾く**。→ 認証なしの DoS フラッドは Lambda 起動も Lambda ログ ingestion も発生させられない(費用がかからない)。stg は dev bypass(JWT ではない)を使う E2E のため authorizer なし(Lambda 側の同じ fail-closed 認可のみ)
33+
- ⚠️ prod デプロイ後の初回ログインで **必ず疎通確認**する。Google ID トークンの `iss` が万一 `accounts.google.com``https://` 無し)だと authorizer が弾くため、ログインできなければ authorizer の issuer 設定を疑う(現行トークンは `https://accounts.google.com`
34+
- **スロットリング**: 単一運用者ツールなのでレート 5 / バースト 10 に絞り、攻撃者が積み上げられる API Gateway リクエスト課金の上限を抑える(人間の操作は毎秒数回で十分)
35+
- **追加コスト源を持たない**: X-Ray(トレーシング)不使用・API Gateway アクセスログ無効。Lambda ログは監査行と 500 のみ(401/403 はアプリログを出さない)。retention は prod 1 年(監査目的・低volume)/ stg 1 週間
36+
- **CloudWatch 費用**: Admin のログ量は極小。艦隊全体の CloudWatch 無料枠超過は mesh-v2 の prod AppSync ログ(無期限保持)が主因であり Admin とは別問題(mesh-v2 側で retention と field log level を見直す)
37+
38+
## 監査ログ
39+
40+
- すべての管理操作は構造化ログ(`{"audit":true,"action":...,"adminEmail":...}`)として CloudWatch `/aws/lambda/SmalrubyAdminHandler{-stage}` に記録
41+
- **prod の保持期間は 1 年**(通常の 1 ヶ月より長い。管理操作は監査対象のため)
42+
- 検索例: CloudWatch Logs Insights で `filter audit = 1 | sort @timestamp desc`
43+
44+
## デプロイ
45+
46+
```bash
47+
cd infra/smalruby-admin
48+
ls -la .env # symlink でステージ選択(.env.stg / .env.prod)
49+
eval "$(aws configure export-credentials --profile smalruby --format env)"
50+
AWS_ACCOUNT_ID=007325983811 AWS_REGION=ap-northeast-1 npx cdk deploy --require-approval never
51+
```
52+
53+
カスタムドメイン: `admin.api.smalruby.app`(prod)/ `stg.admin.api.smalruby.app`(stg)
9.92 KB
Loading
29 KB
Loading
95.6 KB
Loading
35.2 KB
Loading
64.7 KB
Loading

0 commit comments

Comments
 (0)