feat(console): enforce guest sandbox session uuid rotation and unify guest-mode copy
This commit is contained in:
parent
adaf5782fa
commit
f695a0e937
@ -655,7 +655,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',
|
||||
@ -698,7 +698,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: [
|
||||
@ -1442,7 +1442,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
title: '账户',
|
||||
register: '注册',
|
||||
login: '登录',
|
||||
demo: '演示',
|
||||
demo: 'Guest user(演示模式)',
|
||||
welcome: '欢迎,{username}',
|
||||
logout: '退出登录',
|
||||
userCenter: '用户中心',
|
||||
@ -1485,7 +1485,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
userNotFound: '未找到该用户名对应的账户。',
|
||||
genericError: '登录失败,请稍后再试。',
|
||||
serviceUnavailable: '账户服务暂时不可用,请稍后再试。',
|
||||
disclaimer: '此演示登录仅会在浏览期间保留用户名,以便展示个性化的导航体验。',
|
||||
disclaimer: '此 Guest user(演示模式) 登录仅会在浏览期间保留用户名,以便展示个性化的导航体验。',
|
||||
},
|
||||
termsTitle: '服务条款',
|
||||
termsPoints: [
|
||||
|
||||
@ -65,6 +65,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'
|
||||
@ -78,6 +88,92 @@ 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,
|
||||
isReadOnly: true,
|
||||
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', {
|
||||
@ -89,7 +185,7 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return null
|
||||
return buildGuestUser()
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
@ -121,7 +217,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
|
||||
@ -133,7 +229,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 =
|
||||
@ -224,6 +320,18 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
.filter((tenant): tenant is TenantMembership => Boolean(tenant))
|
||||
: undefined
|
||||
|
||||
const effectiveTenantId = normalizedRole === 'guest' ? GUEST_SANDBOX_TENANT_ID : normalizedTenantId
|
||||
const effectiveTenants =
|
||||
normalizedRole === 'guest'
|
||||
? [
|
||||
{
|
||||
id: GUEST_SANDBOX_TENANT_ID,
|
||||
name: GUEST_SANDBOX_TENANT_NAME,
|
||||
role: 'guest' as UserRole,
|
||||
},
|
||||
]
|
||||
: normalizedTenants
|
||||
|
||||
return {
|
||||
id: identifier,
|
||||
uuid: identifier,
|
||||
@ -242,13 +350,13 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
isUser: normalizedRole === 'user',
|
||||
isOperator: normalizedRole === 'operator',
|
||||
isAdmin: normalizedRole === 'admin',
|
||||
isReadOnly: normalizedReadOnly,
|
||||
tenantId: normalizedTenantId,
|
||||
tenants: normalizedTenants,
|
||||
isReadOnly: normalizedRole === 'guest' ? true : normalizedReadOnly,
|
||||
tenantId: effectiveTenantId,
|
||||
tenants: effectiveTenants,
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to resolve user session', error)
|
||||
return null
|
||||
return buildGuestUser()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user