Fix live room iframe CSP blocking and add open-in-new-tab fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-09-04 07:39:52 -05:00
parent f9ab758926
commit 5d741df77b
7 changed files with 66 additions and 6 deletions

View File

@ -13,7 +13,8 @@ from app.data import mock_data
from app.data import community_data from app.data import community_data
from app.services.recommendations import recommend_destinations from app.services.recommendations import recommend_destinations
from app.services.meetup_live import ( from app.services.meetup_live import (
build_lounge_chat_url, build_mirotalk_join_url, can_access_meetup, create_mirotalk_room_slug, build_lounge_chat_url, build_mirotalk_join_url, can_access_meetup,
create_lounge_channel, create_mirotalk_room_slug,
) )
from app.services.auth import get_user_by_token from app.services.auth import get_user_by_token
from app.services import community_store, social_store from app.services import community_store, social_store
@ -368,17 +369,23 @@ async def meetup_session(meetup_id: str, authorization: str | None = Header(None
mode = meetup.get("mode", "offline") mode = meetup.get("mode", "offline")
display = (user or {}).get("name", "访客") display = (user or {}).get("name", "访客")
if mode not in ("online", "hybrid"):
can_join = False
lock = lock or "offline"
session = MeetupSession( session = MeetupSession(
canJoin=can_join and mode in ("online", "hybrid"), canJoin=can_join,
lockReason=lock if not can_join else "", lockReason=lock if not can_join else "",
displayName=display, displayName=display,
mode=mode, mode=mode,
mirotalkRoom=meetup.get("mirotalkRoom") or create_mirotalk_room_slug(meetup), mirotalkRoom=meetup.get("mirotalkRoom") or create_mirotalk_room_slug(meetup),
loungeChannel=meetup.get("loungeChannel", ""), loungeChannel=meetup.get("loungeChannel") or "",
) )
if session.canJoin: if session.canJoin:
session.videoUrl = build_mirotalk_join_url(meetup, display) session.videoUrl = build_mirotalk_join_url(meetup, display)
session.chatUrl = build_lounge_chat_url(meetup) session.chatUrl = build_lounge_chat_url(meetup)
if not session.loungeChannel:
session.loungeChannel = create_lounge_channel(meetup)
return session return session

View File

@ -2,11 +2,14 @@ lounge.nomadro.com {
encode gzip encode gzip
reverse_proxy https://lounge.nomadro.cn { reverse_proxy https://lounge.nomadro.cn {
header_up Host lounge.nomadro.cn header_up Host lounge.nomadro.cn
header_down -Content-Security-Policy
header_down -X-Frame-Options
transport http { transport http {
tls_insecure_skip_verify tls_insecure_skip_verify
} }
} }
header { header {
Content-Security-Policy "frame-ancestors 'self' https://nomadweb.nomadro.com https://nomadro.cn https://www.nomadro.cn http://localhost:3001 http://127.0.0.1:3001" Content-Security-Policy "frame-ancestors 'self' https://nomadweb.nomadro.com https://nomadro.cn https://www.nomadro.cn http://localhost:3001 http://127.0.0.1:3001"
-X-Frame-Options
} }
} }

View File

@ -2,11 +2,15 @@ mirotalk.nomadro.com {
encode gzip encode gzip
reverse_proxy https://mirotalk.nomadro.cn { reverse_proxy https://mirotalk.nomadro.cn {
header_up Host mirotalk.nomadro.cn header_up Host mirotalk.nomadro.cn
# Upstream CSP omits nomadweb.nomadro.com; strip so our frame-ancestors win.
header_down -Content-Security-Policy
header_down -X-Frame-Options
transport http { transport http {
tls_insecure_skip_verify tls_insecure_skip_verify
} }
} }
header { header {
Content-Security-Policy "frame-ancestors 'self' https://nomadweb.nomadro.com https://nomadro.cn https://www.nomadro.cn http://localhost:3001 http://127.0.0.1:3001" Content-Security-Policy "frame-ancestors 'self' https://nomadweb.nomadro.com https://nomadro.cn https://www.nomadro.cn http://localhost:3001 http://127.0.0.1:3001"
-X-Frame-Options
} }
} }

View File

@ -8,6 +8,13 @@ export const metadata: Metadata = {
}; };
const LOGS = [ const LOGS = [
{
date: "2026-09-04",
tag: "直播 iframe 修复",
items: [
"修复 MiroTalk/Lounge 被上游 CSP 拦截导致直播页空白;可新窗口打开视频/聊天",
],
},
{ {
date: "2026-09-04", date: "2026-09-04",
tag: "匹配资料 · 报名日历", tag: "匹配资料 · 报名日历",

View File

@ -11044,10 +11044,23 @@ a.weather-card:hover {
flex-wrap: wrap; flex-wrap: wrap;
} }
.live-layout-btns { margin-left: auto; display: flex; gap: 6px; } .live-layout-btns { margin-left: auto; display: flex; gap: 6px; }
.live-external-links {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 10px 20px;
background: var(--bg-card);
border-bottom: var(--border-glass);
}
.live-iframe-hint {
font-size: 0.8rem;
color: var(--text-muted);
}
.live-stage { .live-stage {
flex: 1; flex: 1;
display: grid; display: grid;
min-height: calc(100vh - 52px); min-height: calc(100vh - 110px);
} }
.live-stage.layout-split { grid-template-columns: 1fr 380px; } .live-stage.layout-split { grid-template-columns: 1fr 380px; }
.live-stage.layout-video { grid-template-columns: 1fr; } .live-stage.layout-video { grid-template-columns: 1fr; }

View File

@ -112,17 +112,37 @@ export default function MeetupLiveClient({ meetupId, title }: { meetupId: string
))} ))}
</div> </div>
</div> </div>
<div className="live-external-links">
{session.videoUrl && (
<a href={session.videoUrl} target="_blank" rel="noopener noreferrer" className="btn btn-sm btn-primary">
{t.live.openVideo}
</a>
)}
{session.chatUrl && (
<a href={session.chatUrl} target="_blank" rel="noopener noreferrer" className="btn btn-sm">
{t.live.openChat}
</a>
)}
<span className="live-iframe-hint">{t.live.iframeHint}</span>
</div>
<div className={`live-stage layout-${layout}`}> <div className={`live-stage layout-${layout}`}>
{(layout === "split" || layout === "video") && session.videoUrl && ( {(layout === "split" || layout === "video") && session.videoUrl && (
<iframe <iframe
title="video" title="video"
src={session.videoUrl} src={session.videoUrl}
className="live-iframe live-video" className="live-iframe live-video"
allow="camera; microphone; display-capture" allow="camera; microphone; display-capture; autoplay; fullscreen"
referrerPolicy="no-referrer-when-downgrade"
/> />
)} )}
{(layout === "split" || layout === "chat") && session.chatUrl && ( {(layout === "split" || layout === "chat") && session.chatUrl && (
<iframe title="chat" src={session.chatUrl} className="live-iframe live-chat" /> <iframe
title="chat"
src={session.chatUrl}
className="live-iframe live-chat"
allow="clipboard-write"
referrerPolicy="no-referrer-when-downgrade"
/>
)} )}
{!session.videoUrl && !session.chatUrl && ( {!session.videoUrl && !session.chatUrl && (
<div className="notif-empty"> <div className="notif-empty">

View File

@ -520,6 +520,9 @@ export const zh = {
layoutVideo: "视频", layoutVideo: "视频",
layoutChat: "聊天", layoutChat: "聊天",
linksMissing: "直播链接尚未配置", linksMissing: "直播链接尚未配置",
openVideo: "新窗口打开视频",
openChat: "新窗口打开聊天",
iframeHint: "若画面空白,请用上方按钮在新窗口打开",
}, },
digital: { digital: {
tag: "🎓 ACADEMY", tag: "🎓 ACADEMY",
@ -1795,6 +1798,9 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
layoutVideo: "Video", layoutVideo: "Video",
layoutChat: "Chat", layoutChat: "Chat",
linksMissing: "Live links are not configured yet", linksMissing: "Live links are not configured yet",
openVideo: "Open video in new tab",
openChat: "Open chat in new tab",
iframeHint: "If the embed is blank, open the rooms in a new tab above",
}, },
digital: { digital: {
tag: "🎓 ACADEMY", tag: "🎓 ACADEMY",