import { TokenDictTypes, TokenDictInterface, AuthToken, UserType, } from '@/src/types/auth/token'; import { CacheService } from '@/src/cache.service'; import { users } from '@prisma/client'; import { PasswordHandlers } from './login_handler'; import { Injectable, ForbiddenException } from '@nestjs/common'; interface LoginFromRedis { key: string; value: AuthToken; } interface SelectFromRedis { key: string; value: TokenDictInterface; } @Injectable() export class RedisHandlers { AUTH_TOKEN = 'AUTH_TOKEN'; constructor( private readonly cacheService: CacheService, private readonly passwordService: PasswordHandlers, ) {} /** * Validates that a Redis key follows the expected format * Format: AUTH_TOKEN:token:token:UUID or AUTH_TOKEN:token:token:*:* this.AUTH_TOKEN:token:token:UUID:UUID */ private validateRedisKey(redisKey: string): boolean { if (!redisKey.startsWith(this.AUTH_TOKEN + ':')) { throw new ForbiddenException( `Invalid Redis key format. Must start with ${this.AUTH_TOKEN}:`, ); } const colonCount = (redisKey.match(/:/g) || []).length; if (colonCount !== 4) { throw new ForbiddenException( `Invalid Redis key format. Must have exactly 5 colons. Found: ${colonCount}`, ); } return true; } public mergeLoginKey(req: Request): string { const acsToken = req.headers['acs']; if (!acsToken) { throw new ForbiddenException('Access token header is missing'); } const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${acsToken}:*:*`; this.validateRedisKey(mergedRedisKey); return mergedRedisKey; } public mergeSelectKey(req: Request): string { const acsToken = req.headers['acs']; if (!acsToken) { throw new ForbiddenException('Access token header is missing'); } const slcToken = req.headers['slc']; if (!slcToken) { throw new ForbiddenException('Select token header is missing'); } const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${slcToken}:*:*`; this.validateRedisKey(mergedRedisKey); return mergedRedisKey; } generateSelectToken(accessToken: string, userUUID: string) { return this.passwordService.createSelectToken(accessToken, userUUID); } generateAccessToken() { return this.passwordService.generateAccessToken(); } private async scanKeys(pattern: string): Promise { this.validateRedisKey(pattern); const client = (this.cacheService as any).client; if (!client) throw new Error('Redis client not available'); const keys: string[] = []; let cursor = '0'; do { const [nextCursor, matchedKeys] = await client.scan( cursor, 'MATCH', pattern, ); cursor = nextCursor; keys.push(...matchedKeys); } while (cursor !== '0'); return keys; } async getLoginFromRedis(req: Request): Promise { const mergedKey = this.mergeLoginKey(req); if (mergedKey.includes('*')) { const keys = await this.scanKeys(mergedKey); if (keys.length === 0) { throw new ForbiddenException('Authorization failed - No matching keys'); } for (const key of keys) { const parts = key.split(':'); if (parts.length >= 3) { if (parts[1] === parts[2]) { const value = await this.cacheService.get(key); if (value) { return { key, value }; } } } } throw new ForbiddenException('Authorization failed - No valid keys'); } const value = await this.cacheService.get(mergedKey); return value ? { key: mergedKey, value } : null; } async getSelectFromRedis(req: Request): Promise { const mergedKey = this.mergeSelectKey(req); if (mergedKey.includes('*')) { const keys = await this.scanKeys(mergedKey); if (keys.length === 0) { throw new ForbiddenException( 'Authorization failed - No matching select keys', ); } for (const key of keys) { const value = await this.cacheService.get(key); if (value) { return { key, value }; } } throw new ForbiddenException( 'Authorization failed - No valid select keys', ); } const value = await this.cacheService.get(mergedKey); return value ? { key: mergedKey, value } : null; } async deleteLoginFromRedis(req: Request): Promise { const mergedKey = this.mergeLoginKey(req); return this.cacheService.delete(mergedKey); } async deleteSelectFromRedis(req: Request): Promise { const mergedKey = this.mergeSelectKey(req); return this.cacheService.delete(mergedKey); } async renewTtlLoginFromRedis(req: Request): Promise { const mergedKey = this.mergeLoginKey(req); const value = await this.cacheService.get(mergedKey); return this.cacheService.set_with_ttl(mergedKey, value, 86400); } async renewTtlSelectFromRedis(req: Request): Promise { const mergedKey = this.mergeSelectKey(req); const value = await this.cacheService.get(mergedKey); return this.cacheService.set_with_ttl(mergedKey, value, 60 * 30); } async setLoginToRedis(token: AuthToken, userUUID: string): Promise { const accessToken = this.generateAccessToken(); const redisKey = `${this.AUTH_TOKEN}:${accessToken}:${accessToken}:${userUUID}:${userUUID}`; await this.cacheService.set_with_ttl(redisKey, token, 60 * 30); return accessToken; } async setSelectToRedis( accessToken: string, token: TokenDictInterface, userUUID: string, livingUUID: string, ): Promise { const selectToken = this.generateSelectToken(accessToken, userUUID); const redisKey = `${this.AUTH_TOKEN}:${accessToken}:${selectToken}:${userUUID}:${livingUUID}`; await this.cacheService.set_with_ttl(redisKey, token, 60 * 30); return selectToken; } }