feat: add online device management and desktop settings integration

This commit is contained in:
2026-02-19 17:34:41 +08:00
parent 365ada1a4a
commit 19f53875c9
7 changed files with 1070 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { UserDB } = require('./database');
const { UserDB, DeviceSessionDB } = require('./database');
const { decryptSecret } = require('./utils/encryption');
const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
@@ -64,13 +64,16 @@ if (JWT_SECRET.length < 32) {
console.log('[安全] ✓ JWT密钥验证通过');
// 生成Access Token短期
function generateToken(user) {
function generateToken(user, sessionId = null) {
const safeSessionId = typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : null;
return jwt.sign(
{
id: user.id,
username: user.username,
is_admin: user.is_admin,
type: 'access'
type: 'access',
sid: safeSessionId,
jti: crypto.randomBytes(12).toString('hex')
},
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRES }
@@ -78,11 +81,13 @@ function generateToken(user) {
}
// 生成Refresh Token长期
function generateRefreshToken(user) {
function generateRefreshToken(user, sessionId = null) {
const safeSessionId = typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : null;
return jwt.sign(
{
id: user.id,
type: 'refresh',
sid: safeSessionId,
// 添加随机标识使每次生成的refresh token不同
jti: crypto.randomBytes(16).toString('hex')
},
@@ -91,8 +96,26 @@ function generateRefreshToken(user) {
);
}
function decodeAccessToken(token) {
if (!token || typeof token !== 'string') return null;
try {
return jwt.verify(token, JWT_SECRET);
} catch {
return null;
}
}
function decodeRefreshToken(token) {
if (!token || typeof token !== 'string') return null;
try {
return jwt.verify(token, REFRESH_SECRET);
} catch {
return null;
}
}
// 验证Refresh Token并返回新的Access Token
function refreshAccessToken(refreshToken) {
function refreshAccessToken(refreshToken, context = {}) {
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
@@ -100,6 +123,18 @@ function refreshAccessToken(refreshToken) {
return { success: false, message: '无效的刷新令牌类型' };
}
const sessionId = typeof decoded.sid === 'string' ? decoded.sid.trim() : '';
if (sessionId) {
const activeSession = DeviceSessionDB.findActiveBySessionId(sessionId);
if (!activeSession || Number(activeSession.user_id) !== Number(decoded.id)) {
return { success: false, message: '当前设备会话已失效,请重新登录' };
}
DeviceSessionDB.touch(sessionId, {
ipAddress: context.ipAddress,
userAgent: context.userAgent
});
}
const user = UserDB.findById(decoded.id);
if (!user) {
@@ -115,11 +150,12 @@ function refreshAccessToken(refreshToken) {
}
// 生成新的access token
const newAccessToken = generateToken(user);
const newAccessToken = generateToken(user, sessionId || null);
return {
success: true,
token: newAccessToken,
sessionId: sessionId || null,
user: {
id: user.id,
username: user.username,
@@ -148,6 +184,29 @@ function authMiddleware(req, res, next) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
if (decoded.type !== 'access') {
return res.status(401).json({
success: false,
message: '无效的令牌类型'
});
}
const sessionId = typeof decoded.sid === 'string' ? decoded.sid.trim() : '';
if (sessionId) {
const activeSession = DeviceSessionDB.findActiveBySessionId(sessionId);
if (!activeSession || Number(activeSession.user_id) !== Number(decoded.id)) {
return res.status(401).json({
success: false,
message: '当前设备已下线,请重新登录'
});
}
DeviceSessionDB.touch(sessionId, {
ipAddress: req.ip || req.socket?.remoteAddress || '',
userAgent: req.get('user-agent') || ''
});
}
const user = UserDB.findById(decoded.id);
if (!user) {
@@ -217,6 +276,8 @@ function authMiddleware(req, res, next) {
// 主题偏好
theme_preference: user.theme_preference || null
};
req.authSessionId = sessionId || null;
req.authTokenJti = typeof decoded.jti === 'string' ? decoded.jti : null;
next();
} catch (error) {
@@ -329,6 +390,8 @@ module.exports = {
JWT_SECRET,
generateToken,
generateRefreshToken,
decodeAccessToken,
decodeRefreshToken,
refreshAccessToken,
authMiddleware,
adminMiddleware,