import { Injectable, UnauthorizedException, } from '@nestjs/common'; 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'; interface SuperOAuthUser { id: string; tenantId: string; email: string | null; nickname: string; isActive: boolean; linkedProviders: string[]; } const COOKIE_NAME = 'session'; const REFRESH_COOKIE_NAME = 'refresh_token'; @Injectable() export class AuthService { private readonly superOauthUrl: string; constructor( @InjectRepository(User) private readonly userRepository: Repository, private readonly configService: ConfigService, ) { this.superOauthUrl = this.configService.getOrThrow('SUPER_OAUTH_URL'); } async setSession(dto: SetSessionDto, res: Response): Promise> { const oauthUser = await this.introspectToken(dto.token); // Upsert user let user = await this.userRepository.findOne({ where: { superOauthId: oauthUser.id }, }); if (!user) { user = this.userRepository.create({ superOauthId: oauthUser.id, username: oauthUser.nickname, email: oauthUser.email, }); } else { user.username = oauthUser.nickname; user.email = oauthUser.email; } await this.userRepository.save(user); // Cookie httpOnly — session = UUID interne const isProduction = this.configService.get('NODE_ENV') === 'production'; res.cookie(COOKIE_NAME, user.id, { httpOnly: true, signed: true, secure: isProduction, sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours }); // Refresh token cookie si fourni if (dto.refreshToken) { res.cookie(REFRESH_COOKIE_NAME, dto.refreshToken, { httpOnly: true, signed: true, secure: isProduction, sameSite: 'lax', maxAge: 30 * 24 * 60 * 60 * 1000, // 30 jours }); } const { superOauthId: _, ...safeUser } = user; return safeUser; } async refreshSession(res: Response, refreshToken: string): Promise<{ success: boolean }> { // Exchange refresh token for new access token via SuperOAuth const response = await fetch(`${this.superOauthUrl}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, }).toString(), }); if (!response.ok) { throw new UnauthorizedException('Refresh token invalide ou expiré'); } const data = await response.json(); if (!data.access_token) { throw new UnauthorizedException('Refresh échoué — pas de token'); } // Validate the new access token to get user data const oauthUser = await this.introspectToken(data.access_token); const user = await this.userRepository.findOne({ where: { superOauthId: oauthUser.id }, }); if (!user) { throw new UnauthorizedException('Utilisateur introuvable après refresh'); } // Set new cookies const isProduction = this.configService.get('NODE_ENV') === 'production'; res.cookie(COOKIE_NAME, user.id, { httpOnly: true, signed: true, secure: isProduction, sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000, }); if (data.refresh_token) { res.cookie(REFRESH_COOKIE_NAME, data.refresh_token, { httpOnly: true, signed: true, secure: isProduction, sameSite: 'lax', maxAge: 30 * 24 * 60 * 60 * 1000, }); } return { success: true }; } async getMe(user: User): Promise> { const { superOauthId: _, ...safeUser } = user; return safeUser; } logout(res: Response): void { res.clearCookie(COOKIE_NAME); res.clearCookie(REFRESH_COOKIE_NAME); } private async introspectToken(token: string): Promise { const response = await fetch(`${this.superOauthUrl}/api/v1/auth/token/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), }); if (!response.ok) { throw new UnauthorizedException('Token SuperOAuth invalide'); } const data = await response.json(); if (!data.data?.valid || !data.data.user) { throw new UnauthorizedException('Token SuperOAuth invalide ou expiré'); } if (!data.data.user.isActive) { throw new UnauthorizedException('Compte SuperOAuth désactivé'); } return data.data.user as SuperOAuthUser; } }