Allow trusted iframe embedding
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
22
src/proxy.ts
22
src/proxy.ts
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user