|
|
@@ -4,20 +4,20 @@
|
|
|
|
|
|
本機能は、share link 経由のページ取得を専用の API エンドポイント `GET /_api/v3/page/shared` として分離する。現行の `GET /_api/v3/page` ルートは、通常認証アクセスと share link アクセスの両方をミドルウェアフラグ(`req.isSharedPage`)と条件分岐で処理しており、責務が混在している。専用エンドポイントを設けることで、各ルートの責務を明確化し、コードの可読性と保守性を向上させる。
|
|
|
|
|
|
-既存のサービス関数(`findPageAndMetaDataByViewer`、`respondWithSinglePage`)を共有ユーティリティとして抽出・再利用することで、ロジックの重複を排除する。さらに、現行実装で未対応だった `security:disableLinkSharing` 設定のチェックをページ取得 API レイヤーに追加し、セキュリティギャップを解消する。
|
|
|
+既存のミドルウェア(`certifySharedPage`)とサービス関数(`findPageAndMetaDataByViewer`)を再利用し、`respondWithSinglePage` を共有ユーティリティとして抽出することで、ロジックの重複を排除する。さらに、現行実装で未対応だった `security:disableLinkSharing` 設定のチェックを `rejectLinkSharingDisabled` ミドルウェアとして新設し、セキュリティギャップを解消する。
|
|
|
|
|
|
### Goals
|
|
|
|
|
|
- share link アクセス専用の `GET /_api/v3/page/shared` エンドポイントを実装する
|
|
|
-- 認証ミドルウェア(`accessTokenParser`、`loginRequired`、`certifySharedPage`)を使用せず、公開エンドポイントとして設計する
|
|
|
-- `findPageAndMetaDataByViewer` および `respondWithSinglePage` を共有ユーティリティとして再利用し、コード重複を排除する
|
|
|
-- `security:disableLinkSharing` 設定を新エンドポイントで正しく適用する
|
|
|
+- 既存の `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` が引き続き使用するため)
|
|
|
+- `certifySharedPage` ミドルウェアの削除(`get-page-info.ts`・`revisions.js` が引き続き使用するため)
|
|
|
- share link の特定リビジョン指定(`revisionId`)対応(初期スコープ外)
|
|
|
- SSR share ページ(`/share/[[...path]]/page-data-props.ts`)の変更
|
|
|
|
|
|
@@ -25,24 +25,6 @@
|
|
|
|
|
|
## Architecture
|
|
|
|
|
|
-### Existing Architecture Analysis
|
|
|
-
|
|
|
-現行の `GET /page` ルートのミドルウェアチェーン:
|
|
|
-
|
|
|
-```
|
|
|
-accessTokenParser → certifySharedPage → loginRequired → validator.getPage → handler
|
|
|
-```
|
|
|
-
|
|
|
-ハンドラー内の条件分岐:
|
|
|
-1. `req.isSharedPage === true` → ShareLink 二次検索 + `findPageAndMetaDataByViewer({ isSharedPage: true })`
|
|
|
-2. `findAll` → `Page.findByPathAndViewer`
|
|
|
-3. デフォルト → `findPageAndMetaDataByViewer`
|
|
|
-
|
|
|
-**既存の設計課題**:
|
|
|
-- `certifySharedPage` は `disableLinkSharing` を未チェック(既存バグ)
|
|
|
-- ShareLink に対して 2 回の DB クエリを実行(ミドルウェアとハンドラーで各 1 回)
|
|
|
-- `certifySharedPage` は期限切れ・無効リンクでもエラーを返さずサイレントにパス
|
|
|
-
|
|
|
### Architecture Pattern & Boundary Map
|
|
|
|
|
|
```mermaid
|
|
|
@@ -57,8 +39,12 @@ graph TB
|
|
|
RespondUtil[respondWithSinglePage - extracted]
|
|
|
end
|
|
|
|
|
|
+ subgraph MiddlewareLayer[Middleware Layer]
|
|
|
+ RejectLSD[rejectLinkSharingDisabled - NEW]
|
|
|
+ CertifySP[certifySharedPage - existing]
|
|
|
+ end
|
|
|
+
|
|
|
subgraph ServiceLayer[Service Layer]
|
|
|
- ValidateSL[validateShareLink - NEW]
|
|
|
FPAMDBV[findPageAndMetaDataByViewer - existing]
|
|
|
end
|
|
|
|
|
|
@@ -70,31 +56,22 @@ graph TB
|
|
|
|
|
|
FetchHook -->|shareLinkId present| NewRoute
|
|
|
FetchHook -->|shareLinkId absent| OldRoute
|
|
|
- NewRoute --> ConfigMgr
|
|
|
- NewRoute --> ValidateSL
|
|
|
+ NewRoute --> RejectLSD
|
|
|
+ RejectLSD --> ConfigMgr
|
|
|
+ NewRoute --> CertifySP
|
|
|
+ CertifySP --> ShareLinkModel
|
|
|
NewRoute --> FPAMDBV
|
|
|
NewRoute --> RespondUtil
|
|
|
OldRoute --> RespondUtil
|
|
|
OldRoute --> FPAMDBV
|
|
|
- ValidateSL --> ShareLinkModel
|
|
|
FPAMDBV --> PageModel
|
|
|
```
|
|
|
|
|
|
**Architecture Integration**:
|
|
|
- Selected pattern: Handler Factory(`getPageInfoHandlerFactory` と同一パターン)
|
|
|
-- Domain boundary: share link バリデーションは `server/service/share-link/` に分離
|
|
|
- Existing patterns preserved: `RequestHandler[]` 返却型、`apiV3FormValidator`、`res.apiv3()`
|
|
|
-- New components: `validateShareLink` サービス、`respondWithSinglePage` ユーティリティ(抽出)、`getPageByShareLinkHandlerFactory` ハンドラー
|
|
|
-- Steering compliance: サーバー・クライアント境界の維持、純粋関数の抽出、命名規約(camelCase)
|
|
|
-
|
|
|
-### Technology Stack
|
|
|
-
|
|
|
-| Layer | Choice / Version | Role in Feature | Notes |
|
|
|
-|-------|------------------|-----------------|-------|
|
|
|
-| Backend | Express.js (既存) | ルート登録・ミドルウェアチェーン | 新規依存なし |
|
|
|
-| Data | MongoDB / Mongoose ^6.13.6 (既存) | ShareLink・Page ドキュメント取得 | ShareLink.findOne 一本化 |
|
|
|
-| Type Safety | TypeScript (既存) | 全インターフェース定義 | `any` 使用禁止 |
|
|
|
-| Validation | express-validator (既存) | `shareLinkId`・`pageId` 入力バリデーション | 既存バリデーター再利用 |
|
|
|
+- New components: `rejectLinkSharingDisabled` ミドルウェア、`respondWithSinglePage` ユーティリティ(抽出)、`getPageByShareLinkHandlerFactory` ハンドラー
|
|
|
+- Reused components: `certifySharedPage` ミドルウェア、`findPageAndMetaDataByViewer` サービス
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -105,28 +82,25 @@ graph TB
|
|
|
```mermaid
|
|
|
sequenceDiagram
|
|
|
participant Client
|
|
|
- participant Handler as GET slash page slash shared
|
|
|
- participant ValidateSL as validateShareLink
|
|
|
- participant ShareLink as ShareLink Model
|
|
|
+ participant RejectLSD as rejectLinkSharingDisabled
|
|
|
+ participant CertifySP as certifySharedPage
|
|
|
+ participant Handler as handler
|
|
|
participant FPAMDBV as findPageAndMetaDataByViewer
|
|
|
participant Page as Page Model
|
|
|
|
|
|
- Client->>Handler: shareLinkId pageId
|
|
|
- Handler->>Handler: validate params MongoId
|
|
|
- Handler->>Handler: check disableLinkSharing config
|
|
|
+ Client->>RejectLSD: shareLinkId pageId
|
|
|
alt disableLinkSharing = true
|
|
|
- Handler-->>Client: 403 link-sharing-disabled
|
|
|
+ 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
|
|
|
- Handler->>ValidateSL: shareLinkId pageId
|
|
|
- ValidateSL->>ShareLink: findOne id relatedPage
|
|
|
- alt not found or relatedPage mismatch
|
|
|
- ValidateSL-->>Handler: not-found error
|
|
|
- Handler-->>Client: 404 share-link-not-found
|
|
|
- else isExpired = true
|
|
|
- ValidateSL-->>Handler: expired error
|
|
|
- Handler-->>Client: 403 share-link-expired
|
|
|
+ CertifySP->>Handler: next
|
|
|
+ alt req.isSharedPage is falsy
|
|
|
+ Handler-->>Client: 404 share-link-invalid
|
|
|
end
|
|
|
- ValidateSL-->>Handler: ShareLinkDocument
|
|
|
Handler->>FPAMDBV: pageId isSharedPage=true
|
|
|
FPAMDBV->>Page: findOne _id
|
|
|
FPAMDBV-->>Handler: pageWithMeta
|
|
|
@@ -135,32 +109,31 @@ sequenceDiagram
|
|
|
```
|
|
|
|
|
|
フロー上の重要決定事項:
|
|
|
-- `disableLinkSharing` チェックはバリデーション前のファーストゲートとして配置し、DB アクセスを不要にする
|
|
|
-- `validateShareLink` は `findOne({ _id, relatedPage })` の単一クエリで存在確認と page 照合を同時実施(旧実装の二重クエリを解消)
|
|
|
+- `rejectLinkSharingDisabled` を `certifySharedPage` の前段に配置し、設定無効時は DB アクセスをスキップする
|
|
|
+- `certifySharedPage` は既存ミドルウェアをそのまま再利用。`req.isSharedPage` フラグで有効性を伝達する
|
|
|
- share link が有効である場合のみ `findPageAndMetaDataByViewer` を呼び出し、`isSharedPage: true` で page grant チェックをスキップする
|
|
|
|
|
|
---
|
|
|
|
|
|
## Requirements Traceability
|
|
|
|
|
|
-| Requirement | Summary | Components | Interfaces | Flows |
|
|
|
-|-------------|---------|------------|------------|-------|
|
|
|
-| 1.1 | 専用エンドポイントの提供 | `getPageByShareLinkHandlerFactory` | `GET /page/shared` API Contract | リクエストフロー全体 |
|
|
|
-| 1.2 | 認証ミドルウェアからの独立 | `getPageByShareLinkHandlerFactory` | ミドルウェアなし | ハンドラー直接実行 |
|
|
|
-| 1.3 | `shareLinkId` と `pageId` 必須パラメータ | `getPageByShareLinkHandlerFactory` | validator 定義 | params バリデーション |
|
|
|
-| 1.4 | 既存と同一レスポンス構造 | `respondWithSinglePage` | `{ page, meta }` | respondWithSinglePage |
|
|
|
-| 2.1–2.2 | ShareLink 存在確認・relatedPage 照合 | `validateShareLink` | Service Interface | validateShareLink フロー |
|
|
|
-| 2.3 | 不一致時 404 | `validateShareLink`, Handler | エラーレスポンス | not-found ブランチ |
|
|
|
-| 2.4 | 期限切れ時 403 | `validateShareLink`, Handler | エラーレスポンス | expired ブランチ |
|
|
|
-| 2.5 | `disableLinkSharing` 時 403 | Handler | エラーレスポンス | config チェック |
|
|
|
-| 3.1 | 最新リビジョン付きページ返却 | `findPageAndMetaDataByViewer`, `respondWithSinglePage` | `IDataWithMeta` | FPAMDBV + respond |
|
|
|
-| 3.2–3.3 | isMovable 等 false・bookmarkCount 0 | `findPageAndMetaDataByViewer` | `IPageInfoExt.isSharedPage` | FPAMDBV 内の分岐 |
|
|
|
-| 3.4 | ページ未存在時 404 | `respondWithSinglePage` | `IPageNotFoundInfo` | not-found メタ |
|
|
|
-| 4.1–4.3 | 認証不要 | `getPageByShareLinkHandlerFactory` | ミドルウェアチェーン | 認証ミドルウェアなし |
|
|
|
-| 5.1 | `findPageAndMetaDataByViewer` 再利用 | `getPageByShareLinkHandlerFactory` | 既存関数呼び出し | FPAMDBV 呼び出し |
|
|
|
-| 5.2 | バリデーション共通化 | `validateShareLink` | 新サービス関数 | validateShareLink |
|
|
|
-| 5.3 | シリアライズロジック非複製 | `respondWithSinglePage` | 抽出ユーティリティ | respondWithSinglePage |
|
|
|
-| 5.4 | `pageId` バリデーター共有 | `getPageByShareLinkHandlerFactory` | express-validator | params バリデーション |
|
|
|
+| 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 バリデーション |
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -168,61 +141,26 @@ sequenceDiagram
|
|
|
|
|
|
### コンポーネント一覧
|
|
|
|
|
|
-| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
|
|
|
-|-----------|--------------|--------|--------------|------------------|-----------|
|
|
|
-| `validateShareLink` | Service | ShareLink の DB バリデーション | 2.1, 2.2, 2.3, 2.4 | ShareLink Model (P0) | Service |
|
|
|
-| `respondWithSinglePage` | Route Utility | ページデータ → API レスポンス変換 | 1.4, 3.1, 3.2, 3.3, 3.4 | ApiV3Response (P0) | Service |
|
|
|
-| `getPageByShareLinkHandlerFactory` | Route Handler | `GET /page/shared` エンドポイント実装 | 全要件 | validateShareLink (P0), findPageAndMetaDataByViewer (P0), respondWithSinglePage (P0) | API |
|
|
|
-| `useFetchCurrentPage` update | Client State | 新エンドポイントへのクライアント移行 | 1.1, 1.3 | apiv3Get (P0) | State |
|
|
|
+| 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
|
|
|
|
|
|
-#### validateShareLink
|
|
|
+#### rejectLinkSharingDisabled
|
|
|
|
|
|
| Field | Detail |
|
|
|
|-------|--------|
|
|
|
-| Intent | ShareLink の存在・pageId 一致・有効期限を DB で検証し、有効な ShareLinkDocument を返す |
|
|
|
-| Requirements | 2.1, 2.2, 2.3, 2.4 |
|
|
|
-
|
|
|
-**Responsibilities & Constraints**
|
|
|
-- `shareLinkId` と `pageId` の両方を条件とした単一の `findOne` クエリで照合(二重クエリ排除)
|
|
|
-- 失敗時は呼び出し元がマッピング可能な型付きエラーオブジェクトを返す
|
|
|
-- `disableLinkSharing` の確認は呼び出し元(ハンドラー)に委ねる
|
|
|
-
|
|
|
-**Dependencies**
|
|
|
-- Outbound: `ShareLink` Model — `findOne` によるドキュメント取得 (P0)
|
|
|
-
|
|
|
-**Contracts**: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
|
|
|
+| File | `apps/app/src/server/middlewares/reject-link-sharing-disabled.ts` |
|
|
|
+| Intent | `security:disableLinkSharing` が有効な場合にリクエストを 403 で拒否するガードミドルウェア |
|
|
|
+| Requirements | 2.5 |
|
|
|
|
|
|
-##### Service Interface
|
|
|
-
|
|
|
-```typescript
|
|
|
-// apps/app/src/server/service/share-link/validate-share-link.ts
|
|
|
-
|
|
|
-type ValidateShareLinkSuccess = { readonly shareLink: ShareLinkDocument };
|
|
|
-type ValidateShareLinkFailure =
|
|
|
- | { readonly type: 'not-found' }
|
|
|
- | { readonly type: 'expired' };
|
|
|
-export type ValidateShareLinkResult =
|
|
|
- | ValidateShareLinkSuccess
|
|
|
- | ValidateShareLinkFailure;
|
|
|
-
|
|
|
-export async function validateShareLink(
|
|
|
- shareLinkId: string,
|
|
|
- pageId: string,
|
|
|
-): Promise<ValidateShareLinkResult>;
|
|
|
-```
|
|
|
-
|
|
|
-- Preconditions: `shareLinkId` と `pageId` は有効な MongoDB ObjectId 文字列
|
|
|
-- Postconditions: 成功時は `{ shareLink }` を返す。`relatedPage` 不一致・未存在時は `{ type: 'not-found' }`、期限切れ時は `{ type: 'expired' }` を返す
|
|
|
-- Invariants: `shareLink.relatedPage` が `pageId` と一致する場合のみ `shareLink` を返す
|
|
|
-
|
|
|
-**Implementation Notes**
|
|
|
-- Integration: `ShareLink.findOne({ _id: { $eq: shareLinkId }, relatedPage: { $eq: pageId } })` の単一クエリで存在確認と relatedPage 照合を同時実施。`isExpired()` メソッドを呼び出して期限確認
|
|
|
-- Validation: ObjectId 形式の検証はハンドラー側の `express-validator` が担当
|
|
|
-- Risks: 型付き結果を使用することで呼び出し元でのステータスコードマッピングが明示的になる
|
|
|
+**Implementation**: `configManager.getConfig('security:disableLinkSharing')` を確認し、`true` の場合は `res.apiv3Err(ErrorV3('Link sharing is disabled', 'link-sharing-disabled'), 403)` を返す。それ以外は `next()` を呼ぶ。
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -230,6 +168,7 @@ export async function validateShareLink(
|
|
|
|
|
|
| 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 |
|
|
|
|
|
|
@@ -239,114 +178,42 @@ export async function validateShareLink(
|
|
|
- ページが存在する場合の `populateDataToShowRevision` 呼び出し
|
|
|
- 既存の `page/index.ts` インライン実装を抽出・置き換え。**新ロジックを追加しない**
|
|
|
|
|
|
-**Dependencies**
|
|
|
-- Inbound: `GET /page` handler — 既存ハンドラーからの呼び出し (P0)
|
|
|
-- Inbound: `getPageByShareLinkHandlerFactory` — 新ハンドラーからの呼び出し (P0)
|
|
|
-- External: `@growi/core` `isIPageNotFoundInfo`, `isUserPage`, `isUsersTopPage` (P1)
|
|
|
-
|
|
|
-**Contracts**: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
|
|
|
-
|
|
|
-##### Service Interface
|
|
|
-
|
|
|
```typescript
|
|
|
-// apps/app/src/server/routes/apiv3/page/respond-with-single-page.ts
|
|
|
-
|
|
|
-import type { HydratedDocument } from 'mongoose';
|
|
|
-import type { IDataWithMeta, IPageInfoExt, IPageNotFoundInfo } from '@growi/core';
|
|
|
-import type { PageDocument } from '~/server/models/page';
|
|
|
-import type { ApiV3Response } from '../interfaces/apiv3-response';
|
|
|
-
|
|
|
-type RespondWithSinglePageOpts = {
|
|
|
- readonly revisionId?: string;
|
|
|
- readonly disableUserPages: boolean;
|
|
|
-};
|
|
|
-
|
|
|
-export const respondWithSinglePage = async (
|
|
|
+export async function respondWithSinglePage(
|
|
|
res: ApiV3Response,
|
|
|
pageWithMeta:
|
|
|
| IDataWithMeta<HydratedDocument<PageDocument>, IPageInfoExt>
|
|
|
| IDataWithMeta<null, IPageNotFoundInfo>,
|
|
|
- opts: RespondWithSinglePageOpts,
|
|
|
+ options?: RespondWithSinglePageOptions,
|
|
|
): Promise<void>;
|
|
|
```
|
|
|
|
|
|
-- Preconditions: `res` は有効な Express レスポンスオブジェクト
|
|
|
-- Postconditions: `res.apiv3()` または `res.apiv3Err()` のいずれか一方を必ず 1 回呼び出す
|
|
|
-- Invariants: share link ページの meta(`isMovable: false` 等)は `findPageAndMetaDataByViewer` が設定済みであり、このユーティリティは meta を変更しない
|
|
|
-
|
|
|
-**Implementation Notes**
|
|
|
-- Integration: `page/index.ts` の既存クロージャを `opts` を受け取るモジュール関数に変換。`page/index.ts` は抽出後にこの関数を import して使用する
|
|
|
-- Validation: `disableUserPages` はハンドラーが `configManager` から取得して渡す
|
|
|
-- Risks: 抽出時に `revisionId` の型(`string | undefined`)と `page.initLatestRevisionField()` の挙動を確認する
|
|
|
-
|
|
|
---
|
|
|
|
|
|
#### 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 |
|
|
|
|
|
|
-**Responsibilities & Constraints**
|
|
|
-- 認証ミドルウェア(`accessTokenParser`、`loginRequired`、`certifySharedPage`)を使用しない
|
|
|
-- `security:disableLinkSharing` チェックを最初のゲートとして実施
|
|
|
-- `validateShareLink` を呼び出して ShareLink バリデーション結果をハンドリング
|
|
|
-- `findPageAndMetaDataByViewer({ isSharedPage: true })` でページデータ取得
|
|
|
-- `respondWithSinglePage` で統一レスポンスを返す
|
|
|
-
|
|
|
-**Dependencies**
|
|
|
-- Inbound: `page/index.ts` — ルーター登録 (P0)
|
|
|
-- Outbound: `validateShareLink` — share link バリデーション (P0)
|
|
|
-- Outbound: `findPageAndMetaDataByViewer` — ページデータ取得 (P0)
|
|
|
-- Outbound: `respondWithSinglePage` — レスポンス生成 (P0)
|
|
|
-- External: `configManager` — `disableLinkSharing`・`disableUserPages` 設定取得 (P0)
|
|
|
-- External: `express-validator` — `shareLinkId`・`pageId` 入力バリデーション (P1)
|
|
|
-
|
|
|
-**Contracts**: Service [ ] / API [x] / Event [ ] / Batch [ ] / State [ ]
|
|
|
-
|
|
|
-##### 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 |
|
|
|
-
|
|
|
-**Request Parameters**:
|
|
|
-
|
|
|
-| Parameter | Type | Required | Description |
|
|
|
-|-----------|------|----------|-------------|
|
|
|
-| `shareLinkId` | MongoId (string) | ✅ | ShareLink の ObjectId |
|
|
|
-| `pageId` | MongoId (string) | ✅ | 参照先ページの ObjectId |
|
|
|
-
|
|
|
-**Handler TypeScript Interface**:
|
|
|
-
|
|
|
-```typescript
|
|
|
-// apps/app/src/server/routes/apiv3/page/get-page-by-share-link.ts
|
|
|
-
|
|
|
-import type { RequestHandler } from 'express';
|
|
|
-import type Crowi from '~/server/crowi';
|
|
|
-
|
|
|
-export const getPageByShareLinkHandlerFactory = (
|
|
|
- crowi: Crowi,
|
|
|
-): RequestHandler[];
|
|
|
-```
|
|
|
-
|
|
|
-**Handler Execution Order**:
|
|
|
+**Middleware Execution Order**:
|
|
|
1. `query('shareLinkId').isMongoId()` バリデーター(必須)
|
|
|
2. `query('pageId').isMongoId()` バリデーター(必須)
|
|
|
3. `apiV3FormValidator`
|
|
|
-4. async handler:
|
|
|
- - `disableLinkSharing` チェック → 403
|
|
|
- - `validateShareLink(shareLinkId, pageId)` → 結果に応じて 403/404
|
|
|
+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 })`
|
|
|
|
|
|
-**Validator sharing**: `pageId` のフォーマットルール(`isMongoId()`)は `validator.getPage` と共通だが、**新エンドポイントでは `optional()` を付けず必須扱いとする**。`validator.getPage` の `query('pageId').isMongoId().optional()` をそのまま流用してはならない。
|
|
|
+##### API Contract
|
|
|
|
|
|
-**Implementation Notes**:
|
|
|
-- Integration: `page/index.ts` で `router.get('/shared', getPageByShareLinkHandlerFactory(crowi))` として登録
|
|
|
-- Validation: handler 内で `configManager.getConfig('security:disableLinkSharing')` および `configManager.getConfig('security:disableUserPages')` を取得
|
|
|
-- Risks: `validateShareLink` 結果の型ガード(`'shareLink' in result`)で成功/失敗を分岐。`shareLink` は取得後に未使用だが、将来のレスポンス拡張(ShareLink メタ情報返却)に備えて保持可能
|
|
|
+| Method | Endpoint | Request | Response | Errors |
|
|
|
+|--------|----------|---------|----------|--------|
|
|
|
+| GET | `/_api/v3/page/shared` | `{ shareLinkId: MongoId, pageId: MongoId }` (query) | `{ page: IPagePopulatedToShowRevision, meta: IPageInfo }` | 400, 403, 404, 500 |
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -364,10 +231,6 @@ export const getPageByShareLinkHandlerFactory = (
|
|
|
4. `validator.getPage` から `shareLinkId` バリデーターを除去
|
|
|
5. `getPageByShareLinkHandlerFactory` を import し `router.get('/shared', ...)` を追加
|
|
|
|
|
|
-**後方互換性**: `GET /page?shareLinkId=xxx&pageId=yyy` への既存リクエストは、クライアント移行後に `shareLinkId` パラメータを無視するのみ(400 は返さない)。
|
|
|
-
|
|
|
-**デプロイ順序(必須)**: `certifySharedPage` の除去とクライアントの `/page/shared` 切り替えは**同一デプロイ**で実施すること。分割デプロイが必要な場合は、クライアント移行を先にデプロイし、`certifySharedPage` 除去を後にする。逆順にすると、クライアントがまだ `/page?shareLinkId=xxx` を呼んでいる状態で `certifySharedPage` がなくなり、`loginRequired` が未認証の share link ユーザーをブロックして share ページが閲覧不能になる。
|
|
|
-
|
|
|
---
|
|
|
|
|
|
### Client Layer
|
|
|
@@ -376,76 +239,36 @@ export const getPageByShareLinkHandlerFactory = (
|
|
|
|
|
|
| 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 |
|
|
|
|
|
|
-**Responsibilities & Constraints**
|
|
|
-- `shareLinkId` が存在する場合に `apiv3Get('/page/shared', ...)` を呼び出す
|
|
|
-- `shareLinkId` が存在しない場合は `apiv3Get('/page', ...)` を引き続き使用
|
|
|
-- レスポンス処理ロジックは変更なし
|
|
|
-
|
|
|
-**Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [x]
|
|
|
-
|
|
|
-##### State Management
|
|
|
-
|
|
|
-**変更箇所**: `apps/app/src/states/page/use-fetch-current-page.ts`
|
|
|
-
|
|
|
```typescript
|
|
|
-// Before
|
|
|
-const { data } = await apiv3Get<FetchedPageResult>('/page', params);
|
|
|
-
|
|
|
-// After
|
|
|
-const endpoint = shareLinkId != null && shareLinkId.length > 0
|
|
|
- ? '/page/shared'
|
|
|
- : '/page';
|
|
|
+const endpoint = params.shareLinkId != null ? '/page/shared' : '/page';
|
|
|
const { data } = await apiv3Get<FetchedPageResult>(endpoint, params);
|
|
|
```
|
|
|
|
|
|
-- Persistence: Jotai atoms(`currentPageDataAtom` 等)への書き込みは変更なし
|
|
|
-- Concurrency: 変更なし
|
|
|
-
|
|
|
-**Implementation Notes**:
|
|
|
-- Integration: `buildApiParams` の戻り値(`params`)は `/page/shared` で必要な `shareLinkId` と `pageId` を既に含んでいるため、パラメータ構造の変更は不要
|
|
|
-- Validation: `/page/shared` では `shareLinkId` と `pageId` の両方が必須。`buildApiParams` の priority B 分岐(`shareLinkId != null && currentPageId != null` の場合に `params.pageId = currentPageId`)が既にこれを保証している
|
|
|
-- Risks: `currentPageId` が未解決の状態(undefined)で share link ページにアクセスした場合、`buildApiParams` の priority B が機能しないケースを確認する(`shouldSkip` フラグで対処済みの可能性が高い)
|
|
|
-
|
|
|
---
|
|
|
|
|
|
## Error Handling
|
|
|
|
|
|
-### Error Strategy
|
|
|
-
|
|
|
-全バリデーションエラーはリクエストの早い段階で検出し、DB アクセスを最小化する。エラーレスポンスは既存の `ErrorV3` / `res.apiv3Err()` パターンに準拠する。
|
|
|
-
|
|
|
### 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-not-found` | 404 |
|
|
|
-| Business Logic (4xx) | ShareLink 期限切れ | `share-link-expired` | 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 |
|
|
|
|
|
|
-### Monitoring
|
|
|
-
|
|
|
-- logger: `loggerFactory('growi:routes:apiv3:page:get-page-by-share-link')` を使用
|
|
|
-- 403/404 は業務上正常な範囲として `logger.debug` レベル、5xx は `logger.error` レベルでログ出力
|
|
|
-
|
|
|
---
|
|
|
|
|
|
## Testing Strategy
|
|
|
|
|
|
### Unit Tests
|
|
|
|
|
|
-`validateShareLink` のユニットテスト(`validate-share-link.spec.ts`):
|
|
|
-- 有効な ShareLink(存在・relatedPage 一致・未期限)→ `{ shareLink }` を返す
|
|
|
-- ShareLink が存在しない → `{ type: 'not-found' }` を返す
|
|
|
-- `relatedPage` が `pageId` と不一致 → `{ type: 'not-found' }` を返す
|
|
|
-- `isExpired() === true` → `{ type: 'expired' }` を返す
|
|
|
-
|
|
|
`respondWithSinglePage` のユニットテスト(`respond-with-single-page.spec.ts`):
|
|
|
- `IPageNotFoundInfo` メタかつ `isForbidden=true` → 403
|
|
|
- `IPageNotFoundInfo` メタかつ `isForbidden=false` → 404
|
|
|
@@ -454,7 +277,7 @@ const { data } = await apiv3Get<FetchedPageResult>(endpoint, params);
|
|
|
|
|
|
### Security Considerations
|
|
|
|
|
|
-- **認証不要エンドポイントのデータ保護**: `validateShareLink` が全ての DB チェックを完了した後にのみ `findPageAndMetaDataByViewer` を呼び出す。バリデーション失敗時はページデータに一切アクセスしない。
|
|
|
-- **`disableLinkSharing` の適用**: 新エンドポイントは既存の未適用バグを修正し、設定が `true` の場合に確実に 403 を返す。
|
|
|
-- **ページ権限のスキップ**: `isSharedPage: true` による `findPageAndMetaDataByViewer` の権限チェックスキップは、ShareLink バリデーション完了後のみ実行されるため、不正アクセスのリスクはない。
|
|
|
+- **認証不要エンドポイントのデータ保護**: `certifySharedPage` が ShareLink の DB チェックを完了し `req.isSharedPage` をセットした後にのみ `findPageAndMetaDataByViewer` を呼び出す。バリデーション失敗時はページデータに一切アクセスしない。
|
|
|
+- **`disableLinkSharing` の適用**: `rejectLinkSharingDisabled` ミドルウェアを `certifySharedPage` の前段に配置し、設定が `true` の場合に確実に 403 を返す。DB アクセスも不要。
|
|
|
+- **ページ権限のスキップ**: `isSharedPage: true` による `findPageAndMetaDataByViewer` の権限チェックスキップは、`certifySharedPage` による ShareLink バリデーション完了後のみ実行されるため、不正アクセスのリスクはない。
|
|
|
- **入力サニタイズ**: `shareLinkId` と `pageId` は `express-validator` で MongoId 形式を強制し、NoSQL インジェクションを防止する。
|