icynet-auth-server/src/modules/features/login/login.controller.ts

327 lines
8.2 KiB
TypeScript

import {
Body,
Controller,
Get,
Post,
Query,
Render,
Req,
Res,
Session,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { Request, Response } from 'express';
import { SessionData } from 'express-session';
import {
UserToken,
UserTokenType,
} from 'src/modules/objects/user-token/user-token.entity';
import { UserTokenService } from 'src/modules/objects/user-token/user-token.service';
import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service';
import { User } from 'src/modules/objects/user/user.entity';
import { UserService } from 'src/modules/objects/user/user.service';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { TokenService } from 'src/modules/utility/services/token.service';
@Controller('/login')
export class LoginController {
constructor(
private readonly userService: UserService,
private readonly totpService: UserTOTPService,
private readonly userTokenService: UserTokenService,
private readonly formUtil: FormUtilityService,
private readonly token: TokenService,
) {}
@Get()
@Render('login/login')
public loginView(@Req() req: Request): Record<string, any> {
return this.formUtil.populateTemplate(req);
}
@Post()
public async loginRequest(
@Req() req: Request,
@Res() res: Response,
@Body() body: { username: string; password: string },
@Query() query: { redirectTo?: string },
) {
const { username, password } = body;
const user = await this.userService.getByUsername(username);
// User exists and password matches
if (
!user ||
!user.activated ||
!(await this.userService.comparePasswords(user.password, password))
) {
req.flash('form', { username });
req.flash('message', {
error: true,
text: 'Invalid username or password',
});
res.redirect(req.originalUrl);
return;
}
if (await this.totpService.userHasTOTP(user)) {
const challenge = { type: 'verify', user: user.uuid };
req.session.challenge = await this.token.encryptChallenge(challenge);
res.redirect(
'/login/verify' +
(query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
return;
}
req.session.user = user.uuid;
res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/');
}
@Get('verify')
public verifyUserTokenView(
@Session() session: SessionData,
@Query() query: { redirectTo?: string },
@Req() req: Request,
@Res() res: Response,
) {
if (!session.challenge) {
req.flash('message', {
error: true,
text: 'An unexpected error occured, please log in again.',
});
res.redirect(
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
return;
}
res.render(
'login/totp-verify',
this.formUtil.populateTemplate(req, session),
);
}
@Post('verify')
public async verifyUserToken(
@Session() session: SessionData,
@Query() query: { redirectTo?: string },
@Body() body: { totp: string },
@Req() req: Request,
@Res() res: Response,
) {
let user: User;
try {
if (!session.challenge) {
throw new Error('No challenge');
}
const challenge = await this.token.decryptChallenge(session.challenge);
if (!challenge || challenge.type !== 'verify' || !challenge.user) {
throw new Error('Bad challenge');
}
user = await this.userService.getByUUID(challenge.user);
if (!user) {
throw new Error('Bad challenge');
}
} catch (e: any) {
req.flash('message', {
error: true,
text: 'An unexpected error occured, please log in again.',
});
res.redirect(
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
return;
}
try {
const totp = await this.totpService.getUserTOTP(user);
if (!this.totpService.validateTOTP(totp.token, body.totp)) {
throw new Error('Invalid code!');
}
} catch (e: any) {
req.flash('message', {
error: true,
text: e.message,
});
res.redirect(req.originalUrl);
return;
}
session.challenge = null;
session.user = user.uuid;
res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/');
}
@Get('activate')
public async activateUser(
@Req() req: Request,
@Res() res: Response,
@Query() query: { token: string },
) {
let user: User;
let token: UserToken;
try {
if (!query || !query.token) {
throw new Error();
}
token = await this.userTokenService.get(
query.token,
UserTokenType.ACTIVATION,
);
if (!token) {
throw new Error();
}
user = token.user;
} catch (e: any) {
req.flash('message', {
error: true,
text: 'Invalid or expired activation link.',
});
res.redirect('/login');
return;
}
user.activated = true;
await this.userService.updateUser(user);
await this.userTokenService.delete(token);
req.flash('message', {
error: false,
text: 'Account has been activated successfully. You may now log in.',
});
res.redirect('/login');
}
@Get('password')
public async recoverView(
@Req() req: Request,
@Res() res: Response,
@Session() session: SessionData,
@Query() query: { token: string },
) {
if (query.token) {
const token = await this.userTokenService.get(
query.token,
UserTokenType.PASSWORD,
);
if (!token) {
req.flash('message', {
error: true,
text: 'Invalid or expired reset link.',
});
res.redirect('/login');
return;
}
res.render('login/password', {
...this.formUtil.populateTemplate(req),
token: true,
});
return;
}
res.render('login/password', {
...this.formUtil.populateTemplate(req),
token: false,
});
}
@Post('password')
@Throttle(3, 60)
public async setNewPassword(
@Req() req: Request,
@Res() res: Response,
@Body()
body: { email?: string; password?: string; password_repeat?: string },
@Query() query: { token: string },
) {
// Email send fragment
if (!query.token) {
if (!body.email || !body.email.match(this.formUtil.emailRegex)) {
req.flash('message', {
error: true,
text: 'A valid email address is mandatory.',
});
res.redirect(req.originalUrl);
return;
}
await this.userService.userPassword(body.email);
req.flash('message', {
error: false,
text: 'If there is an account registered with this email, instructions to reset the password will be sent shortly!',
});
res.redirect('/login');
return;
}
// Change password fragment
const token = await this.userTokenService.get(
query.token,
UserTokenType.PASSWORD,
);
if (!token) {
req.flash('message', {
error: true,
text: 'Invalid or expired reset link.',
});
res.redirect('/login');
return;
}
const { password, password_repeat } = body;
try {
if (!password || !password_repeat) {
throw new Error('Please fill out all of the fields.');
}
if (!password.match(this.formUtil.passwordRegex)) {
throw new Error(
'Password must be at least 8 characters long, contain a capital and lowercase letter and a number',
);
}
if (password !== password_repeat) {
throw new Error('The passwords do not match!');
}
const hashword = await this.userService.hashPassword(password);
token.user.password = hashword;
await this.userService.updateUser(token.user);
await this.userTokenService.delete(token);
req.flash('message', {
error: false,
text: 'Your password has been reset successfully. You may now log in with your new password!',
});
res.redirect('/login');
} catch (e: any) {
req.flash('message', { error: true, text: e.message });
res.redirect(req.originalUrl);
}
}
}