228 lines
6.0 KiB
TypeScript
228 lines
6.0 KiB
TypeScript
import {
|
|
Injectable,
|
|
UnauthorizedException,
|
|
ConflictException,
|
|
Logger,
|
|
Inject,
|
|
} from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import * as argon2 from 'argon2';
|
|
import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
|
import { User, UserRole } from '../../domain/entities/user.entity';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
export interface JwtPayload {
|
|
sub: string; // user ID
|
|
email: string;
|
|
role: string;
|
|
organizationId: string;
|
|
type: 'access' | 'refresh';
|
|
}
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
private readonly logger = new Logger(AuthService.name);
|
|
|
|
constructor(
|
|
@Inject(USER_REPOSITORY)
|
|
private readonly userRepository: UserRepository, // ✅ Correct injection
|
|
private readonly jwtService: JwtService,
|
|
private readonly configService: ConfigService
|
|
) {}
|
|
|
|
/**
|
|
* Register a new user
|
|
*/
|
|
async register(
|
|
email: string,
|
|
password: string,
|
|
firstName: string,
|
|
lastName: string,
|
|
organizationId?: string
|
|
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
|
this.logger.log(`Registering new user: ${email}`);
|
|
|
|
const existingUser = await this.userRepository.findByEmail(email);
|
|
|
|
if (existingUser) {
|
|
throw new ConflictException('User with this email already exists');
|
|
}
|
|
|
|
const passwordHash = await argon2.hash(password, {
|
|
type: argon2.argon2id,
|
|
memoryCost: 65536, // 64 MB
|
|
timeCost: 3,
|
|
parallelism: 4,
|
|
});
|
|
|
|
// Validate or generate organization ID
|
|
const finalOrganizationId = this.validateOrGenerateOrganizationId(organizationId);
|
|
|
|
const user = User.create({
|
|
id: uuidv4(),
|
|
organizationId: finalOrganizationId,
|
|
email,
|
|
passwordHash,
|
|
firstName,
|
|
lastName,
|
|
role: UserRole.USER,
|
|
});
|
|
|
|
const savedUser = await this.userRepository.save(user);
|
|
|
|
const tokens = await this.generateTokens(savedUser);
|
|
|
|
this.logger.log(`User registered successfully: ${email}`);
|
|
|
|
return {
|
|
...tokens,
|
|
user: {
|
|
id: savedUser.id,
|
|
email: savedUser.email,
|
|
firstName: savedUser.firstName,
|
|
lastName: savedUser.lastName,
|
|
role: savedUser.role,
|
|
organizationId: savedUser.organizationId,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Login user with email and password
|
|
*/
|
|
async login(
|
|
email: string,
|
|
password: string
|
|
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
|
this.logger.log(`Login attempt for: ${email}`);
|
|
|
|
const user = await this.userRepository.findByEmail(email);
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('Invalid credentials');
|
|
}
|
|
|
|
if (!user.isActive) {
|
|
throw new UnauthorizedException('User account is inactive');
|
|
}
|
|
|
|
const isPasswordValid = await argon2.verify(user.passwordHash, password);
|
|
|
|
if (!isPasswordValid) {
|
|
throw new UnauthorizedException('Invalid credentials');
|
|
}
|
|
|
|
const tokens = await this.generateTokens(user);
|
|
|
|
this.logger.log(`User logged in successfully: ${email}`);
|
|
|
|
return {
|
|
...tokens,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
role: user.role,
|
|
organizationId: user.organizationId,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Refresh access token using refresh token
|
|
*/
|
|
async refreshAccessToken(
|
|
refreshToken: string
|
|
): Promise<{ accessToken: string; refreshToken: string }> {
|
|
try {
|
|
const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
|
|
secret: this.configService.get('JWT_SECRET'),
|
|
});
|
|
|
|
if (payload.type !== 'refresh') {
|
|
throw new UnauthorizedException('Invalid token type');
|
|
}
|
|
|
|
const user = await this.userRepository.findById(payload.sub);
|
|
|
|
if (!user || !user.isActive) {
|
|
throw new UnauthorizedException('User not found or inactive');
|
|
}
|
|
|
|
const tokens = await this.generateTokens(user);
|
|
|
|
this.logger.log(`Access token refreshed for user: ${user.email}`);
|
|
|
|
return tokens;
|
|
} catch (error: any) {
|
|
this.logger.error(`Token refresh failed: ${error?.message || 'Unknown error'}`);
|
|
throw new UnauthorizedException('Invalid or expired refresh token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate user from JWT payload
|
|
*/
|
|
async validateUser(payload: JwtPayload): Promise<User | null> {
|
|
const user = await this.userRepository.findById(payload.sub);
|
|
|
|
if (!user || !user.isActive) {
|
|
return null;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Generate access and refresh tokens
|
|
*/
|
|
private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> {
|
|
const accessPayload: JwtPayload = {
|
|
sub: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
organizationId: user.organizationId,
|
|
type: 'access',
|
|
};
|
|
|
|
const refreshPayload: JwtPayload = {
|
|
sub: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
organizationId: user.organizationId,
|
|
type: 'refresh',
|
|
};
|
|
|
|
const [accessToken, refreshToken] = await Promise.all([
|
|
this.jwtService.signAsync(accessPayload, {
|
|
expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'),
|
|
}),
|
|
this.jwtService.signAsync(refreshPayload, {
|
|
expiresIn: this.configService.get('JWT_REFRESH_EXPIRATION', '7d'),
|
|
}),
|
|
]);
|
|
|
|
return { accessToken, refreshToken };
|
|
}
|
|
|
|
/**
|
|
* Validate or generate a valid organization ID
|
|
* If provided ID is invalid (not a UUID), generate a new one
|
|
*/
|
|
private validateOrGenerateOrganizationId(organizationId?: string): string {
|
|
// UUID v4 regex pattern
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
|
|
if (organizationId && uuidRegex.test(organizationId)) {
|
|
return organizationId;
|
|
}
|
|
|
|
// Generate new UUID if not provided or invalid
|
|
const newOrgId = uuidv4();
|
|
this.logger.warn(`Invalid or missing organization ID. Generated new ID: ${newOrgId}`);
|
|
return newOrgId;
|
|
}
|
|
}
|