hexabot/api/src/user/controllers/auth.controller.ts
2025-01-30 16:51:59 +01:00

189 lines
5.5 KiB
TypeScript

/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import {
BadRequestException,
Body,
Controller,
Get,
Inject,
InternalServerErrorException,
Param,
Post,
Req,
Res,
Session,
UnauthorizedException,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { CsrfCheck, CsrfGen, CsrfGenAuth } from '@tekuconcept/nestjs-csrf';
import { Request, Response } from 'express';
import { Session as ExpressSession } from 'express-session';
import { config } from '@/config';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
import { Roles } from '@/utils/decorators/roles.decorator';
import { InvitationCreateDto } from '../dto/invitation.dto';
import { UserCreateDto } from '../dto/user.dto';
import { LocalAuthGuard } from '../guards/local-auth.guard';
import { InvitationService } from '../services/invitation.service';
import { UserService } from '../services/user.service';
import { ValidateAccountService } from '../services/validate-account.service';
export class BaseAuthController {
@Inject(EventEmitter2)
private readonly eventEmitter: EventEmitter2;
constructor(protected readonly logger: LoggerService) {}
/**
* Fetches details of the currently authenticated user.
*
* @param req - The request object which includes user details.
*
* @returns The user object from the request.
*/
@Get('me')
me(@Req() req: Request) {
return req.user;
}
/**
* Handles user logout by clearing the session and cookies.
*
* @param session - The current user's session to be destroyed.
* @param res - The response object used to clear the session cookie.
*
* @returns A status object indicating successful logout.
*/
@Post('logout')
@CsrfCheck(false)
@CsrfGen(false)
logout(
@Session() session: ExpressSession,
@Res({ passthrough: true }) res: Response,
) {
this.eventEmitter.emit('hook:user:logout', session);
res.clearCookie(config.session.name);
session.destroy((error) => {
if (error) {
this.logger.error(error);
throw new BadRequestException();
}
});
return { status: 'ok' };
}
}
@UseInterceptors(CsrfInterceptor)
@Controller('auth')
export class LocalAuthController extends BaseAuthController {
constructor(
logger: LoggerService,
private readonly userService: UserService,
private readonly validateAccountService: ValidateAccountService,
private readonly invitationService: InvitationService,
) {
super(logger);
}
/**
* Handles local login process using guard for public users.
*
* @param req - The request object containing user details.
*
* @returns The logged-in user object.
*/
@UseGuards(LocalAuthGuard)
@Roles('public')
@Post('local')
@CsrfCheck(false)
@CsrfGenAuth(true)
login(@Req() req: Request) {
return req.user;
}
/**
* Handles the signup process for new users.
*
* @param userCreateDto - Data Transfer Object containing the new user's information.
*
* @returns Success status object upon successful signup.
*/
@Roles('public')
@Post('signup')
async signup(@Body() userCreateDto: UserCreateDto) {
try {
await this.userService.create(userCreateDto);
return { success: true };
} catch (error) {
this.logger.error(error);
throw new BadRequestException();
}
}
/**
* Accepts an invitation and creates a new user based on the invitation token.
* Verifies the token and ensures it matches the user details.
*
* @param userCreateDto - Data Transfer Object for the new user.
* @param token - The invitation token.
*
* @returns Void, upon successful creation of the user.
*/
@Roles('public')
@Post('accept-invite/:token')
async acceptInvite(
@Body() userCreateDto: UserCreateDto,
@Param('token') token: string,
) {
let decodedToken: InvitationCreateDto;
// Verify token
try {
decodedToken = await this.invitationService.verify(token);
} catch (error) {
if (error.name === 'TokenExpiredError')
throw new UnauthorizedException('Token expired');
else throw new BadRequestException(error.name, error.message);
}
// Verify token matches user
if (decodedToken.email !== userCreateDto.email)
throw new BadRequestException("Email doesn't match invitation email");
if (decodedToken.roles.some((item) => !userCreateDto.roles.includes(item)))
throw new BadRequestException('invitation roles do not match user roles');
try {
// Create user
await this.userService.create({ ...userCreateDto, state: false });
await this.validateAccountService.sendConfirmationEmail({
email: userCreateDto.email,
first_name: userCreateDto.first_name,
});
await this.invitationService.deleteOne({ email: decodedToken.email });
} catch (e) {
this.logger.error(
'Could not send email',
e.message,
e.stack,
'AcceptInvite',
);
throw new InternalServerErrorException('Could not send email');
}
}
}