186 lines
4.9 KiB
TypeScript
186 lines
4.9 KiB
TypeScript
import {
|
|
Injectable,
|
|
UnauthorizedException,
|
|
BadRequestException,
|
|
ConflictException,
|
|
Logger,
|
|
} from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import * as bcrypt from 'bcrypt';
|
|
import { UsersService } from '../users/users.service';
|
|
import { LoginDto, RegisterDto, AuthResponseDto, RefreshTokenDto } from './dto';
|
|
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
|
import { UserRole } from '../users/entities/user.entity';
|
|
import { RefreshTokenService } from './services/refresh-token.service';
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
private readonly logger = new Logger(AuthService.name);
|
|
private readonly BCRYPT_ROUNDS = 10;
|
|
|
|
constructor(
|
|
private readonly usersService: UsersService,
|
|
private readonly jwtService: JwtService,
|
|
private readonly refreshTokenService: RefreshTokenService,
|
|
) {}
|
|
|
|
/**
|
|
* Validate user credentials (used by LocalStrategy)
|
|
*/
|
|
async validateUser(email: string, password: string): Promise<any> {
|
|
const user = await this.usersService.findByEmail(email);
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('Invalid credentials');
|
|
}
|
|
|
|
if (!user.isActive) {
|
|
throw new UnauthorizedException('User account is inactive');
|
|
}
|
|
|
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
|
|
|
if (!isPasswordValid) {
|
|
throw new UnauthorizedException('Invalid credentials');
|
|
}
|
|
|
|
// Return user without password
|
|
const { password: _, ...result } = user;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Register new user
|
|
*/
|
|
async register(registerDto: RegisterDto): Promise<AuthResponseDto> {
|
|
// Check if user already exists
|
|
const existingUser = await this.usersService.findByEmail(registerDto.email);
|
|
|
|
if (existingUser) {
|
|
throw new ConflictException('Email already registered');
|
|
}
|
|
|
|
// Hash password
|
|
const hashedPassword = await this.hashPassword(registerDto.password);
|
|
|
|
// Create user with default role if not provided
|
|
const user = await this.usersService.create({
|
|
...registerDto,
|
|
password: hashedPassword,
|
|
roles: registerDto.roles || [UserRole.USER],
|
|
});
|
|
|
|
// Generate JWT and return
|
|
return this.login(user);
|
|
}
|
|
|
|
/**
|
|
* Login user and generate JWT + Refresh Token
|
|
*/
|
|
async login(user: any): Promise<AuthResponseDto> {
|
|
const payload: JwtPayload = {
|
|
sub: user.id,
|
|
email: user.email,
|
|
roles: user.roles || [],
|
|
};
|
|
|
|
// Generate access token
|
|
const accessToken = this.jwtService.sign(payload);
|
|
|
|
// Generate refresh token
|
|
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
|
user.id,
|
|
);
|
|
|
|
this.logger.log(`User logged in: ${user.email}`);
|
|
|
|
return {
|
|
access_token: accessToken,
|
|
refresh_token: refreshToken,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
roles: user.roles,
|
|
isActive: user.isActive,
|
|
createdAt: user.createdAt,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate JWT token
|
|
*/
|
|
async validateToken(token: string): Promise<any> {
|
|
try {
|
|
const payload = this.jwtService.verify(token);
|
|
return payload;
|
|
} catch (error) {
|
|
throw new UnauthorizedException('Invalid or expired token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh access token using refresh token
|
|
* Implements token rotation for enhanced security
|
|
*/
|
|
async refreshAccessToken(
|
|
refreshTokenDto: RefreshTokenDto,
|
|
): Promise<AuthResponseDto> {
|
|
// Validate refresh token
|
|
const userId = await this.refreshTokenService.validateRefreshToken(
|
|
refreshTokenDto.refreshToken,
|
|
);
|
|
|
|
// Get user details
|
|
const user = await this.usersService.findOne(userId);
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('User not found');
|
|
}
|
|
|
|
// Revoke old refresh token (token rotation)
|
|
await this.refreshTokenService.revokeRefreshToken(
|
|
refreshTokenDto.refreshToken,
|
|
);
|
|
|
|
// Generate new tokens
|
|
this.logger.log(`Token refreshed for user: ${user.email}`);
|
|
return this.login(user);
|
|
}
|
|
|
|
/**
|
|
* Logout user and revoke refresh token
|
|
*/
|
|
async logout(refreshToken: string): Promise<void> {
|
|
if (!refreshToken) {
|
|
throw new BadRequestException('Refresh token is required');
|
|
}
|
|
|
|
await this.refreshTokenService.revokeRefreshToken(refreshToken);
|
|
this.logger.log('User logged out and refresh token revoked');
|
|
}
|
|
|
|
/**
|
|
* Revoke all refresh tokens for a user
|
|
*/
|
|
async revokeAllUserTokens(userId: string): Promise<void> {
|
|
await this.refreshTokenService.revokeAllUserTokens(userId);
|
|
this.logger.log(`All tokens revoked for user: ${userId}`);
|
|
}
|
|
|
|
/**
|
|
* Hash password using bcrypt
|
|
*/
|
|
private async hashPassword(password: string): Promise<string> {
|
|
return bcrypt.hash(password, this.BCRYPT_ROUNDS);
|
|
}
|
|
|
|
/**
|
|
* Verify password hash
|
|
*/
|
|
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
return bcrypt.compare(password, hash);
|
|
}
|
|
}
|