import { Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, IsNull, MoreThan } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { User } from '../../entities/user.entity'; import { RefreshToken } from '../../entities/refresh-token.entity'; interface GoogleProfile { id: string; emails: Array<{ value: string; verified: boolean }>; displayName: string; photos: Array<{ value: string }>; } @Injectable() export class AuthService { constructor( @InjectRepository(User) private readonly userRepository: Repository, @InjectRepository(RefreshToken) private readonly refreshTokenRepository: Repository, private readonly jwtService: JwtService, private readonly configService: ConfigService, ) {} async validateOAuthUser(profile: GoogleProfile): Promise { const email = profile.emails?.[0]?.value; const googleId = profile.id; const displayName = profile.displayName; const avatarUrl = profile.photos?.[0]?.value; if (!email || !googleId) { throw new UnauthorizedException('Invalid Google profile'); } let user = await this.userRepository.findOne({ where: { googleId }, }); if (user) { // Update last login and profile info user.lastLoginAt = new Date(); user.displayName = displayName; user.avatarUrl = avatarUrl; await this.userRepository.save(user); } else { // Create new user user = this.userRepository.create({ googleId, email, displayName, avatarUrl, lastLoginAt: new Date(), }); await this.userRepository.save(user); } return user; } async generateTokens( user: User, ): Promise<{ accessToken: string; refreshToken: string }> { // Generate access token const payload = { sub: user.id, email: user.email }; const accessToken = this.jwtService.sign(payload); // Generate refresh token const refreshTokenValue = this.generateRandomToken(); const hashedToken = await bcrypt.hash(refreshTokenValue, 10); // Calculate expiration (7 days) const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); // Save refresh token to database const refreshTokenEntity = this.refreshTokenRepository.create({ userId: user.id, token: hashedToken, expiresAt, }); await this.refreshTokenRepository.save(refreshTokenEntity); return { accessToken, refreshToken: `${refreshTokenEntity.id}:${refreshTokenValue}`, }; } async refreshTokens( token: string, ): Promise<{ accessToken: string; refreshToken: string; user: User }> { const [tokenId, tokenValue] = token.split(':'); if (!tokenId || !tokenValue) { throw new UnauthorizedException('Invalid refresh token format'); } const storedToken = await this.refreshTokenRepository.findOne({ where: { id: tokenId, revokedAt: IsNull(), expiresAt: MoreThan(new Date()), }, relations: ['user'], }); if (!storedToken) { throw new UnauthorizedException('Invalid or expired refresh token'); } const isValid = await bcrypt.compare(tokenValue, storedToken.token); if (!isValid) { throw new UnauthorizedException('Invalid refresh token'); } // Revoke old token (rotation) await this.revokeRefreshToken(token); // Generate new tokens const { accessToken, refreshToken } = await this.generateTokens( storedToken.user, ); return { accessToken, refreshToken, user: storedToken.user, }; } async revokeRefreshToken(token: string): Promise { const [tokenId] = token.split(':'); if (tokenId) { await this.refreshTokenRepository.update( { id: tokenId }, { revokedAt: new Date() }, ); } } async validateUserById(userId: string): Promise { return this.userRepository.findOne({ where: { id: userId } }); } private generateRandomToken(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 64; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } }