feat: Sprint 1 — backend fondations TetaRdPG
Auth SuperOAuth (JWT validation + httpOnly cookie), entités users/characters/level_thresholds, lazy calculation endurance, seed 100 niveaux, config prod-ready (trust proxy, helmet, CORS, rate limit). Validé : health 200, auth flow, character CRUD, endurance lazy, 401 sans cookie.
This commit is contained in:
89
src/auth/auth.service.ts
Normal file
89
src/auth/auth.service.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Response } from 'express';
|
||||
import { User } from '../user/user.entity';
|
||||
import { SetSessionDto } from './dto/set-session.dto';
|
||||
|
||||
// Payload émis par SuperOAuth
|
||||
interface SuperOAuthPayload {
|
||||
sub: string; // ID provider (Twitch ID, Discord ID…)
|
||||
provider: string; // 'twitch' | 'discord' | 'google' | 'github'
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async setSession(dto: SetSessionDto, res: Response): Promise<Omit<User, 'oauthId'>> {
|
||||
let payload: SuperOAuthPayload;
|
||||
|
||||
try {
|
||||
payload = await this.jwtService.verifyAsync<SuperOAuthPayload>(dto.jwt, {
|
||||
secret: this.configService.get<string>('SUPER_OAUTH_JWT_SECRET'),
|
||||
});
|
||||
} catch {
|
||||
throw new UnauthorizedException('JWT SuperOAuth invalide ou expiré');
|
||||
}
|
||||
|
||||
if (!payload.sub || !payload.provider || !payload.username) {
|
||||
throw new UnauthorizedException('Payload JWT incomplet');
|
||||
}
|
||||
|
||||
// Upsert user
|
||||
let user = await this.userRepository.findOne({
|
||||
where: { oauthId: payload.sub, provider: payload.provider },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = this.userRepository.create({
|
||||
oauthId: payload.sub,
|
||||
provider: payload.provider,
|
||||
username: payload.username,
|
||||
avatarUrl: payload.avatar_url ?? null,
|
||||
});
|
||||
} else {
|
||||
user.username = payload.username;
|
||||
user.avatarUrl = payload.avatar_url ?? null;
|
||||
}
|
||||
|
||||
await this.userRepository.save(user);
|
||||
|
||||
// Cookie httpOnly signé — valeur = UUID interne
|
||||
const isProduction = this.configService.get('NODE_ENV') === 'production';
|
||||
res.cookie('session', user.id, {
|
||||
httpOnly: true,
|
||||
signed: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours
|
||||
});
|
||||
|
||||
const { oauthId: _, ...safeUser } = user;
|
||||
return safeUser;
|
||||
}
|
||||
|
||||
async getMe(user: User): Promise<Omit<User, 'oauthId'>> {
|
||||
const { oauthId: _, ...safeUser } = user;
|
||||
return safeUser;
|
||||
}
|
||||
|
||||
logout(res: Response): void {
|
||||
res.clearCookie('session');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user