fix:修复登录状态经常失效的问题

This commit is contained in:
chengqian committed 2026-07-27 14:03:58 +08:00
1 parent 92c2cc033b
commit ddade6ec54
6 files changed
+90 -23

No files matched your search

+8 -2
View File
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import request, { setTokens, clearTokens, onTokenRefreshed } from '@/utils/request'
import request, { setTokens, clearTokens, onTokenRefreshed, setRememberMe, getRememberMe } from '@/utils/request'
import { setAdminSession, clearAdminSession, ADMIN_ROLES } from '@/utils/permission'
export interface AdminUser {
@@ -35,6 +35,7 @@ export const useAuthStore = defineStore('admin-auth', () => {
const user = ref<AdminUser | null>(null)
const permissions = ref<string[]>([])
const pendingAgreements = ref<PendingAgreement[]>([])
const rememberMe = ref<boolean>(getRememberMe())
const isLoggedIn = computed(() => !!token.value)
@@ -88,7 +89,11 @@ export const useAuthStore = defineStore('admin-auth', () => {
persist()
}
async function login(username: string, password: string, captcha?: { captchaKey: string; captchaX: number }): Promise<any> {
async function login(username: string, password: string, captcha?: { captchaKey: string; captchaX: number }, remember: boolean = true): Promise<any> {
// Save user's "remember me" preference
rememberMe.value = remember
setRememberMe(remember)
const { data } = await request.post('/admin/auth/login', {
username,
password,
@@ -155,6 +160,7 @@ export const useAuthStore = defineStore('admin-auth', () => {
user,
permissions,
pendingAgreements,
rememberMe,
isLoggedIn,
isSuperAdmin,
login,
+31 -7
View File
@@ -24,36 +24,60 @@ declare module 'axios' {
const ACCESS_TOKEN_KEY = 'admin_access_token'
const REFRESH_TOKEN_KEY = 'admin_refresh_token'
const EXPIRES_AT_KEY = 'admin_token_expires_at'
const REMEMBER_ME_KEY = 'admin_remember_me'
const TOKEN_BUFFER_SECONDS = 60
// Dynamic storage selection based on "remember me" preference
function getStorage(): Storage {
// Check user's "remember me" preference (defaults to true for backward compatibility)
const rememberMe = localStorage.getItem(REMEMBER_ME_KEY)
return rememberMe === 'false' ? sessionStorage : localStorage
}
export function setRememberMe(remember: boolean): void {
// Store the preference itself in localStorage (so it persists across sessions)
localStorage.setItem(REMEMBER_ME_KEY, String(remember))
}
export function getRememberMe(): boolean {
const saved = localStorage.getItem(REMEMBER_ME_KEY)
return saved === null ? true : saved === 'true'
}
export function setTokens(tokens: TokenPair): void {
localStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken)
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
localStorage.setItem(EXPIRES_AT_KEY, String(tokens.expiresAt))
const storage = getStorage()
storage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken)
storage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
storage.setItem(EXPIRES_AT_KEY, String(tokens.expiresAt))
}
export function clearTokens(): void {
// Clear from both storages to prevent leftover data
localStorage.removeItem(ACCESS_TOKEN_KEY)
localStorage.removeItem(REFRESH_TOKEN_KEY)
localStorage.removeItem(EXPIRES_AT_KEY)
sessionStorage.removeItem(ACCESS_TOKEN_KEY)
sessionStorage.removeItem(REFRESH_TOKEN_KEY)
sessionStorage.removeItem(EXPIRES_AT_KEY)
}
export function getAccessToken(): string | null {
return localStorage.getItem(ACCESS_TOKEN_KEY)
// Try localStorage first (persistent), then sessionStorage (temporary)
return localStorage.getItem(ACCESS_TOKEN_KEY) || sessionStorage.getItem(ACCESS_TOKEN_KEY)
}
export function getRefreshToken(): string | null {
return localStorage.getItem(REFRESH_TOKEN_KEY)
return localStorage.getItem(REFRESH_TOKEN_KEY) || sessionStorage.getItem(REFRESH_TOKEN_KEY)
}
export function getExpiresAt(): number | null {
const raw = localStorage.getItem(EXPIRES_AT_KEY)
const raw = localStorage.getItem(EXPIRES_AT_KEY) || sessionStorage.getItem(EXPIRES_AT_KEY)
return raw ? Number(raw) : null
}
export function shouldRefresh(): boolean {
const expiresAt = getExpiresAt()
if (expiresAt === null) return true
if (expiresAt === null) return false // No token → no need to refresh
return Date.now() / 1000 + TOKEN_BUFFER_SECONDS >= expiresAt
}
+8 -1
View File
@@ -21,6 +21,7 @@ const { siteName } = useSiteName()
const form = reactive({
username: '',
password: '',
remember: true,
})
// 2FA state
@@ -107,7 +108,7 @@ async function handleLogin() {
loading.value = true
try {
const res = await authStore.login(form.username, form.password, { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX })
const res = await authStore.login(form.username, form.password, { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }, form.remember)
// Check if 2FA is required
if (res?.totp_required) {
provisionalToken.value = res.provisional_token
@@ -238,6 +239,12 @@ async function handle2FAVerify() {
<div style="margin-bottom: 20px;">
<SlideCaptcha ref="captchaRef" v-model="captcha" />
</div>
<!-- Remember Me -->
<AFormItem>
<ACheckbox v-model="form.remember">
记住登录状态(7天免登录)
</ACheckbox>
</AFormItem>
<AFormItem>
<AButton
type="primary"
+8 -2
View File
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import request, { setTokens, clearTokens, onTokenRefreshed } from '@/utils/request'
import request, { setTokens, clearTokens, onTokenRefreshed, setRememberMe, getRememberMe } from '@/utils/request'
import { setTenantSession, clearTenantSession, TENANT_ROLES } from '@/utils/permission'
export interface TenantInfo {
@@ -55,6 +55,7 @@ export const useTenantAuthStore = defineStore('tenant-auth', () => {
const user = ref<TenantUser | null>(null)
const permissions = ref<string[]>([])
const pendingAgreements = ref<PendingAgreement[]>([])
const rememberMe = ref<boolean>(getRememberMe())
const isLoggedIn = computed(() => !!token.value)
@@ -116,7 +117,11 @@ export const useTenantAuthStore = defineStore('tenant-auth', () => {
persist()
}
async function login(account: string, password: string, type: 'ram' | 'admin', captcha?: { captchaKey: string; captchaX: number; turnstileToken?: string }): Promise<any> {
async function login(account: string, password: string, type: 'ram' | 'admin', captcha?: { captchaKey: string; captchaX: number; turnstileToken?: string }, remember: boolean = true): Promise<any> {
// Save user's "remember me" preference
rememberMe.value = remember
setRememberMe(remember)
const { data } = await request.post('/tenant/auth/login', {
account,
password,
@@ -208,6 +213,7 @@ export const useTenantAuthStore = defineStore('tenant-auth', () => {
user,
permissions,
pendingAgreements,
rememberMe,
isLoggedIn,
isOwner,
login,
+31 -7
View File
@@ -24,36 +24,60 @@ declare module 'axios' {
const ACCESS_TOKEN_KEY = 'tenant_access_token'
const REFRESH_TOKEN_KEY = 'tenant_refresh_token'
const EXPIRES_AT_KEY = 'tenant_token_expires_at'
const REMEMBER_ME_KEY = 'tenant_remember_me'
const TOKEN_BUFFER_SECONDS = 60
// Dynamic storage selection based on "remember me" preference
function getStorage(): Storage {
// Check user's "remember me" preference (defaults to true for backward compatibility)
const rememberMe = localStorage.getItem(REMEMBER_ME_KEY)
return rememberMe === 'false' ? sessionStorage : localStorage
}
export function setRememberMe(remember: boolean): void {
// Store the preference itself in localStorage (so it persists across sessions)
localStorage.setItem(REMEMBER_ME_KEY, String(remember))
}
export function getRememberMe(): boolean {
const saved = localStorage.getItem(REMEMBER_ME_KEY)
return saved === null ? true : saved === 'true'
}
export function setTokens(tokens: TokenPair): void {
localStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken)
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
localStorage.setItem(EXPIRES_AT_KEY, String(tokens.expiresAt))
const storage = getStorage()
storage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken)
storage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
storage.setItem(EXPIRES_AT_KEY, String(tokens.expiresAt))
}
export function clearTokens(): void {
// Clear from both storages to prevent leftover data
localStorage.removeItem(ACCESS_TOKEN_KEY)
localStorage.removeItem(REFRESH_TOKEN_KEY)
localStorage.removeItem(EXPIRES_AT_KEY)
sessionStorage.removeItem(ACCESS_TOKEN_KEY)
sessionStorage.removeItem(REFRESH_TOKEN_KEY)
sessionStorage.removeItem(EXPIRES_AT_KEY)
}
export function getAccessToken(): string | null {
return localStorage.getItem(ACCESS_TOKEN_KEY)
// Try localStorage first (persistent), then sessionStorage (temporary)
return localStorage.getItem(ACCESS_TOKEN_KEY) || sessionStorage.getItem(ACCESS_TOKEN_KEY)
}
export function getRefreshToken(): string | null {
return localStorage.getItem(REFRESH_TOKEN_KEY)
return localStorage.getItem(REFRESH_TOKEN_KEY) || sessionStorage.getItem(REFRESH_TOKEN_KEY)
}
export function getExpiresAt(): number | null {
const raw = localStorage.getItem(EXPIRES_AT_KEY)
const raw = localStorage.getItem(EXPIRES_AT_KEY) || sessionStorage.getItem(EXPIRES_AT_KEY)
return raw ? Number(raw) : null
}
export function shouldRefresh(): boolean {
const expiresAt = getExpiresAt()
if (expiresAt === null) return true
if (expiresAt === null) return false // No token → no need to refresh
return Date.now() / 1000 + TOKEN_BUFFER_SECONDS >= expiresAt
}
+4 -4
View File
@@ -145,7 +145,7 @@ async function handleAdminLogin() {
const captchaPayload = useTurnstile.value
? { captchaKey: '', captchaX: 0, turnstileToken: turnstileToken.value }
: { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
const res = await authStore.login(adminForm.email, adminForm.password, 'admin', captchaPayload)
const res = await authStore.login(adminForm.email, adminForm.password, 'admin', captchaPayload, adminForm.remember)
if (res?.totp_required) {
provisionalToken.value = res.provisional_token
show2FA.value = true
@@ -189,7 +189,7 @@ async function handleRamLogin() {
const captchaPayload = useTurnstile.value
? { captchaKey: '', captchaX: 0, turnstileToken: turnstileToken.value }
: { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
const res = await authStore.login(ramForm.account, ramForm.password, 'ram', captchaPayload)
const res = await authStore.login(ramForm.account, ramForm.password, 'ram', captchaPayload, ramForm.remember)
if (res?.totp_required) {
provisionalToken.value = res.provisional_token
show2FA.value = true
@@ -375,7 +375,7 @@ async function handleOAuthLogin(provider: string) {
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500/30 transition-colors cursor-pointer"
/>
<span class="text-sm text-gray-600">记住登录</span>
<span class="text-sm text-gray-600">记住登录状态(7天免登录)</span>
</label>
<router-link
to="/tenant/forgot-password"
@@ -473,7 +473,7 @@ async function handleOAuthLogin(provider: string) {
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500/30 transition-colors cursor-pointer"
/>
<span class="text-sm text-gray-600">记住登录</span>
<span class="text-sm text-gray-600">记住登录状态(7天免登录)</span>
</label>
<router-link
to="/tenant/forgot-password"