feat: add online device management and desktop settings integration
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user