feat(workbench): enforce guest sandbox session uuid rotation and rename demo copy
This commit is contained in:
parent
9b86890bdc
commit
fe127e59b6
@ -625,7 +625,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
title: 'Account',
|
||||
register: 'Register',
|
||||
login: 'Login',
|
||||
demo: 'Demo',
|
||||
demo: 'Guest user(演示模式)',
|
||||
welcome: 'Welcome, {username}',
|
||||
logout: 'Sign out',
|
||||
userCenter: 'User Center',
|
||||
@ -668,7 +668,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
userNotFound: 'We could not find an account with that username.',
|
||||
genericError: 'We could not sign you in. Please try again later.',
|
||||
serviceUnavailable: 'The account service is temporarily unavailable. Please try again shortly.',
|
||||
disclaimer: 'This demo login keeps your username in memory only to personalize navigation while you browse.',
|
||||
disclaimer: 'This Guest user(演示模式) login keeps your username in memory only to personalize navigation while you browse.',
|
||||
},
|
||||
termsTitle: 'Terms of Service',
|
||||
termsPoints: [
|
||||
@ -1306,9 +1306,9 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
},
|
||||
},
|
||||
about: {
|
||||
title: 'About This Demo',
|
||||
subtitle: 'This website is a personal demonstration (Demo) project maintained by an individual.',
|
||||
disclaimer: 'Some AI capabilities on this site are powered by third-party AI services, including NVIDIA APIs and Alibaba Cloud Qwen. Only selected features are currently available for technical exploration and demonstration, and the site is not intended for production use or as a commercial system. Please do not enter any personal data, sensitive information, or confidential content in this Demo. All user inputs and generated outputs may be processed by third-party AI platforms.',
|
||||
title: 'About Guest user(演示模式)',
|
||||
subtitle: 'This website is a personal demonstration (Guest user(演示模式)) project maintained by an individual.',
|
||||
disclaimer: 'Some AI capabilities on this site are powered by third-party AI services, including NVIDIA APIs and Alibaba Cloud Qwen. Only selected features are currently available for technical exploration and demonstration, and the site is not intended for production use or as a commercial system. Please do not enter any personal data, sensitive information, or confidential content in Guest user(演示模式). All user inputs and generated outputs may be processed by third-party AI platforms.',
|
||||
acknowledgments: 'Special thanks to GitHub, ChatGPT, Google AI, NVIDIA AI, and other tools and platforms, which enable individual developers to build AI application prototypes approaching professional SaaS standards at a relatively low cost.',
|
||||
toolsTitle: 'Acknowledged tools and platforms used (in no particular order):',
|
||||
toolsNote: '(The platform names above are listed for technical acknowledgment only and do not imply any official partnership or endorsement.)',
|
||||
@ -1361,7 +1361,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
title: '账户',
|
||||
register: '注册',
|
||||
login: '登录',
|
||||
demo: '演示',
|
||||
demo: 'Guest user(演示模式)',
|
||||
welcome: '欢迎,{username}',
|
||||
logout: '退出登录',
|
||||
userCenter: '用户中心',
|
||||
@ -1891,8 +1891,8 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
},
|
||||
about: {
|
||||
title: '关于本项目',
|
||||
subtitle: '本网站是一个由个人维护的 Demo 项目。',
|
||||
disclaimer: '站点中的部分 AI 能力来自 NVIDIA API、阿里云通义千问等第三方 AI 服务,目前仅开放部分功能用于技术探索与展示,不作为生产环境或商业系统使用。请勿在本 Demo 中输入任何个人信息、敏感数据或机密内容。所有用户输入及生成结果均可能经过第三方 AI 平台处理。',
|
||||
subtitle: '本网站是一个由个人维护的 Guest user(演示模式)项目。',
|
||||
disclaimer: '站点中的部分 AI 能力来自 NVIDIA API、阿里云通义千问等第三方 AI 服务,目前仅开放部分功能用于技术探索与展示,不作为生产环境或商业系统使用。请勿在 Guest user(演示模式)中输入任何个人信息、敏感数据或机密内容。所有用户输入及生成结果均可能经过第三方 AI 平台处理。',
|
||||
acknowledgments: '感谢 GitHub、ChatGPT、Google AI、NVIDIA AI 等工具与服务平台,让个人开发者也能够以相对较低的成本,构建出接近专业 SaaS 水准的 AI 应用原型。',
|
||||
toolsTitle: '致谢与使用的工具与平台(不分先后):',
|
||||
toolsNote: '以上平台名称仅用于技术致谢,不代表任何官方合作或背书!',
|
||||
|
||||
@ -58,6 +58,16 @@ const KNOWN_ROLE_MAP: Record<string, UserRole> = {
|
||||
member: 'user',
|
||||
}
|
||||
|
||||
const GUEST_SESSION_STORAGE_KEY = 'xcontrol.guest.session'
|
||||
const GUEST_SESSION_TTL_MS = 60 * 60 * 1000
|
||||
const GUEST_SANDBOX_TENANT_ID = 'guest-sandbox'
|
||||
const GUEST_SANDBOX_TENANT_NAME = 'Guest Sandbox'
|
||||
|
||||
type GuestSession = {
|
||||
uuid: string
|
||||
issuedAt: number
|
||||
}
|
||||
|
||||
function normalizeRole(input?: string | null): UserRole {
|
||||
if (!input || typeof input !== 'string') {
|
||||
return 'guest'
|
||||
@ -71,6 +81,91 @@ function normalizeRole(input?: string | null): UserRole {
|
||||
return KNOWN_ROLE_MAP[normalized] ?? 'guest'
|
||||
}
|
||||
|
||||
function createUUID(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
|
||||
function readGuestSession(): GuestSession | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
const raw = window.sessionStorage.getItem(GUEST_SESSION_STORAGE_KEY)
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as GuestSession
|
||||
if (
|
||||
typeof parsed?.uuid === 'string' &&
|
||||
parsed.uuid.trim().length > 0 &&
|
||||
typeof parsed?.issuedAt === 'number' &&
|
||||
Number.isFinite(parsed.issuedAt)
|
||||
) {
|
||||
return {
|
||||
uuid: parsed.uuid.trim(),
|
||||
issuedAt: parsed.issuedAt,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse guest session payload', error)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function writeGuestSession(session: GuestSession) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
window.sessionStorage.setItem(GUEST_SESSION_STORAGE_KEY, JSON.stringify(session))
|
||||
}
|
||||
|
||||
function resolveGuestUUID(now = Date.now()): string {
|
||||
const existing = readGuestSession()
|
||||
if (!existing || now - existing.issuedAt >= GUEST_SESSION_TTL_MS) {
|
||||
const next: GuestSession = { uuid: createUUID(), issuedAt: now }
|
||||
writeGuestSession(next)
|
||||
return next.uuid
|
||||
}
|
||||
return existing.uuid
|
||||
}
|
||||
|
||||
function buildGuestUser(): User {
|
||||
const identifier = resolveGuestUUID()
|
||||
return {
|
||||
id: identifier,
|
||||
uuid: identifier,
|
||||
email: 'guest@sandbox.local',
|
||||
name: 'Guest user',
|
||||
username: 'guest',
|
||||
mfaEnabled: false,
|
||||
mfaPending: false,
|
||||
mfa: {
|
||||
totpEnabled: false,
|
||||
totpPending: false,
|
||||
},
|
||||
role: 'guest',
|
||||
groups: ['guest', 'sandbox'],
|
||||
permissions: ['read'],
|
||||
isGuest: true,
|
||||
isUser: false,
|
||||
isOperator: false,
|
||||
isAdmin: false,
|
||||
tenantId: GUEST_SANDBOX_TENANT_ID,
|
||||
tenants: [
|
||||
{
|
||||
id: GUEST_SANDBOX_TENANT_ID,
|
||||
name: GUEST_SANDBOX_TENANT_NAME,
|
||||
role: 'guest',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSessionUser(): Promise<User | null> {
|
||||
try {
|
||||
const response = await fetch('/api/auth/session', {
|
||||
@ -82,7 +177,7 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return null
|
||||
return buildGuestUser()
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
@ -111,7 +206,7 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
|
||||
const sessionUser = payload?.user
|
||||
if (!sessionUser) {
|
||||
return null
|
||||
return buildGuestUser()
|
||||
}
|
||||
|
||||
const { id, uuid, email, name, username, mfaEnabled, mfa, mfaPending, role, groups, permissions } = sessionUser
|
||||
@ -123,7 +218,7 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
: ''
|
||||
|
||||
if (!identifier) {
|
||||
return null
|
||||
return buildGuestUser()
|
||||
}
|
||||
const normalizedName = typeof name === 'string' && name.trim().length > 0 ? name.trim() : undefined
|
||||
const normalizedUsername =
|
||||
@ -207,7 +302,7 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to resolve user session', error)
|
||||
return null
|
||||
return buildGuestUser()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user