コンテンツにスキップ

管理API (Admin API)

問題・問題集を管理するためのAPIサーバー。公開API (app/cmd/server/) とは別のバイナリとして動作する。

概要

項目 値
エントリーポイント app/cmd/admin/main.go
OpenAPI仕様 openapi-admin.yaml
デフォルトポート 8081
認証 CloudFront の Basic Auth + Lambda Function URL の AWS_IAM(OAC で CloudFront のみ署名可)
デプロイ Lambda + Lambda Web Adapter(deploy-admin-api-dev.yml / deploy-admin-api-prod.yml)
Dev URL https://admin.dev.rikako.org/api
Prod URL https://admin.rikako.org/api

/api 配下がこの管理APIで、それ以外のパスは同じ CloudFront が管理画面フロントエンド(S3)へ流す。 POST /publish を叩くときに /api を落とすとフロントエンドの SPA に当たるので注意。

エンドポイント

System

Method Path 説明
GET / ルート
GET /health ヘルスチェック

Questions (CRUD)

Method Path 説明
GET /questions 問題一覧(ページネーション対応)
POST /questions 問題作成
GET /questions/{questionId} 問題取得
PUT /questions/{questionId} 問題更新
DELETE /questions/{questionId} 問題削除

Workbooks (CRUD)

Method Path 説明
GET /workbooks 問題集一覧(ページネーション対応)
POST /workbooks 問題集作成
GET /workbooks/{workbookId} 問題集取得(問題含む)
PUT /workbooks/{workbookId} 問題集更新
DELETE /workbooks/{workbookId} 問題集削除

Categories (CRUD)

Method Path 説明
GET /categories カテゴリ一覧
POST /categories カテゴリ作成
GET /categories/{categoryId} カテゴリ取得
PUT /categories/{categoryId} カテゴリ更新
DELETE /categories/{categoryId} カテゴリ削除

Announcements (CRUD)

Method Path 説明
GET /announcements お知らせ一覧
POST /announcements お知らせ作成
GET /announcements/{announcementId} お知らせ取得
PUT /announcements/{announcementId} お知らせ更新
DELETE /announcements/{announcementId} お知らせ削除

Apps (CRUD)

アプリ(flavor)ごとの設定。app_slug 単位で最低バージョンなどを持つ。

Method Path 説明
GET /apps アプリ一覧
POST /apps アプリ作成
GET /apps/{appId} アプリ取得
PUT /apps/{appId} アプリ更新
DELETE /apps/{appId} アプリ削除

App Status

Method Path 説明
GET /app-status アプリステータス取得(メンテナンス表示など)
PUT /app-status アプリステータス更新

Users(参照のみ)

Method Path 説明
GET /users ユーザー一覧
GET /users/{userId} ユーザー詳細
GET /users/{userId}/answers ユーザーの回答ログ一覧

Publish

Method Path 説明
POST /publish DB の内容を S3 に静的 JSON として書き出す

/publish を叩くまでコンテンツ CDN の内容は変わらない。さらに問題集 Web は ビルド時に JSON を焼き込むため、web にも反映したい場合は publish 後に web を 再デプロイする。詳細は データ同期 と runbook。

Images

Method Path 説明
POST /images/presigned-url 画像アップロード用 Presigned URL 発行

データモデル

リクエスト

CreateQuestion / UpdateQuestion:

{
  "type": "single_choice",
  "text": "問題文",
  "choices": [
    {"text": "選択肢A", "isCorrect": true},
    {"text": "選択肢B", "isCorrect": false}
  ],
  "explanation": "解説(任意)",
  "imageIds": [1, 2]
}
  • choices は2個以上必須
  • isCorrect: true の選択肢が1つ以上必須

CreateWorkbook / UpdateWorkbook:

{
  "title": "問題集名",
  "description": "説明(任意)",
  "questionIds": [1, 2, 3]
}
  • questionIds は順序を保持(order_indexとして保存)

CreatePresignedUrl:

{
  "filename": "photo.png",
  "contentType": "image/png"
}
  • contentType: image/png または image/jpeg

レスポンス

操作 ステータス 内容
GET (一覧) 200 リソース配列 + total
GET (詳細) 200 リソース
POST 201 作成したリソース
PUT 200 更新したリソース
DELETE 204 No Content
Presigned URL 200 {uploadUrl, imageId, cdnUrl}

公開APIとの違い: 管理APIの choices は {text, isCorrect} オブジェクト配列を返す(公開APIは文字列配列)。

環境変数

変数名 説明 デフォルト
DATABASE_URL PostgreSQL接続文字列 postgres://rikako:password@localhost:5432/rikako?sslmode=disable
IMAGE_BASE_URL 画像CDNのベースURL https://example.com
IMAGE_S3_BUCKET 画像用 S3 バケット名(Presigned URL 用) (未設定時は Presigned URL 無効)
CONTENT_S3_BUCKET コンテンツ用 S3 バケット名(/publish の書き出し先) (未設定時は /publish 無効)
DB_USE_POOLER true なら Neon の pooled endpoint に接続する (未設定 = direct)
PORT リッスンポート 8081

Lambda では DATABASE_URL などに ssm:/rikako/<env>/... の参照が入り、起動時に app/internal/secrets.Resolve が実値へ展開する。

アーキテクチャ

graph TB
    Admin[管理画面] -->|CRUD| AdminAPI["Admin API<br/>:8081"]
    AdminAPI --> DB[(PostgreSQL)]
    AdminAPI -->|Presigned URL| S3["S3 画像"]
    AdminAPI -->|"POST /publish"| ContentS3["S3 コンテンツ"]
    Admin -->|Presigned URL で直接アップロード| S3
    S3 --> CF["image.rikako.org"]
    ContentS3 --> ContentCF["content.rikako.org"]

ローカル開発

# PostgreSQL起動
docker compose up -d

# 管理APIサーバー起動
cd app && go run ./cmd/admin

# テスト
cd app && go test ./internal/admin/ -v

# APIコード再生成(openapi-admin.yaml変更時)
cd app && oapi-codegen --config oapi-codegen-admin.yaml ../openapi-admin.yaml

画像アップロードフロー

sequenceDiagram
    participant Client as Admin Client
    participant API as Admin API
    participant DB as PostgreSQL
    participant S3 as S3

    Client->>API: POST /images/presigned-url
    API->>DB: INSERT INTO images (path)
    DB-->>API: imageId
    API->>S3: Generate Presigned PUT URL
    S3-->>API: presigned URL
    API-->>Client: {uploadUrl, imageId, cdnUrl}
    Client->>S3: PUT (upload image)
    Client->>API: POST /questions (with imageIds)
    API->>DB: INSERT question + question_images

今後の予定

  • 認証を Basic Auth から Cognito ベースへ移行するか検討(現状は CloudFront Function の Basic Auth)