From 5fa307b5b775acebb30e184008ae04b2215ea5fa Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Tue, 15 Mar 2022 19:00:15 +0200 Subject: [PATCH] config service, separate user token into module --- .gitignore | 1 + package-lock.json | 11 ++++ package.json | 1 + src/app.module.ts | 2 + src/modules/config/config.interfaces.ts | 25 +++++++ src/modules/config/config.module.ts | 10 +++ src/modules/config/config.providers.ts | 55 ++++++++++++++++ src/modules/config/config.service.ts | 18 +++++ .../features/login/login.controller.ts | 16 +++-- src/modules/features/login/login.module.ts | 3 +- .../two-factor/two-factor.controller.ts | 3 +- .../features/two-factor/two-factor.module.ts | 3 +- .../objects/database/database.providers.ts | 1 + src/modules/objects/email/email.providers.ts | 15 ++--- .../{user => user-token}/user-token.entity.ts | 4 +- .../objects/user-token/user-token.module.ts | 12 ++++ .../user-token/user-token.providers.ts | 10 +++ .../objects/user-token/user-token.service.ts | 53 +++++++++++++++ .../user-totp-token.service.ts | 35 +++++----- .../objects/user/user-totp-token.entity.ts | 35 ---------- src/modules/objects/user/user.module.ts | 8 +-- src/modules/objects/user/user.providers.ts | 13 ---- src/modules/objects/user/user.service.ts | 66 +++++-------------- .../utility/services/form-utility.service.ts | 2 +- src/modules/utility/services/token.service.ts | 9 ++- views/two-factor/activate.pug | 2 +- 26 files changed, 269 insertions(+), 144 deletions(-) create mode 100644 src/modules/config/config.interfaces.ts create mode 100644 src/modules/config/config.module.ts create mode 100644 src/modules/config/config.providers.ts create mode 100644 src/modules/config/config.service.ts rename src/modules/objects/{user => user-token}/user-token.entity.ts (89%) create mode 100644 src/modules/objects/user-token/user-token.module.ts create mode 100644 src/modules/objects/user-token/user-token.providers.ts create mode 100644 src/modules/objects/user-token/user-token.service.ts rename src/modules/objects/{user => user-token}/user-totp-token.service.ts (59%) delete mode 100644 src/modules/objects/user/user-totp-token.entity.ts diff --git a/.gitignore b/.gitignore index ec00722..2e95380 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ lerna-debug.log* # local development environment files .env /devdocker +/config*.toml # front-end items /public/js diff --git a/package-lock.json b/package-lock.json index ec4feed..9e6d498 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", + "toml": "^3.0.0", "typeorm": "^0.2.45", "uuid": "^8.3.2" }, @@ -9016,6 +9017,11 @@ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "node_modules/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -17016,6 +17022,11 @@ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" }, + "toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", diff --git a/package.json b/package.json index 2e42af2..09b5556 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", + "toml": "^3.0.0", "typeorm": "^0.2.45", "uuid": "^8.3.2" }, diff --git a/src/app.module.ts b/src/app.module.ts index 9e93ced..89e1f90 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,7 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { CSRFMiddleware } from './middleware/csrf.middleware'; import { UserMiddleware } from './middleware/user.middleware'; +import { ConfigurationModule } from './modules/config/config.module'; import { LoginModule } from './modules/features/login/login.module'; import { OAuth2Module } from './modules/features/oauth2/oauth2.module'; import { RegisterModule } from './modules/features/register/register.module'; @@ -17,6 +18,7 @@ import { UtilityModule } from './modules/utility/utility.module'; @Module({ imports: [ + ConfigurationModule, UtilityModule, DatabaseModule, EmailModule, diff --git a/src/modules/config/config.interfaces.ts b/src/modules/config/config.interfaces.ts new file mode 100644 index 0000000..5c0f1ff --- /dev/null +++ b/src/modules/config/config.interfaces.ts @@ -0,0 +1,25 @@ +export interface SMTPConfiguration { + host: string; + port: number; + secure: boolean; + auth: { + user: string; + pass: string; + }; +} + +export interface EmailConfiguration { + from: string; + smtp: SMTPConfiguration; +} + +export interface AppConfiguration { + base_url: string; + session_secret: string; + challenge_secret: string; +} + +export interface Configuration { + app: AppConfiguration; + email: EmailConfiguration; +} diff --git a/src/modules/config/config.module.ts b/src/modules/config/config.module.ts new file mode 100644 index 0000000..f85aabe --- /dev/null +++ b/src/modules/config/config.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common'; +import { configProviders } from './config.providers'; +import { ConfigurationService } from './config.service'; + +@Global() +@Module({ + providers: [...configProviders, ConfigurationService], + exports: [ConfigurationService], +}) +export class ConfigurationModule {} diff --git a/src/modules/config/config.providers.ts b/src/modules/config/config.providers.ts new file mode 100644 index 0000000..663035f --- /dev/null +++ b/src/modules/config/config.providers.ts @@ -0,0 +1,55 @@ +import * as toml from 'toml'; +import { resolve } from 'path'; +import { readFile } from 'fs/promises'; +import { Configuration } from './config.interfaces'; + +const CONFIG_ENV = process.env.NODE_ENV === 'production' ? 'prod' : 'dev'; +const CONFIG_FILENAME = process.env.CONFIG || `config.${CONFIG_ENV}.toml`; + +export const configProviders = [ + { + provide: 'CONFIG_PATH', + useValue: resolve(__dirname, '..', '..', '..', CONFIG_FILENAME), + }, + { + provide: 'DEFAULT_CONFIG', + useValue: { + app: { + base_url: 'http://localhost:3000', + session_secret: 'change me!', + challenge_secret: 'change me!', + }, + email: { + from: 'no-reply@localhost', + smtp: { + host: 'localhost', + port: 587, + secure: false, + auth: { + user: 'root', + pass: 'root', + }, + }, + }, + } as Configuration, + }, + { + provide: 'CONFIGURATION', + useFactory: async ( + path: string, + def: Configuration, + ): Promise => { + try { + const file = await readFile(path, { encoding: 'utf-8' }); + return { + ...def, + ...toml.parse(file), + }; + } catch (e: any) { + console.error('Failed to load configuration:', e.message); + return def; + } + }, + inject: ['CONFIG_PATH', 'DEFAULT_CONFIG'], + }, +]; diff --git a/src/modules/config/config.service.ts b/src/modules/config/config.service.ts new file mode 100644 index 0000000..295156f --- /dev/null +++ b/src/modules/config/config.service.ts @@ -0,0 +1,18 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Configuration } from './config.interfaces'; + +@Injectable() +export class ConfigurationService { + constructor( + @Inject('CONFIGURATION') + public config: Configuration, + ) {} + + public get(key: string): T { + return key + .replace(/\[|\]\.?/g, '.') + .split('.') + .filter((s) => s) + .reduce((acc, val) => acc && acc[val], this.config); + } +} diff --git a/src/modules/features/login/login.controller.ts b/src/modules/features/login/login.controller.ts index 39da487..4465b20 100644 --- a/src/modules/features/login/login.controller.ts +++ b/src/modules/features/login/login.controller.ts @@ -14,8 +14,9 @@ import { SessionData } from 'express-session'; import { UserToken, UserTokenType, -} from 'src/modules/objects/user/user-token.entity'; -import { UserTOTPService } from 'src/modules/objects/user/user-totp-token.service'; +} 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'; @@ -26,6 +27,7 @@ export class LoginController { constructor( private readonly userService: UserService, private readonly totpService: UserTOTPService, + private readonly userTokenService: UserTokenService, private readonly formUtil: FormUtilityService, private readonly token: TokenService, ) {} @@ -173,7 +175,7 @@ export class LoginController { throw new Error(); } - token = await this.userService.getUserToken( + token = await this.userTokenService.get( query.token, UserTokenType.ACTIVATION, ); @@ -195,7 +197,7 @@ export class LoginController { user.activated = true; await this.userService.updateUser(user); - await this.userService.deleteUserToken(token); + await this.userTokenService.delete(token); req.flash('message', { error: false, @@ -213,7 +215,7 @@ export class LoginController { @Query() query: { token: string }, ) { if (query.token) { - const token = await this.userService.getUserToken( + const token = await this.userTokenService.get( query.token, UserTokenType.PASSWORD, ); @@ -273,7 +275,7 @@ export class LoginController { } // Change password fragment - const token = await this.userService.getUserToken( + const token = await this.userTokenService.get( query.token, UserTokenType.PASSWORD, ); @@ -309,7 +311,7 @@ export class LoginController { token.user.password = hashword; await this.userService.updateUser(token.user); - await this.userService.deleteUserToken(token); + await this.userTokenService.delete(token); req.flash('message', { error: false, diff --git a/src/modules/features/login/login.module.ts b/src/modules/features/login/login.module.ts index d51f393..617639c 100644 --- a/src/modules/features/login/login.module.ts +++ b/src/modules/features/login/login.module.ts @@ -6,11 +6,12 @@ import { } from '@nestjs/common'; import { FlashMiddleware } from 'src/middleware/flash.middleware'; import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware'; +import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module'; import { UserModule } from 'src/modules/objects/user/user.module'; import { LoginController } from './login.controller'; @Module({ - imports: [UserModule], + imports: [UserModule, UserTokenModule], controllers: [LoginController], }) export class LoginModule implements NestModule { diff --git a/src/modules/features/two-factor/two-factor.controller.ts b/src/modules/features/two-factor/two-factor.controller.ts index adb1796..c506d80 100644 --- a/src/modules/features/two-factor/two-factor.controller.ts +++ b/src/modules/features/two-factor/two-factor.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Get, Post, Req, Res, Session } from '@nestjs/common'; import { Request, Response } from 'express'; import { SessionData } from 'express-session'; -import { UserTOTPService } from 'src/modules/objects/user/user-totp-token.service'; +import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; import { QRCodeService } from 'src/modules/utility/services/qr-code.service'; import { TokenService } from 'src/modules/utility/services/token.service'; @@ -85,6 +85,7 @@ export class TwoFactorController { return; } + // TODO: show the recovery tokens to the user await this.totp.activateTOTP(req.user, secret); session.challenge = null; res.redirect('/'); diff --git a/src/modules/features/two-factor/two-factor.module.ts b/src/modules/features/two-factor/two-factor.module.ts index 7b8447f..3fe300a 100644 --- a/src/modules/features/two-factor/two-factor.module.ts +++ b/src/modules/features/two-factor/two-factor.module.ts @@ -1,11 +1,12 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { AuthMiddleware } from 'src/middleware/auth.middleware'; import { FlashMiddleware } from 'src/middleware/flash.middleware'; +import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module'; import { UserModule } from 'src/modules/objects/user/user.module'; import { TwoFactorController } from './two-factor.controller'; @Module({ - imports: [UserModule], + imports: [UserModule, UserTokenModule], controllers: [TwoFactorController], }) export class TwoFactorModule implements NestModule { diff --git a/src/modules/objects/database/database.providers.ts b/src/modules/objects/database/database.providers.ts index cbc47cc..2bfa279 100644 --- a/src/modules/objects/database/database.providers.ts +++ b/src/modules/objects/database/database.providers.ts @@ -13,6 +13,7 @@ export const databaseProviders = [ database: 'icyauth', entities: [__dirname + '/../**/*.entity{.ts,.js}'], synchronize: true, + logging: ['query', 'error'], }), }, ]; diff --git a/src/modules/objects/email/email.providers.ts b/src/modules/objects/email/email.providers.ts index 4d584d9..f5e3924 100644 --- a/src/modules/objects/email/email.providers.ts +++ b/src/modules/objects/email/email.providers.ts @@ -1,17 +1,12 @@ import * as nodemailer from 'nodemailer'; +import { SMTPConfiguration } from 'src/modules/config/config.interfaces'; +import { ConfigurationService } from 'src/modules/config/config.service'; export const emailProviders = [ { provide: 'EMAIL_TRANSPORT', - useFactory: async () => - nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT, 10) || 587, - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, - }), + useFactory: async (config: ConfigurationService) => + nodemailer.createTransport(config.get('email.smtp')), + inject: [ConfigurationService], }, ]; diff --git a/src/modules/objects/user/user-token.entity.ts b/src/modules/objects/user-token/user-token.entity.ts similarity index 89% rename from src/modules/objects/user/user-token.entity.ts rename to src/modules/objects/user-token/user-token.entity.ts index de94933..177ff2b 100644 --- a/src/modules/objects/user/user-token.entity.ts +++ b/src/modules/objects/user-token/user-token.entity.ts @@ -5,7 +5,7 @@ import { ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { User } from './user.entity'; +import { User } from '../user/user.entity'; export enum UserTokenType { GENERIC = 'generic', @@ -14,6 +14,8 @@ export enum UserTokenType { PASSWORD = 'password', LOGIN = 'login', GDPR = 'gdpr', + TOTP = 'totp', + RECOVERY = 'recovery', } @Entity() diff --git a/src/modules/objects/user-token/user-token.module.ts b/src/modules/objects/user-token/user-token.module.ts new file mode 100644 index 0000000..1212b7c --- /dev/null +++ b/src/modules/objects/user-token/user-token.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from '../database/database.module'; +import { userTokenProviders } from './user-token.providers'; +import { UserTokenService } from './user-token.service'; +import { UserTOTPService } from './user-totp-token.service'; + +@Module({ + imports: [DatabaseModule], + providers: [...userTokenProviders, UserTokenService, UserTOTPService], + exports: [UserTokenService, UserTOTPService], +}) +export class UserTokenModule {} diff --git a/src/modules/objects/user-token/user-token.providers.ts b/src/modules/objects/user-token/user-token.providers.ts new file mode 100644 index 0000000..a5794d7 --- /dev/null +++ b/src/modules/objects/user-token/user-token.providers.ts @@ -0,0 +1,10 @@ +import { Connection } from 'typeorm'; +import { UserToken } from './user-token.entity'; + +export const userTokenProviders = [ + { + provide: 'USER_TOKEN_REPOSITORY', + useFactory: (connection: Connection) => connection.getRepository(UserToken), + inject: ['DATABASE_CONNECTION'], + }, +]; diff --git a/src/modules/objects/user-token/user-token.service.ts b/src/modules/objects/user-token/user-token.service.ts new file mode 100644 index 0000000..bf86461 --- /dev/null +++ b/src/modules/objects/user-token/user-token.service.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { UserToken, UserTokenType } from '../user-token/user-token.entity'; +import { TokenService } from 'src/modules/utility/services/token.service'; +import { User } from '../user/user.entity'; + +@Injectable() +export class UserTokenService { + constructor( + @Inject('USER_TOKEN_REPOSITORY') + private userTokenRepository: Repository, + private token: TokenService, + ) {} + + public async create( + user: User, + type: UserTokenType, + expiresAt?: Date, + ): Promise { + const token = new UserToken(); + token.token = this.token.generateString(64); + token.user = user; + token.type = type; + token.expires_at = expiresAt; + await this.userTokenRepository.save(token); + return token; + } + + public async get(token: string, type: UserTokenType): Promise { + const foundOne = await this.userTokenRepository.findOne( + { + token, + type, + }, + { relations: ['user'] }, + ); + + if (!foundOne) { + return null; + } + + if (foundOne.expires_at < new Date()) { + await this.userTokenRepository.remove(foundOne); + return null; + } + + return foundOne; + } + + public async delete(token: UserToken): Promise { + await this.userTokenRepository.remove(token); + } +} diff --git a/src/modules/objects/user/user-totp-token.service.ts b/src/modules/objects/user-token/user-totp-token.service.ts similarity index 59% rename from src/modules/objects/user/user-totp-token.service.ts rename to src/modules/objects/user-token/user-totp-token.service.ts index f00bf74..bc7a37f 100644 --- a/src/modules/objects/user/user-totp-token.service.ts +++ b/src/modules/objects/user-token/user-totp-token.service.ts @@ -1,9 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; -import { UserTOTPToken } from './user-totp-token.entity'; -import { User } from './user.entity'; +import { User } from '../user/user.entity'; import { TokenService } from 'src/modules/utility/services/token.service'; import { authenticator as totp } from 'otplib'; +import { UserToken, UserTokenType } from './user-token.entity'; totp.options = { window: 2, @@ -12,8 +12,8 @@ totp.options = { @Injectable() export class UserTOTPService { constructor( - @Inject('USER_TOTP_TOKEN_REPOSITORY') - private userTOTPTokenRepository: Repository, + @Inject('USER_TOKEN_REPOSITORY') + private userTokenRepository: Repository, private token: TokenService, ) {} @@ -31,10 +31,10 @@ export class UserTOTPService { * @param user User object * @returns TOTP token */ - public async getUserTOTP(user: User): Promise { - return this.userTOTPTokenRepository.findOne({ + public async getUserTOTP(user: User): Promise { + return this.userTokenRepository.findOne({ user, - activated: true, + type: UserTokenType.TOTP, }); } @@ -50,20 +50,23 @@ export class UserTOTPService { return totp.generateSecret(); } - public async activateTOTP( - user: User, - secret: string, - ): Promise { - const totp = new UserTOTPToken(); - totp.activated = true; + public async activateTOTP(user: User, secret: string): Promise { + const totp = new UserToken(); + const recovery = new UserToken(); + totp.user = user; totp.token = secret; - totp.recovery_token = Array.from({ length: 8 }, () => + totp.type = UserTokenType.TOTP; + + recovery.user = user; + recovery.token = Array.from({ length: 8 }, () => this.token.generateString(8), ).join(' '); + recovery.type = UserTokenType.RECOVERY; - await this.userTOTPTokenRepository.save(totp); + await this.userTokenRepository.save(totp); + await this.userTokenRepository.save(recovery); - return totp; + return [totp, recovery]; } } diff --git a/src/modules/objects/user/user-totp-token.entity.ts b/src/modules/objects/user/user-totp-token.entity.ts deleted file mode 100644 index 84730d4..0000000 --- a/src/modules/objects/user/user-totp-token.entity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; -import { User } from './user.entity'; - -@Entity() -export class UserTOTPToken { - @PrimaryGeneratedColumn() - id: number; - - @Column({ nullable: false, type: 'text' }) - token: string; - - @Column({ nullable: false, type: 'text' }) - recovery_token: string; - - @Column({ default: false, nullable: false }) - activated: boolean; - - @Column({ type: 'timestamp', nullable: true }) - public expires_at: Date; - - @CreateDateColumn({ - type: 'timestamp', - default: () => 'CURRENT_TIMESTAMP(6)', - }) - public created_at: Date; - - @ManyToOne(() => User) - user: User; -} diff --git a/src/modules/objects/user/user.module.ts b/src/modules/objects/user/user.module.ts index 3ab0307..7807363 100644 --- a/src/modules/objects/user/user.module.ts +++ b/src/modules/objects/user/user.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { DatabaseModule } from '../database/database.module'; import { EmailModule } from '../email/email.module'; -import { UserTOTPService } from './user-totp-token.service'; +import { UserTokenModule } from '../user-token/user-token.module'; import { userProviders } from './user.providers'; import { UserService } from './user.service'; @Module({ - imports: [DatabaseModule, EmailModule], - providers: [...userProviders, UserService, UserTOTPService], - exports: [UserService, UserTOTPService], + imports: [DatabaseModule, EmailModule, UserTokenModule], + providers: [...userProviders, UserService], + exports: [UserService], }) export class UserModule {} diff --git a/src/modules/objects/user/user.providers.ts b/src/modules/objects/user/user.providers.ts index 9db0499..ae50841 100644 --- a/src/modules/objects/user/user.providers.ts +++ b/src/modules/objects/user/user.providers.ts @@ -1,6 +1,4 @@ import { Connection } from 'typeorm'; -import { UserToken } from './user-token.entity'; -import { UserTOTPToken } from './user-totp-token.entity'; import { User } from './user.entity'; export const userProviders = [ @@ -9,15 +7,4 @@ export const userProviders = [ useFactory: (connection: Connection) => connection.getRepository(User), inject: ['DATABASE_CONNECTION'], }, - { - provide: 'USER_TOKEN_REPOSITORY', - useFactory: (connection: Connection) => connection.getRepository(UserToken), - inject: ['DATABASE_CONNECTION'], - }, - { - provide: 'USER_TOTP_TOKEN_REPOSITORY', - useFactory: (connection: Connection) => - connection.getRepository(UserTOTPToken), - inject: ['DATABASE_CONNECTION'], - }, ]; diff --git a/src/modules/objects/user/user.service.ts b/src/modules/objects/user/user.service.ts index eb57dac..153f544 100644 --- a/src/modules/objects/user/user.service.ts +++ b/src/modules/objects/user/user.service.ts @@ -1,22 +1,24 @@ import { Inject, Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; -import { UserToken, UserTokenType } from './user-token.entity'; +import { UserTokenType } from '../user-token/user-token.entity'; import { User } from './user.entity'; import { TokenService } from 'src/modules/utility/services/token.service'; import * as bcrypt from 'bcrypt'; import { EmailService } from '../email/email.service'; import { RegistrationEmail } from './email/registration.email'; import { ForgotPasswordEmail } from './email/forgot-password.email'; +import { UserTokenService } from '../user-token/user-token.service'; +import { ConfigurationService } from 'src/modules/config/config.service'; @Injectable() export class UserService { constructor( @Inject('USER_REPOSITORY') private userRepository: Repository, - @Inject('USER_TOKEN_REPOSITORY') - private userTokenRepository: Repository, + private userToken: UserTokenService, private token: TokenService, private email: EmailService, + private config: ConfigurationService, ) {} public async getById(id: number): Promise { @@ -68,50 +70,8 @@ export class UserService { return bcrypt.hash(password, salt); } - public async createUserToken( - user: User, - type: UserTokenType, - expiresAt?: Date, - ): Promise { - const token = new UserToken(); - token.token = this.token.generateString(64); - token.user = user; - token.type = type; - token.expires_at = expiresAt; - await this.userTokenRepository.save(token); - return token; - } - - public async getUserToken( - token: string, - type: UserTokenType, - ): Promise { - const foundOne = await this.userTokenRepository.findOne( - { - token, - type, - }, - { relations: ['user'] }, - ); - - if (!foundOne) { - return null; - } - - if (foundOne.expires_at < new Date()) { - await this.userTokenRepository.remove(foundOne); - return null; - } - - return foundOne; - } - - public async deleteUserToken(token: UserToken): Promise { - await this.userTokenRepository.remove(token); - } - public async sendActivationEmail(user: User): Promise { - const activationToken = await this.createUserToken( + const activationToken = await this.userToken.create( user, UserTokenType.ACTIVATION, new Date(Date.now() + 3600 * 1000), @@ -120,7 +80,9 @@ export class UserService { try { const content = RegistrationEmail( user.username, - `${process.env.BASE_URL}/login/activate?token=${activationToken.token}`, + `${this.config.get('app.base_url')}/login/activate?token=${ + activationToken.token + }`, ); await this.email.sendEmailTemplate( user.email, @@ -128,13 +90,13 @@ export class UserService { content, ); } catch (e) { - await this.userTokenRepository.remove(activationToken); + await this.userToken.delete(activationToken); throw e; } } public async sendPasswordEmail(user: User): Promise { - const passwordToken = await this.createUserToken( + const passwordToken = await this.userToken.create( user, UserTokenType.PASSWORD, new Date(Date.now() + 3600 * 1000), @@ -143,7 +105,9 @@ export class UserService { try { const content = ForgotPasswordEmail( user.username, - `${process.env.BASE_URL}/login/password?token=${passwordToken.token}`, + `${this.config.get('app.base_url')}/login/password?token=${ + passwordToken.token + }`, ); await this.email.sendEmailTemplate( user.email, @@ -151,7 +115,7 @@ export class UserService { content, ); } catch (e) { - await this.userTokenRepository.remove(passwordToken); + await this.userToken.delete(passwordToken); // silently fail } } diff --git a/src/modules/utility/services/form-utility.service.ts b/src/modules/utility/services/form-utility.service.ts index 1140cda..b6c63e3 100644 --- a/src/modules/utility/services/form-utility.service.ts +++ b/src/modules/utility/services/form-utility.service.ts @@ -6,7 +6,7 @@ import { SessionData } from 'express-session'; export class FormUtilityService { public emailRegex = /^[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; - public passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/; + public passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d\w\W]{8,}$/; public usernameRegex = /^[a-zA-Z0-9_\-.]{3,26}$/; public mergeObjectArray(flash: Record[]): Record { diff --git a/src/modules/utility/services/token.service.ts b/src/modules/utility/services/token.service.ts index 5cd6a37..231ad15 100644 --- a/src/modules/utility/services/token.service.ts +++ b/src/modules/utility/services/token.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import * as crypto from 'crypto'; +import { ConfigurationService } from 'src/modules/config/config.service'; import { v4 } from 'uuid'; const IV_LENGTH = 16; @@ -7,6 +8,8 @@ const ALGORITHM = 'aes-256-cbc'; @Injectable() export class TokenService { + constructor(private config: ConfigurationService) {} + public generateString(length: number): string { return crypto.randomBytes(length).toString('hex').slice(0, length); } @@ -61,13 +64,15 @@ export class TokenService { ): Promise { return this.encrypt( JSON.stringify(challenge), - process.env.CHALLENGE_SECRET, + this.config.get('app.challenge_secret'), ); } public async decryptChallenge( challenge: string, ): Promise> { - return JSON.parse(this.decrypt(challenge, process.env.CHALLENGE_SECRET)); + return JSON.parse( + this.decrypt(challenge, this.config.get('app.challenge_secret')), + ); } } diff --git a/views/two-factor/activate.pug b/views/two-factor/activate.pug index 93563ec..41f6443 100644 --- a/views/two-factor/activate.pug +++ b/views/two-factor/activate.pug @@ -19,7 +19,7 @@ block body div.qr-preview img(src=qrcode) - form(method="post") + form(method="post",autocomplete="off") div.form-container input#csrf(type="hidden", name="csrf", value=csrf) label.form-label(for="code") Code from authenticator app