feat: add auth module with login and access control

This commit is contained in:
2026-05-07 14:34:42 +08:00
parent 2d56bc2889
commit b8ba0ea6a0
15 changed files with 501 additions and 34 deletions

View File

@@ -1,18 +1,20 @@
import { computed, ref } from 'vue'
import {
loginBootstrapAdmin,
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 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 =
@@ -74,6 +76,67 @@ function readStoredUsername() {
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
@@ -82,17 +145,6 @@ function readLastActivityAt() {
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
@@ -107,19 +159,22 @@ function isSessionExpired(now = Date.now()) {
return now - lastActivityAt > authIdleTimeoutMs
}
function persistAuthState(value, username = '') {
function persistAuthState(value, user = null) {
if (typeof window === 'undefined') {
return
}
if (value) {
window.sessionStorage.setItem(AUTH_STORAGE_KEY, 'true')
window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(username || '').trim())
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)
}
@@ -213,7 +268,7 @@ function syncAuthSession(options = {}) {
if (!readAuthState()) {
loggedIn.value = false
currentUser.value = buildCurrentUser('')
currentUser.value = buildAnonymousUser()
clearSessionTimeout()
return false
}
@@ -224,7 +279,7 @@ function syncAuthSession(options = {}) {
}
loggedIn.value = true
currentUser.value = buildCurrentUser(readStoredUsername())
currentUser.value = readStoredUser()
scheduleSessionTimeout()
return true
}
@@ -250,7 +305,7 @@ const databaseTestMessage = ref('')
const loginSubmitting = ref(false)
const loginError = ref('')
const loggedIn = ref(readAuthState() && !isSessionExpired())
const currentUser = ref(buildCurrentUser(readStoredUsername()))
const currentUser = ref(readStoredUser())
if (!loggedIn.value && readAuthState()) {
persistAuthState(false)
@@ -288,7 +343,7 @@ function resetFromClientEnv() {
applyBootstrapState(readClientBootstrapState())
clearSetupRuntimeState()
loginError.value = ''
currentUser.value = buildCurrentUser(readStoredUsername())
currentUser.value = readStoredUser()
}
async function handleSetupSubmit(payload) {
@@ -382,19 +437,20 @@ async function handleLogin(credentials) {
loginError.value = ''
try {
await loginBootstrapAdmin({
const response = await loginByAccount({
username: credentials.username,
password: credentials.password
})
const user = response?.user || buildAnonymousUser()
loggedIn.value = true
persistAuthState(true, credentials.username)
currentUser.value = buildCurrentUser(credentials.username)
persistAuthState(true, user)
currentUser.value = user
touchAuthActivity(true)
return true
} catch (error) {
logout('invalid', { redirect: false })
loginError.value = error.message || '登录失败,请检查管理员账号和密码。'
loginError.value = error.message || '登录失败,请检查账号和密码。'
toast(loginError.value)
return false
} finally {
@@ -408,7 +464,7 @@ function logout(reason = 'manual', options = {}) {
loggedIn.value = false
persistAuthState(false)
currentUser.value = buildCurrentUser('')
currentUser.value = buildAnonymousUser()
clearSessionTimeout()
if (notify) {
@@ -421,7 +477,7 @@ function logout(reason = 'manual', options = {}) {
}
function handleRecoverPassword() {
toast('请联系系统管理员重置密码。管理员密码不会写入 .env。')
toast('请联系系统管理员重置账号密码。')
}
function handleSsoLogin() {
@@ -430,7 +486,7 @@ function handleSsoLogin() {
function resolveEntryRoute() {
loggedIn.value = syncAuthSession()
currentUser.value = buildCurrentUser(readStoredUsername())
currentUser.value = readStoredUser()
if (!isInitialized.value) {
return { name: 'setup' }
@@ -440,7 +496,7 @@ function resolveEntryRoute() {
return { name: 'login' }
}
return { name: 'app-overview' }
return resolveDefaultAuthorizedRoute(currentUser.value)
}
export function useSystemState() {