mirror of
https://github.com/whyour/qinglong.git
synced 2025-11-08 15:06:08 +08:00
Implement multi-device login support - allow multiple concurrent sessions
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
044deabed3
commit
168bdb4178
|
|
@ -8,7 +8,7 @@ import path from 'path';
|
|||
import { v4 as uuidV4 } from 'uuid';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import config from '../config';
|
||||
import { isDemoEnv } from '../config/util';
|
||||
import { isDemoEnv, getToken } from '../config/util';
|
||||
const route = Router();
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
|
|
@ -56,7 +56,8 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
await userService.logout(req.platform);
|
||||
const token = getToken(req);
|
||||
await userService.logout(req.platform, token);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,15 @@ export interface LoginLogInfo {
|
|||
status?: LoginStatus;
|
||||
}
|
||||
|
||||
export interface TokenInfo {
|
||||
value: string;
|
||||
timestamp: number;
|
||||
ip: string;
|
||||
address: string;
|
||||
platform: string;
|
||||
expiration?: number;
|
||||
}
|
||||
|
||||
export interface AuthInfo {
|
||||
username: string;
|
||||
password: string;
|
||||
|
|
@ -58,7 +67,7 @@ export interface AuthInfo {
|
|||
platform: string;
|
||||
isTwoFactorChecking: boolean;
|
||||
token: string;
|
||||
tokens: Record<string, string>;
|
||||
tokens: Record<string, string | TokenInfo[]>;
|
||||
twoFactorActivated: boolean;
|
||||
twoFactorSecret: string;
|
||||
avatar: string;
|
||||
|
|
|
|||
|
|
@ -79,9 +79,28 @@ export default ({ app }: { app: Application }) => {
|
|||
const authInfo = await shareStore.getAuthInfo();
|
||||
if (authInfo && headerToken) {
|
||||
const { token = '', tokens = {} } = authInfo;
|
||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||
|
||||
// Check legacy token field
|
||||
if (headerToken === token) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
|
||||
const platformTokens = tokens[req.platform];
|
||||
if (platformTokens) {
|
||||
if (typeof platformTokens === 'string') {
|
||||
// Legacy format: single string token
|
||||
if (headerToken === platformTokens) {
|
||||
return next();
|
||||
}
|
||||
} else if (Array.isArray(platformTokens)) {
|
||||
// New format: array of TokenInfo objects
|
||||
const tokenExists = platformTokens.some((t) => t.value === headerToken);
|
||||
if (tokenExists) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ export default async ({ server }: { server: Server }) => {
|
|||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||
if (authInfo) {
|
||||
const { token = '', tokens = {} } = authInfo;
|
||||
if (headerToken === token || tokens[platform] === headerToken) {
|
||||
|
||||
// Check legacy token field
|
||||
if (headerToken === token) {
|
||||
sockService.addClient(conn);
|
||||
|
||||
conn.on('data', (message) => {
|
||||
|
|
@ -32,6 +34,34 @@ export default async ({ server }: { server: Server }) => {
|
|||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
|
||||
const platformTokens = tokens[platform];
|
||||
if (platformTokens) {
|
||||
let isValidToken = false;
|
||||
|
||||
if (typeof platformTokens === 'string') {
|
||||
// Legacy format: single string token
|
||||
isValidToken = headerToken === platformTokens;
|
||||
} else if (Array.isArray(platformTokens)) {
|
||||
// New format: array of TokenInfo objects
|
||||
isValidToken = platformTokens.some((t) => t.value === headerToken);
|
||||
}
|
||||
|
||||
if (isValidToken) {
|
||||
sockService.addClient(conn);
|
||||
|
||||
conn.on('data', (message) => {
|
||||
conn.write(message);
|
||||
});
|
||||
|
||||
conn.on('close', function () {
|
||||
sockService.removeClient(conn);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn.close('404');
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
SystemModelInfo,
|
||||
LoginStatus,
|
||||
AuthInfo,
|
||||
TokenInfo,
|
||||
} from '../data/system';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
|
|
@ -101,12 +102,19 @@ export default class UserService {
|
|||
algorithm: 'HS384',
|
||||
});
|
||||
|
||||
const tokenInfo: TokenInfo = {
|
||||
value: token,
|
||||
timestamp,
|
||||
ip,
|
||||
address,
|
||||
platform: req.platform,
|
||||
};
|
||||
|
||||
const updatedTokens = this.addTokenToList(tokens, req.platform, tokenInfo);
|
||||
|
||||
await this.updateAuthInfo(content, {
|
||||
token,
|
||||
tokens: {
|
||||
...tokens,
|
||||
[req.platform]: token,
|
||||
},
|
||||
tokens: updatedTokens,
|
||||
lastlogon: timestamp,
|
||||
retries: 0,
|
||||
lastip: ip,
|
||||
|
|
@ -180,11 +188,13 @@ export default class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
public async logout(platform: string): Promise<any> {
|
||||
public async logout(platform: string, tokenValue: string): Promise<any> {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const updatedTokens = this.removeTokenFromList(authInfo.tokens, platform, tokenValue);
|
||||
|
||||
await this.updateAuthInfo(authInfo, {
|
||||
token: '',
|
||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||
token: authInfo.token === tokenValue ? '' : authInfo.token,
|
||||
tokens: updatedTokens,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -364,6 +374,85 @@ export default class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
private normalizeTokens(tokens: Record<string, string | TokenInfo[]>): Record<string, TokenInfo[]> {
|
||||
const normalized: Record<string, TokenInfo[]> = {};
|
||||
|
||||
for (const [platform, value] of Object.entries(tokens)) {
|
||||
if (typeof value === 'string') {
|
||||
// Legacy format: convert string token to TokenInfo array
|
||||
if (value) {
|
||||
normalized[platform] = [{
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
ip: '',
|
||||
address: '',
|
||||
platform,
|
||||
}];
|
||||
} else {
|
||||
normalized[platform] = [];
|
||||
}
|
||||
} else {
|
||||
// Already in new format
|
||||
normalized[platform] = value || [];
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private addTokenToList(
|
||||
tokens: Record<string, string | TokenInfo[]>,
|
||||
platform: string,
|
||||
tokenInfo: TokenInfo,
|
||||
maxTokensPerPlatform: number = 10
|
||||
): Record<string, TokenInfo[]> {
|
||||
const normalized = this.normalizeTokens(tokens);
|
||||
|
||||
if (!normalized[platform]) {
|
||||
normalized[platform] = [];
|
||||
}
|
||||
|
||||
// Add new token
|
||||
normalized[platform].unshift(tokenInfo);
|
||||
|
||||
// Limit the number of active tokens per platform
|
||||
if (normalized[platform].length > maxTokensPerPlatform) {
|
||||
normalized[platform] = normalized[platform].slice(0, maxTokensPerPlatform);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private removeTokenFromList(
|
||||
tokens: Record<string, string | TokenInfo[]>,
|
||||
platform: string,
|
||||
tokenValue: string
|
||||
): Record<string, TokenInfo[]> {
|
||||
const normalized = this.normalizeTokens(tokens);
|
||||
|
||||
if (normalized[platform]) {
|
||||
normalized[platform] = normalized[platform].filter(
|
||||
(t) => t.value !== tokenValue
|
||||
);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private findTokenInList(
|
||||
tokens: Record<string, string | TokenInfo[]>,
|
||||
platform: string,
|
||||
tokenValue: string
|
||||
): TokenInfo | undefined {
|
||||
const normalized = this.normalizeTokens(tokens);
|
||||
|
||||
if (normalized[platform]) {
|
||||
return normalized[platform].find((t) => t.value === tokenValue);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
||||
const { retries, twoFactorActivated, password, username } = info;
|
||||
const authInfo = await this.getAuthInfo();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user