feat: add auth module with login and access control
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user