feat: email templates
feat: read me updates feat: implement BullMQ
This commit is contained in:
parent
8a283f46e7
commit
bebe215463
22 changed files with 142 additions and 15 deletions
0
docs/README.md
Normal file
0
docs/README.md
Normal file
|
@ -2,8 +2,10 @@ 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,13 +8,16 @@ 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/database-config.service';
|
import { TypeOrmConfigService } from './database/service/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 { BullConfigService } from './redis/service/bull-config.service';
|
||||||
|
import { RedisService } from './redis/service/redis.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -54,8 +57,13 @@ import { ClsModule } from 'nestjs-cls';
|
||||||
global: true,
|
global: true,
|
||||||
middleware: { mount: false },
|
middleware: { mount: false },
|
||||||
}),
|
}),
|
||||||
MailModule,
|
|
||||||
RedisModule,
|
RedisModule,
|
||||||
|
BullModule.forRootAsync({
|
||||||
|
imports: [RedisModule],
|
||||||
|
useClass: BullConfigService,
|
||||||
|
inject: [RedisService],
|
||||||
|
}),
|
||||||
|
MailModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Body, Controller, Get, Post, Render, Res, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Render, Res, UseGuards } from '@nestjs/common';
|
||||||
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
import { ApiExcludeEndpoint, ApiTags } 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';
|
||||||
|
@ -10,6 +10,7 @@ import { User } from '../decorators/user.decorator';
|
||||||
import { LoginGuard } from '../guard/login.guard';
|
import { LoginGuard } from '../guard/login.guard';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
|
@ApiTags('Authentication')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
|
|
@ -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/redis.service';
|
import { RedisService } from '../../redis/service/redis.service';
|
||||||
|
|
||||||
const TCLIENT = 7;
|
const TCLIENT = 7;
|
||||||
const TGRANT = 13;
|
const TGRANT = 13;
|
||||||
|
|
|
@ -26,8 +26,8 @@ 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/redis.service';
|
import { RedisService } from '../../redis/service/redis.service';
|
||||||
import { UserService } from 'src/user/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';
|
||||||
import { context, trace } from '@opentelemetry/api';
|
import { context, trace } from '@opentelemetry/api';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
|
||||||
import { UserService } from '../../user/user.service';
|
import { UserService } from '../../user/service/user.service';
|
||||||
import { RedisService } from '../../redis/redis.service';
|
import { RedisService } from '../../redis/service/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,6 +92,10 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
0
src/config/bull.config.ts
Normal file
0
src/config/bull.config.ts
Normal file
|
@ -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,6 +6,7 @@ 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);
|
||||||
|
@ -22,6 +23,20 @@ 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 './redis.service';
|
import { RedisService } from './service/redis.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [RedisService, ConfigService],
|
providers: [RedisService, ConfigService],
|
||||||
|
|
17
src/redis/service/bull-config.service.ts
Normal file
17
src/redis/service/bull-config.service.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
12
src/user/controller/user.controller.ts
Normal file
12
src/user/controller/user.controller.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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) {}
|
||||||
|
}
|
4
src/user/jobs/userIndex.serive.ts
Normal file
4
src/user/jobs/userIndex.serive.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* This service indexes all the users into redis search index
|
||||||
|
* TODO: This service should be ran as a listener job
|
||||||
|
*/
|
|
@ -3,15 +3,16 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
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/redis.service';
|
import { RedisService } from '../../redis/service/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';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -173,4 +174,60 @@ 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,3 +7,10 @@ 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}`;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { RedisModule } from '../redis/redis.module';
|
import { RedisModule } from '../redis/redis.module';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './service/user.service';
|
||||||
import { DATABASE_ENTITIES } from 'src/database/database.entities';
|
import { DATABASE_ENTITIES } from 'src/database/database.entities';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
0
views/auth/account-disabled.hbs
Normal file
0
views/auth/account-disabled.hbs
Normal file
0
views/auth/consent.hbs
Normal file
0
views/auth/consent.hbs
Normal file
0
views/auth/logout.hbs
Normal file
0
views/auth/logout.hbs
Normal file
0
views/auth/verify-email.hbs
Normal file
0
views/auth/verify-email.hbs
Normal file
Loading…
Reference in a new issue