Allow trusted iframe embedding

This commit is contained in:
FengLee
2026-05-14 20:06:41 +08:00
parent fdee295098
commit 50daaf8fb2
6 changed files with 32 additions and 2 deletions

View File

@@ -49,6 +49,10 @@ LOCAL_DB_SERVICE_ROLE_KEY=local-service-role-key
# NEXT_PUBLIC_APP_URL=https://your-domain.com
# APP_BASE_URL=https://your-domain.com
# ----- 第三方平台 iframe 嵌入白名单 (可选) -----
# 留空时默认允许同源和 mozheAPI 域名嵌入。多个来源用逗号或空格分隔。
# MIAOJING_FRAME_ANCESTORS=https://mozhevip.top https://*.mozhevip.top
# ----- 生产安全密钥 (生产环境必须设置) -----
# 建议使用 openssl rand -hex 32 生成
# DATA_ENCRYPTION_KEY=

View File

@@ -36,6 +36,8 @@ This file is the required entry point for Codex work in this repository. Its job
- This local clone was reset to `origin/main` before these docs were written.
- Remote: `https://git.toplee.cn/fenglee/miaojingAI.git`
- If server deployment is requested later, verify the active runtime tree and PM2 cwd before editing. Do not assume a production tree from memory.
- Production access verified on 2026-05-14 used `ssh -p 5238 root@124.174.9.29`; PM2 still served the live tree from `/opt/miaojingAI` through Node under `/data/miaojingAI/node/node-v24.15.0-linux-x64/bin`, with web/API/console ports `8000/8100/8200`. `/root/miaojingAI` may coexist and must not be treated as live without PM2 confirmation.
- When syncing source into `/opt/miaojingAI`, preserve production-only runtime files such as `.env.local`, `node_modules`, `.next`, `dist`, `backups`, local storage, and the production `ecosystem.config.cjs`. The repository copy may point at `/root/miaojingAI` and ports `5000/5100/5200`; overwriting production `ecosystem.config.cjs` breaks the live nginx upstream until restored.
## Fast Routing Map

View File

@@ -219,6 +219,8 @@ Runtime:
- `src/app/api/admin/upgrade/route.ts`
- `src/components/admin/system-upgrade-tab.tsx`
Production note from the 2026-05-14 update: the reachable SSH endpoint was `root@124.174.9.29 -p 5238`, while PM2 still served `/opt/miaojingAI` with Node/PM2 under `/data/miaojingAI/node/node-v24.15.0-linux-x64/bin`. The live ports were `8000` for web, `8100` for API, and `8200` for console. Do not overwrite production `ecosystem.config.cjs` with a repository or dev-server copy during rsync-style source updates; it can switch PM2 back to `/root/miaojingAI` and ports `5000/5100/5200`.
When changing deploy/upgrade behavior, validate package limits, disk checks, backup creation, rollback paths, restore safety backups, PM2 restart command, and health checks.
All new development must be designed so the production server can be updated later through the admin console upgrade package flow. Classify every deployable change before handoff:
@@ -244,6 +246,7 @@ Admin data export/import is a portability layer, separate from the full tar back
- Admin auth: role must be `admin` or `enterprise_admin`.
- Internal generation: protected by `x-miaojing-generation-internal`.
- Local file serving: must preserve storage key normalization and path traversal guards.
- Browser embedding: `src/proxy.ts` owns CSP security headers. `frame-ancestors` defaults to self plus mozheAPI origins and can be overridden with `MIAOJING_FRAME_ANCESTORS`; if an external ancestor is allowed, omit `X-Frame-Options` because `SAMEORIGIN` would still block the iframe.
- Admin destructive actions: keep environment gates, admin checks, and limits.
## Verification Strategy

View File

@@ -34,6 +34,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| Disabled canvas/`画布` appears again in public UI | `src/components/navbar.tsx`, `src/app/canvas/page.tsx`, `docs/codex-miaojing/feature-code-index.md` | Navbar should not include `/canvas`, and `/canvas` should continue to call `notFound()` unless the product explicitly re-enables the legacy canvas feature. |
| Announcement not popping up | `src/components/announcement-popup.tsx`, `src/app/api/announcements/route.ts`, `src/components/app-shell.tsx` | App shell includes popup, active date range, local/session dismissal behavior, GET payload shape. |
| Announcement admin edit fails | `src/components/admin/announcement-tab.tsx`, `src/app/api/announcements/route.ts` | Admin token, required fields, `starts_at`/`expires_at` compatibility. |
| Third-party platform iframe shows `miaojing.toplee.cn refused to connect` | `src/proxy.ts`, `.env.example`, reverse-proxy response headers | Check `Content-Security-Policy frame-ancestors` and `X-Frame-Options`. External iframe embedding requires the parent origin in `MIAOJING_FRAME_ANCESTORS` or the default mozheAPI allowlist, and `X-Frame-Options` must not be sent when third-party ancestors are allowed. Also verify the outer nginx/CDN is not adding its own stricter frame headers. |
## Creation And Generation
@@ -115,6 +116,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| Build or type check fails | `package.json`, `scripts/build.sh`, `next.config.ts`, failing source file, `CODEX_MIAOJING_MEMORY.md` | First run `corepack pnpm install --frozen-lockfile` if dependency modules are missing. Current audited baseline already recorded failures for missing `@/lib/model-display` and canvas type errors. Distinguish pre-existing source errors from your docs/change. |
| PM2 app not updated | `ecosystem.config.cjs`, `scripts/start.sh`, `scripts/deploy-or-upgrade.sh` | Process cwd, role ports, environment variables, `pm2 startOrReload ecosystem.config.cjs --update-env`. |
| Production uses different checkout | `ecosystem.config.cjs`, PM2 process env/cwd | Always verify PM2 cwd before editing production. |
| Production returns 502 after source sync or PM2 reload | `ecosystem.config.cjs`, `scripts/start.sh`, PM2 env, nginx upstream ports | Check whether production `ecosystem.config.cjs` was overwritten by a repo/dev copy. Live production on 2026-05-14 used `/opt/miaojingAI`, Node `/data/miaojingAI/node/node-v24.15.0-linux-x64/bin`, and ports `8000/8100/8200`; a repo copy pointing at `/root/miaojingAI` and `5000/5100/5200` breaks nginx until the production config is restored and PM2 reloaded. |
| Upgrade package cleanup failed | `scripts/deploy-or-upgrade.sh`, `scripts/admin-upgrade-runner.mjs`, `src/app/api/admin/upgrade/route.ts` | Cleanup trap, backup paths, state dir, disk space guards. |
| Unsure whether a change is safe for hot update or needs cold update | `scripts/admin-upgrade-runner.mjs`, `src/app/api/admin/upgrade/route.ts`, `src/components/admin/system-upgrade-tab.tsx`, `docs/codex-miaojing/architecture.md` | Treat source/API/server/dependency/schema/env/runtime/script changes as cold-update candidates. Hot updates should be static/public asset-only and must pass runner preflight without restart. Verify backup, rollback, package limits, disk checks, PM2 restart expectations, and `/api/health` before marking deploy-facing work complete. |

View File

@@ -15,6 +15,7 @@ Use this document to jump directly to code before broad searching.
| Announcement popup | `src/components/announcement-popup.tsx`, `src/app/api/announcements/route.ts`, `src/app/globals.css` | Frontend popup behavior plus backend announcement CRUD. Desktop dialog is intentionally wide (`max-w-5xl`) for long Markdown notices; scrollbar styling is inherited from the global glass scrollbar rules. |
| Site config sync | `src/components/site-config-sync.tsx`, `src/lib/site-config.ts`, `src/app/api/site-config/route.ts` | Site name, tab title, logo, favicon, policy Markdown, filing, membership switch. |
| Visit tracking | `src/components/visit-tracker.tsx`, `src/app/api/site-stats/route.ts` | Public visit counter. |
| Security headers and iframe embedding | `src/proxy.ts`, `.env.example` | CSP is set in the Next proxy. `frame-ancestors` controls which external platforms may embed MiaoJing in an iframe; `MIAOJING_FRAME_ANCESTORS` can override the default self + mozheAPI allowlist. When external ancestors are allowed, do not send `X-Frame-Options: SAMEORIGIN`, because it blocks third-party iframes. |
## Public Pages

View File

@@ -15,11 +15,27 @@ const PROTECTED_PATHS = [
'/api/admin/',
];
const DEFAULT_FRAME_ANCESTORS = [
"'self'",
'https://mozhevip.top',
'https://*.mozhevip.top',
];
function getFrameAncestors(): string[] {
const configured = process.env.MIAOJING_FRAME_ANCESTORS
?.split(/[,\s]+/)
.map(origin => origin.trim())
.filter(Boolean);
return configured && configured.length > 0 ? ["'self'", ...configured] : DEFAULT_FRAME_ANCESTORS;
}
function buildContentSecurityPolicy(request: NextRequest): string {
const isHttps = request.nextUrl.protocol === 'https:' || request.headers.get('x-forwarded-proto') === 'https';
const scriptSrc = ["'self'", "'unsafe-inline'", 'blob:'];
if (process.env.NODE_ENV !== 'production') scriptSrc.push("'unsafe-eval'");
const frameAncestors = getFrameAncestors();
const directives = [
["default-src", "'self'"],
["script-src", ...scriptSrc],
@@ -33,7 +49,7 @@ function buildContentSecurityPolicy(request: NextRequest): string {
['object-src', "'none'"],
['base-uri', "'self'"],
['form-action', "'self'"],
['frame-ancestors', "'self'"],
['frame-ancestors', ...frameAncestors],
];
if (isHttps) directives.push(['upgrade-insecure-requests']);
@@ -46,7 +62,9 @@ function applySecurityHeaders(response: NextResponse, request: NextRequest): Nex
response.headers.set('Content-Security-Policy', buildContentSecurityPolicy(request));
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
if (getFrameAncestors().length === 1) {
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
}
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
response.headers.set('X-DNS-Prefetch-Control', 'off');