2022-03-09 18:37:04 +00:00
|
|
|
import {
|
|
|
|
Body,
|
|
|
|
Controller,
|
|
|
|
Get,
|
|
|
|
Post,
|
|
|
|
Query,
|
|
|
|
Render,
|
|
|
|
Req,
|
|
|
|
Res,
|
|
|
|
Session,
|
|
|
|
} from '@nestjs/common';
|
2022-03-20 12:09:36 +00:00
|
|
|
import { Throttle } from '@nestjs/throttler';
|
2022-03-09 18:37:04 +00:00
|
|
|
import { Request, Response } from 'express';
|
|
|
|
import { SessionData } from 'express-session';
|
2022-09-09 17:12:22 +00:00
|
|
|
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
|
|
|
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
2022-03-10 18:31:05 +00:00
|
|
|
import {
|
|
|
|
UserToken,
|
|
|
|
UserTokenType,
|
2022-03-15 17:00:15 +00:00
|
|
|
} 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';
|
2022-03-09 18:37:04 +00:00
|
|
|
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,
|
2022-03-10 18:31:05 +00:00
|
|
|
private readonly totpService: UserTOTPService,
|
2022-03-15 17:00:15 +00:00
|
|
|
private readonly userTokenService: UserTokenService,
|
2022-03-09 18:37:04 +00:00
|
|
|
private readonly formUtil: FormUtilityService,
|
|
|
|
private readonly token: TokenService,
|
2022-09-09 17:12:22 +00:00
|
|
|
private readonly audit: AuditService,
|
2022-03-09 18:37:04 +00:00
|
|
|
) {}
|
|
|
|
|
|
|
|
@Get()
|
2022-03-09 21:10:08 +00:00
|
|
|
@Render('login/login')
|
2022-03-20 17:05:21 +00:00
|
|
|
public loginView(
|
|
|
|
@Req() req: Request,
|
|
|
|
@Query('redirectTo') redirectTo?: string,
|
2022-09-18 07:22:57 +00:00
|
|
|
): Record<string, unknown> {
|
2022-03-20 17:05:21 +00:00
|
|
|
return this.formUtil.populateTemplate(req, {
|
|
|
|
query: redirectTo
|
|
|
|
? new URLSearchParams({ redirectTo }).toString()
|
|
|
|
: undefined,
|
|
|
|
});
|
2022-03-09 18:37:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Post()
|
|
|
|
public async loginRequest(
|
|
|
|
@Req() req: Request,
|
|
|
|
@Res() res: Response,
|
2022-08-18 09:34:05 +00:00
|
|
|
@Body() body: { username: string; password: string; remember: boolean },
|
2022-03-20 17:05:21 +00:00
|
|
|
@Query('redirectTo') redirectTo?: string,
|
2022-03-09 18:37:04 +00:00
|
|
|
) {
|
2022-08-18 09:34:05 +00:00
|
|
|
const { username, password, remember } = this.formUtil.trimmed(body, [
|
|
|
|
'username',
|
|
|
|
]);
|
2022-03-09 18:37:04 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-09-09 17:12:22 +00:00
|
|
|
await this.audit.auditRequest(req, AuditAction.LOGIN, req.session.id, user);
|
|
|
|
|
2022-03-10 18:31:05 +00:00
|
|
|
if (await this.totpService.userHasTOTP(user)) {
|
2022-08-18 09:34:05 +00:00
|
|
|
const challenge = { type: 'verify', user: user.uuid, remember };
|
2022-08-27 08:59:26 +00:00
|
|
|
const encrypted = await this.token.encryptChallenge(challenge);
|
2022-03-09 18:37:04 +00:00
|
|
|
res.redirect(
|
2022-08-27 08:59:26 +00:00
|
|
|
`login/verify?challenge=${encrypted}${
|
2022-09-10 09:59:04 +00:00
|
|
|
redirectTo ? '&redirectTo=' + encodeURIComponent(redirectTo) : ''
|
2022-08-27 08:59:26 +00:00
|
|
|
}`,
|
2022-03-09 18:37:04 +00:00
|
|
|
);
|
2022-03-09 20:03:29 +00:00
|
|
|
return;
|
2022-03-09 18:37:04 +00:00
|
|
|
}
|
|
|
|
|
2022-08-18 09:34:05 +00:00
|
|
|
// Extend session cookie to a month
|
|
|
|
if (remember) {
|
|
|
|
const month = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
req.session.cookie.maxAge = month;
|
|
|
|
req.session.cookie.expires = new Date(Date.now() + month);
|
|
|
|
}
|
|
|
|
|
2022-03-10 18:31:05 +00:00
|
|
|
req.session.user = user.uuid;
|
2022-03-20 17:05:21 +00:00
|
|
|
res.redirect(redirectTo ? decodeURIComponent(redirectTo) : '/');
|
2022-03-09 18:37:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Get('verify')
|
|
|
|
public verifyUserTokenView(
|
|
|
|
@Req() req: Request,
|
|
|
|
@Res() res: Response,
|
2022-08-27 08:59:26 +00:00
|
|
|
@Query() query: { redirectTo: string; challenge: string },
|
2022-03-09 18:37:04 +00:00
|
|
|
) {
|
2022-08-27 08:59:26 +00:00
|
|
|
if (!query.challenge) {
|
2022-03-09 18:37:04 +00:00
|
|
|
req.flash('message', {
|
|
|
|
error: true,
|
|
|
|
text: 'An unexpected error occured, please log in again.',
|
|
|
|
});
|
|
|
|
|
2022-08-27 08:59:26 +00:00
|
|
|
res.redirect(
|
2022-09-10 09:59:04 +00:00
|
|
|
'/login' +
|
|
|
|
(query.redirectTo
|
|
|
|
? '?redirectTo=' + encodeURIComponent(query.redirectTo)
|
|
|
|
: ''),
|
2022-08-27 08:59:26 +00:00
|
|
|
);
|
2022-03-09 18:37:04 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-20 18:27:09 +00:00
|
|
|
res.render('login/totp-verify', this.formUtil.populateTemplate(req));
|
2022-03-09 18:37:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Post('verify')
|
|
|
|
public async verifyUserToken(
|
|
|
|
@Session() session: SessionData,
|
|
|
|
@Body() body: { totp: string },
|
|
|
|
@Req() req: Request,
|
|
|
|
@Res() res: Response,
|
2022-08-27 08:59:26 +00:00
|
|
|
@Query() query: { redirectTo: string; challenge: string },
|
2022-03-09 18:37:04 +00:00
|
|
|
) {
|
|
|
|
let user: User;
|
2022-08-18 09:34:05 +00:00
|
|
|
let remember = false;
|
2022-03-09 18:37:04 +00:00
|
|
|
|
|
|
|
try {
|
2022-08-27 08:59:26 +00:00
|
|
|
if (!query.challenge) {
|
2022-03-09 18:37:04 +00:00
|
|
|
throw new Error('No challenge');
|
|
|
|
}
|
|
|
|
|
2022-09-18 07:22:57 +00:00
|
|
|
const decrypted = await this.token.decryptChallenge<{
|
|
|
|
type: 'verify';
|
|
|
|
user: string;
|
|
|
|
remember: boolean;
|
|
|
|
}>(query.challenge);
|
2022-08-27 08:59:26 +00:00
|
|
|
if (!decrypted || decrypted.type !== 'verify' || !decrypted.user) {
|
2022-03-09 18:37:04 +00:00
|
|
|
throw new Error('Bad challenge');
|
|
|
|
}
|
|
|
|
|
2022-08-27 08:59:26 +00:00
|
|
|
user = await this.userService.getByUUID(decrypted.user);
|
2022-03-09 18:37:04 +00:00
|
|
|
if (!user) {
|
|
|
|
throw new Error('Bad challenge');
|
|
|
|
}
|
2022-08-18 09:34:05 +00:00
|
|
|
|
2022-08-27 08:59:26 +00:00
|
|
|
remember = decrypted.remember;
|
2022-09-18 07:22:57 +00:00
|
|
|
} catch (e: unknown) {
|
2022-03-09 18:37:04 +00:00
|
|
|
req.flash('message', {
|
|
|
|
error: true,
|
|
|
|
text: 'An unexpected error occured, please log in again.',
|
|
|
|
});
|
|
|
|
|
2022-08-27 08:59:26 +00:00
|
|
|
res.redirect(
|
2022-09-10 09:59:04 +00:00
|
|
|
'/login' +
|
|
|
|
(query.redirectTo
|
|
|
|
? '?redirectTo=' + encodeURIComponent(query.redirectTo)
|
|
|
|
: ''),
|
2022-08-27 08:59:26 +00:00
|
|
|
);
|
2022-03-09 18:37:04 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2022-03-10 18:31:05 +00:00
|
|
|
const totp = await this.totpService.getUserTOTP(user);
|
2022-03-09 18:37:04 +00:00
|
|
|
|
2022-03-10 18:31:05 +00:00
|
|
|
if (!this.totpService.validateTOTP(totp.token, body.totp)) {
|
2022-03-09 18:37:04 +00:00
|
|
|
throw new Error('Invalid code!');
|
|
|
|
}
|
2022-09-18 07:22:57 +00:00
|
|
|
} catch (e: unknown) {
|
2022-03-09 18:37:04 +00:00
|
|
|
req.flash('message', {
|
|
|
|
error: true,
|
2022-09-18 07:22:57 +00:00
|
|
|
text: (e as Error).message,
|
2022-03-09 18:37:04 +00:00
|
|
|
});
|
|
|
|
res.redirect(req.originalUrl);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-18 09:34:05 +00:00
|
|
|
// Extend session cookie to a month
|
|
|
|
if (remember) {
|
|
|
|
const month = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
req.session.cookie.maxAge = month;
|
|
|
|
}
|
|
|
|
|
2022-03-10 18:31:05 +00:00
|
|
|
session.user = user.uuid;
|
2022-08-27 08:59:26 +00:00
|
|
|
res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/');
|
2022-03-09 18:37:04 +00:00
|
|
|
}
|
2022-03-10 18:31:05 +00:00
|
|
|
|
|
|
|
@Get('activate')
|
|
|
|
public async activateUser(
|
|
|
|
@Req() req: Request,
|
|
|
|
@Res() res: Response,
|
|
|
|
@Query() query: { token: string },
|
2022-03-20 17:05:21 +00:00
|
|
|
@Query('redirectTo') redirectTo?: string,
|
2022-03-10 18:31:05 +00:00
|
|
|
) {
|
2022-03-20 17:05:21 +00:00
|
|
|
const loginPath = `/login${
|
|
|
|
redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''
|
|
|
|
}`;
|
2022-03-10 18:31:05 +00:00
|
|
|
let user: User;
|
|
|
|
let token: UserToken;
|
2022-03-20 17:05:21 +00:00
|
|
|
|
2022-03-10 18:31:05 +00:00
|
|
|
try {
|
|
|
|
if (!query || !query.token) {
|
|
|
|
throw new Error();
|
|
|
|
}
|
|
|
|
|
2022-03-15 17:00:15 +00:00
|
|
|
token = await this.userTokenService.get(
|
2022-03-10 18:31:05 +00:00
|
|
|
query.token,
|
|
|
|
UserTokenType.ACTIVATION,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
throw new Error();
|
|
|
|
}
|
|
|
|
|
|
|
|
user = token.user;
|
2022-09-18 07:22:57 +00:00
|
|
|
} catch (e: unknown) {
|
2022-03-10 18:31:05 +00:00
|
|
|
req.flash('message', {
|
|
|
|
error: true,
|
|
|
|
text: 'Invalid or expired activation link.',
|
|
|
|
});
|
|
|
|
|
2022-03-20 17:05:21 +00:00
|
|
|
res.redirect(loginPath);
|
2022-03-10 18:31:05 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
user.activated = true;
|
|
|
|
await this.userService.updateUser(user);
|
2022-03-15 17:00:15 +00:00
|
|
|
await this.userTokenService.delete(token);
|
2022-03-10 18:31:05 +00:00
|
|
|
|
|
|
|
req.flash('message', {
|
|
|
|
error: false,
|
|
|
|
text: 'Account has been activated successfully. You may now log in.',
|
|
|
|
});
|
|
|
|
|
2022-03-20 17:05:21 +00:00
|
|
|
res.redirect(loginPath);
|
2022-03-10 18:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Get('password')
|
|
|
|
public async recoverView(
|
|
|
|
@Req() req: Request,
|
|
|
|
@Res() res: Response,
|
|
|
|
@Query() query: { token: string },
|
|
|
|
) {
|
|
|
|
if (query.token) {
|
2022-03-15 17:00:15 +00:00
|
|
|
const token = await this.userTokenService.get(
|
2022-03-10 18:31:05 +00:00
|
|
|
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', {
|
2022-03-20 14:50:12 +00:00
|
|
|
...this.formUtil.populateTemplate(req),
|
2022-03-10 18:31:05 +00:00
|
|
|
token: true,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
res.render('login/password', {
|
2022-03-20 14:50:12 +00:00
|
|
|
...this.formUtil.populateTemplate(req),
|
2022-03-10 18:31:05 +00:00
|
|
|
token: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@Post('password')
|
2022-03-20 12:09:36 +00:00
|
|
|
@Throttle(3, 60)
|
2022-03-10 18:31:05 +00:00
|
|
|
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
|
2022-03-15 17:00:15 +00:00
|
|
|
const token = await this.userTokenService.get(
|
2022-03-10 18:31:05 +00:00
|
|
|
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);
|
2022-03-15 17:00:15 +00:00
|
|
|
await this.userTokenService.delete(token);
|
2022-09-09 17:12:22 +00:00
|
|
|
await this.audit.auditRequest(req, AuditAction.PASSWORD_CHANGE, 'token');
|
2022-03-10 18:31:05 +00:00
|
|
|
|
|
|
|
req.flash('message', {
|
|
|
|
error: false,
|
|
|
|
text: 'Your password has been reset successfully. You may now log in with your new password!',
|
|
|
|
});
|
|
|
|
|
|
|
|
res.redirect('/login');
|
2022-09-18 07:22:57 +00:00
|
|
|
} catch (e: unknown) {
|
|
|
|
req.flash('message', { error: true, text: (e as Error).message });
|
2022-03-10 18:31:05 +00:00
|
|
|
res.redirect(req.originalUrl);
|
|
|
|
}
|
|
|
|
}
|
2022-03-09 18:37:04 +00:00
|
|
|
}
|