Shun Miyazawa 1 день назад
Родитель
Сommit
e2275e8e9a

+ 0 - 283
.kiro/specs/sharelink-page-api/design.md

@@ -1,283 +0,0 @@
-# Technical Design: sharelink-page-api
-
-## Overview
-
-本機能は、share link 経由のページ取得を専用の API エンドポイント `GET /_api/v3/page/shared` として分離する。現行の `GET /_api/v3/page` ルートは、通常認証アクセスと share link アクセスの両方をミドルウェアフラグ(`req.isSharedPage`)と条件分岐で処理しており、責務が混在している。専用エンドポイントを設けることで、各ルートの責務を明確化し、コードの可読性と保守性を向上させる。
-
-既存のミドルウェア(`certifySharedPage`)とサービス関数(`findPageAndMetaDataByViewer`)を再利用し、`respondWithSinglePage` を共有ユーティリティとして抽出することで、ロジックの重複を排除する。さらに、現行実装で未対応だった `security:disableLinkSharing` 設定のチェックを `rejectLinkSharingDisabled` ミドルウェアとして新設し、セキュリティギャップを解消する。
-
-### Goals
-
-- share link アクセス専用の `GET /_api/v3/page/shared` エンドポイントを実装する
-- 既存の `certifySharedPage` ミドルウェアを再利用して ShareLink バリデーションを行う
-- `rejectLinkSharingDisabled` ミドルウェアを新設し、`security:disableLinkSharing` 設定を正しく適用する
-- `respondWithSinglePage` を共有ユーティリティとして抽出し、`GET /page` と `GET /page/shared` で共用する
-- 既存の `GET /page` ルートから share link 関連のコードを除去してシンプル化する
-
-### Non-Goals
-
-- `GET /_api/v3/page/info`(`get-page-info.ts`)の share link 対応リファクタリング(別スコープ)
-- `certifySharedPage` ミドルウェアの削除(`get-page-info.ts`・`revisions.js` が引き続き使用するため)
-- share link の特定リビジョン指定(`revisionId`)対応(初期スコープ外)
-- SSR share ページ(`/share/[[...path]]/page-data-props.ts`)の変更
-
----
-
-## Architecture
-
-### Architecture Pattern & Boundary Map
-
-```mermaid
-graph TB
-    subgraph ClientLayer[Client Layer]
-        FetchHook[useFetchCurrentPage]
-    end
-
-    subgraph PageRouter[Page Router - page slash]
-        NewRoute[GET slash shared - NEW]
-        OldRoute[GET slash - existing cleanup]
-        RespondUtil[respondWithSinglePage - extracted]
-    end
-
-    subgraph MiddlewareLayer[Middleware Layer]
-        RejectLSD[rejectLinkSharingDisabled - NEW]
-        CertifySP[certifySharedPage - existing]
-    end
-
-    subgraph ServiceLayer[Service Layer]
-        FPAMDBV[findPageAndMetaDataByViewer - existing]
-    end
-
-    subgraph DataLayer[Data Layer]
-        ShareLinkModel[ShareLink Model]
-        PageModel[Page Model]
-        ConfigMgr[configManager]
-    end
-
-    FetchHook -->|shareLinkId present| NewRoute
-    FetchHook -->|shareLinkId absent| OldRoute
-    NewRoute --> RejectLSD
-    RejectLSD --> ConfigMgr
-    NewRoute --> CertifySP
-    CertifySP --> ShareLinkModel
-    NewRoute --> FPAMDBV
-    NewRoute --> RespondUtil
-    OldRoute --> RespondUtil
-    OldRoute --> FPAMDBV
-    FPAMDBV --> PageModel
-```
-
-**Architecture Integration**:
-- Selected pattern: Handler Factory(`getPageInfoHandlerFactory` と同一パターン)
-- Existing patterns preserved: `RequestHandler[]` 返却型、`apiV3FormValidator`、`res.apiv3()`
-- New components: `rejectLinkSharingDisabled` ミドルウェア、`respondWithSinglePage` ユーティリティ(抽出)、`getPageByShareLinkHandlerFactory` ハンドラー
-- Reused components: `certifySharedPage` ミドルウェア、`findPageAndMetaDataByViewer` サービス
-
----
-
-## System Flows
-
-### 新エンドポイント リクエストフロー
-
-```mermaid
-sequenceDiagram
-    participant Client
-    participant RejectLSD as rejectLinkSharingDisabled
-    participant CertifySP as certifySharedPage
-    participant Handler as handler
-    participant FPAMDBV as findPageAndMetaDataByViewer
-    participant Page as Page Model
-
-    Client->>RejectLSD: shareLinkId pageId
-    alt disableLinkSharing = true
-        RejectLSD-->>Client: 403 link-sharing-disabled
-    end
-    RejectLSD->>CertifySP: next
-    CertifySP->>CertifySP: ShareLink.findOne id relatedPage
-    alt valid and not expired
-        CertifySP->>CertifySP: req.isSharedPage = true
-    end
-    CertifySP->>Handler: next
-    alt req.isSharedPage is falsy
-        Handler-->>Client: 404 share-link-invalid
-    end
-    Handler->>FPAMDBV: pageId isSharedPage=true
-    FPAMDBV->>Page: findOne _id
-    FPAMDBV-->>Handler: pageWithMeta
-    Handler->>Handler: respondWithSinglePage
-    Handler-->>Client: 200 page meta
-```
-
-フロー上の重要決定事項:
-- `rejectLinkSharingDisabled` を `certifySharedPage` の前段に配置し、設定無効時は DB アクセスをスキップする
-- `certifySharedPage` は既存ミドルウェアをそのまま再利用。`req.isSharedPage` フラグで有効性を伝達する
-- share link が有効である場合のみ `findPageAndMetaDataByViewer` を呼び出し、`isSharedPage: true` で page grant チェックをスキップする
-
----
-
-## Requirements Traceability
-
-| Requirement | Summary | Components | Flows |
-|-------------|---------|------------|-------|
-| 1.1 | 専用エンドポイントの提供 | `getPageByShareLinkHandlerFactory` | リクエストフロー全体 |
-| 1.2 | 認証ミドルウェアからの独立 | `getPageByShareLinkHandlerFactory` | `accessTokenParser`・`loginRequired` を使用しない |
-| 1.3 | `shareLinkId` と `pageId` 必須パラメータ | validator 定義 | params バリデーション |
-| 1.4 | 既存と同一レスポンス構造 | `respondWithSinglePage` | respondWithSinglePage |
-| 2.1–2.2 | ShareLink 存在確認・relatedPage 照合 | `certifySharedPage` | certifySharedPage 内部 |
-| 2.3–2.4 | 不一致・期限切れ時エラー | Handler(`!req.isSharedPage` チェック) | 404 share-link-invalid |
-| 2.5 | `disableLinkSharing` 時 403 | `rejectLinkSharingDisabled` | config チェック |
-| 3.1 | 最新リビジョン付きページ返却 | `findPageAndMetaDataByViewer`, `respondWithSinglePage` | FPAMDBV + respond |
-| 3.2–3.3 | isMovable 等 false・bookmarkCount 0 | `findPageAndMetaDataByViewer` | FPAMDBV 内の分岐 |
-| 3.4 | ページ未存在時 404 | `respondWithSinglePage` | not-found メタ |
-| 4.1–4.3 | 認証不要 | `getPageByShareLinkHandlerFactory` | 認証ミドルウェアなし |
-| 5.1 | `findPageAndMetaDataByViewer` 再利用 | `getPageByShareLinkHandlerFactory` | FPAMDBV 呼び出し |
-| 5.2 | バリデーション共通化 | `certifySharedPage` 再利用 | ミドルウェアチェーン |
-| 5.3 | シリアライズロジック非複製 | `respondWithSinglePage` | 抽出ユーティリティ |
-| 5.4 | `pageId` バリデーター共有 | express-validator | params バリデーション |
-
----
-
-## Components and Interfaces
-
-### コンポーネント一覧
-
-| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies |
-|-----------|--------------|--------|--------------|------------------|
-| `rejectLinkSharingDisabled` | Middleware | `disableLinkSharing` 設定チェック | 2.5 | configManager (P0) |
-| `respondWithSinglePage` | Route Utility | ページデータ → API レスポンス変換 | 1.4, 3.1, 3.2, 3.3, 3.4 | ApiV3Response (P0) |
-| `getPageByShareLinkHandlerFactory` | Route Handler | `GET /page/shared` エンドポイント実装 | 全要件 | certifySharedPage (P0), findPageAndMetaDataByViewer (P0), respondWithSinglePage (P0) |
-| `useFetchCurrentPage` update | Client State | 新エンドポイントへのクライアント移行 | 1.1, 1.3 | apiv3Get (P0) |
-
----
-
-### Server Layer
-
-#### rejectLinkSharingDisabled
-
-| Field | Detail |
-|-------|--------|
-| File | `apps/app/src/server/middlewares/reject-link-sharing-disabled.ts` |
-| Intent | `security:disableLinkSharing` が有効な場合にリクエストを 403 で拒否するガードミドルウェア |
-| Requirements | 2.5 |
-
-**Implementation**: `configManager.getConfig('security:disableLinkSharing')` を確認し、`true` の場合は `res.apiv3Err(ErrorV3('Link sharing is disabled', 'link-sharing-disabled'), 403)` を返す。それ以外は `next()` を呼ぶ。
-
----
-
-#### respondWithSinglePage
-
-| Field | Detail |
-|-------|--------|
-| File | `apps/app/src/server/routes/apiv3/page/respond-with-single-page.ts` |
-| Intent | ページデータと meta 情報を `ApiV3Response` 形式に変換して返す共有ユーティリティ |
-| Requirements | 1.4, 3.1, 3.2, 3.3, 3.4 |
-
-**Responsibilities & Constraints**
-- `IPageNotFoundInfo` メタの場合に 403/404 を返す
-- `security:disableUserPages` 設定に基づくユーザーページの制限
-- ページが存在する場合の `populateDataToShowRevision` 呼び出し
-- 既存の `page/index.ts` インライン実装を抽出・置き換え。**新ロジックを追加しない**
-
-```typescript
-export async function respondWithSinglePage(
-  res: ApiV3Response,
-  pageWithMeta:
-    | IDataWithMeta<HydratedDocument<PageDocument>, IPageInfoExt>
-    | IDataWithMeta<null, IPageNotFoundInfo>,
-  options?: RespondWithSinglePageOptions,
-): Promise<void>;
-```
-
----
-
-#### getPageByShareLinkHandlerFactory
-
-| Field | Detail |
-|-------|--------|
-| File | `apps/app/src/server/routes/apiv3/page/get-page-by-share-link.ts` |
-| Intent | `GET /page/shared` エンドポイントのミドルウェア配列を生成するファクトリー |
-| Requirements | 1.1, 1.2, 1.3, 1.4, 2.1–2.5, 3.1–3.4, 4.1–4.3, 5.1–5.4 |
-
-**Middleware Execution Order**:
-1. `query('shareLinkId').isMongoId()` バリデーター(必須)
-2. `query('pageId').isMongoId()` バリデーター(必須)
-3. `apiV3FormValidator`
-4. `rejectLinkSharingDisabled` — `disableLinkSharing` 設定チェック → 403
-5. `certifySharedPage` — ShareLink 存在確認・期限チェック → `req.isSharedPage = true`
-6. async handler:
-   - `!req.isSharedPage` → 404 `share-link-invalid`
-   - `findPageAndMetaDataByViewer(pageService, pageGrantService, { pageId, path: null, isSharedPage: true })`
-   - `respondWithSinglePage(res, pageWithMeta, { disableUserPages })`
-
-##### API Contract
-
-| Method | Endpoint | Request | Response | Errors |
-|--------|----------|---------|----------|--------|
-| GET | `/_api/v3/page/shared` | `{ shareLinkId: MongoId, pageId: MongoId }` (query) | `{ page: IPagePopulatedToShowRevision, meta: IPageInfo }` | 400, 403, 404, 500 |
-
----
-
-#### page/index.ts クリーンアップ
-
-| Field | Detail |
-|-------|--------|
-| Intent | `GET /page` ルートから share link 関連コードを除去し、`respondWithSinglePage` を抽出ユーティリティに置き換え |
-| Requirements | 5.2, 5.3 |
-
-**変更内容**:
-1. `respondWithSinglePage` クロージャを削除 → `respond-with-single-page.ts` を import
-2. `certifySharedPage` をミドルウェアチェーンから除去
-3. `isSharedPage` 条件分岐をハンドラーから除去
-4. `validator.getPage` から `shareLinkId` バリデーターを除去
-5. `getPageByShareLinkHandlerFactory` を import し `router.get('/shared', ...)` を追加
-
----
-
-### Client Layer
-
-#### useFetchCurrentPage update
-
-| Field | Detail |
-|-------|--------|
-| File | `apps/app/src/states/page/use-fetch-current-page.ts` |
-| Intent | share link アクセス時の API エンドポイントを `/page` から `/page/shared` に変更 |
-| Requirements | 1.1, 1.3 |
-
-```typescript
-const endpoint = params.shareLinkId != null ? '/page/shared' : '/page';
-const { data } = await apiv3Get<FetchedPageResult>(endpoint, params);
-```
-
----
-
-## Error Handling
-
-### Error Categories and Responses
-
-| Category | Scenario | Error Code | HTTP Status |
-|----------|----------|------------|-------------|
-| User Errors (4xx) | `shareLinkId` / `pageId` 未指定・不正形式 | `validation-failed` | 400 |
-| Business Logic (4xx) | `disableLinkSharing=true` | `link-sharing-disabled` | 403 |
-| Business Logic (4xx) | ShareLink 未存在・relatedPage 不一致・期限切れ | `share-link-invalid` | 404 |
-| Business Logic (4xx) | ページ未存在 | `page-not-found` | 404 |
-| Business Logic (4xx) | `disableUserPages` でユーザーページへのアクセス | `page-is-forbidden` | 403 |
-| System Errors (5xx) | DB エラー・populate 失敗 | `get-page-failed` | 500 |
-
----
-
-## Testing Strategy
-
-### Unit Tests
-
-`respondWithSinglePage` のユニットテスト(`respond-with-single-page.spec.ts`):
-- `IPageNotFoundInfo` メタかつ `isForbidden=true` → 403
-- `IPageNotFoundInfo` メタかつ `isForbidden=false` → 404
-- `disableUserPages=true` でユーザーページ → 403
-- 正常ページ → `res.apiv3({ page, meta })` が呼ばれる
-
-### Security Considerations
-
-- **認証不要エンドポイントのデータ保護**: `certifySharedPage` が ShareLink の DB チェックを完了し `req.isSharedPage` をセットした後にのみ `findPageAndMetaDataByViewer` を呼び出す。バリデーション失敗時はページデータに一切アクセスしない。
-- **`disableLinkSharing` の適用**: `rejectLinkSharingDisabled` ミドルウェアを `certifySharedPage` の前段に配置し、設定が `true` の場合に確実に 403 を返す。DB アクセスも不要。
-- **ページ権限のスキップ**: `isSharedPage: true` による `findPageAndMetaDataByViewer` の権限チェックスキップは、`certifySharedPage` による ShareLink バリデーション完了後のみ実行されるため、不正アクセスのリスクはない。
-- **入力サニタイズ**: `shareLinkId` と `pageId` は `express-validator` で MongoId 形式を強制し、NoSQL インジェクションを防止する。

+ 0 - 188
.kiro/specs/sharelink-page-api/gap-analysis.md

@@ -1,188 +0,0 @@
-# Gap Analysis: sharelink-page-api
-
-## 1. 現状調査 (Current State Investigation)
-
-### 調査対象ファイル
-
-| ファイル | 役割 |
-|---|---|
-| `apps/app/src/server/routes/apiv3/page/index.ts` | 既存の `GET /page` ルートハンドラ |
-| `apps/app/src/server/middlewares/certify-shared-page.js` | share link 検証ミドルウェア |
-| `apps/app/src/server/service/page/find-page-and-meta-data-by-viewer.ts` | ページデータ取得サービス関数 |
-| `apps/app/src/server/models/share-link.ts` | ShareLink Mongoose モデル |
-| `apps/app/src/server/routes/apiv3/share-links.js` | ShareLink CRUD ルート |
-| `apps/app/src/pages/share/[[...path]]/page-data-props.ts` | SSR 用 share link ページ取得 |
-| `apps/app/src/states/page/use-fetch-current-page.ts` | クライアント側 page API 呼び出しフック |
-
-### 既存ミドルウェアチェーン(`GET /page` ルート)
-
-```
-accessTokenParser → certifySharedPage → loginRequired → validator.getPage → handler
-```
-
-handler 内の分岐:
-1. `isSharedPage=true` の場合 → `ShareLink.findOne` (2回目) + `findPageAndMetaDataByViewer({ isSharedPage: true })`
-2. `findAll` の場合 → `Page.findByPathAndViewer`
-3. デフォルト → `findPageAndMetaDataByViewer`
-
-### クライアント側の呼び出しパターン(`use-fetch-current-page.ts`)
-
-```typescript
-// share link 表示時に生成されるパラメータ
-{ pageId: currentPageId, shareLinkId: shareLinkId }
-// → apiv3Get('/page', { pageId, shareLinkId }) で呼び出し
-```
-
----
-
-## 2. 要件フィージビリティ分析 (Requirements Feasibility Analysis)
-
-### 要件 → 資産マップ
-
-| 要件 | 既存資産 | ギャップ種別 | 詳細 |
-|---|---|---|---|
-| Req 1: 専用エンドポイント | なし | **Missing** | 新規ルートハンドラが必要 |
-| Req 2: シェアリンク検証 | `certifySharedPage.js`(部分的) | **Gap** | `disableLinkSharing` 未チェック・DB 二重クエリ問題 |
-| Req 3: ページデータ返却 | `findPageAndMetaDataByViewer` | **Gap** | `respondWithSinglePage` ヘルパーがインライン定義のため未抽出 |
-| Req 4: 認証不要 | `loginRequired(isGuestAllowed: true)` で代替中 | **Constraint** | 既存ルートの設計では `certifySharedPage` + `loginRequired` に依存 |
-| Req 5: コード重複回避 | `findPageAndMetaDataByViewer` は再利用可能 | **Gap** | バリデーション・レスポンスシリアライズが抽出されていない |
-
-### 発見された既知の問題(既存実装のバグ/設計課題)
-
-#### 問題 1: DB 二重クエリ
-
-`certifySharedPage` ミドルウェアで 1 回 `ShareLink.findOne` → ハンドラでさらに `ShareLink.findOne` を実行。専用エンドポイントでは 1 回に集約できる。
-
-```javascript
-// certifySharedPage.js: 1回目
-ShareLink.findOne({ _id: shareLinkId, relatedPage: pageId })
-
-// page/index.ts handler: 2回目
-ShareLink.findOne({ _id: shareLinkId })
-```
-
-#### 問題 2: `disableLinkSharing` がページ取得 API で未チェック
-
-- `share-links.js` の CRUD ルート(作成・一覧)では `linkSharingRequired` ミドルウェアで `disableLinkSharing` をチェックしている
-- `certifySharedPage` ミドルウェアは `disableLinkSharing` を**チェックしない**
-- `GET /page` ハンドラも `disableLinkSharing` を**チェックしない**
-- → `disableLinkSharing=true` 設定でも、既存の share link 経由でページデータを取得可能な状態
-
-専用エンドポイントでは、この設定を適切にチェックすることで要件 2-5 を満たす。
-
-#### 問題 3: `certifySharedPage` のサイレント・パス
-
-期限切れ / 無効なリンクの場合、`certifySharedPage` はエラーを返さず `next()` を呼ぶ。その後 `loginRequired` が未認証リクエストをブロックする設計。専用エンドポイントでは、無効リンクを明示的なエラーレスポンス(403/404)で返すべき。
-
-### 再利用可能な既存資産
-
-**完全に再利用可能(変更不要):**
-- `findPageAndMetaDataByViewer` — `isSharedPage: true` オプション対応済み
-- `ShareLink` モデル + `isExpired()` メソッド
-- `page.populateDataToShowRevision()` メソッド
-- `configManager.getConfig('security:disableLinkSharing')`
-- `configManager.getConfig('security:disableUserPages')`
-- `apiV3FormValidator` ミドルウェア
-- `ErrorV3` エラークラス + `res.apiv3Err()` / `res.apiv3()` レスポンスヘルパー
-
-**抽出・リファクタリングが必要:**
-- `respondWithSinglePage` — `page/index.ts` ハンドラ内インライン定義 → 共有ユーティリティに抽出
-- share link 検証ロジック — `certifySharedPage.js` の DB クエリ部分 → 純粋関数として抽出
-
-**不要(専用エンドポイントには使用しない):**
-- `accessTokenParser` — 認証不要のため
-- `loginRequired` — 認証不要のため
-- `certifySharedPage` ミドルウェア本体 — 専用エンドポイントでは使用しない(ただし既存ルートの互換性維持のため削除はしない)
-
----
-
-## 3. 実装アプローチ選択肢 (Implementation Approach Options)
-
-### Option A: `GET /page/shared` として page ルートに追加
-
-**新規ファイル**: `apps/app/src/server/routes/apiv3/page/get-page-by-share-link.ts`
-
-ハンドラーファクトリーパターン(`get-page-info.ts` と同様):
-
-```
-GET /_api/v3/page/shared?shareLinkId=xxx&pageId=yyy
-```
-
-ミドルウェアチェーン(シンプル化):
-```
-validateShareLinkParams → handler
-```
-
-- `page/index.ts` に `router.get('/shared', getPageByShareLinkHandlerFactory(crowi))` を追加
-- `respondWithSinglePage` を `page/respond-with-single-page.ts` として抽出
-- share link バリデーション純粋関数を抽出
-
-**Trade-offs:**
-- ✅ `get-page-info.ts` と同一パターン(コードレビューが容易)
-- ✅ TypeScript ネイティブ
-- ✅ `page/` ディレクトリ内で完結
-- ✅ 既存ルートへの影響最小
-- ❌ クライアント側 URL 変更が必要(`/page` → `/page/shared`)
-
-### Option B: `GET /share-links/:shareLinkId/page` として share-links ルートに追加
-
-```
-GET /_api/v3/share-links/:shareLinkId/page?pageId=yyy
-```
-
-- `share-links.js` に新ルートを追加
-- RESTful 設計として最もセマンティック
-
-**Trade-offs:**
-- ✅ RESTful な URL 構造(share link がリソースの起点)
-- ✅ share link 関連ロジックが share-links ルートに集約される
-- ❌ `share-links.js` は CommonJS/JavaScript → TypeScript 化またはJS で書く必要
-- ❌ クライアント側 URL 変更が必要かつ変更量が大きい
-
-### Option C: Hybrid(推奨)
-
-**Phase 1**: Option A と同様に `GET /page/shared` を新規作成(TypeScript)
-- 新ファイル: `page/get-page-by-share-link.ts`
-- `respondWithSinglePage` ユーティリティ抽出: `page/respond-with-single-page.ts`
-- share link バリデーション純粋関数抽出(`certifySharedPage` から)
-
-**Phase 2**: クライアント側を新エンドポイントに移行
-- `use-fetch-current-page.ts` の API 呼び出しを `/page/shared` に変更
-
-**Phase 3**: 既存ルートの share link ブランチ除去(オプション)
-- `page/index.ts` から `isSharedPage` 分岐を削除
-- `certifySharedPage` ミドルウェアの使用箇所を精査して不要になれば削除
-
-**Trade-offs:**
-- ✅ 段階的に安全に移行できる
-- ✅ TypeScript ネイティブ
-- ✅ 確立されたパターンに従う
-- ✅ 既存ルートの動作を壊さない
-- ❌ Phase 1 完了後に古い実装が一時的に並存する
-
----
-
-## 4. 実装複雑度・リスク評価
-
-| 項目 | 評価 | 根拠 |
-|---|---|---|
-| **努力量** | **S(1〜3 日)** | 全サービス関数は既存。パターン確立済み(`get-page-info.ts` と同様)。主な作業は新ハンドラファイル + 2 つのユーティリティ抽出 + クライアント側変更 |
-| **リスク** | **Low** | 新しいアーキテクチャパターンなし・新規依存なし・対象スコープが明確 |
-
----
-
-## 5. 設計フェーズへの推薦事項
-
-### 推奨アプローチ
-**Option C(Hybrid)** を推薦。`get-page-info.ts` と同一のハンドラファクトリーパターンで `get-page-by-share-link.ts` を実装する。
-
-### 設計フェーズで確定すべき事項
-
-1. **エンドポイント URL の確定**: `GET /page/shared` か `GET /share-links/:id/page` か
-2. **クライアント側変更のスコープ**: `use-fetch-current-page.ts` の変更内容と後方互換性方針
-3. **`respondWithSinglePage` 抽出の設計**: 型シグネチャと依存関係
-4. **`disableLinkSharing` チェックの位置**: ミドルウェアとして抽出するか、ハンドラ内に配置するか
-5. **既存 `GET /page` ルートの share link ブランチの扱い**: Phase 3 で削除するか並存させるか
-
-### Research Needed
-- `certifySharedPage` が他のルート(`get-page-info.ts` など)でも使われているため、削除・変更の影響範囲を確認(→ 設計フェーズで調査)

+ 0 - 74
.kiro/specs/sharelink-page-api/requirements.md

@@ -1,74 +0,0 @@
-# Requirements Document
-
-## Introduction
-
-本仕様は、GROWI の share link 専用ページ取得 API エンドポイントの要件を定義する。
-
-現行の `/_api/v3/page` ルートは、通常の認証アクセスと share link アクセスの両方を単一ルートで処理している。ミドルウェアチェーン(`accessTokenParser → certifySharedPage → loginRequired`)と handler 内の条件分岐によって混在しており、コードの可読性と保守性に課題がある。
-
-本機能では share link アクセス専用のエンドポイントを分離することで、責務を明確化する。同時に、既存のミドルウェア・サービス関数を最大限に再利用し、ロジックの重複を最小化することを設計原則とする。
-
----
-
-## Requirements
-
-### Requirement 1: 専用エンドポイントの提供
-
-**Objective:** As a クライアントアプリケーション, I want share link 専用の独立した API エンドポイント, so that share link 経由のページアクセスが通常の認証付きアクセスと明確に分離される。
-
-#### Acceptance Criteria
-
-1. The Share Link Page API shall provide a dedicated endpoint distinct from the general `/_api/v3/page` endpoint for share link-based page access.
-2. When a request is sent to the dedicated share link endpoint, the Share Link Page API shall process it independently of the authenticated page access middleware chain (`accessTokenParser`, `loginRequired`).
-3. The Share Link Page API shall accept `shareLinkId` and `pageId` as required request parameters.
-4. The Share Link Page API shall return a response in the same JSON structure as the existing page API (`{ page, meta }`).
-
----
-
-### Requirement 2: シェアリンクの検証
-
-**Objective:** As a GROWI システム, I want share link validation before returning any page data, so that 無効・期限切れのリンクからページデータが漏洩しない。
-
-#### Acceptance Criteria
-
-1. When a request is received, the Share Link Page API shall verify that a ShareLink document with the specified `shareLinkId` exists in the database and its `relatedPage` field matches the specified `pageId`.
-2. If the ShareLink document does not exist, its `relatedPage` does not match `pageId`, or the ShareLink has expired, the Share Link Page API shall return a 404 error response without exposing any page data.
-3. While link sharing is disabled via the `security:disableLinkSharing` configuration, the Share Link Page API shall return a 403 error response for all requests regardless of link validity.
-
----
-
-### Requirement 3: ページデータの返却
-
-**Objective:** As a share link 閲覧者, I want to receive page content and metadata via the API, so that ページを正常にレンダリングできる。
-
-#### Acceptance Criteria
-
-1. When a valid and non-expired share link is provided, the Share Link Page API shall return the page document including its latest revision data.
-2. When returning page data for a share link request, the Share Link Page API shall set `isMovable`, `isDeletable`, `isAbleToDeleteCompletely`, and `isRevertible` to `false` in the meta field.
-3. When returning page data for a share link request, the Share Link Page API shall return `bookmarkCount` as `0`.
-4. If the referenced page does not exist, the Share Link Page API shall return a 404 error response.
-
----
-
-### Requirement 4: 認証不要のアクセス
-
-**Objective:** As an 未認証ユーザー, I want to access share link pages without logging in, so that share link の公開アクセス性が担保される。
-
-#### Acceptance Criteria
-
-1. The Share Link Page API shall not require user authentication (session cookie, access token) to process requests.
-2. When an unauthenticated request includes a valid and non-expired `shareLinkId`, the Share Link Page API shall return the page data.
-3. While link sharing is enabled, the Share Link Page API shall serve page data to any requester regardless of authentication state.
-
----
-
-### Requirement 5: コード重複の最小化
-
-**Objective:** As a 開発者, I want the share link endpoint to reuse existing middleware and service layer code, so that ロジックの重複による保守コストとバグリスクを低減できる。
-
-#### Acceptance Criteria
-
-1. The Share Link Page API shall reuse the existing page data retrieval service function (e.g., `findPageAndMetaDataByViewer`) with the `isSharedPage: true` option, rather than reimplementing page fetch and metadata computation logic.
-2. The Share Link Page API shall reuse the existing `certifySharedPage` middleware for ShareLink validation, rather than duplicating the validation logic.
-3. The Share Link Page API implementation shall not duplicate the page response serialization logic already present in the existing `/_api/v3/page` route handler. The extracted `respondWithSinglePage` utility shall be shared between both endpoints.
-4. Where applicable, common request validators (e.g., `pageId` format checks) shall be shared between the dedicated endpoint and the existing route rather than redefined.

+ 0 - 135
.kiro/specs/sharelink-page-api/research.md

@@ -1,135 +0,0 @@
-# Research & Design Decisions: sharelink-page-api
-
----
-
-## Summary
-
-- **Feature**: `sharelink-page-api`
-- **Discovery Scope**: Extension(既存 page API ルートへの拡張)
-- **Key Findings**:
-  1. `findPageAndMetaDataByViewer` は `isSharedPage: true` オプションを既にサポートしており、変更不要で再利用可能
-  2. `certifySharedPage` ミドルウェアには 2 つの既知の設計課題がある:`disableLinkSharing` 未チェック、および無効/期限切れリンクのサイレントパス(エラーを返さず次のミドルウェアへ)
-  3. 現行の `GET /page` ルートは share link アクセスで `ShareLink.findOne` を 2 回実行する(`certifySharedPage` + ハンドラ)。専用エンドポイントで 1 回に集約できる
-
----
-
-## Research Log
-
-### `certifySharedPage` ミドルウェアの動作分析
-
-- **Context**: 専用エンドポイントで `certifySharedPage` をそのまま再利用できるか検証
-- **Sources Consulted**: `apps/app/src/server/middlewares/certify-shared-page.js`
-- **Findings**:
-  - `pageId` または `shareLinkId` が null の場合、検証をスキップして `next()` を呼ぶ
-  - ShareLink が見つからないか期限切れの場合も `next()` を呼ぶ(`req.isSharedPage` を設定しない)
-  - `security:disableLinkSharing` 設定を一切チェックしない
-  - 設計上「フラグ設定専用ミドルウェア」であり、`loginRequired` と組み合わせて初めて機能する
-- **Implications**: 専用エンドポイントでは `certifySharedPage` を使わず、明示的なバリデーション関数を新規作成する
-
-### `disableLinkSharing` 設定の適用範囲調査
-
-- **Context**: `disableLinkSharing=true` 時に既存の share link 経由でページ取得 API が保護されているか確認
-- **Sources Consulted**: `share-links.js` の `linkSharingRequired` ミドルウェア、`certify-shared-page.js`、`page/index.ts`
-- **Findings**:
-  - `disableLinkSharing` は ShareLink の**作成・一覧取得**ルートのみで確認されている(`share-links.js` の `linkSharingRequired`)
-  - `GET /page?shareLinkId=xxx` 経由でのページ**取得**は、`disableLinkSharing=true` でも保護されていない(既存バグ)
-  - SSR の share ページ (`page-data-props.ts`) もこの設定をチェックしていない
-- **Implications**: 新専用エンドポイントでこの設定チェックを追加することで、要件 2.5 を満たしつつ既存バグを修正する
-
-### クライアント側 API 呼び出しパターン調査
-
-- **Context**: クライアントが `GET /page` に `shareLinkId` を渡す仕組みを確認
-- **Sources Consulted**: `apps/app/src/states/page/use-fetch-current-page.ts`
-- **Findings**:
-  - `buildApiParams` 関数内で `shareLinkId` が存在する場合に `params.shareLinkId = shareLinkId` と `params.pageId = currentPageId` を設定
-  - 最終的に `apiv3Get('/page', params)` として呼び出し
-  - コメントに「required by certifySharedPage middleware」と明記されており、ミドルウェア依存を意識した設計
-- **Implications**: 新エンドポイントへの移行時に `apiv3Get` の第 1 引数を条件分岐させるだけでよい(パラメータ構造は変わらない)
-
-### 既存ハンドラーファクトリーパターン確認
-
-- **Context**: 新ハンドラーファイルが従うべきパターンを確認
-- **Sources Consulted**: `get-page-info.ts`、`page/index.ts`
-- **Findings**:
-  - `getPageInfoHandlerFactory(crowi): RequestHandler[]` の形式でファクトリーを export
-  - `page/index.ts` で `router.get('/info', getPageInfoHandlerFactory(crowi))` として登録(Express はミドルウェア配列を直接受け付ける)
-  - `certifySharedPage` と `loginRequired` の両方を使用している(新エンドポイントでは両方不要)
-- **Implications**: 新ファイル `get-page-by-share-link.ts` は同一のファクトリーパターンで実装し、`GET /page/shared` として登録する
-
----
-
-## Architecture Pattern Evaluation
-
-| Option | Description | Strengths | Risks / Limitations | Notes |
-|--------|-------------|-----------|---------------------|-------|
-| A: `GET /page/shared` | `page/` ディレクトリ内に新ハンドラーファイルを追加 | 既存パターンと一致・TypeScript ネイティブ・クライアント変更が最小 | クライアント URL 変更が必要 | **採用** |
-| B: `GET /share-links/:id/page` | `share-links.js` ルーターに追加 | RESTful なリソース URL 設計 | `share-links.js` が CommonJS/JS・クライアント変更が大きい | 非採用 |
-| C: `GET /share-links/page` | クエリパラメータ方式で `share-links.js` に追加 | パラメータ構造が変わらない | JS ファイルへの追加・名前空間が直感的でない | 非採用 |
-
----
-
-## Design Decisions
-
-### Decision: エンドポイント URL の選択
-
-- **Context**: share link 専用エンドポイントの URL パスをどこに配置するか
-- **Alternatives Considered**:
-  1. `GET /page/shared` — `page/` ディレクトリ内の新ハンドラー
-  2. `GET /share-links/:id/page` — RESTful リソース設計
-  3. `GET /share-links/page` — クエリパラメータ方式
-- **Selected Approach**: `GET /page/shared` を採用
-- **Rationale**: `get-page-info.ts` と完全に同じハンドラーファクトリーパターンを踏襲でき、TypeScript ネイティブ。クライアント側の変更は API パスの条件分岐のみで最小限。`page/` ディレクトリ内でルーター登録まで完結する。
-- **Trade-offs**: URL がリソース中心でない点は妥協点だが、GROWI の既存 `/page/info` パターンと一貫性がある
-- **Follow-up**: 実装時に OpenAPI ドキュメントも更新する
-
-### Decision: ShareLink バリデーションの実装方式
-
-- **Context**: share link の DB バリデーション(存在確認・期限確認・relatedPage 照合)をどこに置くか
-- **Alternatives Considered**:
-  1. `certifySharedPage` ミドルウェアを改修して再利用
-  2. ハンドラー内にインライン実装
-  3. 新規サービス関数として抽出(`server/service/share-link/validate-share-link.ts`)
-- **Selected Approach**: 新規サービス関数 `validateShareLink` を `server/service/share-link/validate-share-link.ts` に作成
-- **Rationale**: `certifySharedPage` は「フラグ設定ミドルウェア」として設計されており、単体でエラーレスポンスを返す責務を持たない。`server/service/page/find-page-and-meta-data-by-viewer.ts` と同じ service 層パターンに従い、純粋な非同期バリデーション関数として実装することで、他ルートからも再利用可能になる(例: `get-page-info.ts` の将来的なリファクタリング)
-- **Trade-offs**: 新規ディレクトリ `server/service/share-link/` の作成が必要
-- **Follow-up**: `certifySharedPage` は `get-page-info.ts` でまだ使用されているため、このスペック内では削除せず残す
-
-### Decision: `respondWithSinglePage` の抽出
-
-- **Context**: ページデータをレスポンスに変換するロジックの重複を避ける方法
-- **Alternatives Considered**:
-  1. 新ハンドラーにインラインで複製
-  2. `page/index.ts` 内のクロージャを共有ユーティリティとして抽出
-- **Selected Approach**: `page/respond-with-single-page.ts` として抽出・export
-- **Rationale**: 要件 5.3 を直接満たす。既存の `page/index.ts` ハンドラーと新ハンドラーの両方が同じロジックを参照できる
-- **Trade-offs**: `res` オブジェクト(`ApiV3Response` 型)と設定値を引数として受け取る設計が必要
-- **Follow-up**: `initLatestRevisionField` の呼び出し引数(`revisionId`)の扱いを実装時に確認
-
-### Decision: `disableLinkSharing` チェックの位置
-
-- **Context**: グローバル設定の確認をどのレイヤーで行うか
-- **Alternatives Considered**:
-  1. `validateShareLink` サービス関数内に含める
-  2. 新ハンドラーの先頭で確認(リクエストレイヤー)
-- **Selected Approach**: ハンドラーの先頭で確認
-- **Rationale**: `validateShareLink` は DB レベルの検証(link の存在・有効性)に責務を限定する。グローバル設定の確認はリクエスト処理の最初のゲートとして、ハンドラーが明示的に担当する
-- **Trade-offs**: なし(シンプルかつ透明性が高い)
-
-### Decision: 既存 `GET /page` ハンドラーのクリーンアップ方針
-
-- **Context**: 新エンドポイント追加後、旧ルートの share link ブランチをどうするか
-- **Selected Approach**: 同一スコープ内でクリーンアップを実施
-  - `certifySharedPage` を `GET /page` ミドルウェアチェーンから除去
-  - `isSharedPage` 条件分岐をハンドラーから除去
-  - `shareLinkId` パラメータバリデーターを除去
-  - クライアントを新エンドポイントに移行してから除去
-- **Rationale**: ゾンビコードを残さない。クライアント側の移行と backend のクリーンアップを同一 PR にまとめることで不整合期間をゼロにする
-- **Trade-offs**: `get-page-info.ts` はまだ `certifySharedPage` を使用しているため、`certifySharedPage` ファイル自体は削除しない
-
----
-
-## Risks & Mitigations
-
-- **クライアント移行の原子性**: クライアント (`use-fetch-current-page.ts`) と backend のデプロイが分離すると、移行期間中に新エンドポイントが存在しないタイミングが生じる可能性がある — 同一 PR で backend と client を同時変更することで回避
-- **`get-page-info.ts` への影響**: `certifySharedPage` は引き続き `get-page-info.ts` で使用される — このスペックのスコープ外として明示し、別タスク化を検討
-- **`disableLinkSharing` の既存動作変更**: 新エンドポイントで `disableLinkSharing` を正しくチェックすることは、現行の未チェック状態からの動作変更になる — セキュリティ改善として位置づけ、リリースノートに明記する

+ 0 - 22
.kiro/specs/sharelink-page-api/spec.json

@@ -1,22 +0,0 @@
-{
-  "feature_name": "sharelink-page-api",
-  "created_at": "2026-04-13T00:00:00.000Z",
-  "updated_at": "2026-04-13T04:29:18.643Z",
-  "language": "ja",
-  "phase": "implementation-complete",
-  "approvals": {
-    "requirements": {
-      "generated": true,
-      "approved": true
-    },
-    "design": {
-      "generated": true,
-      "approved": true
-    },
-    "tasks": {
-      "generated": true,
-      "approved": true
-    }
-  },
-  "ready_for_implementation": true
-}

+ 0 - 52
.kiro/specs/sharelink-page-api/tasks.md

@@ -1,52 +0,0 @@
-# Implementation Plan
-
-- [x] 1. (P) ShareLink バリデーションサービスの実装とテスト
-- [x] 1.1 (P) ShareLink のデータベースバリデーション関数を実装する
-  - share link ID とページ ID の両方を照合条件とした単一クエリで ShareLink を取得し、ミドルウェアで行っていた二重クエリを解消する
-  - 照合成功・リンク未存在/pageId 不一致・期限切れの 3 パターンを判別可能な結果型(discriminated union)で返す
-  - `disableLinkSharing` 設定の確認はハンドラー層に委ねて関数の責務を DB バリデーションのみに限定する
-  - `server/service/share-link/` ディレクトリを新規作成してこのサービスを配置し、将来的に他ルートからも再利用できる構造にする
-  - _Requirements: 2.1, 2.2, 2.3, 2.4_
-
-- [x] 1.2 (P) バリデーション関数のユニットテストを実装する
-  - 有効なリンク(ShareLink が存在・relatedPage が一致・期限内)のとき成功結果を返すことを確認する
-  - ShareLink が存在しない、または relatedPage が pageId と不一致のとき "not-found" 結果を返すことを確認する
-  - `isExpired()` が true のとき "expired" 結果を返すことを確認する
-  - _Requirements: 2.1, 2.2, 2.3, 2.4_
-
-- [x] 2. (P) ページレスポンスユーティリティの抽出とテスト
-- [x] 2.1 (P) 既存ページ取得ハンドラー内のレスポンス生成ロジックを共有ユーティリティとして抽出する
-  - `GET /page` ハンドラー内のインライン関数(ページデータ・meta を受け取って API レスポンスを返す処理)を独立したモジュール関数に変換する
-  - レスポンスオブジェクト・ページデータ・オプション(revisionId・disableUserPages)を引数として受け取るよう設計する
-  - 既存の `GET /page` ハンドラーがこのユーティリティを import して使用するよう書き換え、動作が変わらないことを確認する
-  - _Requirements: 5.3_
-
-- [x] 2.2 (P) レスポンスユーティリティのユニットテストを実装する
-  - ページが forbidden(`isForbidden: true`)のとき 403 が返ることを確認する
-  - ページが見つからない(`isNotFound: true`)のとき 404 が返ることを確認する
-  - `disableUserPages` が有効なユーザーページで 403 が返ることを確認する
-  - 正常ページで `{ page, meta }` を含むレスポンスが返ることを確認する
-  - _Requirements: 1.4, 3.1, 3.2, 3.3, 3.4_
-
-- [x] 3. share link 専用エンドポイントの実装・登録・テスト
-- [x] 3.1 `GET /page/shared` ハンドラーを実装してルーターに登録する
-  - `shareLinkId` と `pageId` の両方を MongoId 形式の必須パラメータとして受け取る(`optional()` を使用しない)
-  - リクエスト処理の最初のゲートとして `disableLinkSharing` 設定を確認し、無効時は 403 を返す
-  - Task 1 のバリデーション関数を呼び出し、"not-found" 結果には 404、"expired" 結果には 403 を返す
-  - バリデーション成功後に `isSharedPage: true` オプションでページデータを取得し、Task 2 のレスポンスユーティリティで返す
-  - 認証ミドルウェア(`accessTokenParser`・`loginRequired`・`certifySharedPage`)を一切使用しない公開エンドポイントとする
-  - ページルーターに `GET /shared` として登録し、`getPageInfoHandlerFactory` と同じパターンで組み込む
-  - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.5, 3.1, 4.1, 4.2, 4.3, 5.1, 5.4_
-
-- [ ] 4. クライアント更新と既存ルートのクリーンアップ
-- [x] 4.1 share link アクセス時のページ取得 API 呼び出しを新エンドポイントに切り替える
-  - `shareLinkId` が存在するかどうかで呼び出し先を `/page/shared` と `/page` に条件分岐させる
-  - パラメータ構造(`shareLinkId` + `pageId`)は既存の `buildApiParams` の出力をそのまま使用できるため変更不要であることを確認する
-  - _Requirements: 1.1, 1.3_
-
-- [x] 4.2 `GET /page` ルートから share link 関連コードを除去してシンプル化する
-  - `certifySharedPage` をミドルウェアチェーンから除去する
-  - `isSharedPage` 条件分岐をハンドラーから除去する
-  - `shareLinkId` パラメータバリデーターをバリデーター定義から除去する
-  - **必須**: Task 4.1 のクライアント更新と同一デプロイで実施すること。先に除去すると未移行クライアントが share link アクセス時に `loginRequired` によりブロックされる
-  - _Requirements: 5.2, 5.3_