Compare commits

...

3 commits

Author SHA1 Message Date
62bb9531c0 chore: began implementing more pages 2024-08-08 17:47:42 -04:00
72293167ad chore: update dependencies 2024-08-02 14:43:03 -04:00
bebe215463 feat: email templates
feat: read me updates
feat: implement BullMQ
2024-07-30 16:39:31 -04:00
24 changed files with 674 additions and 538 deletions

0
docs/README.md Normal file
View file

View file

@ -27,7 +27,7 @@
},
"dependencies": {
"@atech/postal": "^1.0.0",
"@nestjs/bullmq": "^10.1.1",
"@nestjs/bullmq": "^10.2.0",
"@nestjs/common": "^10.3.10",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.3.10",
@ -38,7 +38,7 @@
"@nestjs/typeorm": "^10.0.2",
"@opentelemetry/api": "^1.9.0",
"argon2": "^0.40.3",
"bullmq": "^5.9.0",
"bullmq": "^5.12.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cookie-parser": "^1.4.6",
@ -47,7 +47,7 @@
"hbs": "^4.2.0",
"ioredis": "^5.4.1",
"keygrip": "^1.1.0",
"mysql2": "^3.10.2",
"mysql2": "^3.11.0",
"nanoid": "^5.0.7",
"nestjs-cls": "^4.4.0",
"nestjs-otel": "^6.1.1",
@ -58,38 +58,38 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20",
"typia": "^6.5.1",
"typia": "^6.6.2",
"uuid": "^10.0.0",
"wildcard": "^2.0.1"
},
"devDependencies": {
"@nestjs/cli": "^10.4.2",
"@nestjs/schematics": "^10.1.2",
"@nestjs/schematics": "^10.1.3",
"@nestjs/testing": "^10.3.10",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/keygrip": "^1.0.6",
"@types/node": "^20.14.10",
"@types/node": "^20.14.14",
"@types/oidc-provider": "^8.5.1",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"sequelize-cli": "^6.6.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"tailwindcss": "^3.4.5",
"ts-jest": "^29.2.2",
"tailwindcss": "^3.4.7",
"ts-jest": "^29.2.4",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"ts-patch": "^3.2.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.5.2"
"typescript": "^5.5.4"
},
"jest": {
"moduleFileExtensions": [

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,10 @@ import { Controller, Get, Redirect, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LoginGuard } from './auth/guard/login.guard';
import { MailService } from './mail/mail.service';
import { ApiExcludeController } from '@nestjs/swagger';
@Controller()
@ApiExcludeController()
export class AppController {
constructor(
private readonly appService: AppService,

View file

@ -8,13 +8,16 @@ import { RedisModule } from './redis/redis.module';
import { OpenTelemetryModule } from 'nestjs-otel';
import databaseConfig from './config/database.config';
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 { UserModule } from './user/user.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { AuthMiddleware } from './auth/middleware/auth.middleware';
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({
imports: [
@ -54,8 +57,13 @@ import { ClsModule } from 'nestjs-cls';
global: true,
middleware: { mount: false },
}),
MailModule,
RedisModule,
BullModule.forRootAsync({
imports: [RedisModule],
useClass: BullConfigService,
inject: [RedisService],
}),
MailModule,
UserModule,
AuthModule,
],

View file

@ -1,5 +1,5 @@
import { Body, Controller, Get, Post, Render, Res, UseGuards } from '@nestjs/common';
import { ApiExcludeEndpoint } from '@nestjs/swagger';
import { Body, Controller, Get, Post, Query, Render, Res, UseGuards } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { AuthService } from '../services/auth.service';
import { ForgotPasswordDto } from '../dto/forgotPassword.dto';
@ -9,19 +9,19 @@ import { Response } from 'express';
import { User } from '../decorators/user.decorator';
import { LoginGuard } from '../guard/login.guard';
// TODO: Implement RateLimit
@Controller('auth')
@ApiTags('Authentication')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
// TODO: Implement RateLimit
public async postLogin(
@Body() body: LoginUserDto,
@Res({ passthrough: true }) res: Response,
): Promise<any> {
const sessionData = await this.authService.login(body.username, body.password);
// process the sessionData.cookies and set it in the response
sessionData.cookiesForms.forEach((cookie) => {
res.cookie(cookie.name, cookie.value, cookie.options);
});
@ -29,19 +29,18 @@ export class AuthController {
return sessionData.sessionId;
}
// TODO: Implement RateLimit
@Post('register')
public async postRegister(@Body() body: CreateUserDto): Promise<any> {
return await this.authService.register(body.username, body.email, body.password);
}
// TODO: Implement RateLimit
@Post('reset-password')
public async postForgotPassword(@Body() body: ForgotPasswordDto): Promise<any> {
return await this.authService.forgotPassword(body.email);
}
// Render pages
// ==== Render pages ==== //
@Get('login')
@UseGuards(LoginGuard)
@Render('auth/login')
@ -54,6 +53,17 @@ 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')
@UseGuards(LoginGuard)
@Render('auth/register')
@ -74,9 +84,25 @@ export class AuthController {
};
}
@Get('auth-test')
@Get('verify-email')
@UseGuards(LoginGuard)
@Render('auth/verify-email')
@ApiExcludeEndpoint()
public async getAuthTest(@User() user: any): Promise<any> {
public async getVerifyEmail(@Query('code') code?: string): 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;
}
}

View file

@ -9,7 +9,7 @@ import { OidcClient } from '../../database/models/oidc_client.model';
import { OidcGrant } from '../../database/models/oidc_grant.model';
import { OidcRefreshToken } from '../../database/models/oidc_refresh_token.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 TGRANT = 13;

View file

@ -26,8 +26,8 @@ import {
} from './oidc.const';
import { ConfigService } from '@nestjs/config';
import { DataSource } from 'typeorm';
import { RedisService } from '../../redis/redis.service';
import { UserService } from 'src/user/user.service';
import { RedisService } from '../../redis/service/redis.service';
import { UserService } from '../../user/service/user.service';
import { Span } from 'nestjs-otel';
import generateId from './helper/nanoid.helper';
import { context, trace } from '@opentelemetry/api';

View file

@ -1,7 +1,7 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from '../../user/user.service';
import { RedisService } from '../../redis/redis.service';
import { UserService } from '../../user/service/user.service';
import { RedisService } from '../../redis/service/redis.service';
import { MailService } from '../../mail/mail.service';
import { getResetKey, PASSWORD_RESET_EXPIRATION } from '../auth.const';
import { OidcService } from '../oidc/core.service';
@ -92,6 +92,10 @@ export class AuthService {
throw new BadRequestException('Invalid credentials');
}
if (!user.emailVerified) {
throw new UnauthorizedException('Email not verified');
}
return this.oidcService.createSession(user.id);
}

View file

View file

@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { DATABASE_MIGRATION } from './database.migration';
import { DATABASE_MIGRATION } from '../database.migration';
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {

View file

@ -6,6 +6,7 @@ import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import * as cookieParser from 'cookie-parser';
import { ClsMiddleware } from 'nestjs-cls';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
@ -22,6 +23,20 @@ async function bootstrap() {
}).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
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { RedisService } from './redis.service';
import { RedisService } from './service/redis.service';
@Module({
providers: [RedisService, ConfigService],

View 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,
};
}
}

View 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) {}
}

View 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
*/

View file

@ -3,15 +3,16 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { hash, verify } from 'argon2';
import { User } from '../database/models/user.model';
import { RedisService } from '../redis/redis.service';
import { User } from '../../database/models/user.model';
import { RedisService } from '../../redis/service/redis.service';
import { Span } from 'nestjs-otel';
import {
DISABLED_USER_ERROR,
INVALID_CREDENTIALS_ERROR,
passwordResetKeyGenerate,
USER_NOT_FOUND_ERROR,
userCacheKey,
} from './user.constant';
} from '../user.constant';
import { ClsService } from 'nestjs-cls';
@Injectable()
@ -173,4 +174,60 @@ export class UserService {
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;
}
}

View file

@ -7,3 +7,10 @@ export const DISABLED_USER_ERROR = 'User is disabled';
export const userCacheTTL = 60 * 60 * 24; // 24 hours
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}`;

View file

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
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';
@Module({

View file

0
views/auth/consent.hbs Normal file
View file

0
views/auth/logout.hbs Normal file
View file

View file