import { computed, ref } from 'vue' import { saveBootstrapConfig, testBootstrapDatabase, testBootstrapRuntime } from '../services/bootstrap.js' import { login as loginByAccount } from '../services/auth.js' import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js' import { useToast } from './useToast.js' const AUTH_STORAGE_KEY = 'x-financial-authenticated' const AUTH_USERNAME_KEY = 'x-financial-auth-username' const AUTH_USER_KEY = 'x-financial-auth-user' const AUTH_LAST_ACTIVITY_KEY = 'x-financial-auth-last-activity' const DEFAULT_USER_NAME = '系统管理员' const DEFAULT_USER_ROLE = '管理员' const SESSION_ACTIVITY_EVENTS = ['pointerdown', 'keydown', 'scroll', 'touchstart', 'visibilitychange'] const authIdleTimeoutMinutes = Number(import.meta.env.VITE_AUTH_IDLE_TIMEOUT_MINUTES || 30) const authIdleTimeoutMs = Number.isFinite(authIdleTimeoutMinutes) && authIdleTimeoutMinutes > 0 ? authIdleTimeoutMinutes * 60 * 1000 : 30 * 60 * 1000 let sessionRouter = null let sessionTimeoutHandle = 0 let sessionMonitoringInstalled = false let lastActivityWriteAt = 0 function readClientBootstrapState() { const env = import.meta.env return { initialized: String(env.VITE_SETUP_COMPLETED || '').toLowerCase() === 'true', company: { name: env.VITE_COMPANY_NAME || '', code: env.VITE_COMPANY_CODE || '', admin_email: env.VITE_ADMIN_EMAIL || '' }, web: { host: env.VITE_WEB_HOST || '127.0.0.1', port: Number(env.VITE_WEB_PORT || 5173) }, server: { host: env.VITE_SERVER_HOST || '127.0.0.1', port: Number(env.VITE_SERVER_PORT || 8000) }, database: { driver: 'postgresql', host: env.VITE_POSTGRES_HOST || '127.0.0.1', port: Number(env.VITE_POSTGRES_PORT || 5432), name: env.VITE_POSTGRES_DB || 'x_financial', username: env.VITE_POSTGRES_USER || 'postgres', password_configured: false }, redis: { enabled: Boolean(env.VITE_REDIS_URL), url: env.VITE_REDIS_URL || '' } } } function readAuthState() { if (typeof window === 'undefined') { return false } return window.sessionStorage.getItem(AUTH_STORAGE_KEY) === 'true' } function readStoredUsername() { if (typeof window === 'undefined') { return '' } return window.sessionStorage.getItem(AUTH_USERNAME_KEY) || '' } function buildAnonymousUser() { return { username: '', name: '', role: '', roleCodes: [], email: '', avatar: '', isAdmin: false } } function buildLegacyAdminUser(username = '') { const normalized = String(username || '').trim() const name = normalized || DEFAULT_USER_NAME return { username: normalized, name, role: DEFAULT_USER_ROLE, roleCodes: ['manager'], email: '', avatar: name.slice(0, 1).toUpperCase(), isAdmin: true } } function readStoredUser() { if (typeof window === 'undefined') { return buildAnonymousUser() } const raw = window.sessionStorage.getItem(AUTH_USER_KEY) if (raw) { try { const payload = JSON.parse(raw) if (payload && typeof payload === 'object') { const username = String(payload.username || '').trim() const name = String(payload.name || username || DEFAULT_USER_NAME).trim() const roleCodes = Array.isArray(payload.roleCodes) ? payload.roleCodes.filter(Boolean) : [] return { username, name, role: String(payload.role || DEFAULT_USER_ROLE), roleCodes, email: String(payload.email || ''), avatar: String(payload.avatar || name.slice(0, 1).toUpperCase()), isAdmin: Boolean(payload.isAdmin) } } } catch { return buildLegacyAdminUser(readStoredUsername()) } } const legacyUsername = readStoredUsername() return legacyUsername ? buildLegacyAdminUser(legacyUsername) : buildAnonymousUser() } function readLastActivityAt() { if (typeof window === 'undefined') { return 0 } return Number(window.sessionStorage.getItem(AUTH_LAST_ACTIVITY_KEY) || 0) } function isSessionExpired(now = Date.now()) { if (!readAuthState()) { return false } const lastActivityAt = readLastActivityAt() if (!lastActivityAt) { return true } return now - lastActivityAt > authIdleTimeoutMs } function persistAuthState(value, user = null) { if (typeof window === 'undefined') { return } if (value) { window.sessionStorage.setItem(AUTH_STORAGE_KEY, 'true') const normalizedUser = user || buildAnonymousUser() window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(normalizedUser.username || '').trim()) window.sessionStorage.setItem(AUTH_USER_KEY, JSON.stringify(normalizedUser)) return } window.sessionStorage.removeItem(AUTH_STORAGE_KEY) window.sessionStorage.removeItem(AUTH_USERNAME_KEY) window.sessionStorage.removeItem(AUTH_USER_KEY) window.sessionStorage.removeItem(AUTH_LAST_ACTIVITY_KEY) } function clearSessionTimeout() { if (typeof window === 'undefined' || !sessionTimeoutHandle) { return } window.clearTimeout(sessionTimeoutHandle) sessionTimeoutHandle = 0 } function redirectToLogin() { if (sessionRouter?.currentRoute?.value?.name === 'login') { return } if (sessionRouter) { sessionRouter.replace({ name: 'login' }) return } if (typeof window !== 'undefined' && window.location.pathname !== '/login') { window.location.assign('/login') } } function scheduleSessionTimeout() { clearSessionTimeout() if (typeof window === 'undefined' || !readAuthState()) { return } const lastActivityAt = readLastActivityAt() if (!lastActivityAt) { return } const remaining = authIdleTimeoutMs - (Date.now() - lastActivityAt) if (remaining <= 0) { logout('timeout', { notify: true }) return } sessionTimeoutHandle = window.setTimeout(() => { logout('timeout', { notify: true }) }, remaining) } function touchAuthActivity(force = false) { if (typeof window === 'undefined' || !readAuthState()) { return } const now = Date.now() if (!force && now - lastActivityWriteAt < 1000) { scheduleSessionTimeout() return } window.sessionStorage.setItem(AUTH_LAST_ACTIVITY_KEY, String(now)) lastActivityWriteAt = now scheduleSessionTimeout() } function handleSessionActivity(event) { if (typeof document !== 'undefined' && event?.type === 'visibilitychange' && document.visibilityState !== 'visible') { return } touchAuthActivity() } function installSessionMonitoring() { if (sessionMonitoringInstalled || typeof window === 'undefined') { return } sessionMonitoringInstalled = true SESSION_ACTIVITY_EVENTS.forEach((eventName) => { window.addEventListener(eventName, handleSessionActivity, { passive: true }) }) } function syncAuthSession(options = {}) { const shouldNotify = Boolean(options.notify) if (!readAuthState()) { loggedIn.value = false currentUser.value = buildAnonymousUser() clearSessionTimeout() return false } if (isSessionExpired()) { logout('timeout', { notify: shouldNotify, redirect: false }) return false } loggedIn.value = true currentUser.value = readStoredUser() scheduleSessionTimeout() return true } export function installSessionNavigation(router) { sessionRouter = router installSessionMonitoring() if (readAuthState() && !isSessionExpired()) { scheduleSessionTimeout() } } const bootstrapState = ref(readClientBootstrapState()) const setupSubmitting = ref(false) const setupError = ref('') const runtimeTesting = ref(false) const databaseTesting = ref(false) const runtimeTestPassed = ref(false) const databaseTestPassed = ref(false) const runtimeTestMessage = ref('') const databaseTestMessage = ref('') const loginSubmitting = ref(false) const loginError = ref('') const loggedIn = ref(readAuthState() && !isSessionExpired()) const currentUser = ref(readStoredUser()) if (!loggedIn.value && readAuthState()) { persistAuthState(false) } const { toast } = useToast() const companyProfile = computed(() => ({ name: bootstrapState.value.company?.name || '', code: bootstrapState.value.company?.code || '', adminEmail: bootstrapState.value.company?.admin_email || '' })) function updateCompanyProfilePreview(payload = {}) { const currentCompany = bootstrapState.value.company || {} bootstrapState.value = { ...bootstrapState.value, company: { ...currentCompany, ...(payload.name !== undefined ? { name: payload.name } : {}), ...(payload.code !== undefined ? { code: payload.code } : {}), ...(payload.adminEmail !== undefined ? { admin_email: payload.adminEmail } : {}) } } } const isInitialized = computed(() => Boolean(bootstrapState.value.initialized)) function applyBootstrapState(state) { bootstrapState.value = state if (!state.initialized) { logout('reset', { redirect: false }) } } function clearSetupRuntimeState() { runtimeTesting.value = false databaseTesting.value = false runtimeTestPassed.value = false databaseTestPassed.value = false runtimeTestMessage.value = '' databaseTestMessage.value = '' setupError.value = '' } function resetFromClientEnv() { applyBootstrapState(readClientBootstrapState()) clearSetupRuntimeState() loginError.value = '' currentUser.value = readStoredUser() } async function handleSetupSubmit(payload) { if (!runtimeTestPassed.value) { setupError.value = '请先完成运行端口检测。' toast(setupError.value) return false } if (!databaseTestPassed.value) { setupError.value = '请先完成数据库连接检测。' toast(setupError.value) return false } setupSubmitting.value = true setupError.value = '' try { const state = await saveBootstrapConfig(payload) applyBootstrapState(state) toast('初始化配置已写入。现在可以进入登录页。') return true } catch (error) { setupError.value = error.message || '初始化配置写入失败,请稍后重试。' toast(setupError.value) return false } finally { setupSubmitting.value = false } } async function handleRuntimeTest(payload) { runtimeTesting.value = true runtimeTestMessage.value = '' setupError.value = '' try { const result = await testBootstrapRuntime(payload) runtimeTestPassed.value = true runtimeTestMessage.value = result.detail || '端口占用检测通过。' toast(runtimeTestMessage.value) } catch (error) { runtimeTestPassed.value = false runtimeTestMessage.value = error.message || '端口占用检测失败。' toast(runtimeTestMessage.value) } finally { runtimeTesting.value = false } } async function handleDatabaseTest(payload) { databaseTesting.value = true databaseTestMessage.value = '' setupError.value = '' try { const result = await testBootstrapDatabase(payload) databaseTestPassed.value = true databaseTestMessage.value = result.detail || '数据库连接检测通过。' toast(databaseTestMessage.value) } catch (error) { databaseTestPassed.value = false databaseTestMessage.value = error.message || '数据库连接检测失败。' toast(databaseTestMessage.value) } finally { databaseTesting.value = false } } function handleRuntimeDirty() { runtimeTestPassed.value = false runtimeTestMessage.value = '' if (setupError.value === '请先完成运行端口检测。') { setupError.value = '' } } function handleDatabaseDirty() { databaseTestPassed.value = false databaseTestMessage.value = '' if (setupError.value === '请先完成数据库连接检测。') { setupError.value = '' } } async function handleLogin(credentials) { loginSubmitting.value = true loginError.value = '' try { const response = await loginByAccount({ username: credentials.username, password: credentials.password }) const user = response?.user || buildAnonymousUser() loggedIn.value = true persistAuthState(true, user) currentUser.value = user touchAuthActivity(true) return true } catch (error) { logout('invalid', { redirect: false }) loginError.value = error.message || '登录失败,请检查账号和密码。' toast(loginError.value) return false } finally { loginSubmitting.value = false } } function logout(reason = 'manual', options = {}) { const notify = options.notify ?? reason === 'timeout' const redirect = options.redirect ?? reason !== 'invalid' loggedIn.value = false persistAuthState(false) currentUser.value = buildAnonymousUser() clearSessionTimeout() if (notify) { toast(reason === 'timeout' ? '登录已超时,请重新登录。' : '已退出登录。') } if (redirect) { redirectToLogin() } } function handleRecoverPassword() { toast('请联系系统管理员重置账号密码。') } function handleSsoLogin() { toast('SSO 登录暂未启用。') } function resolveEntryRoute() { loggedIn.value = syncAuthSession() currentUser.value = readStoredUser() if (!isInitialized.value) { return { name: 'setup' } } if (!loggedIn.value) { return { name: 'login' } } return resolveDefaultAuthorizedRoute(currentUser.value) } export function useSystemState() { return { bootstrapState, companyProfile, currentUser, databaseTestMessage, databaseTestPassed, databaseTesting, handleDatabaseDirty, handleDatabaseTest, handleLogin, handleRecoverPassword, handleRuntimeDirty, handleRuntimeTest, handleSetupSubmit, handleSsoLogin, isInitialized, loggedIn, loginError, loginSubmitting, logout, resetFromClientEnv, resolveEntryRoute, runtimeTestMessage, runtimeTestPassed, runtimeTesting, setupError, setupSubmitting, syncAuthSession, updateCompanyProfilePreview } }