config service, separate user token into module
This commit is contained in:
parent
5967e0da24
commit
5fa307b5b7
1
.gitignore
vendored
1
.gitignore
vendored
@ -37,6 +37,7 @@ lerna-debug.log*
|
|||||||
# local development environment files
|
# local development environment files
|
||||||
.env
|
.env
|
||||||
/devdocker
|
/devdocker
|
||||||
|
/config*.toml
|
||||||
|
|
||||||
# front-end items
|
# front-end items
|
||||||
/public/js
|
/public/js
|
||||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -26,6 +26,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
|
"toml": "^3.0.0",
|
||||||
"typeorm": "^0.2.45",
|
"typeorm": "^0.2.45",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
@ -9016,6 +9017,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
||||||
"integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ="
|
"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": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
||||||
"integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ="
|
"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": {
|
"tough-cookie": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
|
"toml": "^3.0.0",
|
||||||
"typeorm": "^0.2.45",
|
"typeorm": "^0.2.45",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
|
@ -3,6 +3,7 @@ import { AppController } from './app.controller';
|
|||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { CSRFMiddleware } from './middleware/csrf.middleware';
|
import { CSRFMiddleware } from './middleware/csrf.middleware';
|
||||||
import { UserMiddleware } from './middleware/user.middleware';
|
import { UserMiddleware } from './middleware/user.middleware';
|
||||||
|
import { ConfigurationModule } from './modules/config/config.module';
|
||||||
import { LoginModule } from './modules/features/login/login.module';
|
import { LoginModule } from './modules/features/login/login.module';
|
||||||
import { OAuth2Module } from './modules/features/oauth2/oauth2.module';
|
import { OAuth2Module } from './modules/features/oauth2/oauth2.module';
|
||||||
import { RegisterModule } from './modules/features/register/register.module';
|
import { RegisterModule } from './modules/features/register/register.module';
|
||||||
@ -17,6 +18,7 @@ import { UtilityModule } from './modules/utility/utility.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
UtilityModule,
|
UtilityModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
EmailModule,
|
EmailModule,
|
||||||
|
25
src/modules/config/config.interfaces.ts
Normal file
25
src/modules/config/config.interfaces.ts
Normal file
@ -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;
|
||||||
|
}
|
10
src/modules/config/config.module.ts
Normal file
10
src/modules/config/config.module.ts
Normal file
@ -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 {}
|
55
src/modules/config/config.providers.ts
Normal file
55
src/modules/config/config.providers.ts
Normal file
@ -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<Configuration> => {
|
||||||
|
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'],
|
||||||
|
},
|
||||||
|
];
|
18
src/modules/config/config.service.ts
Normal file
18
src/modules/config/config.service.ts
Normal file
@ -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<T>(key: string): T {
|
||||||
|
return key
|
||||||
|
.replace(/\[|\]\.?/g, '.')
|
||||||
|
.split('.')
|
||||||
|
.filter((s) => s)
|
||||||
|
.reduce((acc, val) => acc && acc[val], this.config);
|
||||||
|
}
|
||||||
|
}
|
@ -14,8 +14,9 @@ import { SessionData } from 'express-session';
|
|||||||
import {
|
import {
|
||||||
UserToken,
|
UserToken,
|
||||||
UserTokenType,
|
UserTokenType,
|
||||||
} from 'src/modules/objects/user/user-token.entity';
|
} from 'src/modules/objects/user-token/user-token.entity';
|
||||||
import { UserTOTPService } from 'src/modules/objects/user/user-totp-token.service';
|
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 { User } from 'src/modules/objects/user/user.entity';
|
||||||
import { UserService } from 'src/modules/objects/user/user.service';
|
import { UserService } from 'src/modules/objects/user/user.service';
|
||||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||||
@ -26,6 +27,7 @@ export class LoginController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly totpService: UserTOTPService,
|
private readonly totpService: UserTOTPService,
|
||||||
|
private readonly userTokenService: UserTokenService,
|
||||||
private readonly formUtil: FormUtilityService,
|
private readonly formUtil: FormUtilityService,
|
||||||
private readonly token: TokenService,
|
private readonly token: TokenService,
|
||||||
) {}
|
) {}
|
||||||
@ -173,7 +175,7 @@ export class LoginController {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
token = await this.userService.getUserToken(
|
token = await this.userTokenService.get(
|
||||||
query.token,
|
query.token,
|
||||||
UserTokenType.ACTIVATION,
|
UserTokenType.ACTIVATION,
|
||||||
);
|
);
|
||||||
@ -195,7 +197,7 @@ export class LoginController {
|
|||||||
|
|
||||||
user.activated = true;
|
user.activated = true;
|
||||||
await this.userService.updateUser(user);
|
await this.userService.updateUser(user);
|
||||||
await this.userService.deleteUserToken(token);
|
await this.userTokenService.delete(token);
|
||||||
|
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: false,
|
error: false,
|
||||||
@ -213,7 +215,7 @@ export class LoginController {
|
|||||||
@Query() query: { token: string },
|
@Query() query: { token: string },
|
||||||
) {
|
) {
|
||||||
if (query.token) {
|
if (query.token) {
|
||||||
const token = await this.userService.getUserToken(
|
const token = await this.userTokenService.get(
|
||||||
query.token,
|
query.token,
|
||||||
UserTokenType.PASSWORD,
|
UserTokenType.PASSWORD,
|
||||||
);
|
);
|
||||||
@ -273,7 +275,7 @@ export class LoginController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Change password fragment
|
// Change password fragment
|
||||||
const token = await this.userService.getUserToken(
|
const token = await this.userTokenService.get(
|
||||||
query.token,
|
query.token,
|
||||||
UserTokenType.PASSWORD,
|
UserTokenType.PASSWORD,
|
||||||
);
|
);
|
||||||
@ -309,7 +311,7 @@ export class LoginController {
|
|||||||
token.user.password = hashword;
|
token.user.password = hashword;
|
||||||
|
|
||||||
await this.userService.updateUser(token.user);
|
await this.userService.updateUser(token.user);
|
||||||
await this.userService.deleteUserToken(token);
|
await this.userTokenService.delete(token);
|
||||||
|
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -6,11 +6,12 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
||||||
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.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 { UserModule } from 'src/modules/objects/user/user.module';
|
||||||
import { LoginController } from './login.controller';
|
import { LoginController } from './login.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule],
|
imports: [UserModule, UserTokenModule],
|
||||||
controllers: [LoginController],
|
controllers: [LoginController],
|
||||||
})
|
})
|
||||||
export class LoginModule implements NestModule {
|
export class LoginModule implements NestModule {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Body, Controller, Get, Post, Req, Res, Session } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Req, Res, Session } from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { SessionData } from 'express-session';
|
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 { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||||
import { QRCodeService } from 'src/modules/utility/services/qr-code.service';
|
import { QRCodeService } from 'src/modules/utility/services/qr-code.service';
|
||||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||||
@ -85,6 +85,7 @@ export class TwoFactorController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: show the recovery tokens to the user
|
||||||
await this.totp.activateTOTP(req.user, secret);
|
await this.totp.activateTOTP(req.user, secret);
|
||||||
session.challenge = null;
|
session.challenge = null;
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||||
import { AuthMiddleware } from 'src/middleware/auth.middleware';
|
import { AuthMiddleware } from 'src/middleware/auth.middleware';
|
||||||
import { FlashMiddleware } from 'src/middleware/flash.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 { UserModule } from 'src/modules/objects/user/user.module';
|
||||||
import { TwoFactorController } from './two-factor.controller';
|
import { TwoFactorController } from './two-factor.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule],
|
imports: [UserModule, UserTokenModule],
|
||||||
controllers: [TwoFactorController],
|
controllers: [TwoFactorController],
|
||||||
})
|
})
|
||||||
export class TwoFactorModule implements NestModule {
|
export class TwoFactorModule implements NestModule {
|
||||||
|
@ -13,6 +13,7 @@ export const databaseProviders = [
|
|||||||
database: 'icyauth',
|
database: 'icyauth',
|
||||||
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
|
logging: ['query', 'error'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
|
import { SMTPConfiguration } from 'src/modules/config/config.interfaces';
|
||||||
|
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||||
|
|
||||||
export const emailProviders = [
|
export const emailProviders = [
|
||||||
{
|
{
|
||||||
provide: 'EMAIL_TRANSPORT',
|
provide: 'EMAIL_TRANSPORT',
|
||||||
useFactory: async () =>
|
useFactory: async (config: ConfigurationService) =>
|
||||||
nodemailer.createTransport({
|
nodemailer.createTransport(config.get<SMTPConfiguration>('email.smtp')),
|
||||||
host: process.env.SMTP_HOST,
|
inject: [ConfigurationService],
|
||||||
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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from './user.entity';
|
import { User } from '../user/user.entity';
|
||||||
|
|
||||||
export enum UserTokenType {
|
export enum UserTokenType {
|
||||||
GENERIC = 'generic',
|
GENERIC = 'generic',
|
||||||
@ -14,6 +14,8 @@ export enum UserTokenType {
|
|||||||
PASSWORD = 'password',
|
PASSWORD = 'password',
|
||||||
LOGIN = 'login',
|
LOGIN = 'login',
|
||||||
GDPR = 'gdpr',
|
GDPR = 'gdpr',
|
||||||
|
TOTP = 'totp',
|
||||||
|
RECOVERY = 'recovery',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
12
src/modules/objects/user-token/user-token.module.ts
Normal file
12
src/modules/objects/user-token/user-token.module.ts
Normal file
@ -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 {}
|
10
src/modules/objects/user-token/user-token.providers.ts
Normal file
10
src/modules/objects/user-token/user-token.providers.ts
Normal file
@ -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'],
|
||||||
|
},
|
||||||
|
];
|
53
src/modules/objects/user-token/user-token.service.ts
Normal file
53
src/modules/objects/user-token/user-token.service.ts
Normal file
@ -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<UserToken>,
|
||||||
|
private token: TokenService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async create(
|
||||||
|
user: User,
|
||||||
|
type: UserTokenType,
|
||||||
|
expiresAt?: Date,
|
||||||
|
): Promise<UserToken> {
|
||||||
|
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<UserToken> {
|
||||||
|
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<void> {
|
||||||
|
await this.userTokenRepository.remove(token);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { UserTOTPToken } from './user-totp-token.entity';
|
import { User } from '../user/user.entity';
|
||||||
import { User } from './user.entity';
|
|
||||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||||
import { authenticator as totp } from 'otplib';
|
import { authenticator as totp } from 'otplib';
|
||||||
|
import { UserToken, UserTokenType } from './user-token.entity';
|
||||||
|
|
||||||
totp.options = {
|
totp.options = {
|
||||||
window: 2,
|
window: 2,
|
||||||
@ -12,8 +12,8 @@ totp.options = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserTOTPService {
|
export class UserTOTPService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('USER_TOTP_TOKEN_REPOSITORY')
|
@Inject('USER_TOKEN_REPOSITORY')
|
||||||
private userTOTPTokenRepository: Repository<UserTOTPToken>,
|
private userTokenRepository: Repository<UserToken>,
|
||||||
private token: TokenService,
|
private token: TokenService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -31,10 +31,10 @@ export class UserTOTPService {
|
|||||||
* @param user User object
|
* @param user User object
|
||||||
* @returns TOTP token
|
* @returns TOTP token
|
||||||
*/
|
*/
|
||||||
public async getUserTOTP(user: User): Promise<UserTOTPToken> {
|
public async getUserTOTP(user: User): Promise<UserToken> {
|
||||||
return this.userTOTPTokenRepository.findOne({
|
return this.userTokenRepository.findOne({
|
||||||
user,
|
user,
|
||||||
activated: true,
|
type: UserTokenType.TOTP,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,20 +50,23 @@ export class UserTOTPService {
|
|||||||
return totp.generateSecret();
|
return totp.generateSecret();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async activateTOTP(
|
public async activateTOTP(user: User, secret: string): Promise<UserToken[]> {
|
||||||
user: User,
|
const totp = new UserToken();
|
||||||
secret: string,
|
const recovery = new UserToken();
|
||||||
): Promise<UserTOTPToken> {
|
|
||||||
const totp = new UserTOTPToken();
|
|
||||||
totp.activated = true;
|
|
||||||
totp.user = user;
|
totp.user = user;
|
||||||
totp.token = secret;
|
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),
|
this.token.generateString(8),
|
||||||
).join(' ');
|
).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];
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
|
||||||
}
|
|
@ -1,13 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DatabaseModule } from '../database/database.module';
|
import { DatabaseModule } from '../database/database.module';
|
||||||
import { EmailModule } from '../email/email.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 { userProviders } from './user.providers';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule, EmailModule],
|
imports: [DatabaseModule, EmailModule, UserTokenModule],
|
||||||
providers: [...userProviders, UserService, UserTOTPService],
|
providers: [...userProviders, UserService],
|
||||||
exports: [UserService, UserTOTPService],
|
exports: [UserService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { Connection } from 'typeorm';
|
import { Connection } from 'typeorm';
|
||||||
import { UserToken } from './user-token.entity';
|
|
||||||
import { UserTOTPToken } from './user-totp-token.entity';
|
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
|
||||||
export const userProviders = [
|
export const userProviders = [
|
||||||
@ -9,15 +7,4 @@ export const userProviders = [
|
|||||||
useFactory: (connection: Connection) => connection.getRepository(User),
|
useFactory: (connection: Connection) => connection.getRepository(User),
|
||||||
inject: ['DATABASE_CONNECTION'],
|
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'],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Repository } from 'typeorm';
|
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 { User } from './user.entity';
|
||||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { EmailService } from '../email/email.service';
|
import { EmailService } from '../email/email.service';
|
||||||
import { RegistrationEmail } from './email/registration.email';
|
import { RegistrationEmail } from './email/registration.email';
|
||||||
import { ForgotPasswordEmail } from './email/forgot-password.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()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('USER_REPOSITORY')
|
@Inject('USER_REPOSITORY')
|
||||||
private userRepository: Repository<User>,
|
private userRepository: Repository<User>,
|
||||||
@Inject('USER_TOKEN_REPOSITORY')
|
private userToken: UserTokenService,
|
||||||
private userTokenRepository: Repository<UserToken>,
|
|
||||||
private token: TokenService,
|
private token: TokenService,
|
||||||
private email: EmailService,
|
private email: EmailService,
|
||||||
|
private config: ConfigurationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getById(id: number): Promise<User> {
|
public async getById(id: number): Promise<User> {
|
||||||
@ -68,50 +70,8 @@ export class UserService {
|
|||||||
return bcrypt.hash(password, salt);
|
return bcrypt.hash(password, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createUserToken(
|
|
||||||
user: User,
|
|
||||||
type: UserTokenType,
|
|
||||||
expiresAt?: Date,
|
|
||||||
): Promise<UserToken> {
|
|
||||||
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<UserToken> {
|
|
||||||
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<void> {
|
|
||||||
await this.userTokenRepository.remove(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendActivationEmail(user: User): Promise<void> {
|
public async sendActivationEmail(user: User): Promise<void> {
|
||||||
const activationToken = await this.createUserToken(
|
const activationToken = await this.userToken.create(
|
||||||
user,
|
user,
|
||||||
UserTokenType.ACTIVATION,
|
UserTokenType.ACTIVATION,
|
||||||
new Date(Date.now() + 3600 * 1000),
|
new Date(Date.now() + 3600 * 1000),
|
||||||
@ -120,7 +80,9 @@ export class UserService {
|
|||||||
try {
|
try {
|
||||||
const content = RegistrationEmail(
|
const content = RegistrationEmail(
|
||||||
user.username,
|
user.username,
|
||||||
`${process.env.BASE_URL}/login/activate?token=${activationToken.token}`,
|
`${this.config.get<string>('app.base_url')}/login/activate?token=${
|
||||||
|
activationToken.token
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
await this.email.sendEmailTemplate(
|
await this.email.sendEmailTemplate(
|
||||||
user.email,
|
user.email,
|
||||||
@ -128,13 +90,13 @@ export class UserService {
|
|||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.userTokenRepository.remove(activationToken);
|
await this.userToken.delete(activationToken);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendPasswordEmail(user: User): Promise<void> {
|
public async sendPasswordEmail(user: User): Promise<void> {
|
||||||
const passwordToken = await this.createUserToken(
|
const passwordToken = await this.userToken.create(
|
||||||
user,
|
user,
|
||||||
UserTokenType.PASSWORD,
|
UserTokenType.PASSWORD,
|
||||||
new Date(Date.now() + 3600 * 1000),
|
new Date(Date.now() + 3600 * 1000),
|
||||||
@ -143,7 +105,9 @@ export class UserService {
|
|||||||
try {
|
try {
|
||||||
const content = ForgotPasswordEmail(
|
const content = ForgotPasswordEmail(
|
||||||
user.username,
|
user.username,
|
||||||
`${process.env.BASE_URL}/login/password?token=${passwordToken.token}`,
|
`${this.config.get<string>('app.base_url')}/login/password?token=${
|
||||||
|
passwordToken.token
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
await this.email.sendEmailTemplate(
|
await this.email.sendEmailTemplate(
|
||||||
user.email,
|
user.email,
|
||||||
@ -151,7 +115,7 @@ export class UserService {
|
|||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.userTokenRepository.remove(passwordToken);
|
await this.userToken.delete(passwordToken);
|
||||||
// silently fail
|
// silently fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { SessionData } from 'express-session';
|
|||||||
export class FormUtilityService {
|
export class FormUtilityService {
|
||||||
public emailRegex =
|
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])+$/;
|
/^[-!#$%&'*+\/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 usernameRegex = /^[a-zA-Z0-9_\-.]{3,26}$/;
|
||||||
|
|
||||||
public mergeObjectArray(flash: Record<string, any>[]): Record<string, any> {
|
public mergeObjectArray(flash: Record<string, any>[]): Record<string, any> {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
const IV_LENGTH = 16;
|
const IV_LENGTH = 16;
|
||||||
@ -7,6 +8,8 @@ const ALGORITHM = 'aes-256-cbc';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenService {
|
export class TokenService {
|
||||||
|
constructor(private config: ConfigurationService) {}
|
||||||
|
|
||||||
public generateString(length: number): string {
|
public generateString(length: number): string {
|
||||||
return crypto.randomBytes(length).toString('hex').slice(0, length);
|
return crypto.randomBytes(length).toString('hex').slice(0, length);
|
||||||
}
|
}
|
||||||
@ -61,13 +64,15 @@ export class TokenService {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return this.encrypt(
|
return this.encrypt(
|
||||||
JSON.stringify(challenge),
|
JSON.stringify(challenge),
|
||||||
process.env.CHALLENGE_SECRET,
|
this.config.get<string>('app.challenge_secret'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async decryptChallenge(
|
public async decryptChallenge(
|
||||||
challenge: string,
|
challenge: string,
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
return JSON.parse(this.decrypt(challenge, process.env.CHALLENGE_SECRET));
|
return JSON.parse(
|
||||||
|
this.decrypt(challenge, this.config.get<string>('app.challenge_secret')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ block body
|
|||||||
div.qr-preview
|
div.qr-preview
|
||||||
img(src=qrcode)
|
img(src=qrcode)
|
||||||
|
|
||||||
form(method="post")
|
form(method="post",autocomplete="off")
|
||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="csrf", value=csrf)
|
input#csrf(type="hidden", name="csrf", value=csrf)
|
||||||
label.form-label(for="code") Code from authenticator app
|
label.form-label(for="code") Code from authenticator app
|
||||||
|
Reference in New Issue
Block a user