Compare commits
1 commit
main
...
feat/env-v
Author | SHA1 | Date | |
---|---|---|---|
17ed2c892d |
28 changed files with 1135 additions and 680 deletions
65
.eslintrc.js
65
.eslintrc.js
|
@ -17,9 +17,66 @@ module.exports = {
|
||||||
},
|
},
|
||||||
ignorePatterns: ['.eslintrc.js'],
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/interface-name-prefix': 'off',
|
"curly": "error",
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
"simple-import-sort/imports": "error",
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
"simple-import-sort/exports": "error",
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
"import/first": "error",
|
||||||
|
"import/newline-after-import": "error",
|
||||||
|
"import/no-duplicates": "error",
|
||||||
|
"eqeqeq": "warn",
|
||||||
|
"no-lonely-if": "error",
|
||||||
|
"no-multi-assign": "error",
|
||||||
|
"@typescript-eslint/no-shadow": "warn",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"no-useless-rename": "error",
|
||||||
|
"object-shorthand": "error",
|
||||||
|
"one-var-declaration-per-line": "error",
|
||||||
|
"prefer-object-spread": "error",
|
||||||
|
"no-unreachable-loop": "error",
|
||||||
|
"no-template-curly-in-string": "error",
|
||||||
|
"default-case-last": "error",
|
||||||
|
"no-array-constructor": "error",
|
||||||
|
"no-else-return": "error",
|
||||||
|
"no-negated-condition": "error",
|
||||||
|
"array-callback-return": "error",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "error",
|
||||||
|
"no-type-assertion/no-type-assertion": "warn",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "error",
|
||||||
|
"@typescript-eslint/no-extraneous-class": "off",
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "warn",
|
||||||
|
"@typescript-eslint/require-await": "warn",
|
||||||
|
"@typescript-eslint/restrict-plus-operands": "warn",
|
||||||
|
"@typescript-eslint/no-non-null-asserted-optional-chain": "warn",
|
||||||
|
"@typescript-eslint/no-misused-promises": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"checksVoidReturn": {
|
||||||
|
"attributes": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/await-thenable": "warn",
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/no-unnecessary-condition": "warn",
|
||||||
|
"@typescript-eslint/non-nullable-type-assertion-style": "warn",
|
||||||
|
"@typescript-eslint/no-confusing-void-expression": "warn",
|
||||||
|
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||||
|
"@typescript-eslint/no-invalid-void-type": "warn",
|
||||||
|
"@typescript-eslint/dot-notation": "warn",
|
||||||
|
"@typescript-eslint/prefer-optional-chain": "warn",
|
||||||
|
"@typescript-eslint/no-meaningless-void-operator": "warn",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-arguments": "warn",
|
||||||
|
"@typescript-eslint/consistent-indexed-object-style": "warn",
|
||||||
|
"unused-imports/no-unused-imports": "error"
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
126
.eslintrc.json
Normal file
126
.eslintrc.json
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/strict-type-checked",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||||
|
"plugin:@typescript-eslint/strict",
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"plugin:cypress/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:sonarjs/recommended"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"@typescript-eslint",
|
||||||
|
"prettier",
|
||||||
|
"simple-import-sort",
|
||||||
|
"no-type-assertion",
|
||||||
|
"sonarjs",
|
||||||
|
"import",
|
||||||
|
"unused-imports"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allowExpressions": true,
|
||||||
|
"allowTypedFunctionExpressions": true,
|
||||||
|
"allowHigherOrderFunctions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@next/next/no-img-element": "off",
|
||||||
|
"curly": "error",
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
|
"simple-import-sort/exports": "error",
|
||||||
|
"import/first": "error",
|
||||||
|
"import/newline-after-import": "error",
|
||||||
|
"import/no-duplicates": "error",
|
||||||
|
"eqeqeq": "warn",
|
||||||
|
"no-lonely-if": "error",
|
||||||
|
"no-multi-assign": "error",
|
||||||
|
"@typescript-eslint/no-shadow": "warn",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"no-useless-rename": "error",
|
||||||
|
"object-shorthand": "error",
|
||||||
|
"one-var-declaration-per-line": "error",
|
||||||
|
"prefer-object-spread": "error",
|
||||||
|
"no-unreachable-loop": "error",
|
||||||
|
"no-template-curly-in-string": "error",
|
||||||
|
"default-case-last": "error",
|
||||||
|
"no-array-constructor": "error",
|
||||||
|
"no-else-return": "error",
|
||||||
|
"no-negated-condition": "error",
|
||||||
|
"array-callback-return": "error",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "error",
|
||||||
|
"no-type-assertion/no-type-assertion": "warn",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "error",
|
||||||
|
"@typescript-eslint/no-extraneous-class": "off",
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "warn",
|
||||||
|
"@typescript-eslint/require-await": "warn",
|
||||||
|
"@typescript-eslint/restrict-plus-operands": "warn",
|
||||||
|
"@typescript-eslint/no-non-null-asserted-optional-chain": "warn",
|
||||||
|
"@typescript-eslint/no-misused-promises": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"checksVoidReturn": {
|
||||||
|
"attributes": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/await-thenable": "warn",
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/no-unnecessary-condition": "warn",
|
||||||
|
"@typescript-eslint/non-nullable-type-assertion-style": "warn",
|
||||||
|
"@typescript-eslint/no-confusing-void-expression": "warn",
|
||||||
|
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||||
|
"@typescript-eslint/no-invalid-void-type": "warn",
|
||||||
|
"@typescript-eslint/dot-notation": "warn",
|
||||||
|
"@typescript-eslint/prefer-optional-chain": "warn",
|
||||||
|
"@typescript-eslint/no-meaningless-void-operator": "warn",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-arguments": "warn",
|
||||||
|
"@typescript-eslint/consistent-indexed-object-style": "warn",
|
||||||
|
"unused-imports/no-unused-imports": "error"
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,5 +3,8 @@
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"printWidth": 100
|
"printWidth": 100,
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true,
|
||||||
|
"plugins": ["@trivago/prettier-plugin-sort-imports"]
|
||||||
}
|
}
|
29
package.json
29
package.json
|
@ -27,7 +27,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atech/postal": "^1.0.0",
|
"@atech/postal": "^1.0.0",
|
||||||
"@nestjs/bullmq": "^10.2.0",
|
"@nestjs/bullmq": "^10.1.1",
|
||||||
"@nestjs/common": "^10.3.10",
|
"@nestjs/common": "^10.3.10",
|
||||||
"@nestjs/config": "^3.2.3",
|
"@nestjs/config": "^3.2.3",
|
||||||
"@nestjs/core": "^10.3.10",
|
"@nestjs/core": "^10.3.10",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"argon2": "^0.40.3",
|
"argon2": "^0.40.3",
|
||||||
"bullmq": "^5.12.0",
|
"bullmq": "^5.9.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
@ -46,8 +46,9 @@
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"hbs": "^4.2.0",
|
"hbs": "^4.2.0",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"joi": "^17.13.3",
|
||||||
"keygrip": "^1.1.0",
|
"keygrip": "^1.1.0",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.10.2",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"nestjs-cls": "^4.4.0",
|
"nestjs-cls": "^4.4.0",
|
||||||
"nestjs-otel": "^6.1.1",
|
"nestjs-otel": "^6.1.1",
|
||||||
|
@ -58,38 +59,42 @@
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"typia": "^6.6.2",
|
"typia": "^6.5.1",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"wildcard": "^2.0.1"
|
"wildcard": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.8.0",
|
||||||
"@nestjs/cli": "^10.4.2",
|
"@nestjs/cli": "^10.4.2",
|
||||||
"@nestjs/schematics": "^10.1.3",
|
"@nestjs/schematics": "^10.1.2",
|
||||||
"@nestjs/testing": "^10.3.10",
|
"@nestjs/testing": "^10.3.10",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/keygrip": "^1.0.6",
|
"@types/keygrip": "^1.0.6",
|
||||||
"@types/node": "^20.14.14",
|
"@types/node": "^20.14.10",
|
||||||
"@types/oidc-provider": "^8.5.1",
|
"@types/oidc-provider": "^8.5.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/parser": "^7.16.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.5",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.2",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"ts-patch": "^3.2.1",
|
"ts-patch": "^3.2.1",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.2",
|
||||||
|
"typescript-eslint": "^7.17.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|
1298
pnpm-lock.yaml
1298
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -2,10 +2,8 @@ import { Controller, Get, Redirect, UseGuards } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { LoginGuard } from './auth/guard/login.guard';
|
import { LoginGuard } from './auth/guard/login.guard';
|
||||||
import { MailService } from './mail/mail.service';
|
import { MailService } from './mail/mail.service';
|
||||||
import { ApiExcludeController } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
@ApiExcludeController()
|
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly appService: AppService,
|
private readonly appService: AppService,
|
||||||
|
|
|
@ -8,16 +8,14 @@ import { RedisModule } from './redis/redis.module';
|
||||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||||
import databaseConfig from './config/database.config';
|
import databaseConfig from './config/database.config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { TypeOrmConfigService } from './database/service/database-config.service';
|
import { TypeOrmConfigService } from './database/database-config.service';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { AuthMiddleware } from './auth/middleware/auth.middleware';
|
import { AuthMiddleware } from './auth/middleware/auth.middleware';
|
||||||
import { ClsModule } from 'nestjs-cls';
|
import { ClsModule } from 'nestjs-cls';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { validate } from './config/env.validation';
|
||||||
import { BullConfigService } from './redis/service/bull-config.service';
|
|
||||||
import { RedisService } from './redis/service/redis.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -40,6 +38,7 @@ import { RedisService } from './redis/service/redis.service';
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
|
validate,
|
||||||
cache: true,
|
cache: true,
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
load: [config, databaseConfig],
|
load: [config, databaseConfig],
|
||||||
|
@ -57,13 +56,8 @@ import { RedisService } from './redis/service/redis.service';
|
||||||
global: true,
|
global: true,
|
||||||
middleware: { mount: false },
|
middleware: { mount: false },
|
||||||
}),
|
}),
|
||||||
RedisModule,
|
|
||||||
BullModule.forRootAsync({
|
|
||||||
imports: [RedisModule],
|
|
||||||
useClass: BullConfigService,
|
|
||||||
inject: [RedisService],
|
|
||||||
}),
|
|
||||||
MailModule,
|
MailModule,
|
||||||
|
RedisModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Body, Controller, Get, Post, Query, Render, Res, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Render, Res, UseGuards } from '@nestjs/common';
|
||||||
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
|
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { ForgotPasswordDto } from '../dto/forgotPassword.dto';
|
import { ForgotPasswordDto } from '../dto/forgotPassword.dto';
|
||||||
|
@ -9,19 +9,19 @@ import { Response } from 'express';
|
||||||
import { User } from '../decorators/user.decorator';
|
import { User } from '../decorators/user.decorator';
|
||||||
import { LoginGuard } from '../guard/login.guard';
|
import { LoginGuard } from '../guard/login.guard';
|
||||||
|
|
||||||
// TODO: Implement RateLimit
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@ApiTags('Authentication')
|
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
|
// TODO: Implement RateLimit
|
||||||
public async postLogin(
|
public async postLogin(
|
||||||
@Body() body: LoginUserDto,
|
@Body() body: LoginUserDto,
|
||||||
@Res({ passthrough: true }) res: Response,
|
@Res({ passthrough: true }) res: Response,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const sessionData = await this.authService.login(body.username, body.password);
|
const sessionData = await this.authService.login(body.username, body.password);
|
||||||
|
|
||||||
|
// process the sessionData.cookies and set it in the response
|
||||||
sessionData.cookiesForms.forEach((cookie) => {
|
sessionData.cookiesForms.forEach((cookie) => {
|
||||||
res.cookie(cookie.name, cookie.value, cookie.options);
|
res.cookie(cookie.name, cookie.value, cookie.options);
|
||||||
});
|
});
|
||||||
|
@ -29,18 +29,19 @@ export class AuthController {
|
||||||
return sessionData.sessionId;
|
return sessionData.sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Implement RateLimit
|
||||||
@Post('register')
|
@Post('register')
|
||||||
public async postRegister(@Body() body: CreateUserDto): Promise<any> {
|
public async postRegister(@Body() body: CreateUserDto): Promise<any> {
|
||||||
return await this.authService.register(body.username, body.email, body.password);
|
return await this.authService.register(body.username, body.email, body.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Implement RateLimit
|
||||||
@Post('reset-password')
|
@Post('reset-password')
|
||||||
public async postForgotPassword(@Body() body: ForgotPasswordDto): Promise<any> {
|
public async postForgotPassword(@Body() body: ForgotPasswordDto): Promise<any> {
|
||||||
return await this.authService.forgotPassword(body.email);
|
return await this.authService.forgotPassword(body.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==== Render pages ==== //
|
// Render pages
|
||||||
|
|
||||||
@Get('login')
|
@Get('login')
|
||||||
@UseGuards(LoginGuard)
|
@UseGuards(LoginGuard)
|
||||||
@Render('auth/login')
|
@Render('auth/login')
|
||||||
|
@ -53,17 +54,6 @@ export class AuthController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('login/totp')
|
|
||||||
@UseGuards(LoginGuard)
|
|
||||||
@Render('auth/login-totp')
|
|
||||||
@ApiExcludeEndpoint()
|
|
||||||
public async getLoginTotp(): Promise<any> {
|
|
||||||
return {
|
|
||||||
login: 'login',
|
|
||||||
methods: ['authenticator', 'email'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('register')
|
@Get('register')
|
||||||
@UseGuards(LoginGuard)
|
@UseGuards(LoginGuard)
|
||||||
@Render('auth/register')
|
@Render('auth/register')
|
||||||
|
@ -84,25 +74,9 @@ export class AuthController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('verify-email')
|
@Get('auth-test')
|
||||||
@UseGuards(LoginGuard)
|
|
||||||
@Render('auth/verify-email')
|
|
||||||
@ApiExcludeEndpoint()
|
@ApiExcludeEndpoint()
|
||||||
public async getVerifyEmail(@Query('code') code?: string): Promise<any> {
|
public async getAuthTest(@User() user: any): Promise<any> {
|
||||||
if (!code) {
|
|
||||||
//TODO: Write error page.
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
login: 'login',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Work on interaction view.
|
|
||||||
@Get('interaction/:id')
|
|
||||||
@ApiExcludeEndpoint()
|
|
||||||
public async getInteraction(@User() user: any): Promise<any> {
|
|
||||||
// TODO: If user is not logged in. Set a cookie to redirect to this page after login.
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { OidcClient } from '../../database/models/oidc_client.model';
|
||||||
import { OidcGrant } from '../../database/models/oidc_grant.model';
|
import { OidcGrant } from '../../database/models/oidc_grant.model';
|
||||||
import { OidcRefreshToken } from '../../database/models/oidc_refresh_token.model';
|
import { OidcRefreshToken } from '../../database/models/oidc_refresh_token.model';
|
||||||
import { OidcSession } from '../../database/models/oidc_session.model';
|
import { OidcSession } from '../../database/models/oidc_session.model';
|
||||||
import { RedisService } from '../../redis/service/redis.service';
|
import { RedisService } from '../../redis/redis.service';
|
||||||
|
|
||||||
const TCLIENT = 7;
|
const TCLIENT = 7;
|
||||||
const TGRANT = 13;
|
const TGRANT = 13;
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
} from './oidc.const';
|
} from './oidc.const';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { RedisService } from '../../redis/service/redis.service';
|
import { RedisService } from '../../redis/redis.service';
|
||||||
import { UserService } from '../../user/service/user.service';
|
import { UserService } from '../../user/service/user.service';
|
||||||
import { Span } from 'nestjs-otel';
|
import { Span } from 'nestjs-otel';
|
||||||
import generateId from './helper/nanoid.helper';
|
import generateId from './helper/nanoid.helper';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { UserService } from '../../user/service/user.service';
|
import { UserService } from '../../user/service/user.service';
|
||||||
import { RedisService } from '../../redis/service/redis.service';
|
import { RedisService } from '../../redis/redis.service';
|
||||||
import { MailService } from '../../mail/mail.service';
|
import { MailService } from '../../mail/mail.service';
|
||||||
import { getResetKey, PASSWORD_RESET_EXPIRATION } from '../auth.const';
|
import { getResetKey, PASSWORD_RESET_EXPIRATION } from '../auth.const';
|
||||||
import { OidcService } from '../oidc/core.service';
|
import { OidcService } from '../oidc/core.service';
|
||||||
|
@ -92,10 +92,6 @@ export class AuthService {
|
||||||
throw new BadRequestException('Invalid credentials');
|
throw new BadRequestException('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.emailVerified) {
|
|
||||||
throw new UnauthorizedException('Email not verified');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.oidcService.createSession(user.id);
|
return this.oidcService.createSession(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default registerAs('database', () => {
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
host: process.env['DATABASE_HOST'],
|
host: process.env['DATABASE_HOST'],
|
||||||
port: parseInt(process.env['DATABASE_PORT'] || '0', 10),
|
port: parseInt(process.env['DATABASE_PORT'] || '3306', 10),
|
||||||
database: process.env['DATABASE_NAME'],
|
database: process.env['DATABASE_NAME'],
|
||||||
username: process.env['DATABASE_USERNAME'],
|
username: process.env['DATABASE_USERNAME'],
|
||||||
password: process.env['DATABASE_PASSWORD'],
|
password: process.env['DATABASE_PASSWORD'],
|
||||||
|
|
100
src/config/env.validation.ts
Normal file
100
src/config/env.validation.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Max,
|
||||||
|
Min,
|
||||||
|
validateSync,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
enum Environment {
|
||||||
|
Development = 'development',
|
||||||
|
Production = 'production',
|
||||||
|
Test = 'test',
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvironmentVariables {
|
||||||
|
@IsEnum(Environment)
|
||||||
|
@IsOptional()
|
||||||
|
NODE_ENV: Environment;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
BASE_URL: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
APP_NAME: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DATABASE_HOST: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@Min(0)
|
||||||
|
@Max(65535)
|
||||||
|
DATABASE_PORT: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DATABASE_USERNAME: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DATABASE_PASSWORD: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DATABASE_NAME: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
DATABASE_SYNCHRONIZE: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
DATABASE_DISABLE_MIGRATIONS: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
DATABASE_DEBUG_LOGGING: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
REDIS_HOST: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@Min(0)
|
||||||
|
@Max(65535)
|
||||||
|
REDIS_PORT: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
REDIS_PASSWORD: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@Min(0)
|
||||||
|
@Max(10)
|
||||||
|
REDIS_DB: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
POSTAL_BASE_URL: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
POSTAL_API_KEY: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
FROM_EMAIL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate(config: Record<string, unknown>) {
|
||||||
|
const validatedConfig = plainToInstance(EnvironmentVariables, config, {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
});
|
||||||
|
const errors = validateSync(validatedConfig, { skipMissingProperties: false });
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(errors.toString());
|
||||||
|
}
|
||||||
|
return validatedConfig;
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import type { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
|
import type { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { DATABASE_MIGRATION } from '../database.migration';
|
import { DATABASE_MIGRATION } from './database.migration';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
|
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
|
15
src/main.ts
15
src/main.ts
|
@ -6,7 +6,6 @@ import { AppModule } from './app.module';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
import { ClsMiddleware } from 'nestjs-cls';
|
import { ClsMiddleware } from 'nestjs-cls';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
|
@ -23,20 +22,6 @@ async function bootstrap() {
|
||||||
}).use,
|
}).use,
|
||||||
);
|
);
|
||||||
|
|
||||||
//Swagger Documentation
|
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
|
||||||
.setTitle('Waterwolf Identity Provider')
|
|
||||||
.setDescription('An OpenSource Identity Provider written by Waterwolf')
|
|
||||||
.setVersion('1.0')
|
|
||||||
.addTag('Authentication', 'Inital login and registration')
|
|
||||||
.addTag('Client')
|
|
||||||
.addTag('Organization')
|
|
||||||
.addTag('User')
|
|
||||||
.build();
|
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
|
||||||
SwaggerModule.setup('api', app, document);
|
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
app.useStaticAssets(join(__dirname, '..', 'public'));
|
app.useStaticAssets(join(__dirname, '..', 'public'));
|
||||||
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
import { RedisService } from './service/redis.service';
|
import { RedisService } from './redis.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [RedisService, ConfigService],
|
providers: [RedisService, ConfigService],
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { SharedBullConfigurationFactory } from '@nestjs/bullmq';
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { RedisService } from './redis.service';
|
|
||||||
import { QueueOptions } from 'bullmq';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BullConfigService implements SharedBullConfigurationFactory {
|
|
||||||
logger = new Logger('BullConfigService');
|
|
||||||
constructor(private readonly redisService: RedisService) {}
|
|
||||||
|
|
||||||
async createSharedConfiguration(): Promise<QueueOptions> {
|
|
||||||
this.logger.debug('Creating BullMQ configuration');
|
|
||||||
return {
|
|
||||||
connection: this.redisService.ioredis,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
import { UserService } from '../service/user.service';
|
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
@Controller({
|
|
||||||
path: 'user',
|
|
||||||
version: '1',
|
|
||||||
})
|
|
||||||
@ApiTags('User')
|
|
||||||
export class UserController {
|
|
||||||
constructor(private readonly userService: UserService) {}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
/**
|
|
||||||
* This service indexes all the users into redis search index
|
|
||||||
* TODO: This service should be ran as a listener job
|
|
||||||
*/
|
|
|
@ -4,12 +4,11 @@ import { Repository } from 'typeorm';
|
||||||
import { hash, verify } from 'argon2';
|
import { hash, verify } from 'argon2';
|
||||||
|
|
||||||
import { User } from '../../database/models/user.model';
|
import { User } from '../../database/models/user.model';
|
||||||
import { RedisService } from '../../redis/service/redis.service';
|
import { RedisService } from '../../redis/redis.service';
|
||||||
import { Span } from 'nestjs-otel';
|
import { Span } from 'nestjs-otel';
|
||||||
import {
|
import {
|
||||||
DISABLED_USER_ERROR,
|
DISABLED_USER_ERROR,
|
||||||
INVALID_CREDENTIALS_ERROR,
|
INVALID_CREDENTIALS_ERROR,
|
||||||
passwordResetKeyGenerate,
|
|
||||||
USER_NOT_FOUND_ERROR,
|
USER_NOT_FOUND_ERROR,
|
||||||
userCacheKey,
|
userCacheKey,
|
||||||
} from '../user.constant';
|
} from '../user.constant';
|
||||||
|
@ -174,60 +173,4 @@ export class UserService {
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a validation code and stores it in Redis.
|
|
||||||
* @param userId The user's ID
|
|
||||||
* @returns string The generated code
|
|
||||||
*/
|
|
||||||
@Span()
|
|
||||||
async generatePasswordResetCode(userId: string): Promise<string> {
|
|
||||||
const code = Math.random().toString(36).slice(-8);
|
|
||||||
await this.redisService.set(passwordResetKeyGenerate(code), userId, 60 * 60 * 24);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a password reset code
|
|
||||||
* @param code The password reset code
|
|
||||||
* @returns string The user's ID
|
|
||||||
*/
|
|
||||||
@Span()
|
|
||||||
async validatePasswordResetCode(code: string): Promise<string> {
|
|
||||||
const userId = await this.redisService.get(passwordResetKeyGenerate(code));
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new UnauthorizedException('Invalid or expired code');
|
|
||||||
}
|
|
||||||
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a email verification code and stores it in Redis.
|
|
||||||
* @param userId The user's ID
|
|
||||||
* @returns string The generated code
|
|
||||||
*/
|
|
||||||
@Span()
|
|
||||||
async generateEmailVerificationCode(userId: string): Promise<string> {
|
|
||||||
const code = Math.random().toString(36).slice(-8);
|
|
||||||
await this.redisService.set(passwordResetKeyGenerate(code), userId, 60 * 60 * 24);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a email verification code
|
|
||||||
* @param code The email verification code
|
|
||||||
* @returns string The user's ID
|
|
||||||
*/
|
|
||||||
@Span()
|
|
||||||
async validateEmailVerificationCode(code: string): Promise<string> {
|
|
||||||
const userId = await this.redisService.get(passwordResetKeyGenerate(code));
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new UnauthorizedException('Invalid or expired code');
|
|
||||||
}
|
|
||||||
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,3 @@ export const DISABLED_USER_ERROR = 'User is disabled';
|
||||||
|
|
||||||
export const userCacheTTL = 60 * 60 * 24; // 24 hours
|
export const userCacheTTL = 60 * 60 * 24; // 24 hours
|
||||||
export const userCacheKey = 'ww-auth:user';
|
export const userCacheKey = 'ww-auth:user';
|
||||||
export const emailVerifyKey = 'ww-auth:email-verify:';
|
|
||||||
export const passwordResetKey = 'ww-auth:password-reset:';
|
|
||||||
|
|
||||||
export const userCacheKeyGenerate = (lookup: string | number) => `${userCacheKey}:${lookup}`;
|
|
||||||
export const emailVerifyKeyGenerate = (lookup: string | number) => `${emailVerifyKey}:${lookup}`;
|
|
||||||
export const passwordResetKeyGenerate = (lookup: string | number) =>
|
|
||||||
`${passwordResetKey}:${lookup}`;
|
|
||||||
|
|
Loading…
Reference in a new issue