power-pages · git:20260731.57d0835 · 2026-07-31 · sha256 077a0bcf7632fd51
power-pages git:20260731.57d0835A
Immutable. This exact content is served forever at /api/v1/blob/077a0bcf7632fd51.
---
name: power-pages
description: "Power Pages Code Site (SPA) の開発・ビルド・デプロイ。pac pages upload-code-site でサイト作成からデプロイまで完結する。"
category: ui
triggers:
- "Power Pages"
- "pac pages"
- "upload-code-site"
- "コードサイト"
- "code site"
- "SPA"
- "ポータル"
- "外部サイト"
- "Power Pages デプロイ"
- "site settings"
- "テーブル権限"
- "table permissions"
- "Web ロール"
- "Enhanced Data Model"
- "powerpagecomponent"
- "403 Forbidden"
- "404 Resource not found"
- "9004010C"
---
# Power Pages Code Site (SPA) 開発・デプロイスキル
> **公式リファレンス**: [Power Pages でシングルページ アプリケーションを作成して展開する | Microsoft Learn](https://learn.microsoft.com/ja-jp/power-pages/configure/create-code-sites)
> **新スキル参照**: [microsoft/power-platform-skills - power-pages](https://github.com/microsoft/power-platform-skills/tree/main/plugins/power-pages)
---
## サブリファレンス(必要に応じて参照)
| リファレンス | 内容 |
|---|---|
| [upstream 優先構成ガイド](references/upstream-alignment.md) | `microsoft/power-platform-skills` 基準での責務分離・実行順序・刷新方針 |
| [Dataverse クライアント実装](references/dataverse-client.md) | `powerPagesFetch`/`powerPagesFetchResponse`・`WebApiErrorCode`・OData ヘルパー・ページネーション・サービスレイヤーパターン・**Code Apps との対比** |
| [認証実装](references/authentication.md) | **SSO・サインアウト・ログインボタン・認証ガード・UI フロー**の実コード一式・サーバー側 IdP/サイト設定・Code Apps との対比 |
| [認証・認可・テーブル権限(詳細)](references/auth-authz.md) | EDM 2.0 Code Site の認証/認可/テーブル権限・Web API 共有クライアント・SSO+プロフィール編集・Web ロール管理・テーブル Web API 有効化・site-settings 永続化 |
| [Enhanced Data Model テーブル権限](references/enhanced-data-model-permissions.md) | EDM 2.0 のテーブル権限設定・3 レイヤー権限・N:N バグ・ワークアラウンド |
| [運用と落とし穴](references/operations-and-pitfalls.md) | ビルド・デプロイ・サイト再起動・よくあるエラーと解決策 |
| [トラブルシューティング](references/troubleshooting.md) | エラーコード早見表・デバッグ用 Site Settings・既知の無害な警告 |
| [レガシー参照用チェックリスト](references/legacy-checklist.md) | 旧構成の参照用チェックリスト |
| [デザインシステム](references/design-pattern.md) | UI コンポーネント・テーマ・レイアウトの指針 |
| [デザインテンプレート集](references/design-templates.md) | 5 種類の配色テンプレート定義。設計時に提案→選択→適用 |
| [アクセススコープ設計(Self / Account)](references/access-scope-design.md) | Self / Account の比較・scope 値・権限マトリクス・管理者への依頼テンプレート・セキュリティ根拠 |
| [紐づけ依頼メールフロー](references/account-link-request-flow.md) | 未紐づけユーザーから管理者への依頼テーブル設計・Power Automate フロー定義 |
| [管理者用紐づけ画面](references/account-link-admin-app.md) | contact ↔ account を管理者が割り当てる Code Apps 画面の仕様(実装は [code-apps/templates/account-link-admin](../code-apps/templates/account-link-admin/)) |
> **Dataverse 接続と認証の実装方法はこのファイルで概要を説明し、完全なサンプルコードは上記 References にまとめている。**
---
## 刷新版の構成原則(upstream 優先)
このスキルは `microsoft/power-platform-skills/plugins/power-pages` の以下 4 スキルを優先参照して構成する。
| 領域 | upstream スキル | このスキル内の着地 |
|---|---|---|
| 認証・認可 | `setup-auth` | `references/authentication.md` |
| Web ロール | `create-webroles` | `references/enhanced-data-model-permissions.md` |
| Dataverse CRUD | `integrate-webapi` | `references/dataverse-client.md` |
| 権限監査 | `audit-permissions` | `reviews/*` + `scripts/review_pre_deploy.py` |
**標準実行順序(刷新後)**
1. デプロイ基盤準備(`.powerpages-site` 作成)
2. Web ロール整備
3. 認証導線(SSO/ログイン/ログアウト)整備
4. Dataverse Web API CRUD 実装
5. 権限監査(ロール・テーブル権限整合)
> ⚠️ **デプロイ後の必須ステップ**: `pac pages upload-code-site` でテーブル権限 YAML をデプロイしても、
> type=18 の content JSON 内 `adx_entitypermission_webrole` が空のまま残り、Web ロール紐付けが効かない。デプロイ直後に
> `python scripts/setup_permissions.py` を実行して content の `adx_entitypermission_webrole` を書き込み、`review_pre_deploy.py` の
> チェック 3.7 が ✅ になることを確認する(さもないと管理者を含む全ユーザーが 403)。詳細は教訓 14。
> 詳細な責務分離と判断基準は [upstream 優先構成ガイド](references/upstream-alignment.md) を正本として扱う。
---
## microsoft/power-platform-skills 比較(認証・認可 vs Dataverse CRUD)
| 観点 | ユーザー認証・Webロール認可 | Dataverse Web API 連携 CRUD |
|---|---|---|
| 上流スキル | `setup-auth` + `create-webroles` | `integrate-webapi` |
| 主目的 | ログイン/ログアウト、認証状態判定、ロールベース UI 制御 | `/_api` 経由の読み書き(`powerPagesFetch`/`powerPagesFetchResponse`、OData ヘルパー) |
| 主な成果物 | `authService.ts`(AUTH_PROVIDERS 配列)・`use-auth.ts`・ログイン UI・Web ロール YAML(`.powerpages-site/web-roles/`) | `powerPagesApi.ts`、テーブル別 service/hooks、CRUD 画面 |
| サーバー側必須設定 | IdP site settings、Web ロール、テーブル権限へのロール紐付け | テーブル権限(type=18 + `adx_entitypermission_webrole`)、必要時のみ Webapi 設定 |
| 失敗時の代表症状 | ログインループ、`/profile` 強制遷移、未認証判定ミス | 401(90040107) / 403 / 404(9004010C, 9004010D) |
| 依存関係 | 先に認証導線を整える(ユーザー実体: contact) | 認証済みセッション Cookie 前提で CRUD を実行 |
**推奨適用順:**
1. `setup-auth` で認証導線を整備
2. `create-webroles` でロールを確定
3. `integrate-webapi` で CRUD 実装
4. `audit-permissions` で権限妥当性を最終監査
---
## 核心原則
1. **`pac pages upload-code-site` がサイト作成とデプロイの両方を行う** — API でサイトを事前作成する必要はない
2. **初回は Inactive Sites に作成される** → PP API (`2022-03-01-preview`) で `activate_site.py` を使ってアクティブ化
3. **デプロイは upload-code-site → relink → restart の3ステップ** — `upload-code-site` は既存テーブル権限の Web ロール紐付けを消すため、毎回 `scripts/relink_table_permissions.py`(または統合の `scripts/deploy_site.py`)で再付与する(教訓 15)。**`npm run deploy`(build && upload-code-site だけ)で終わらせない**
4. **Post-Upload Fix は不要** — `upload-code-site` が header/footer/page を正しく構成する
5. **`.powerpages-site/` は upload-code-site が自動管理する** — ただし `site-settings/` YAML は手動追加して永続化できる(下記参照)
6. **`adx_website` レコードは絶対に削除しない** — EDM 2.0 でもランタイムが起動時に参照する
7. **環境のクリーンアップ時は PP API のサイト一覧と照合してから削除する** — 誤削除で 500 エラー
8. **`credentials: "same-origin"` を使う** — `"include"` ではない(same-site Cookie 認証)
9. **powerpagecomponent type=18 には `powerpagesitelanguageid` が必須** — 未設定だと 404 になる
10. **報告者・作成者は Contact Lookup で追跡する** — Power Pages では `createdby` はアプリケーションユーザーになるため使えない。ログインユーザーの Contact 情報を自動取得し入力不要にする(教訓 19)
11. **デザイン系の変更は必ず `npm run dev`(localhost)で見た目を確認してから本番デプロイする** — 本番デプロイ → ブラウザ確認 → 再デプロイのループは 1 サイクルあたり数十秒〜数分かかり非効率。レイアウト・配色・レスポンシブ崩れは localhost で先に潰し、本番デプロイは最終確認のみに留める(教訓 20)。ただし Power Pages 本体のテーマ CSS(Bootstrap 既定の見出し色など)はローカルには存在せず本番でのみ衝突しうるため、色指定は見出し要素に明示的な `color` を必ず設定し、デプロイ後の最終確認も省略しない(詳細は [トラブルシューティング](references/troubleshooting.md))
12. **取引先企業(`account`)配下の「業務テーブル」は Account スコープ(`756150002`)で権限を切る** — 全件公開になるグローバルアクセスで逃げない。子テーブル権限(`accountrelationship` + create)を作り、POST 時に account Lookup を `@odata.bind` する。`read` は通るのに `create` だけ 403 になる典型パターンの原因(教訓 21)
13. **`account` テーブル自身に Account スコープを設定してはいけない** — Web API が 500(`9004010A`)を返し、会社名の参照も子レコードの bind も一切できなくなる。正解は **Contact スコープ(`756150001`)+ `contact_customer_accounts`**。これで「自分の `parentcustomerid` が指す 1 件」だけが返る(実機検証済み)
14. **Lookup バインドには参照する側・される側「両方」の 追加(Append)と 追加先(AppendTo)が要る** — 片側だけでは 403。`You don't have permission to associate or disassociate table X to Y` が出たら、まず不足している側の権限を疑う(実機で 4 通りの真理値表を検証済み)
15. **account Lookup を省いた POST は 403 ではなく 201 で通ってしまう** — できるのは誰にも見えない孤立レコード。テーブル権限では防げないため、クライアント側で account 未取得なら作成 UI を出さない+サーバー側(フロー等)で `null` を検知する二段構えにする(実機検証済み)
## ワークフロー
```
初回:
npm run build
→ pac pages upload-code-site ← Inactive Sites に作成
→ py portal/scripts/activate_site.py ← PP API でアクティブ化 (api-version=2022-03-01-preview)
→ py portal/scripts/setup_contact_webapi.py
→ py .github/skills/power-pages/scripts/setup_inquiry_reporter.py ← 報告者 Contact Lookup (教訓 19)
→ py .github/skills/power-pages/scripts/relink_table_permissions.py ← ★ロール再付与+再起動
2回目以降(推奨: 統合スクリプトで一括実行):
py .github/skills/power-pages/scripts/deploy_site.py
(ビルド → upload-code-site → ★relink → 検証 → 再起動 を 1 コマンドで実行)
2回目以降(手動の場合):
npm run build → pac pages upload-code-site
→ py .github/skills/power-pages/scripts/relink_table_permissions.py ← ★必須(省くと 403)
Account アクセスを採用する場合(初回デプロイ後に 1 回だけ・実機検証済みの順序):
py .github/skills/power-pages/scripts/setup_access_scope.py --scope account
→ py .github/skills/power-pages/scripts/setup_account_link_request.py
→ py .github/skills/power-pages/scripts/deploy_flow_account_link_request.py
→ 管理者用 Code App をデプロイ(code-apps/templates/account-link-admin)
→ py .github/skills/power-pages/scripts/setup_access_scope.py --scope account --verify-only
```
> ⚠️ **`PAGES_SITE_NAME` を `.env` に必ず設定する**。未設定だと最終更新のサイトが暗黙で選ばれ、
> 別サイトに権限を書き込む事故が起きる。
> ⚠️ **`npm run build && pac pages upload-code-site` だけで終わらせると、既存テーブル権限の
> Web ロール紐付けが消えて全件 403 になる**(教訓 15)。`relink_table_permissions.py` を
> 毎回実行するか、`deploy_site.py` で一括実行すること。両スクリプトともハードコードなし・
> すべて `.env` 管理(`DATAVERSE_URL` / `ENV_ID` / `PAGES_WEBSITE_ID` / `PP_SUBDOMAIN` /
> `RELINK_WEBROLE_NAMES` / `PORTAL_DIR`)。
### アクティベーション詳細
> **参照**: [microsoft/power-platform-skills activate-site](https://github.com/microsoft/power-platform-skills/tree/main/plugins/power-pages/skills/activate-site)
初回の `pac pages upload-code-site` はサイトを **Inactive Sites** に作成する。
アクティブ化は **Power Platform API** (`api-version=2022-03-01-preview`) で行う。
```
POST https://api.powerplatform.com/powerpages/environments/{ENV_ID}/websites?api-version=2022-03-01-preview
Body:
{
"name": "<PAGES_SITE_NAME>",
"subdomain": "<PAGES_SUBDOMAIN>",
"templateName": "DefaultPortalTemplate",
"dataverseOrganizationId": "<org_id>",
"selectedBaseLanguage": 1033,
"websiteRecordId": "<powerpagesiteid>" ← pac pages upload-code-site が作った ID
}
Response: 202 Accepted + Operation-Location ヘッダー
→ Operation-Location を 10 秒間隔でポーリング
→ OperationComplete = 成功、OperationFailed = 失敗
```
| パラメータ | 取得方法 |
|---|---|
| `ENV_ID` | .env |
| `PAGES_SITE_NAME` | .env / powerpages.config.json の siteName |
| `PAGES_SUBDOMAIN` | .env / ユーザー指定 |
| `dataverseOrganizationId` | `GET /api/data/v9.2/organizations?$select=organizationid` |
| `websiteRecordId` | `GET /api/data/v9.2/powerpagesites` から name で検索 |
**⚠️ API バージョン注意**: `2022-03-01-preview` を使用すること。`2024-10-01` ではアクティベーションが正しく動作しない。
## 前提条件
| ツール | バージョン | 用途 |
|--------|-----------|------|
| `pac` (Power Platform CLI) | 1.44+ | サイト作成・アップロード |
| `node` + `npm` | 18+ | SPA ビルド |
| Python 3 | 3.10+ | デプロイスクリプト(任意) |
> **pac CLI 注意**: サブコマンドは `pac pages` (例: `pac pages list`, `pac pages upload-code-site`)。
> `pac power-pages` は無効。`pac pages help` でコマンド一覧を確認できる。
### .env パラメータ
```env
DATAVERSE_URL=https://{org}.crm.dynamics.com/
ENV_ID= # Power Platform 環境 ID
PAGES_SITE_NAME= # サイト名 (powerpages.config.json の siteName と一致)
PAGES_SUBDOMAIN= # サブドメイン (例: myportal → myportal.powerappsportals.com)
# Account アクセススコープを使う場合(Step 4)
ACCESS_SCOPE=account # self | account
ACCOUNT_CHILD_TABLES= # 論理名:リレーションスキーマ名 のカンマ区切り
ACCOUNT_LINK_REQUEST_TABLE= # 紐づけ依頼テーブルの論理名
ACCOUNT_LINK_REQUEST_RELATIONSHIP= # contact との 1:N スキーマ名
ACCOUNT_LINK_ADMIN_RECIPIENT= # 依頼通知メールの宛先(配布リスト推奨)
```
## プロジェクト構造(公式準拠 / upstream 推奨)
```text
portal/
├── src/
│ ├── App.tsx ← ルート (HashRouter + Routes)
│ ├── main.tsx ← エントリポイント
│ ├── index.css ← Tailwind CSS
│ ├── components/
│ │ ├── site-layout.tsx ← ヘッダー・ナビ・プロフィールドロップダウン
│ │ ├── require-auth.tsx ← 認証ガードコンポーネント
│ │ ├── mode-toggle.tsx ← ダーク/ライト切替
│ │ └── ui/ ← shadcn/ui コンポーネント
│ ├── hooks/
│ │ └── use-auth.ts ← SSO 認証フック
│ ├── shared/
│ │ ├── powerPagesApi.ts ← ★ Web API 共有クライアント (powerPagesFetch/buildODataUrl 等)
│ │ └── services/
│ │ └── <table>Service.ts ← テーブルごとの CRUD サービス
│ ├── types/
│ │ └── <table>.ts ← エンティティ型・ドメイン型・マッパー
│ ├── lib/
│ │ └── utils.ts ← cn() ユーティリティ
│ ├── config.ts ← サイト名・ロゴ等のブランディング設定(.env の VITE_* を集約)
│ └── pages/
│ ├── home.tsx ← ランディングページ
│ └── profile.tsx ← プロフィール編集 (★ powerPagesApi.ts を使用)
├── dist-site/ ← ビルド出力 (compiledPath)
├── .powerpages-site/ ← upload-code-site が管理 + site-settings YAML 手動追加可
│ └── site-settings/ ← Webapi/* 設定を YAML で永続化
├── .env.example ← ★ ブランディング等の VITE_* 変数サンプル(コピーして .env を作成)
├── powerpages.config.json ← CLI 設定ファイル (必須)
├── package.json
├── vite.config.ts
└── scripts/
├── deploy.py ← デプロイスクリプト (Build→Upload→Restart)
├── activate_site.py ← PP API サイトアクティベーション
├── setup_auth.py ← Entra ID SSO 認証設定 (Site Settings + Liquid 注入)
└── setup_contact_webapi.py ← Contact Web API 有効化 (EDM 2.0 対応)
```
---
## サイト名・ロゴのブランディング設定(`.env` / `src/config.ts`)
デプロイごとに変わる**ブランディング値はコードに直書きせず**、ビルド時の環境変数で差し替える。
テンプレートの `.env.example` を `.env` にコピーして値を編集する(`.env` は `.gitignore` 済み)。
```bash
cp .env.example .env # 値を編集してから npm run build
```
| 変数 | 用途 | 既定値 |
|------|------|--------|
| `VITE_SITE_NAME` | サイト/ブランド表示名(ヘッダーロゴ・フッター・ブラウザタブのタイトル) | `Power Pages` |
| `VITE_SITE_LOGO_MARK` | ヘッダーロゴのマーク(1〜2 文字の頭文字) | `P` |
- すべて `VITE_` プレフィックス必須(Vite はこの接頭辞の変数のみクライアントへ公開)。
- 値は `src/config.ts`(`SITE_NAME` / `SITE_LOGO_MARK`)に集約し、未設定時は既定値へフォールバック。
- バンドルに同梱されブラウザに露出するため、**秘密情報は置かない**。
- `home.tsx` / `site-layout.tsx` は `@/config` を import して参照、`main.tsx` が `document.title` を設定。
---
## powerpages.config.json(必須)
```json
{
"siteName": "MySite",
"compiledPath": "dist-site",
"defaultLandingPage": "index.html"
}
```
### bundleFilePatterns(オプション)
`pac pages upload-code-site` は古いバンドルファイルを自動クリーンアップする。
デフォルトパターン: `main.*.js`, `main.*.css`, `vendor.*.js`, `index-*.js`, `index-*.css` 等 10 種。
Vite のデフォルト出力(`index-{hash}.js`)はカバーされるが、カスタムの命名規則を使う場合は明示指定する:
```json
{
"siteName": "MySite",
"compiledPath": "dist-site",
"defaultLandingPage": "index.html",
"bundleFilePatterns": ["app.*.js", "app.*.css", "style.*.css"]
}
```
---
## テンプレートとサンプルの使い分け
| フォルダ | 種別 | 内容 | 使いどころ |
|---|---|---|---|
| [templates/corporate-lp/](templates/corporate-lp/) | **プロジェクト雛形** | React + TypeScript + Vite + shadcn/ui。認証導線・`/profile`・Dataverse クライアント込み | **新規開発の既定の出発点**。コピーして中身を差し替える |
| [templates/minimal-code-site/](templates/minimal-code-site/) | **プロジェクト雛形** | ビルド不要の素の HTML/CSS/JS 1 セット | `upload-code-site` → アクティブ化 → 再起動の**配線だけを疎通確認**したいとき |
| [templates/access-scope/](templates/access-scope/) | **部分テンプレート(コード片)** | 取引先企業の読み取り専用表示と紐づけ依頼ボタン | 既存アプリに機能として**追加**する(雛形ではない) |
| [samples/portal/](samples/portal/) | **参照用サンプル** | 動作済みサイトの実体(`.powerpages-site` の web-templates / page-templates を含む) | 実装の答え合わせ・生成物の構造確認。**コピー元にはしない** |
> `templates/` = コピーして使うもの、`samples/` = 読んで参考にするもの。
> プロジェクト雛形はフォルダごとコピーし、部分テンプレートは該当ファイルだけを既存の
> `src/hooks/` や `src/components/` に配置する。
---
## Step 1: SPA 開発
### Vite 設定(必須制約)
```typescript
// vite.config.ts
export default defineConfig({
base: "./", // 相対パス(必須)
build: {
outDir: "dist-site", // powerpages.config.json と一致
rollupOptions: {
output: { inlineDynamicImports: true }, // 単一バンドル(推奨)
},
},
});
```
| 制約 | 理由 |
|------|------|
| `base: "./"` | Power Pages のパス構造に対応 |
| `inlineDynamicImports: true` | コード分割するとロード順問題が発生 |
| **Hash ルーティング必須** | History API モードは直接 URL アクセスで 404 |
| 静的 SPA のみ | SSR / ISR 非対応 |
### package.json scripts
```json
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"upload": "pac pages upload-code-site --rootPath . --compiledPath ./dist",
"deploy": "py ../.github/skills/power-pages/scripts/deploy_site.py"
}
}
```
### ★ ローカル確認を先に行う(デザイン変更時は必須)
レイアウト・配色・レスポンシブ対応など**見た目に関わる変更**は、本番デプロイ前に必ず `npm run dev` で確認する。
```bash
cd portal
npm run dev # http://localhost:5173 などで起動
# ブラウザで表示・配色・レスポンシブを確認してから次のコミット/デプロイへ進む
```
| 確認できること | 確認できないこと(デプロイ後に要再確認) |
|---|---|
| コンポーネント構造・レイアウト崩れ・レスポンシブ | Power Pages 本体のテーマ CSS との衝突(例: Bootstrap 既定の見出し色が明示指定のない `<h1>` 等に強制適用される) |
| 自前 CSS の配色・グラデーション・アニメーション | 認証リダイレクト・Web ロール・テーブル権限まわりの挙動 |
| コンポーネント間の状態遷移・フォームバリデーション | Power Pages ランタイムが注入する外部スクリプト/スタイルとの相互作用 |
> **教訓 20**: `<h1>` 等の見出し要素は、親要素に `color: #fff` を設定していても Power Pages 本体のテーマ CSS(`h1` への既定色指定)に上書きされることがある(継承より明示指定が常に優先されるため)。見出し要素には**必ず明示的に `color` を指定**し、ローカル確認だけで満足せず本番デプロイ後にも目視確認する。
---
## Step 2: 初回デプロイ
### 2-A: JavaScript ファイルのアップロード許可
環境で `.js` がブロックされている場合:
1. [Power Platform 管理センター](https://admin.powerplatform.microsoft.com/) → 環境選択
2. 設定 → プライバシー + セキュリティ
3. ブロックされた添付ファイルから `js` を削除
### 2-B: ビルド & アップロード
```bash
cd portal
npm run build
pac pages upload-code-site --rootPath .
```
### 2-C: サイトのアクティブ化(PP API 経由)
```bash
py portal/scripts/activate_site.py
```
スクリプトが以下を自動実行:
1. PP API でサイトが既にアクティブか確認
2. Dataverse から Organization ID と Website Record ID を取得
3. パラメータ確認後、PP API に POST
4. Operation-Location をポーリングして完了待ち(最大 5 分)
> カスタムサブドメインを指定する場合: `py portal/scripts/activate_site.py --subdomain my-portal`
### 2-D: Contact Web API 有効化(★初回必須)
```bash
py portal/scripts/setup_contact_webapi.py
```
### 2-E: サイト再起動
```bash
py .github/skills/standard/scripts/_restart.py
```
> アクティブ化後、URL にアクセスできるまで **60〜90秒** かかる。
---
## Step 3: 再デプロイ(2回目以降)
### 推奨: 統合スクリプトで一括実行(再現性が高い)
```bash
cd portal
py ../.github/skills/power-pages/scripts/deploy_site.py
# ビルド → upload-code-site → ★ロール再付与 → 検証 → 再起動 を 1 コマンドで実行する。
# upload-code-site が消す Web ロール紐付けを毎回必ず再付与するため、relink 忘れによる
# 全件 403 事故(教訓 15)を構造的に防ぐ。ハードコードなし・すべて .env 管理。
```
### 手動で段階実行する場合
```bash
cd portal
npm run build
pac pages upload-code-site --rootPath . --compiledPath ./dist
# ★ upload-code-site は既存 type=18 の content.adx_entitypermission_webrole を消すため、
# デプロイのたびに全テーブル権限の Web ロールを再付与する(教訓 15)。これを省くと 403。
py ../.github/skills/power-pages/scripts/relink_table_permissions.py
# → relink スクリプトが PAGES_WEBSITE_ID(推奨)または PP_SUBDOMAIN 設定時に自動で再起動する
# (未設定の場合は手動再起動)
```
> `/profile`(contact Self)を使う場合は、初回のみ `setup_contact_self.py` で
> contact 権限と `Webapi/contact/enabled|fields` を作成しておく(教訓 16):
> ```bash
> py ../.github/skills/power-pages/scripts/setup_contact_self.py
> ```
> 取引先企業(`account`)配下のレコードを扱う場合や、参照可能範囲を Self / Account から
> 選ぶ場合は **Step 4** を実行する(教訓 21)。
---
## Step 4: アクセススコープを決めてテーブル権限を構成する(Self / Account)
サインインユーザーが**誰のデータを見られるか**を確定し、そのスコープで権限・UI・運用を揃える。
背景と根拠は [アクセススコープ設計](references/access-scope-design.md)。
### 4-A: AskUserQuestion でスコープを確定する(実装前に必須)
**テーブル権限を作る前に必ず質問する。** ここを飛ばすと権限・UI・運用を同時にやり直すことになる。
| # | 質問 | 選択肢 |
|---|---|---|
| 1 | サインインユーザーの参照可能範囲は? | **Self アクセス**(本人のレコードだけ)/ **Account アクセス**(所属する取引先企業配下) |
| 2 | (Account のときのみ)管理者が contact ↔ 取引先企業を紐づける **Code Apps 管理画面**をどう用意しますか? | **テンプレートから作成する(既定)** / **作成しない(モデル駆動型アプリで運用)** |
- **Self アクセス** — 本人の `contact` と本人が作成したレコードのみ。BtoC・個人利用向け。
- **Account アクセス** — `contact` に紐づく **取引先企業(`account`)配下**を同僚と共有。BtoB ポータル向け。
回答を `.env` の `ACCESS_SCOPE`(`self` / `account`)と `ADMIN_LINK_APP`(`true` / `false`)に記録し、
以降の分岐に使う。迷っている場合の既定は **Self**(後から Account を足せるが、逆は利用者影響が大きい)。
> **Account を選んだら紐づけ手段は必須**。`contact.parentcustomerid` が空のままではどのデータも見えないため、
> 質問 2 は「作る/作らない」ではなく「Code Apps 管理画面か、モデル駆動型アプリ運用か」の選択である。
> 既定は [templates/account-link-admin](../code-apps/templates/account-link-admin/) からの作成(`ADMIN_LINK_APP=true`)。
### 4-B: リレーションスキーマ名を控える
権限に指定するのは**リレーションのスキーマ名**であり、表示名でも Lookup 列名でもない。
```bash
cd portal
python ../.github/skills/power-pages/scripts/setup_access_scope.py --list-relationships
```
### 4-C: Self アクセスを構成する
```bash
python ../.github/skills/power-pages/scripts/setup_access_scope.py --scope self
```
- `contact`: `scope=756150004`(Self)read/write、`appendto=true`
- 業務テーブル: `scope=756150001`(Contact)+ `contactrelationship`、read/write/create
- 各テーブルに `Webapi/{table}/enabled=true` と `Webapi/{table}/fields`
### 4-D: Account アクセスを構成する
サインイン時に作られた `contact` に関連付いている `account` を
**参照できるが編集・削除はできない**権限セットにする。
```bash
# .env の ACCOUNT_CHILD_TABLES(論理名:リレーションスキーマ名 のカンマ区切り)を使う
python ../.github/skills/power-pages/scripts/setup_access_scope.py --scope account
```
| # | テーブル | scope | リレーション | read | write | create | delete | append | appendto |
|---|---|---|---|---|---|---|---|---|---|
| 1 | `account` | **Contact(`756150001`)** | `contact_customer_accounts` | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 2 | 業務テーブル | Account(`756150002`) | `accountrelationship` | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| 3 | `contact` | Self(`756150004`) | なし | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
- **#1 に Account スコープを使わない**。`account` テーブル自身を Account スコープにすると
Web API が 500(`9004010A`)を返し、参照も bind もできなくなる(実機検証済み)。
Contact スコープ + `contact_customer_accounts`(account 1:N contact = `parentcustomerid`)にすれば
自社 1 件だけが返る。`verify()` は旧構成が残っていたら NG を出す。
- **#1 の `write`/`delete` は必ず `false`**。スクリプトは送信直前に検証し、`true` なら中止する。
- **`append`/`appendto` は編集権限ではない**。Lookup バインドには
**参照する側とされる側の両方**で `append` と `appendto` が必要で、片側だけだと 403 になる。
- Web ロールは既定で **Authenticated Users**。専用ロールにする場合は `ACCESS_SCOPE_WEBROLE_NAME` を設定する
(存在しなければ作成されるが、**contact への割り当ては別途必要**)。
- **グローバルアクセス(`756150000`)で代用しない**。全社の取引先企業が全ユーザーに見えてしまう。
- **account Lookup を省いた POST は 201 で通る**(403 にならない)。できるのは誰にも見えない
孤立レコードなので、クライアント側で account 未取得なら作成 UI を出さないこと。
### 4-E: 取引先企業をプロファイル画面に読み取り専用で表示する
`/profile` に会社情報を**表示専用**で出す(入力欄にしない)。
```typescript
const me = await powerPagesFetch<{ _parentcustomerid_value: string | null }>(
`/_api/contacts(${user.contactId})?$select=_parentcustomerid_value`
);
const accountId = me._parentcustomerid_value; // null = 未紐づけ
```
実装は [templates/access-scope/](templates/access-scope/) の `use-account-access.ts` /
`AccountProfileSection.tsx` を `src/hooks/`・`src/components/` に配置する。
> **なぜユーザーに選ばせないのか**: 取引先企業を自由に選べると、他社を選ぶだけで
> 他社データにアクセスできてしまう(権限昇格)。**紐づけはアプリ管理者だけが行う**。
> `Webapi/contact/fields` に `parentcustomerid` を含めない運用にすると PATCH 自体が拒否され二重に安全。
### 4-F: 未紐づけユーザー向けの「管理者に紐づけを依頼」を構成する
1. 依頼テーブル `{prefix}_accountlinkrequest` を作成する([dataverse](../dataverse/SKILL.md) スキル、
列定義は [紐づけ依頼メールフロー](references/account-link-request-flow.md))。
2. 依頼テーブルの権限と Web API を作成する(**作成と自分の依頼の参照だけ**を許可)。
```bash
python ../.github/skills/power-pages/scripts/setup_account_link_request.py
```
3. 依頼メール送信フローをデプロイする。
```bash
python ../.github/skills/power-pages/scripts/deploy_flow_account_link_request.py
```
接続参照の作成 → フロー作成 → 有効化 → Webhook 登録(`/start`)までを自動で行う。
事前に Dataverse と Office 365 Outlook の接続を make.powerautomate.com で作っておく(API では作れない)。
トリガーは **Dataverse「行が追加されたとき」**、アクションは **Office 365 Outlook「メールの送信」**。
宛先は `.env` の `ACCOUNT_LINK_ADMIN_RECIPIENT`。
4. プロファイル画面のボタンから依頼レコードを POST する(テンプレート実装済み)。
> **HTTP トリガーのフローを直接呼ばない**。匿名で叩ける URL は踏み台になる。
> 認証済みセッションで Dataverse にレコードを作り、サーバー側のフローで送信する。
### 4-G: 管理者用 Code Apps 画面を構築する(Account アクセスでは必須)
アドオンテンプレート **[code-apps/templates/account-link-admin](../code-apps/templates/account-link-admin/)** を使う。
[code-apps](../code-apps/SKILL.md) スキルで `generic-base` から scaffold し、テンプレートの `src/` を重ねて
ルート(`/account-link`)とナビ項目を追加するだけで動く。データソースは
`shared_commondataserviceforapps` を 1 回追加すれば `contacts` / `accounts` /
`{prefix}_accountlinkrequests` をすべて扱える。
画面仕様・更新処理・監査の考え方は [管理者用紐づけ画面](references/account-link-admin-app.md) に従う。
「作成しない」を選んだ場合は、モデル駆動型アプリの取引先担当者フォームで
取引先企業を設定する運用手順を README に記載する(紐づけ手段そのものは省略できない)。
### 4-H: 検証する
```bash
python ../.github/skills/power-pages/scripts/setup_access_scope.py --scope account --verify-only
```
- [ ] 4-A の AskUserQuestion を実施し、`.env` の `ACCESS_SCOPE` に記録した
- [ ] `account` 権限は **Contact スコープ + `contact_customer_accounts`** で read のみ(`write`/`delete` が `false`)
- [ ] `account` / 業務テーブル / `contact` のすべてに `append` と `appendto` がある
- [ ] 業務テーブル権限に `accountrelationship`(スキーマ名)と `create=true` がある
- [ ] すべての type=18 に Web ロールが **content JSON と N:N association の両方**で紐付いている
- [ ] `Webapi/{table}/enabled|fields` がアクセスする全テーブルにある
- [ ] プロファイル画面で取引先企業が**読み取り専用**で表示される
- [ ] 未紐づけユーザーに依頼ボタンが出て、押すとメールが届く
- [ ] `upload-code-site` の後に `relink_table_permissions.py` を実行した(省くと全件 403)
> 再起動の反映には 60〜90 秒かかる。確認は InPrivate ウィンドウで行う。
> 異常系は [トラブルシューティング](references/troubleshooting.md) を参照。
---
## レビュースキル(品質ゲート)
Power Pages の品質を標準的に維持するための **設計前レビュー** と **デプロイ前レビュー** を提供する。
| レビュー | タイミング | ドキュメント | スクリプト |
|---|---|---|---|
| **設計前レビュー** | テーブル設計完了後、SPA 実装開始前 | [reviews/pre-design-review.md](reviews/pre-design-review.md) | `scripts/review_pre_design.py` |
| **デプロイ前レビュー** | `npm run build` 後、`pac pages upload-code-site` 前 | [reviews/pre-deploy-review.md](reviews/pre-deploy-review.md) | `scripts/review_pre_deploy.py` |
### 呼び出し方
```bash
# 設計前レビュー(ローカル静的チェックのみ、Dataverse 接続不要)
cd portal
python ../.github/skills/power-pages/scripts/review_pre_design.py
# デプロイ前レビュー(ビルド出力 + Dataverse API チェック)
cd portal
python ../.github/skills/power-pages/scripts/review_pre_deploy.py
# CI/CD でリモートチェックをスキップする場合
SKIP_REMOTE=1 python ../.github/skills/power-pages/scripts/review_pre_deploy.py
```
> **標準フロー**: 設計前レビュー → 実装 → **`npm run dev` でローカル確認(デザイン変更時は必須・教訓 20)** → ビルド → デプロイ前レビュー → デプロイ → 本番での目視確認
---