mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: implement user and device cache invalidation in AuthService
This commit is contained in:
@@ -5,10 +5,10 @@
|
||||
accessTokenTtlSeconds: 7200,
|
||||
// Refresh token lifetime in milliseconds.
|
||||
// 刷新令牌有效期(毫秒)。
|
||||
refreshTokenTtlMs: 30 * 24 * 60 * 60 * 1000,
|
||||
refreshTokenTtlMs: 365 * 24 * 60 * 60 * 1000,
|
||||
// Grace window for previous refresh token after rotation (ms).
|
||||
// 刷新令牌轮换后的旧令牌宽限窗口(毫秒)。
|
||||
refreshTokenOverlapGraceMs: 60 * 1000,
|
||||
refreshTokenOverlapGraceMs: 30 * 60 * 1000,
|
||||
// Refresh token random byte length.
|
||||
// 刷新令牌随机字节长度。
|
||||
refreshTokenRandomBytes: 32,
|
||||
|
||||
@@ -526,6 +526,7 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
await storage.deleteRefreshTokensByUserId(user.id);
|
||||
AuthService.invalidateUserCache(user.id);
|
||||
await storage.createAuditLog({
|
||||
id: generateUUID(),
|
||||
actorUserId: user.id,
|
||||
@@ -587,6 +588,7 @@ export async function handleSetTotpStatus(request: Request, env: Env, userId: st
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
await storage.deleteRefreshTokensByUserId(user.id);
|
||||
AuthService.invalidateUserCache(user.id);
|
||||
return jsonResponse({ enabled: true, recoveryCode: user.totpRecoveryCode, object: 'twoFactor' });
|
||||
}
|
||||
|
||||
@@ -601,6 +603,7 @@ export async function handleSetTotpStatus(request: Request, env: Env, userId: st
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
await storage.deleteRefreshTokensByUserId(user.id);
|
||||
AuthService.invalidateUserCache(user.id);
|
||||
return jsonResponse({ enabled: false, object: 'twoFactor' });
|
||||
}
|
||||
|
||||
@@ -708,6 +711,7 @@ export async function handleRecoverTwoFactor(request: Request, env: Env): Promis
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
await storage.deleteRefreshTokensByUserId(user.id);
|
||||
AuthService.invalidateUserCache(user.id);
|
||||
await rateLimit.clearLoginAttempts(recoverLimitKey);
|
||||
|
||||
return jsonResponse({
|
||||
@@ -801,6 +805,7 @@ async function apiKey(request: Request, env: Env, userId: string, rotate: boolea
|
||||
}
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
AuthService.invalidateUserCache(user.id);
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Env, User, Invite } from '../types';
|
||||
import { AuthService } from '../services/auth';
|
||||
import { StorageService } from '../services/storage';
|
||||
import { jsonResponse, errorResponse } from '../utils/response';
|
||||
import { generateUUID } from '../utils/uuid';
|
||||
@@ -222,6 +223,7 @@ export async function handleAdminSetUserStatus(
|
||||
if (nextStatus === 'banned') {
|
||||
await storage.deleteRefreshTokensByUserId(target.id);
|
||||
}
|
||||
AuthService.invalidateUserCache(target.id);
|
||||
await writeAuditLog(storage, actorUser.id, 'admin.user.status', 'user', target.id, {
|
||||
status: nextStatus,
|
||||
});
|
||||
@@ -280,6 +282,7 @@ export async function handleAdminDeleteUser(
|
||||
|
||||
await storage.deleteRefreshTokensByUserId(target.id);
|
||||
await storage.deleteUserById(target.id);
|
||||
AuthService.invalidateUserCache(target.id);
|
||||
await writeAuditLog(storage, actorUser.id, 'admin.user.delete', 'user', target.id, {
|
||||
email: target.email,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Device, DevicePendingAuthRequest, DeviceResponse, ProtectedDeviceResponse as ProtectedDeviceWireResponse } from '../types';
|
||||
import { Env } from '../types';
|
||||
import { getOnlineUserDevices, notifyUserLogout } from '../durable/notifications-hub';
|
||||
import { AuthService } from '../services/auth';
|
||||
import { StorageService } from '../services/storage';
|
||||
import { errorResponse, jsonResponse } from '../utils/response';
|
||||
import { readKnownDeviceProbe } from '../utils/device';
|
||||
@@ -309,6 +310,7 @@ export async function handleDeleteDevice(
|
||||
await storage.deleteRefreshTokensByDevice(userId, normalized);
|
||||
const deleted = await storage.deleteDevice(userId, normalized);
|
||||
if (deleted) {
|
||||
AuthService.invalidateDeviceCache(userId, normalized);
|
||||
notifyUserLogout(env, userId, normalized);
|
||||
}
|
||||
return jsonResponse({ success: deleted });
|
||||
@@ -352,6 +354,7 @@ export async function handleDeleteAllDevices(request: Request, env: Env, userId:
|
||||
user.securityStamp = generateUUID();
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
AuthService.invalidateUserCache(userId);
|
||||
notifyUserLogout(env, userId, null);
|
||||
return jsonResponse({ success: true, removedTrusted, removedSessions: removedSessions ?? 0, removedDevices });
|
||||
}
|
||||
@@ -483,6 +486,7 @@ export async function handleDeactivateDevice(
|
||||
await storage.deleteRefreshTokensByDevice(userId, normalized);
|
||||
const deleted = await storage.deleteDevice(userId, normalized);
|
||||
if (deleted) {
|
||||
AuthService.invalidateDeviceCache(userId, normalized);
|
||||
notifyUserLogout(env, userId, normalized);
|
||||
}
|
||||
return jsonResponse({ success: deleted });
|
||||
|
||||
@@ -32,6 +32,17 @@ function resolveTotpSecret(userSecret: string | null): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveDeviceSession(
|
||||
storage: StorageService,
|
||||
userId: string,
|
||||
deviceInfo: ReturnType<typeof readAuthRequestDeviceInfo>
|
||||
): Promise<{ identifier: string; sessionStamp: string } | null> {
|
||||
if (!deviceInfo.deviceIdentifier) return null;
|
||||
const existingDevice = await storage.getDevice(userId, deviceInfo.deviceIdentifier);
|
||||
const sessionStamp = String(existingDevice?.sessionStamp || '').trim() || generateUUID();
|
||||
return { identifier: deviceInfo.deviceIdentifier, sessionStamp };
|
||||
}
|
||||
|
||||
function shouldUseWebSession(request: Request): boolean {
|
||||
return String(request.headers.get('X-NodeWarden-Web-Session') || '').trim() === '1';
|
||||
}
|
||||
@@ -320,10 +331,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
||||
}
|
||||
|
||||
// Persist device only after successful password + (optional) 2FA verification.
|
||||
const deviceSession =
|
||||
deviceInfo.deviceIdentifier
|
||||
? { identifier: deviceInfo.deviceIdentifier, sessionStamp: generateUUID() }
|
||||
: null;
|
||||
const deviceSession = await resolveDeviceSession(storage, user.id, deviceInfo);
|
||||
if (deviceSession) {
|
||||
await storage.upsertDevice(
|
||||
user.id,
|
||||
@@ -413,10 +421,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
||||
}
|
||||
|
||||
// Persist device only after successful client credential verification.
|
||||
const deviceSession =
|
||||
deviceInfo.deviceIdentifier
|
||||
? { identifier: deviceInfo.deviceIdentifier, sessionStamp: generateUUID() }
|
||||
: null;
|
||||
const deviceSession = await resolveDeviceSession(storage, user.id, deviceInfo);
|
||||
if (deviceSession) {
|
||||
await storage.upsertDevice(
|
||||
user.id,
|
||||
|
||||
@@ -32,6 +32,25 @@ export class AuthService {
|
||||
this.storage = new StorageService(env.DB);
|
||||
}
|
||||
|
||||
static invalidateUserCache(userId: string): void {
|
||||
const normalizedUserId = String(userId || '').trim();
|
||||
if (!normalizedUserId) return;
|
||||
AuthService.userCache.delete(normalizedUserId);
|
||||
const prefix = `${normalizedUserId}:`;
|
||||
for (const key of AuthService.deviceCache.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
AuthService.deviceCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static invalidateDeviceCache(userId: string, deviceId: string): void {
|
||||
const normalizedUserId = String(userId || '').trim();
|
||||
const normalizedDeviceId = String(deviceId || '').trim();
|
||||
if (!normalizedUserId || !normalizedDeviceId) return;
|
||||
AuthService.deviceCache.delete(`${normalizedUserId}:${normalizedDeviceId}`);
|
||||
}
|
||||
|
||||
private readCachedUser(userId: string): User | null | undefined {
|
||||
const cached = AuthService.userCache.get(userId);
|
||||
if (!cached) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user