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

@@ -507,6 +507,27 @@ function initDatabase() {
)
`);
// 在线设备会话(用于设备管理和强制下线)
db.exec(`
CREATE TABLE IF NOT EXISTS user_device_sessions (
session_id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
client_type TEXT NOT NULL DEFAULT 'web',
device_id TEXT,
device_name TEXT,
platform TEXT,
ip_address TEXT,
user_agent TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_active_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
revoked_at DATETIME,
revoked_reason TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
// 文件哈希索引(用于秒传)
db.exec(`
CREATE TABLE IF NOT EXISTS user_file_hash_index (
@@ -554,6 +575,12 @@ function initDatabase() {
CREATE INDEX IF NOT EXISTS idx_upload_sessions_hash
ON upload_sessions(user_id, file_hash, file_size);
-- 在线设备会话索引
CREATE INDEX IF NOT EXISTS idx_user_device_sessions_user_active
ON user_device_sessions(user_id, revoked_at, expires_at, last_active_at);
CREATE INDEX IF NOT EXISTS idx_user_device_sessions_expires
ON user_device_sessions(expires_at);
-- 秒传索引
CREATE INDEX IF NOT EXISTS idx_file_hash_index_lookup
ON user_file_hash_index(user_id, storage_type, file_hash, file_size);
@@ -2615,6 +2642,169 @@ const UploadSessionDB = {
}
};
const DeviceSessionDB = {
_normalizeSessionId(sessionId) {
return typeof sessionId === 'string' ? sessionId.trim() : '';
},
_normalizeText(value, maxLength = 255) {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
if (!trimmed) return null;
return trimmed.slice(0, maxLength);
},
_normalizeClientType(value) {
const normalized = String(value || '').trim().toLowerCase();
if (['web', 'desktop', 'mobile', 'api'].includes(normalized)) {
return normalized;
}
return 'web';
},
create(data = {}) {
const sessionId = this._normalizeSessionId(data.sessionId);
const userId = Number(data.userId);
const clientType = this._normalizeClientType(data.clientType);
const deviceId = this._normalizeText(data.deviceId, 128);
const deviceName = this._normalizeText(data.deviceName, 120);
const platform = this._normalizeText(data.platform, 80);
const ipAddress = this._normalizeText(data.ipAddress, 80);
const userAgent = this._normalizeText(data.userAgent, 1024);
const ttlDays = Math.max(1, Math.min(30, Number(data.ttlDays || 7)));
if (!sessionId || !Number.isFinite(userId) || userId <= 0) {
return null;
}
db.prepare(`
INSERT INTO user_device_sessions (
session_id, user_id, client_type, device_id, device_name,
platform, ip_address, user_agent, created_at, last_active_at,
expires_at, revoked_at, revoked_reason, updated_at
) VALUES (
?, ?, ?, ?, ?,
?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'),
datetime('now', 'localtime', '+' || ? || ' days'), NULL, NULL, datetime('now', 'localtime')
)
ON CONFLICT(session_id) DO UPDATE SET
user_id = excluded.user_id,
client_type = excluded.client_type,
device_id = excluded.device_id,
device_name = excluded.device_name,
platform = excluded.platform,
ip_address = excluded.ip_address,
user_agent = excluded.user_agent,
last_active_at = datetime('now', 'localtime'),
expires_at = datetime('now', 'localtime', '+' || ? || ' days'),
revoked_at = NULL,
revoked_reason = NULL,
updated_at = datetime('now', 'localtime')
`).run(
sessionId,
Math.floor(userId),
clientType,
deviceId,
deviceName,
platform,
ipAddress,
userAgent,
ttlDays,
ttlDays
);
return this.findBySessionId(sessionId);
},
findBySessionId(sessionId) {
const sid = this._normalizeSessionId(sessionId);
if (!sid) return null;
return db.prepare('SELECT * FROM user_device_sessions WHERE session_id = ?').get(sid);
},
findActiveBySessionId(sessionId) {
const sid = this._normalizeSessionId(sessionId);
if (!sid) return null;
return db.prepare(`
SELECT *
FROM user_device_sessions
WHERE session_id = ?
AND revoked_at IS NULL
AND expires_at > datetime('now', 'localtime')
LIMIT 1
`).get(sid);
},
listActiveByUser(userId, limit = 60) {
const uid = Number(userId);
if (!Number.isFinite(uid) || uid <= 0) return [];
const safeLimit = Math.max(1, Math.min(200, Math.floor(Number(limit) || 60)));
return db.prepare(`
SELECT *
FROM user_device_sessions
WHERE user_id = ?
AND revoked_at IS NULL
AND expires_at > datetime('now', 'localtime')
ORDER BY datetime(COALESCE(last_active_at, created_at)) DESC, created_at DESC
LIMIT ?
`).all(Math.floor(uid), safeLimit);
},
touch(sessionId, options = {}) {
const sid = this._normalizeSessionId(sessionId);
if (!sid) return { changes: 0 };
const ipAddress = this._normalizeText(options.ipAddress, 80);
const userAgent = this._normalizeText(options.userAgent, 1024);
return db.prepare(`
UPDATE user_device_sessions
SET ip_address = COALESCE(?, ip_address),
user_agent = COALESCE(?, user_agent),
last_active_at = datetime('now', 'localtime'),
updated_at = datetime('now', 'localtime')
WHERE session_id = ?
AND revoked_at IS NULL
AND expires_at > datetime('now', 'localtime')
`).run(ipAddress, userAgent, sid);
},
revoke(sessionId, userId = null, reason = 'manual') {
const sid = this._normalizeSessionId(sessionId);
if (!sid) return { changes: 0 };
const revokeReason = this._normalizeText(reason, 120) || 'manual';
if (userId !== null && userId !== undefined) {
const uid = Number(userId);
if (!Number.isFinite(uid) || uid <= 0) return { changes: 0 };
return db.prepare(`
UPDATE user_device_sessions
SET revoked_at = datetime('now', 'localtime'),
revoked_reason = ?,
updated_at = datetime('now', 'localtime')
WHERE session_id = ?
AND user_id = ?
AND revoked_at IS NULL
`).run(revokeReason, sid, Math.floor(uid));
}
return db.prepare(`
UPDATE user_device_sessions
SET revoked_at = datetime('now', 'localtime'),
revoked_reason = ?,
updated_at = datetime('now', 'localtime')
WHERE session_id = ?
AND revoked_at IS NULL
`).run(revokeReason, sid);
},
cleanupExpired(keepDays = 30) {
const safeDays = Math.max(1, Math.min(365, Math.floor(Number(keepDays) || 30)));
return db.prepare(`
DELETE FROM user_device_sessions
WHERE (expires_at <= datetime('now', 'localtime', '-' || ? || ' days'))
OR (revoked_at IS NOT NULL AND revoked_at <= datetime('now', 'localtime', '-' || ? || ' days'))
`).run(safeDays, safeDays);
}
};
const FileHashIndexDB = {
normalizeStorageType(storageType) {
return storageType === 'oss' ? 'oss' : 'local';
@@ -2960,6 +3150,7 @@ module.exports = {
DownloadTrafficReportDB,
DownloadTrafficReservationDB,
UploadSessionDB,
DeviceSessionDB,
FileHashIndexDB,
DownloadTrafficIngestDB,
SystemLogDB,