fix:修复登录状态经常失效的问题
This commit is contained in:
1 parent
92c2cc033b
commit
ddade6ec54
6 files changed
+90
-23
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user