import { computed, ref } from 'vue' import { loginBootstrapAdmin, saveBootstrapConfig, testBootstrapDatabase, testBootstrapRuntime } from '../services/bootstrap.js' import { useToast } from './useToast.js' const AUTH_STORAGE_KEY = 'x-financial-authenticated' const AUTH_USERNAME_KEY = 'x-financial-auth-username' 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 readLastActivityAt() { if (typeof window === 'undefined') { return 0 } return Number(window.sessionStorage.getItem(AUTH_LAST_ACTIVITY_KEY) || 0) } function buildCurrentUser(username = '') { const normalized = String(username || '').trim() const name = normalized || DEFAULT_USER_NAME return { name, role: DEFAULT_USER_ROLE, avatar: name.slice(0, 1).toUpperCase() } } function isSessionExpired(now = Date.now()) { if (!readAuthState()) { return false } const lastActivityAt = readLastActivityAt() if (!lastActivityAt) { return true } return now - lastActivityAt > authIdleTimeoutMs } function persistAuthState(value, username = '') { if (typeof window === 'undefined') { return } if (value) { window.sessionStorage.setItem(AUTH_STORAGE_KEY, 'true') window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(username || '').trim()) return } window.sessionStorage.removeItem(AUTH_STORAGE_KEY) window.sessionStorage.removeItem(AUTH_USERNAME_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 = buildCurrentUser('') clearSessionTimeout() return false } if (isSessionExpired()) { logout('timeout', { notify: shouldNotify, redirect: false }) return false } loggedIn.value = true currentUser.value = buildCurrentUser(readStoredUsername()) 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(buildCurrentUser(readStoredUsername())) 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 || '' })) 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 = buildCurrentUser(readStoredUsername()) } 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 { await loginBootstrapAdmin({ username: credentials.username, password: credentials.password }) loggedIn.value = true persistAuthState(true, credentials.username) currentUser.value = buildCurrentUser(credentials.username) 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 = buildCurrentUser('') clearSessionTimeout() if (notify) { toast(reason === 'timeout' ? '登录已超时,请重新登录。' : '已退出登录。') } if (redirect) { redirectToLogin() } } function handleRecoverPassword() { toast('请联系系统管理员重置密码。管理员密码不会写入 .env。') } function handleSsoLogin() { toast('SSO 登录暂未启用。') } function resolveEntryRoute() { loggedIn.value = syncAuthSession() currentUser.value = buildCurrentUser(readStoredUsername()) if (!isInitialized.value) { return { name: 'setup' } } if (!loggedIn.value) { return { name: 'login' } } return { name: 'app-overview' } } 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 } }