Gestão de Sessões
As sessões do CSGOFlip são armazenadas no Redis com suporte a múltiplos dispositivos, rastreamento de IP e revogação instantânea.
Estrutura da Sessão
typescript
interface Session {
id: string; // ID único (32 caracteres hex)
userId: bigint; // ID do usuário
steamId: string; // Steam ID (para referência rápida)
ip: string; // IP de criação
userAgent: string; // Browser/Device info
deviceId?: string; // Fingerprint do dispositivo
createdAt: Date; // Quando foi criada
lastActivity: Date; // Última atividade
expiresAt: Date; // Quando expira (7 dias)
}Armazenamento no Redis
┌─────────────────────────────────────────────────────────────┐
│ REDIS │
├─────────────────────────────────────────────────────────────┤
│ │
│ session:{sessionId} │
│ ├── Value: JSON da sessão │
│ └── TTL: 604800 segundos (7 dias) │
│ │
│ user_sessions:{userId} │
│ └── Set de sessionIds do usuário │
│ │
└─────────────────────────────────────────────────────────────┘Operações
Criar Sessão
typescript
async create(data: CreateSessionData): Promise<Session> {
const sessionId = crypto.randomBytes(16).toString('hex');
const session: Session = {
id: sessionId,
userId: data.userId,
steamId: data.steamId,
ip: data.ip,
userAgent: data.userAgent,
deviceId: data.deviceId,
createdAt: new Date(),
lastActivity: new Date(),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
};
// Salva sessão com TTL
await this.redis.setex(
`session:${sessionId}`,
604800,
JSON.stringify(session),
);
// Adiciona ao índice do usuário
await this.redis.sadd(`user_sessions:${data.userId}`, sessionId);
return session;
}Validar Sessão
typescript
async validate(sessionId: string): Promise<Session | null> {
const data = await this.redis.get(`session:${sessionId}`);
if (!data) return null;
const session = JSON.parse(data) as Session;
// Verifica expiração
if (new Date(session.expiresAt) < new Date()) {
await this.delete(sessionId);
return null;
}
// Atualiza última atividade (sliding expiration)
session.lastActivity = new Date();
await this.redis.setex(
`session:${sessionId}`,
604800,
JSON.stringify(session),
);
return session;
}Deletar Sessão (Logout)
typescript
async delete(sessionId: string): Promise<void> {
const data = await this.redis.get(`session:${sessionId}`);
if (data) {
const session = JSON.parse(data) as Session;
await this.redis.srem(`user_sessions:${session.userId}`, sessionId);
}
await this.redis.del(`session:${sessionId}`);
}Logout de Todos os Dispositivos
typescript
async deleteAllForUser(userId: bigint): Promise<number> {
const sessionIds = await this.redis.smembers(`user_sessions:${userId}`);
if (sessionIds.length > 0) {
await this.redis.del(
...sessionIds.map(id => `session:${id}`),
);
await this.redis.del(`user_sessions:${userId}`);
}
return sessionIds.length;
}Listar Sessões do Usuário
typescript
async getUserSessions(userId: bigint): Promise<Session[]> {
const sessionIds = await this.redis.smembers(`user_sessions:${userId}`);
const sessions: Session[] = [];
for (const sessionId of sessionIds) {
const data = await this.redis.get(`session:${sessionId}`);
if (data) {
sessions.push(JSON.parse(data));
}
}
return sessions.sort(
(a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
);
}Múltiplos Dispositivos
O usuário pode estar logado em vários dispositivos simultaneamente:
typescript
// Exemplo de sessões de um usuário
const sessions = await sessionService.getUserSessions(userId);
// Resultado:
// [
// { id: 'abc123', userAgent: 'Chrome/120 Windows', ip: '177.x.x.x', lastActivity: '2024-01-15 14:30' },
// { id: 'def456', userAgent: 'Safari/17 iPhone', ip: '189.x.x.x', lastActivity: '2024-01-15 10:00' },
// { id: 'ghi789', userAgent: 'Firefox/121 Linux', ip: '200.x.x.x', lastActivity: '2024-01-14 22:15' },
// ]Limite de Sessões
typescript
async create(data: CreateSessionData): Promise<Session> {
const existingSessions = await this.getUserSessions(data.userId);
// Máximo 5 sessões simultâneas
if (existingSessions.length >= 5) {
// Remove a mais antiga
const oldest = existingSessions[existingSessions.length - 1];
await this.delete(oldest.id);
this.logger.info(`Removed oldest session for user ${data.userId}`);
}
// Cria nova sessão...
}Detecção de Atividade Suspeita
Mudança de IP
typescript
async validateWithSecurityCheck(
sessionId: string,
currentIp: string
): Promise<Session | null> {
const session = await this.validate(sessionId);
if (!session) return null;
// IP mudou?
if (session.ip !== currentIp) {
// Verifica se é mesmo país/região
const isSameRegion = await this.geoService.isSameRegion(
session.ip,
currentIp
);
if (!isSameRegion) {
// Log de segurança
await this.auditService.log({
action: 'SUSPICIOUS_IP_CHANGE',
userId: session.userId,
metadata: {
sessionId,
originalIp: session.ip,
newIp: currentIp,
},
});
// Opcional: notifica usuário
await this.notificationService.send(session.userId, {
type: 'SECURITY_ALERT',
title: 'Login de novo local detectado',
message: `Detectamos acesso de ${currentIp}. Foi você?`,
});
}
}
return session;
}Device Fingerprinting
typescript
// Frontend envia fingerprint do dispositivo
const deviceId = await generateDeviceFingerprint();
// Backend valida
async validateDevice(session: Session, deviceId: string): boolean {
if (session.deviceId && session.deviceId !== deviceId) {
// Device mudou - possível session hijacking
await this.auditService.log({
action: 'DEVICE_MISMATCH',
userId: session.userId,
metadata: {
expectedDevice: session.deviceId,
receivedDevice: deviceId,
},
});
return false;
}
return true;
}Cookie Configuration
typescript
// Configuração segura do cookie de sessão
response.setCookie('sessionId', session.id, {
httpOnly: true, // Não acessível via JavaScript
secure: true, // Apenas HTTPS (produção)
sameSite: 'lax', // Proteção CSRF
maxAge: 604800, // 7 dias em segundos
path: '/',
domain: '.csgoflip.com', // Subdomínios inclusos
});Endpoints da API
GET /auth/sessions
Lista sessões do usuário logado:
json
{
"sessions": [
{
"id": "abc123...",
"ip": "177.xxx.xxx.xxx",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
"createdAt": "2024-01-10T14:30:00Z",
"lastActivity": "2024-01-15T16:45:00Z",
"isCurrent": true
},
{
"id": "def456...",
"ip": "189.xxx.xxx.xxx",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) Safari/604.1",
"createdAt": "2024-01-08T09:15:00Z",
"lastActivity": "2024-01-14T22:30:00Z",
"isCurrent": false
}
]
}DELETE /auth/sessions/:id
Revoga uma sessão específica:
typescript
@Delete('sessions/:id')
async revokeSession(
@Param('id') sessionId: string,
@CurrentUser() user: User,
) {
const session = await this.sessionService.getById(sessionId);
// Verifica que sessão pertence ao usuário
if (session?.userId !== user.id) {
throw new ForbiddenException('Not your session');
}
await this.sessionService.delete(sessionId);
return { message: 'Session revoked' };
}POST /auth/logout-all
Revoga todas as sessões:
typescript
@Post('logout-all')
async logoutAll(@CurrentUser() user: User) {
const count = await this.sessionService.deleteAllForUser(user.id);
return {
message: 'All sessions revoked',
sessionsRevoked: count,
};
}Redis Cluster
Para alta disponibilidade:
typescript
// config/redis.config.ts
export const redisConfig = {
// Redis Sentinel para failover automático
sentinels: [
{ host: 'sentinel-1.internal', port: 26379 },
{ host: 'sentinel-2.internal', port: 26379 },
{ host: 'sentinel-3.internal', port: 26379 },
],
name: 'mymaster',
// Ou Redis Cluster para escala horizontal
cluster: [
{ host: 'redis-1.internal', port: 6379 },
{ host: 'redis-2.internal', port: 6379 },
{ host: 'redis-3.internal', port: 6379 },
],
};Arquivos Fonte
Principais Arquivos
src/infrastructure/auth/session.service.ts- Serviço de sessõessrc/presentation/controllers/auth.controller.ts- Endpointssrc/infrastructure/redis/redis.service.ts- Cliente Redis
