production-evyos-systems-an.../ServicesApi/src/utils/auth/redis_handlers.ts

194 lines
5.9 KiB
TypeScript

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<string[]> {
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<LoginFromRedis | null> {
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<SelectFromRedis | null> {
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<any> {
const mergedKey = this.mergeLoginKey(req);
return this.cacheService.delete(mergedKey);
}
async deleteSelectFromRedis(req: Request): Promise<any> {
const mergedKey = this.mergeSelectKey(req);
return this.cacheService.delete(mergedKey);
}
async renewTtlLoginFromRedis(req: Request): Promise<any> {
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<any> {
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<any> {
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<any> {
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;
}
}