feat: implement JWT auth and optimize token validation

- Separate JWT login (21d) from check-in token
- Unify check-in token validation with verify_checkin_authorization()
- Update API docs for dual-token architecture
This commit is contained in:
2026-01-05 23:02:50 +08:00
parent b32b53853a
commit a9b141fc69
13 changed files with 464 additions and 336 deletions
+9 -41
View File
@@ -34,49 +34,17 @@ client.interceptors.response.use(
const { status, data } = error.response;
if (status === 401) {
const errorDetail = data.detail || data.message || '';
// JWT token 过期或无效:需要重新登录
// 注意:打卡业务的 authorization token 过期不会影响网站登录状态
localStorage.removeItem('token');
localStorage.removeItem('user');
// 检查用户是否设置了密码
const user = JSON.parse(localStorage.getItem('user') || '{}');
const hasPassword = user.has_password || false;
// Token 过期的情况
if (errorDetail.includes('过期')) {
if (hasPassword) {
// 有密码的用户:不强制退出,只显示警告
// 不清除 localStorage,让用户继续使用
console.warn('Token 已过期,但用户设置了密码,允许继续使用');
// 返回错误但不跳转登录页
return Promise.reject({
status,
message: '登录凭证已过期,部分功能可能受限,建议刷新凭证',
data,
tokenExpired: true,
});
} else {
// 没有密码的用户:必须重新登录
localStorage.removeItem('token');
localStorage.removeItem('user');
// 延迟跳转,避免阻塞当前异步请求的错误处理
setTimeout(() => {
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}, 100);
// 延迟跳转到登录页
setTimeout(() => {
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
} else {
// 其他 401 错误(无效 Token 等):清除登录状态
localStorage.removeItem('token');
localStorage.removeItem('user');
setTimeout(() => {
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}, 100);
}
}, 100);
}
// 返回统一的错误对象
+1
View File
@@ -190,6 +190,7 @@
<!-- QR Code Modal for Token Refresh -->
<QRCodeModal
v-model:visible="qrcodeModalVisible"
:alias="authStore.user?.alias || ''"
@success="handleQRCodeSuccess"
@error="handleQRCodeError"
/>
+7 -33
View File
@@ -31,6 +31,7 @@
v-model:value="qrcodeForm.alias"
placeholder="请输入您的用户名"
size="large"
autocomplete="username"
allow-clear
@keyup.enter="handleQRCodeLogin"
>
@@ -66,6 +67,7 @@
v-model:value="passwordForm.alias"
placeholder="请输入您的用户名"
size="large"
autocomplete="username"
allow-clear
>
<template #prefix>
@@ -79,6 +81,7 @@
v-model:value="passwordForm.password"
placeholder="请输入密码"
size="large"
autocomplete="current-password"
@keyup.enter="handlePasswordLogin"
>
<template #prefix>
@@ -235,46 +238,17 @@ const handlePasswordLogin = async () => {
);
if (response.success) {
// 使用 authStore 保存认证信息
const user = {
id: response.user_id,
alias: response.alias,
role: response.role || 'user',
is_approved: response.is_approved !== false,
};
// 保存 JWT token 和用户信息
authStore.setAuth(response.token, response.user);
// 如果没有 authorization(测试账号),使用 user_id 作为认证凭据
const authToken = response.authorization || `user_id:${response.user_id}`;
authStore.setAuth(authToken, user);
// 只有当有真实 authorization 时才获取完整用户信息
if (response.authorization) {
try {
await authStore.fetchCurrentUser();
} catch (err) {
console.warn('获取完整用户信息失败,使用基本信息:', err);
// 即使失败也继续登录流程
}
} else {
// 没有 authorization 的测试账号,提示用户需要扫码绑定
message.info({
content: '您正在使用密码登录模式。如需使用打卡功能,请先扫码绑定 QQ。',
duration: 5,
});
}
// 如果有 Token 警告,显示提示
// 如果有打卡 Token 警告,显示提示(不影响网站登录)
if (response.token_warning && response.warning_message) {
message.warning({
content: response.warning_message,
duration: 5,
});
} else if (response.authorization) {
// 只有有 token 的用户才显示"欢迎回来"
message.success(`欢迎回来,${response.alias}`);
} else {
// 测试账号登录成功提示
message.success(`登录成功,${response.alias}`);
message.success(`欢迎回来,${response.user.alias}`);
}
// 跳转到重定向页面或仪表盘