260 lines
7.6 KiB
TypeScript
260 lines
7.6 KiB
TypeScript
import {
|
|
Controller,
|
|
Post,
|
|
Body,
|
|
HttpCode,
|
|
HttpStatus,
|
|
UseGuards,
|
|
Get,
|
|
Inject,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { AuthService } from '../auth/auth.service';
|
|
import { LoginDto, RegisterDto, AuthResponseDto, RefreshTokenDto } from '../dto/auth-login.dto';
|
|
import { Public } from '../decorators/public.decorator';
|
|
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
|
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
|
import { UserMapper } from '../mappers/user.mapper';
|
|
import { InvitationService } from '../services/invitation.service';
|
|
|
|
/**
|
|
* Authentication Controller
|
|
*
|
|
* Handles user authentication endpoints:
|
|
* - POST /auth/register - User registration
|
|
* - POST /auth/login - User login
|
|
* - POST /auth/refresh - Token refresh
|
|
* - POST /auth/logout - User logout (placeholder)
|
|
* - GET /auth/me - Get current user profile
|
|
*/
|
|
@ApiTags('Authentication')
|
|
@Controller('auth')
|
|
export class AuthController {
|
|
constructor(
|
|
private readonly authService: AuthService,
|
|
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
|
|
private readonly invitationService: InvitationService
|
|
) {}
|
|
|
|
/**
|
|
* Register a new user
|
|
*
|
|
* Creates a new user account and returns access + refresh tokens.
|
|
*
|
|
* @param dto - Registration data (email, password, firstName, lastName, organizationId)
|
|
* @returns Access token, refresh token, and user info
|
|
*/
|
|
@Public()
|
|
@Post('register')
|
|
@HttpCode(HttpStatus.CREATED)
|
|
@ApiOperation({
|
|
summary: 'Register new user',
|
|
description: 'Create a new user account with email and password. Returns JWT tokens.',
|
|
})
|
|
@ApiResponse({
|
|
status: 201,
|
|
description: 'User successfully registered',
|
|
type: AuthResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 409,
|
|
description: 'User with this email already exists',
|
|
})
|
|
@ApiResponse({
|
|
status: 400,
|
|
description: 'Validation error (invalid email, weak password, etc.)',
|
|
})
|
|
async register(@Body() dto: RegisterDto): Promise<AuthResponseDto> {
|
|
// If invitation token is provided, verify and use it
|
|
let invitationOrganizationId: string | undefined;
|
|
let invitationRole: string | undefined;
|
|
|
|
if (dto.invitationToken) {
|
|
const invitation = await this.invitationService.verifyInvitation(dto.invitationToken);
|
|
|
|
// Verify email matches invitation
|
|
if (invitation.email.toLowerCase() !== dto.email.toLowerCase()) {
|
|
throw new NotFoundException('Invitation email does not match registration email');
|
|
}
|
|
|
|
invitationOrganizationId = invitation.organizationId;
|
|
invitationRole = invitation.role;
|
|
|
|
// Override firstName/lastName from invitation if not provided
|
|
dto.firstName = dto.firstName || invitation.firstName;
|
|
dto.lastName = dto.lastName || invitation.lastName;
|
|
}
|
|
|
|
const result = await this.authService.register(
|
|
dto.email,
|
|
dto.password,
|
|
dto.firstName,
|
|
dto.lastName,
|
|
invitationOrganizationId || dto.organizationId,
|
|
dto.organization,
|
|
invitationRole
|
|
);
|
|
|
|
// Mark invitation as used if provided
|
|
if (dto.invitationToken) {
|
|
await this.invitationService.markInvitationAsUsed(dto.invitationToken);
|
|
}
|
|
|
|
return {
|
|
accessToken: result.accessToken,
|
|
refreshToken: result.refreshToken,
|
|
user: result.user,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Login with email and password
|
|
*
|
|
* Authenticates a user and returns access + refresh tokens.
|
|
*
|
|
* @param dto - Login credentials (email, password)
|
|
* @returns Access token, refresh token, and user info
|
|
*/
|
|
@Public()
|
|
@Post('login')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({
|
|
summary: 'User login',
|
|
description: 'Authenticate with email and password. Returns JWT tokens.',
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Login successful',
|
|
type: AuthResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Invalid credentials or inactive account',
|
|
})
|
|
async login(@Body() dto: LoginDto): Promise<AuthResponseDto> {
|
|
const result = await this.authService.login(dto.email, dto.password);
|
|
|
|
return {
|
|
accessToken: result.accessToken,
|
|
refreshToken: result.refreshToken,
|
|
user: result.user,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Refresh access token
|
|
*
|
|
* Obtains a new access token using a valid refresh token.
|
|
*
|
|
* @param dto - Refresh token
|
|
* @returns New access token
|
|
*/
|
|
@Public()
|
|
@Post('refresh')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({
|
|
summary: 'Refresh access token',
|
|
description:
|
|
'Get a new access token using a valid refresh token. Refresh tokens are long-lived (7 days).',
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Token refreshed successfully',
|
|
schema: {
|
|
properties: {
|
|
accessToken: { type: 'string', example: 'eyJhbGciOiJIUzI1NiIs...' },
|
|
},
|
|
},
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Invalid or expired refresh token',
|
|
})
|
|
async refresh(@Body() dto: RefreshTokenDto): Promise<{ accessToken: string }> {
|
|
const result = await this.authService.refreshAccessToken(dto.refreshToken);
|
|
|
|
return { accessToken: result.accessToken };
|
|
}
|
|
|
|
/**
|
|
* Logout (placeholder)
|
|
*
|
|
* Currently a no-op endpoint. With JWT, logout is typically handled client-side
|
|
* by removing tokens. For more security, implement token blacklisting with Redis.
|
|
*
|
|
* @returns Success message
|
|
*/
|
|
@UseGuards(JwtAuthGuard)
|
|
@Post('logout')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiBearerAuth()
|
|
@ApiOperation({
|
|
summary: 'Logout',
|
|
description: 'Logout the current user. Currently handled client-side by removing tokens.',
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Logout successful',
|
|
schema: {
|
|
properties: {
|
|
message: { type: 'string', example: 'Logout successful' },
|
|
},
|
|
},
|
|
})
|
|
async logout(): Promise<{ message: string }> {
|
|
// TODO: Implement token blacklisting with Redis for more security
|
|
// For now, logout is handled client-side by removing tokens
|
|
return { message: 'Logout successful' };
|
|
}
|
|
|
|
/**
|
|
* Get current user profile
|
|
*
|
|
* Returns the profile of the currently authenticated user with complete details.
|
|
*
|
|
* @param user - Current user from JWT token
|
|
* @returns User profile with firstName, lastName, etc.
|
|
*/
|
|
@UseGuards(JwtAuthGuard)
|
|
@Get('me')
|
|
@ApiBearerAuth()
|
|
@ApiOperation({
|
|
summary: 'Get current user profile',
|
|
description: 'Returns the complete profile of the authenticated user.',
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'User profile retrieved successfully',
|
|
schema: {
|
|
properties: {
|
|
id: { type: 'string', format: 'uuid' },
|
|
email: { type: 'string', format: 'email' },
|
|
firstName: { type: 'string' },
|
|
lastName: { type: 'string' },
|
|
role: { type: 'string', enum: ['ADMIN', 'MANAGER', 'USER', 'VIEWER'] },
|
|
organizationId: { type: 'string', format: 'uuid' },
|
|
isActive: { type: 'boolean' },
|
|
createdAt: { type: 'string', format: 'date-time' },
|
|
updatedAt: { type: 'string', format: 'date-time' },
|
|
},
|
|
},
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - invalid or missing token',
|
|
})
|
|
async getProfile(@CurrentUser() user: UserPayload) {
|
|
// Fetch complete user details from database
|
|
const fullUser = await this.userRepository.findById(user.id);
|
|
|
|
if (!fullUser) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
// Return complete user data with firstName and lastName
|
|
return UserMapper.toDto(fullUser);
|
|
}
|
|
}
|