Compare commits
11 Commits
6050d9d3a3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e71db785c | |||
|
|
3f3a674acb | ||
|
|
879b32b946 | ||
|
|
2d3d1f57a7 | ||
|
|
3b304e4158 | ||
|
|
1f0a27a2e1 | ||
|
|
da3a9d6880 | ||
|
|
4b8348e32f | ||
|
|
ba90bf221b | ||
|
|
fece4dcee1 | ||
|
|
333fd824e3 |
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Template for Fullstack monorepo
|
||||
|
||||
## Techs
|
||||
|
||||
- NESTJS : web backend framework for nodejs and TypeScript, annotation/decorator based
|
||||
121
apps/backend/assets/ejs-templates/mail/account-created.ejs
Normal file
121
apps/backend/assets/ejs-templates/mail/account-created.ejs
Normal file
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title %></title>
|
||||
<style>
|
||||
/* General body styling for all clients */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
/* Container for the email content */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
/* Header section */
|
||||
.header {
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
/* Main content section */
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
/* Button styling */
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
/* List items styling */
|
||||
.feature-item {
|
||||
padding: 5px 0;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #f4f4f4">
|
||||
<table
|
||||
class="container"
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
class="header"
|
||||
style="
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
"
|
||||
>
|
||||
<h1 style="margin: 0"><%= bodyTitle %></h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content" style="padding: 20px">
|
||||
<p>
|
||||
Thank you for signing up for our service. We're excited to have you
|
||||
on board!
|
||||
</p>
|
||||
|
||||
<p>Here are some of the key features you can now enjoy:</p>
|
||||
|
||||
<p style="text-align: center; margin: 30px 0">
|
||||
<a
|
||||
href="<%= resetLink %>"
|
||||
class="button"
|
||||
style="
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have any questions, feel free to reply to this email or visit
|
||||
our
|
||||
<a href="http://goolge.com" style="color: #007bff">support center</a
|
||||
>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
"
|
||||
>
|
||||
<p>© <%= currentYear %> Cowsi. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
4
apps/backend/assets/i18n/en.json
Normal file
4
apps/backend/assets/i18n/en.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mail.create-user.title": "Welcome to Cowsi!",
|
||||
"mail.create-user.body-title": "Welcome to Cowsi {{name}}!"
|
||||
}
|
||||
4
apps/backend/assets/i18n/fr.json
Normal file
4
apps/backend/assets/i18n/fr.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mail.create-user.title": "Bienvenue sur Cowsi!",
|
||||
"mail.create-user.body-title": "Bienvenue sur Cowsi {{name}}!"
|
||||
}
|
||||
22
apps/backend/example.env
Normal file
22
apps/backend/example.env
Normal file
@@ -0,0 +1,22 @@
|
||||
# PostgreSQL for kysely codegen
|
||||
DATABASE_URL=
|
||||
# PostgreSQL
|
||||
DATABASE=
|
||||
DATABASE_USER=
|
||||
DATABASE_PASSWORD=
|
||||
DATABASE_HOST=
|
||||
DATABASE_PORT=
|
||||
|
||||
# Nodemailer
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USER=
|
||||
MAIL_PASSWORD=
|
||||
|
||||
# APP
|
||||
APP_DOMAIN=
|
||||
APP_ENV_TYPE=
|
||||
|
||||
# Secrets
|
||||
SECRET_AUTH_SIGN=
|
||||
SECRET_AUTH_HMAC=
|
||||
@@ -3,6 +3,9 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
"deleteOutDir": true,
|
||||
"assets": [
|
||||
{ "include": "assets/**/*", "watchAssets": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,22 @@
|
||||
"@my-monorepo/common": "workspace:*",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"argon2": "^0.44.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"ejs": "^3.1.10",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next-fs-backend": "^2.6.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "^0.28.3",
|
||||
"nodemailer": "^7.0.5",
|
||||
"passport": "^0.7.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^8.16.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -41,9 +54,12 @@
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/pg": "^8.15.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
|
||||
7
apps/backend/src/app-error.filter.spec.ts
Normal file
7
apps/backend/src/app-error.filter.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AppErrorFilter } from './app-error.filter';
|
||||
|
||||
describe('AppErrorFilter', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new AppErrorFilter()).toBeDefined();
|
||||
});
|
||||
});
|
||||
13
apps/backend/src/app-error.filter.ts
Normal file
13
apps/backend/src/app-error.filter.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||
import { AppError } from './lib/app-error';
|
||||
|
||||
@Catch(AppError)
|
||||
export class AppErrorFilter<T extends AppError> implements ExceptionFilter {
|
||||
catch(exception: T) {
|
||||
if (exception.status) {
|
||||
throw new HttpException(exception.message, exception.status, {
|
||||
cause: exception,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './app/auth/auth.controller';
|
||||
import { DatabaseService } from './database/database.service';
|
||||
import { AuthService } from './app/auth/auth.service';
|
||||
import { UserRegistryService } from './app/users/user-registry.service';
|
||||
import { EnvService } from './env/env.service';
|
||||
import { AuthFeatureModule } from './app/auth/auth-feature/auth.module';
|
||||
import { UsersFeatureModule } from './app/users/user-feature/users-feature.module';
|
||||
import { CoreModule } from './services/core/core.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AuthController],
|
||||
providers: [DatabaseService, AuthService, UserRegistryService, EnvService],
|
||||
imports: [CoreModule, AuthFeatureModule, UsersFeatureModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
10
apps/backend/src/app/auth/auth-core/auth-core.module.ts
Normal file
10
apps/backend/src/app/auth/auth-core/auth-core.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthCoreService } from './auth-core.service';
|
||||
import { AuthRegistry } from './auth-registry';
|
||||
|
||||
const PROVIDERS = [AuthCoreService, AuthRegistry];
|
||||
@Module({
|
||||
providers: PROVIDERS,
|
||||
exports: PROVIDERS,
|
||||
})
|
||||
export class AuthCoreModule {}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthCoreService } from './auth-core.service';
|
||||
|
||||
describe('AuthCoreService', () => {
|
||||
let service: AuthCoreService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthCoreService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthCoreService>(AuthCoreService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
80
apps/backend/src/app/auth/auth-core/auth-core.service.ts
Normal file
80
apps/backend/src/app/auth/auth-core/auth-core.service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthRegistry } from './auth-registry';
|
||||
import * as dayjs from 'dayjs';
|
||||
import { AuthTokenPayload, isDefined, ResetAuth } from '@my-monorepo/common';
|
||||
import { CryptoService } from '../../../services/core/crypto/crypto.service';
|
||||
import { EnvService } from '../../../services/core/env/env.service';
|
||||
|
||||
const RESET_TOKEN_VALIDITY_MINUTES = 15;
|
||||
|
||||
const REFRESH_TOKEN_VALIDITY_DAYS = 7;
|
||||
|
||||
export interface TokenWithHmac {
|
||||
token: string;
|
||||
hmac: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthCoreService {
|
||||
constructor(
|
||||
private authRegistry: AuthRegistry,
|
||||
private cryptoService: CryptoService,
|
||||
private envService: EnvService,
|
||||
) {}
|
||||
|
||||
async addNewResetToken(
|
||||
userId: string,
|
||||
): Promise<{ token: string; user_id: string }> {
|
||||
const res = await this.authRegistry.addResetToken({
|
||||
token: this.cryptoService.generateSecureToken(),
|
||||
userId,
|
||||
validUntil: dayjs().add(RESET_TOKEN_VALIDITY_MINUTES, 'minutes'),
|
||||
});
|
||||
if (!isDefined(res)) {
|
||||
throw new Error('cannot add reset token');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async checkAndInvalidateResetToken(resetAuth: ResetAuth): Promise<boolean> {
|
||||
const res = await this.authRegistry.checkAndInvalidateResetToken(resetAuth);
|
||||
return res > 0;
|
||||
}
|
||||
|
||||
async addNewRefreshToken(userId: string): Promise<TokenWithHmac> {
|
||||
const { token } = await this.authRegistry.addRefreshToken({
|
||||
userId,
|
||||
token: this.createUserAuthJwt({ userId }),
|
||||
validUntil: dayjs().add(REFRESH_TOKEN_VALIDITY_DAYS, 'days'),
|
||||
});
|
||||
|
||||
// TODO : add hash and return validity (req and in JWT)
|
||||
return { token, hmac: this.generateAuthHmac(token) };
|
||||
}
|
||||
|
||||
async verifyPassword(opts: {
|
||||
hashedPassword: string;
|
||||
plainTextPassword: string;
|
||||
}): Promise<boolean> {
|
||||
return this.cryptoService.verifyPassword(
|
||||
opts.hashedPassword,
|
||||
opts.plainTextPassword,
|
||||
);
|
||||
}
|
||||
|
||||
createAccessToken(userId: string): TokenWithHmac {
|
||||
const token = this.createUserAuthJwt({ userId });
|
||||
return { token, hmac: this.generateAuthHmac(token) };
|
||||
}
|
||||
|
||||
private createUserAuthJwt(payload: AuthTokenPayload): string {
|
||||
return this.cryptoService.createJwt(payload);
|
||||
}
|
||||
|
||||
private generateAuthHmac(payload: string): string {
|
||||
return this.cryptoService.generateHmac(
|
||||
payload,
|
||||
this.envService.config.secrets.authHmac,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthRegistry } from './auth-registry';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
describe('AuthRegistry', () => {
|
||||
let service: AuthRegistry;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService],
|
||||
providers: [AuthRegistry],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
service = module.get<AuthRegistry>(AuthRegistry);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
66
apps/backend/src/app/auth/auth-core/auth-registry.ts
Normal file
66
apps/backend/src/app/auth/auth-core/auth-registry.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ResetAuth } from '@my-monorepo/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as dayjs from 'dayjs';
|
||||
import { DatabaseService } from '../../../services/core/database/database.service';
|
||||
|
||||
export interface CreateResetToken {
|
||||
userId: string;
|
||||
token: string;
|
||||
validUntil: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthRegistry {
|
||||
get database() {
|
||||
return this.databaseService.database;
|
||||
}
|
||||
constructor(private databaseService: DatabaseService) {}
|
||||
|
||||
async addResetToken(
|
||||
resetAuth: CreateResetToken,
|
||||
): Promise<{ token: string; user_id: string } | undefined> {
|
||||
return this.database
|
||||
.insertInto('reset_tokens')
|
||||
.values({
|
||||
user_id: resetAuth.userId,
|
||||
token: resetAuth.token,
|
||||
valid_until: resetAuth.validUntil.toDate(),
|
||||
})
|
||||
.returning(['user_id', 'token'])
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async addRefreshToken(opts: {
|
||||
userId: string;
|
||||
token: string;
|
||||
validUntil: dayjs.Dayjs;
|
||||
}): Promise<{ token: string; user_id: string }> {
|
||||
return this.database
|
||||
.insertInto('refresh_tokens')
|
||||
.values({
|
||||
user_id: opts.userId,
|
||||
token: opts.token,
|
||||
valid_until: opts.validUntil.toDate(),
|
||||
})
|
||||
.returning(['user_id', 'token'])
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async checkAndInvalidateResetToken(resetAuth: ResetAuth): Promise<bigint> {
|
||||
const res = await this.database
|
||||
.deleteFrom('reset_tokens')
|
||||
.where('user_id', '=', resetAuth.userId)
|
||||
.where('token', '=', resetAuth.resetToken)
|
||||
.where('valid_until', '>=', dayjs().toDate())
|
||||
.executeTakeFirst();
|
||||
console.log(
|
||||
this.database
|
||||
.deleteFrom('reset_tokens')
|
||||
.where('user_id', '=', resetAuth.userId)
|
||||
.where('token', '=', resetAuth.resetToken)
|
||||
.where('valid_until', '>=', dayjs().toDate())
|
||||
.compile(),
|
||||
);
|
||||
return res.numDeletedRows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthControllerService } from './auth-controller.service';
|
||||
|
||||
describe('AuthControllerService', () => {
|
||||
let service: AuthControllerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthControllerService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthControllerService>(AuthControllerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CredentialAuth, ResetAuth } from '@my-monorepo/common';
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { CryptoService } from '../../../services/core/crypto/crypto.service';
|
||||
import { UsersCoreService } from '../../users/users-core/users-core.service';
|
||||
import { AuthCoreService, TokenWithHmac } from '../auth-core/auth-core.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthControllerService {
|
||||
constructor(
|
||||
private userCoreService: UsersCoreService,
|
||||
private authCoreService: AuthCoreService,
|
||||
private cryptoService: CryptoService,
|
||||
) {}
|
||||
|
||||
async handleCredentialsAuth(credentials: CredentialAuth): Promise<{
|
||||
refreshToken: TokenWithHmac;
|
||||
accessToken: TokenWithHmac;
|
||||
}> {
|
||||
const userWithHash =
|
||||
await this.userCoreService.getUserIdAndHashPass(credentials);
|
||||
if (!userWithHash || !userWithHash.hash) {
|
||||
throw new UnauthorizedException('invalid credentials');
|
||||
}
|
||||
const { userId, hash } = userWithHash;
|
||||
const isPasswordValid = await this.authCoreService.verifyPassword({
|
||||
hashedPassword: hash,
|
||||
plainTextPassword: credentials.password,
|
||||
});
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('invalid credentials');
|
||||
}
|
||||
|
||||
const refreshToken = await this.authCoreService.addNewRefreshToken(userId);
|
||||
const accessToken = this.authCoreService.createAccessToken(userId);
|
||||
return {
|
||||
refreshToken,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
async resetPassword(resetData: ResetAuth) {
|
||||
const isValidToken =
|
||||
await this.authCoreService.checkAndInvalidateResetToken(resetData);
|
||||
if (!isValidToken) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await this.userCoreService.setHashPassword(
|
||||
resetData.userId,
|
||||
await this.cryptoService.hashPassword(resetData.newPassword),
|
||||
);
|
||||
}
|
||||
}
|
||||
72
apps/backend/src/app/auth/auth-feature/auth.controller.ts
Normal file
72
apps/backend/src/app/auth/auth-feature/auth.controller.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Body, Controller, Post, Res } from '@nestjs/common';
|
||||
import { AuthControllerService } from './auth-controller.service';
|
||||
import { ZodValidationPipe } from '../../../lib/nestjs/zod-validation/zod-validation.pipe';
|
||||
import {
|
||||
CredentialAuth,
|
||||
credentialAuthParser,
|
||||
ResetAuth,
|
||||
resetAuthParser,
|
||||
} from '@my-monorepo/common';
|
||||
import { Response } from 'express';
|
||||
import { EnvService, EnvType } from '../../../services/core/env/env.service';
|
||||
|
||||
const REFRESH_TOKEN_COOKIE_NAME = 'refresh';
|
||||
const REFRESH_HASH_COOKIE_NAME = `${REFRESH_TOKEN_COOKIE_NAME}_hash`;
|
||||
const ACCESS_TOKEN_COOKIE_NAME = 'access';
|
||||
const ACCESS_HASH_COOKIE_NAME = `${ACCESS_TOKEN_COOKIE_NAME}_hash`;
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private authService: AuthControllerService,
|
||||
private envService: EnvService,
|
||||
) {}
|
||||
|
||||
@Post('credentials')
|
||||
async postCredentials(
|
||||
@Body(new ZodValidationPipe(credentialAuthParser))
|
||||
credentials: CredentialAuth,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
const { accessToken, refreshToken } =
|
||||
await this.authService.handleCredentialsAuth(credentials);
|
||||
|
||||
console.log('>>> paul-debug', {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
domain: this.envService.config.app.domain,
|
||||
secure: this.envService.config.app.envType === EnvType.Production,
|
||||
});
|
||||
response.cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken.token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
domain: this.envService.config.app.domain,
|
||||
secure: this.envService.config.app.envType === EnvType.Production,
|
||||
});
|
||||
response.cookie(REFRESH_HASH_COOKIE_NAME, refreshToken.hmac, {
|
||||
httpOnly: false,
|
||||
sameSite: 'strict',
|
||||
domain: this.envService.config.app.domain,
|
||||
secure: this.envService.config.app.envType === EnvType.Production,
|
||||
});
|
||||
response.cookie(ACCESS_TOKEN_COOKIE_NAME, accessToken.token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
domain: this.envService.config.app.domain,
|
||||
secure: this.envService.config.app.envType === EnvType.Production,
|
||||
});
|
||||
response.cookie(ACCESS_HASH_COOKIE_NAME, accessToken.hmac, {
|
||||
httpOnly: false,
|
||||
sameSite: 'strict',
|
||||
domain: this.envService.config.app.domain,
|
||||
secure: this.envService.config.app.envType === EnvType.Production,
|
||||
});
|
||||
}
|
||||
|
||||
@Post('reset')
|
||||
async postReset(
|
||||
@Body(new ZodValidationPipe(resetAuthParser)) resetData: ResetAuth,
|
||||
): Promise<void> {
|
||||
await this.authService.resetPassword(resetData);
|
||||
}
|
||||
}
|
||||
12
apps/backend/src/app/auth/auth-feature/auth.module.ts
Normal file
12
apps/backend/src/app/auth/auth-feature/auth.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersCoreModule } from '../../users/users-core/users-core.module';
|
||||
import { AuthControllerService } from './auth-controller.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthCoreModule } from '../auth-core/auth-core.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersCoreModule, AuthCoreModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthControllerService],
|
||||
})
|
||||
export class AuthFeatureModule {}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Controller, Post } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
@Post('credentials')
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async postCredentials() {
|
||||
return this.authService.verifyCredentials();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { UserRegistryService } from '../users/user-registry.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(private userRegistry: UserRegistryService) {}
|
||||
|
||||
async verifyCredentials() {
|
||||
const isValidCredential = await this.userRegistry.existsEmailPassword(
|
||||
'',
|
||||
'',
|
||||
);
|
||||
if (!isValidCredential) {
|
||||
throw new UnauthorizedException('invalid credentials');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersControllerService } from './users-controller.service';
|
||||
|
||||
describe('UsersControllerService', () => {
|
||||
let service: UsersControllerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersControllerService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersControllerService>(UsersControllerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { CreateUser } from '@my-monorepo/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
LOCALIZATION_SERVICE,
|
||||
LocalizationService,
|
||||
} from '../../../services/core/localization/localization.service';
|
||||
import { MailerService } from '../../../services/core/mailer/mailer.service';
|
||||
import { MailTemplateEnum } from '../../../services/core/template-renderer/mail-template-data';
|
||||
import { TemplateRendererService } from '../../../services/core/template-renderer/template-renderer.service';
|
||||
import { AuthCoreService } from '../../auth/auth-core/auth-core.service';
|
||||
import { UsersCoreService } from '../users-core/users-core.service';
|
||||
import { LinkBuilderService } from '../../../services/core/link-builder/link-builder.service';
|
||||
|
||||
@Injectable()
|
||||
export class UsersControllerService {
|
||||
constructor(
|
||||
private userCoreService: UsersCoreService,
|
||||
private mailerService: MailerService,
|
||||
@Inject(LOCALIZATION_SERVICE)
|
||||
private localizationService: LocalizationService,
|
||||
private templateRendererService: TemplateRendererService,
|
||||
private authCoreService: AuthCoreService,
|
||||
private linkBuilderService: LinkBuilderService,
|
||||
) {}
|
||||
|
||||
async createUser(newUser: CreateUser): Promise<void> {
|
||||
// FIXME : return result once auth is done
|
||||
const createUserResult = await this.userCoreService.addUser(newUser);
|
||||
if (!createUserResult.ok) {
|
||||
return;
|
||||
}
|
||||
const createdUser = createUserResult.data;
|
||||
const resetToken = await this.authCoreService.addNewResetToken(
|
||||
createdUser.id,
|
||||
);
|
||||
const resetLink = this.linkBuilderService.buildLinkToFrontend(['reset'], {
|
||||
userId: resetToken.user_id,
|
||||
token: resetToken.token,
|
||||
});
|
||||
|
||||
// build mail
|
||||
const mailTitle = this.localizationService.translate(
|
||||
'mail.create-user.title',
|
||||
);
|
||||
const mailBodyTitle = this.localizationService.translate(
|
||||
'mail.create-user.body-title',
|
||||
{ name: newUser.firstName },
|
||||
);
|
||||
const mailHtml = await this.templateRendererService.renderMailTemplate(
|
||||
MailTemplateEnum.CreateAccount,
|
||||
{
|
||||
title: mailTitle,
|
||||
bodyTitle: mailBodyTitle,
|
||||
resetLink: resetLink,
|
||||
},
|
||||
);
|
||||
void this.mailerService.sendEmail({
|
||||
toAdresses: [newUser.email],
|
||||
subject: mailTitle,
|
||||
html: mailHtml,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersControllerService } from './users-controller.service';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersCoreModule } from '../users-core/users-core.module';
|
||||
import { AuthCoreModule } from '../../auth/auth-core/auth-core.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersCoreModule, AuthCoreModule],
|
||||
providers: [UsersControllerService],
|
||||
controllers: [UsersController],
|
||||
})
|
||||
export class UsersFeatureModule {}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersController } from './users.controller';
|
||||
|
||||
describe('UsersController', () => {
|
||||
let controller: UsersController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [UsersController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<UsersController>(UsersController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
16
apps/backend/src/app/users/user-feature/users.controller.ts
Normal file
16
apps/backend/src/app/users/user-feature/users.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CreateUser, createUserParser } from '@my-monorepo/common';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ZodValidationPipe } from '../../../lib/nestjs/zod-validation/zod-validation.pipe';
|
||||
import { UsersControllerService } from './users-controller.service';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private userService: UsersControllerService) {}
|
||||
|
||||
@Post()
|
||||
async createUser(
|
||||
@Body(new ZodValidationPipe(createUserParser)) newUser: CreateUser,
|
||||
) {
|
||||
return this.userService.createUser(newUser);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../../database/database.service';
|
||||
import { isDefined } from '@my-monorepo/common';
|
||||
|
||||
@Injectable()
|
||||
export class UserRegistryService {
|
||||
constructor(private databaseService: DatabaseService) {}
|
||||
|
||||
async existsEmailPassword(email: string, hashedPassword: string) {
|
||||
const id = await this.databaseService.database
|
||||
.selectFrom('users')
|
||||
.select('id')
|
||||
.where((eb) => eb('email', '=', email).and('hash', '=', hashedPassword))
|
||||
.executeTakeFirst();
|
||||
|
||||
return isDefined(id);
|
||||
}
|
||||
}
|
||||
10
apps/backend/src/app/users/users-core/users-core.module.ts
Normal file
10
apps/backend/src/app/users/users-core/users-core.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersCoreService } from './users-core.service';
|
||||
import { UsersRegistry } from './users-registry';
|
||||
|
||||
const PROVIDERS = [UsersCoreService, UsersRegistry];
|
||||
@Module({
|
||||
providers: PROVIDERS,
|
||||
exports: PROVIDERS,
|
||||
})
|
||||
export class UsersCoreModule {}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersCoreService } from './users-core.service';
|
||||
|
||||
describe('UsersCoreService', () => {
|
||||
let service: UsersCoreService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersCoreService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersCoreService>(UsersCoreService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
36
apps/backend/src/app/users/users-core/users-core.service.ts
Normal file
36
apps/backend/src/app/users/users-core/users-core.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UsersRegistry } from './users-registry';
|
||||
import { CreateUser, CredentialAuth, Result, User } from '@my-monorepo/common';
|
||||
import { UserEmailAlreadyExistError } from './users-error';
|
||||
|
||||
@Injectable()
|
||||
export class UsersCoreService {
|
||||
constructor(private userRegistry: UsersRegistry) {}
|
||||
|
||||
async getUserIdAndHashPass(
|
||||
credentials: CredentialAuth,
|
||||
): Promise<{ userId: string; hash?: string } | undefined> {
|
||||
return await this.userRegistry.getUserIdPasswordFromEmail(
|
||||
credentials.email,
|
||||
);
|
||||
}
|
||||
|
||||
async addUser(
|
||||
newUser: CreateUser,
|
||||
): Promise<Result<User, UserEmailAlreadyExistError>> {
|
||||
return await this.userRegistry.addUser(newUser);
|
||||
}
|
||||
|
||||
async setHashPassword(userId: string, password: string): Promise<boolean> {
|
||||
const updateRes = await this.userRegistry.updatePasswordHash(
|
||||
userId,
|
||||
password,
|
||||
);
|
||||
return updateRes.numUpdatedRows > 0;
|
||||
}
|
||||
|
||||
async getHashPassword(userId: string) {
|
||||
const res = await this.userRegistry.getPasswordHash(userId);
|
||||
return res?.hash ?? undefined;
|
||||
}
|
||||
}
|
||||
11
apps/backend/src/app/users/users-core/users-error.ts
Normal file
11
apps/backend/src/app/users/users-core/users-error.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AppError } from '../../../lib/app-error';
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
|
||||
export class UserEmailAlreadyExistError extends AppError {
|
||||
constructor() {
|
||||
super({
|
||||
message: 'User email already exists',
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
});
|
||||
}
|
||||
}
|
||||
18
apps/backend/src/app/users/users-core/users-registry.spec.ts
Normal file
18
apps/backend/src/app/users/users-core/users-registry.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersRegistry } from './users-registry';
|
||||
|
||||
describe('UsersRegistry', () => {
|
||||
let provider: UsersRegistry;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersRegistry],
|
||||
}).compile();
|
||||
|
||||
provider = module.get<UsersRegistry>(UsersRegistry);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(provider).toBeDefined();
|
||||
});
|
||||
});
|
||||
88
apps/backend/src/app/users/users-core/users-registry.ts
Normal file
88
apps/backend/src/app/users/users-core/users-registry.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../../../services/core/database/database.service';
|
||||
import { CreateUser, isDefined, Result, User } from '@my-monorepo/common';
|
||||
import { UserEmailAlreadyExistError } from './users-error';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRegistry {
|
||||
get database() {
|
||||
return this.databaseService.database;
|
||||
}
|
||||
|
||||
constructor(private databaseService: DatabaseService) {}
|
||||
|
||||
async getUserIdPasswordFromEmail(
|
||||
email: string,
|
||||
): Promise<{ userId: string; hash?: string } | undefined> {
|
||||
const user = await this.database
|
||||
.selectFrom('users')
|
||||
.select(['id', 'hash'])
|
||||
.where((eb) => eb('email', '=', email))
|
||||
.executeTakeFirst();
|
||||
|
||||
return isDefined(user)
|
||||
? {
|
||||
userId: user.id,
|
||||
hash: user.hash ?? undefined,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
async addUser(
|
||||
newUser: CreateUser,
|
||||
): Promise<Result<User, UserEmailAlreadyExistError>> {
|
||||
const rowWithEmail = await this.database
|
||||
.selectFrom('users')
|
||||
.select('id')
|
||||
.where('email', '=', newUser.email)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (isDefined(rowWithEmail)) {
|
||||
return Result.error(new UserEmailAlreadyExistError());
|
||||
}
|
||||
|
||||
const userQuery = this.database
|
||||
.insertInto('users')
|
||||
.values({
|
||||
email: newUser.email,
|
||||
first_name: newUser.firstName,
|
||||
last_name: newUser.lastName,
|
||||
})
|
||||
.returning([
|
||||
'id',
|
||||
'email',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
'first_name',
|
||||
'last_name',
|
||||
]);
|
||||
const user = await userQuery.executeTakeFirst();
|
||||
if (!isDefined(user)) {
|
||||
return Result.error(new UserEmailAlreadyExistError());
|
||||
}
|
||||
return Result.success<User>({
|
||||
id: user.id,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
email: user.email,
|
||||
createdAt: user.created_at,
|
||||
updatedAt: user.updated_at,
|
||||
deletedAt: user.deleted_at ?? undefined,
|
||||
});
|
||||
}
|
||||
async updatePasswordHash(userId: string, hashedPassword: string) {
|
||||
return this.database
|
||||
.updateTable('users')
|
||||
.set({ hash: hashedPassword })
|
||||
.where('users.id', '=', userId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async getPasswordHash(userId: string) {
|
||||
return this.database
|
||||
.selectFrom('users')
|
||||
.select('users.hash')
|
||||
.where('users.id', '=', userId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
}
|
||||
22
apps/backend/src/database/db.d.ts
vendored
22
apps/backend/src/database/db.d.ts
vendored
@@ -13,6 +13,26 @@ export type Int8 = ColumnType<string, bigint | number | string, bigint | number
|
||||
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export interface RefreshTokens {
|
||||
created_at: Generated<Timestamp>;
|
||||
deleted_at: Timestamp | null;
|
||||
id: Generated<Int8>;
|
||||
token: string;
|
||||
updated_at: Generated<Timestamp>;
|
||||
user_id: Generated<Int8>;
|
||||
valid_until: Timestamp;
|
||||
}
|
||||
|
||||
export interface ResetTokens {
|
||||
created_at: Generated<Timestamp>;
|
||||
deleted_at: Timestamp | null;
|
||||
id: Generated<Int8>;
|
||||
token: string;
|
||||
updated_at: Generated<Timestamp>;
|
||||
user_id: Generated<Int8>;
|
||||
valid_until: Timestamp;
|
||||
}
|
||||
|
||||
export interface Users {
|
||||
created_at: Generated<Timestamp>;
|
||||
deleted_at: Timestamp | null;
|
||||
@@ -25,5 +45,7 @@ export interface Users {
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
refresh_tokens: RefreshTokens;
|
||||
reset_tokens: ResetTokens;
|
||||
users: Users;
|
||||
}
|
||||
|
||||
27
apps/backend/src/env/env.service.ts
vendored
27
apps/backend/src/env/env.service.ts
vendored
@@ -1,27 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import z from 'zod';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
const envParser = z.object({
|
||||
DATABASE: z.string(),
|
||||
DATABASE_USER: z.string(),
|
||||
DATABASE_PASSWORD: z.string(),
|
||||
DATABASE_HOST: z.string(),
|
||||
DATABASE_PORT: z.coerce.number(),
|
||||
});
|
||||
|
||||
export type EnvConfig = Readonly<z.infer<typeof envParser>>;
|
||||
|
||||
@Injectable()
|
||||
export class EnvService {
|
||||
readonly config: EnvConfig;
|
||||
|
||||
constructor() {
|
||||
console.log('paul-debug', dotenv);
|
||||
const res = dotenv.config();
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
this.config = envParser.parse(res.parsed);
|
||||
}
|
||||
}
|
||||
14
apps/backend/src/lib/app-error.ts
Normal file
14
apps/backend/src/lib/app-error.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
|
||||
export class AppError extends Error {
|
||||
public readonly status?: HttpStatus = 500;
|
||||
|
||||
constructor(opts: {
|
||||
message: string;
|
||||
status?: HttpStatus;
|
||||
options?: ErrorOptions;
|
||||
}) {
|
||||
super(opts.message, opts.options);
|
||||
this.status = opts.status;
|
||||
}
|
||||
}
|
||||
11
apps/backend/src/lib/nestjs/cookies/cookies-decorator.ts
Normal file
11
apps/backend/src/lib/nestjs/cookies/cookies-decorator.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ZodType } from 'zod';
|
||||
|
||||
export const Cookies = createParamDecorator(
|
||||
<T>(data: { key: string; parser: ZodType<T> }, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest<Request>();
|
||||
const res = data.parser.safeParse(request.cookies?.[data.key]);
|
||||
return res.success ? res.data : undefined;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ZodValidationPipe } from './zod-validation.pipe';
|
||||
|
||||
describe('ZodValidationPipe', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new ZodValidationPipe()).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PipeTransform, BadRequestException } from '@nestjs/common';
|
||||
import { ZodSchema } from 'zod';
|
||||
|
||||
export class ZodValidationPipe<Z extends ZodSchema> implements PipeTransform {
|
||||
constructor(private schema: Z) {}
|
||||
|
||||
transform(value: unknown) {
|
||||
const parsedValue = this.schema.safeParse(value);
|
||||
if (!parsedValue.success) {
|
||||
const invalidFields = parsedValue.error.errors
|
||||
.map((v) => v.path.join('.'))
|
||||
.join(', ');
|
||||
throw new BadRequestException(
|
||||
`Validation failed for fields : ${invalidFields}`,
|
||||
);
|
||||
}
|
||||
// FIXME improve this typing if possible
|
||||
return parsedValue.data as unknown;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { AppErrorFilter } from './app-error.filter';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalFilters(new AppErrorFilter());
|
||||
app.use(cookieParser());
|
||||
console.log('Starting server on port', process.env.PORT ?? 3000);
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('reset_tokens')
|
||||
.addColumn('id', 'bigserial', (col) => col.primaryKey())
|
||||
.addColumn('user_id', 'bigserial', (col) => col.notNull())
|
||||
.addForeignKeyConstraint(
|
||||
'fk_user_id_reset_tokens_users',
|
||||
['user_id'],
|
||||
'users',
|
||||
['id'],
|
||||
(col) => col.onDelete('cascade'),
|
||||
)
|
||||
.addColumn('token', 'text', (col) => col.notNull())
|
||||
.addUniqueConstraint('uniq_user_id_tokens_reset_tokens', [
|
||||
'user_id',
|
||||
'token',
|
||||
])
|
||||
.addColumn('valid_until', 'timestamptz', (col) => col.notNull())
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||
await db.schema.dropTable('reset_tokens').execute();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('refresh_tokens')
|
||||
.addColumn('id', 'bigserial', (col) => col.primaryKey())
|
||||
.addColumn('user_id', 'bigserial', (col) => col.notNull())
|
||||
.addForeignKeyConstraint(
|
||||
'fk_user_id_refresh_tokens_users',
|
||||
['user_id'],
|
||||
'users',
|
||||
['id'],
|
||||
(col) => col.onDelete('cascade'),
|
||||
)
|
||||
.addColumn('token', 'text', (col) => col.notNull())
|
||||
.addUniqueConstraint('uniq_user_id_tokens_refresh_tokens', [
|
||||
'user_id',
|
||||
'token',
|
||||
])
|
||||
.addColumn('valid_until', 'timestamptz', (col) => col.notNull())
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||
await db.schema.dropTable('refresh_tokens').execute();
|
||||
}
|
||||
27
apps/backend/src/services/core/core.module.ts
Normal file
27
apps/backend/src/services/core/core.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { DatabaseService } from './database/database.service';
|
||||
import { EnvService } from './env/env.service';
|
||||
import { LOCALIZATION_SERVICE_PROVIDER } from './localization/localization.service';
|
||||
import { LoggerService } from './logger/logger.service';
|
||||
import { MailerService } from './mailer/mailer.service';
|
||||
import { TemplateRendererService } from './template-renderer/template-renderer.service';
|
||||
import { LinkBuilderService } from './link-builder/link-builder.service';
|
||||
import { CryptoService } from './crypto/crypto.service';
|
||||
|
||||
const DECLARATIONS = [
|
||||
DatabaseService,
|
||||
EnvService,
|
||||
LOCALIZATION_SERVICE_PROVIDER,
|
||||
LoggerService,
|
||||
MailerService,
|
||||
TemplateRendererService,
|
||||
LinkBuilderService,
|
||||
CryptoService,
|
||||
];
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: DECLARATIONS,
|
||||
exports: DECLARATIONS,
|
||||
})
|
||||
export class CoreModule {}
|
||||
18
apps/backend/src/services/core/crypto/crypto.service.spec.ts
Normal file
18
apps/backend/src/services/core/crypto/crypto.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CryptoService } from './crypto.service';
|
||||
|
||||
describe('CryptoService', () => {
|
||||
let service: CryptoService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [CryptoService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CryptoService>(CryptoService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
49
apps/backend/src/services/core/crypto/crypto.service.ts
Normal file
49
apps/backend/src/services/core/crypto/crypto.service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as crypto from 'crypto';
|
||||
import * as argon2 from 'argon2';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { EnvService } from '../env/env.service';
|
||||
import { isDefined } from '@my-monorepo/common';
|
||||
|
||||
@Injectable()
|
||||
export class CryptoService {
|
||||
constructor(private envService: EnvService) {}
|
||||
|
||||
generateSecureToken(length = 32): string {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
hashPassword(password: string): Promise<string> {
|
||||
return argon2.hash(password);
|
||||
}
|
||||
|
||||
verifyPassword(
|
||||
passwordHash: string,
|
||||
clearTextPassword: string,
|
||||
): Promise<boolean> {
|
||||
return argon2.verify(passwordHash, clearTextPassword);
|
||||
}
|
||||
|
||||
createJwt(payload: string | object): string {
|
||||
return jwt.sign(payload, this.envService.config.secrets.authSign);
|
||||
}
|
||||
|
||||
async verifyJwt(token: string): Promise<string | object | undefined> {
|
||||
return await new Promise<string | object | undefined>((resolve) => {
|
||||
jwt.verify(
|
||||
token,
|
||||
this.envService.config.secrets.authSign,
|
||||
(err, decoded) =>
|
||||
resolve(!isDefined(err) && isDefined(decoded) ? decoded : undefined),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
generateHmac(payload: string, key: string) {
|
||||
return crypto.createHmac('sha256', key).update(payload).digest('hex');
|
||||
}
|
||||
|
||||
validateHmac(payload: string, hmac: string, key: string): boolean {
|
||||
return this.generateHmac(payload, key) === hmac;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import { Kysely, PostgresDialect } from 'kysely';
|
||||
import { DB } from './db';
|
||||
import { DB } from '../../../database/db';
|
||||
import { EnvService } from '../env/env.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -10,14 +10,14 @@ export class DatabaseService {
|
||||
database: Kysely<DB>;
|
||||
|
||||
constructor(envService: EnvService) {
|
||||
const config = envService.config;
|
||||
const config = envService.config.database;
|
||||
const dialect = new PostgresDialect({
|
||||
pool: new Pool({
|
||||
database: config.DATABASE,
|
||||
host: config.DATABASE_HOST,
|
||||
user: config.DATABASE_USER,
|
||||
password: config.DATABASE_PASSWORD,
|
||||
port: config.DATABASE_PORT,
|
||||
database: config.name,
|
||||
host: config.host,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
port: config.port,
|
||||
max: 10,
|
||||
}),
|
||||
});
|
||||
63
apps/backend/src/services/core/env/env.service.ts
vendored
Normal file
63
apps/backend/src/services/core/env/env.service.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import z from 'zod';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
export enum EnvType {
|
||||
Dev = 'dev',
|
||||
Production = 'production',
|
||||
}
|
||||
|
||||
const envParser = z
|
||||
.object({
|
||||
DATABASE: z.string(),
|
||||
DATABASE_USER: z.string(),
|
||||
DATABASE_PASSWORD: z.string(),
|
||||
DATABASE_HOST: z.string(),
|
||||
DATABASE_PORT: z.coerce.number(),
|
||||
MAIL_HOST: z.string(),
|
||||
MAIL_PORT: z.coerce.number(),
|
||||
MAIL_USER: z.string().email(),
|
||||
MAIL_PASSWORD: z.string(),
|
||||
APP_DOMAIN: z.string(),
|
||||
APP_ENV_TYPE: z.nativeEnum(EnvType),
|
||||
SECRET_AUTH_SIGN: z.string(),
|
||||
SECRET_AUTH_HMAC: z.string(),
|
||||
})
|
||||
.transform((parsed) => ({
|
||||
database: {
|
||||
name: parsed.DATABASE,
|
||||
user: parsed.DATABASE_USER,
|
||||
password: parsed.DATABASE_PASSWORD,
|
||||
host: parsed.DATABASE_HOST,
|
||||
port: parsed.DATABASE_PORT,
|
||||
},
|
||||
mail: {
|
||||
host: parsed.MAIL_HOST,
|
||||
port: parsed.MAIL_PORT,
|
||||
user: parsed.MAIL_USER,
|
||||
password: parsed.MAIL_PASSWORD,
|
||||
},
|
||||
app: {
|
||||
domain: parsed.APP_DOMAIN,
|
||||
envType: parsed.APP_ENV_TYPE,
|
||||
},
|
||||
secrets: {
|
||||
authSign: parsed.SECRET_AUTH_SIGN,
|
||||
authHmac: parsed.SECRET_AUTH_HMAC,
|
||||
},
|
||||
}));
|
||||
|
||||
export type EnvConfig = Readonly<z.infer<typeof envParser>>;
|
||||
|
||||
@Injectable()
|
||||
export class EnvService {
|
||||
readonly config: EnvConfig;
|
||||
|
||||
constructor() {
|
||||
const res = dotenv.config();
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
this.config = envParser.parse(res.parsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LinkBuilderService } from './link-builder.service';
|
||||
|
||||
describe('LinkBuilderService', () => {
|
||||
let service: LinkBuilderService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [LinkBuilderService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LinkBuilderService>(LinkBuilderService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EnvService } from '../env/env.service';
|
||||
|
||||
@Injectable()
|
||||
export class LinkBuilderService {
|
||||
constructor(private envService: EnvService) {}
|
||||
|
||||
buildLinkToFrontend(
|
||||
path: string[],
|
||||
queryParams: Record<string, string> = {},
|
||||
) {
|
||||
const base = this.envService.config.app.domain;
|
||||
const segments = path.join('/');
|
||||
const queryParamsList = Object.entries(queryParams);
|
||||
const joinedQueryParam = queryParamsList
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&');
|
||||
const queryParamsStr =
|
||||
queryParamsList.length > 0 ? `?${joinedQueryParam}` : '';
|
||||
return `${base}/${segments}${queryParamsStr}`;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserRegistryService } from './user-registry.service';
|
||||
import { LocalizationService } from './localization.service';
|
||||
|
||||
describe('UserRegistryService', () => {
|
||||
let service: UserRegistryService;
|
||||
describe('LocalizationService', () => {
|
||||
let service: LocalizationService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserRegistryService],
|
||||
providers: [LocalizationService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserRegistryService>(UserRegistryService);
|
||||
service = module.get<LocalizationService>(LocalizationService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@@ -0,0 +1,38 @@
|
||||
import { isSupportedLang } from '@my-monorepo/common';
|
||||
import { FactoryProvider, Scope } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import * as i18next from 'i18next';
|
||||
import * as i18nextBackend from 'i18next-fs-backend';
|
||||
|
||||
export const LOCALIZATION_SERVICE = 'LOCALIZATION_SERVICE';
|
||||
|
||||
export const LOCALIZATION_SERVICE_PROVIDER: Readonly<
|
||||
FactoryProvider<LocalizationService>
|
||||
> = {
|
||||
provide: LOCALIZATION_SERVICE,
|
||||
scope: Scope.REQUEST,
|
||||
useFactory: async (req: Request) => {
|
||||
const instance = i18next.createInstance().use(i18nextBackend as any);
|
||||
const supportedLang = req
|
||||
.acceptsLanguages()
|
||||
.find((l) => isSupportedLang(l));
|
||||
await instance.init<i18nextBackend.FsBackendOptions>({
|
||||
lng: supportedLang,
|
||||
fallbackLng: 'en',
|
||||
debug: false,
|
||||
backend: {
|
||||
loadPath: `assets/i18n/{{lng}}.json`,
|
||||
},
|
||||
});
|
||||
return new LocalizationService(instance);
|
||||
},
|
||||
inject: ['REQUEST'],
|
||||
};
|
||||
|
||||
export class LocalizationService {
|
||||
constructor(private readonly i18n: i18next.i18n) {}
|
||||
|
||||
translate(key: string, args?: Record<string, unknown>): string {
|
||||
return this.i18n.t(key, args);
|
||||
}
|
||||
}
|
||||
18
apps/backend/src/services/core/logger/logger.service.spec.ts
Normal file
18
apps/backend/src/services/core/logger/logger.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LoggerService } from './logger.service';
|
||||
|
||||
describe('LoggerService', () => {
|
||||
let service: LoggerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [LoggerService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LoggerService>(LoggerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
4
apps/backend/src/services/core/logger/logger.service.ts
Normal file
4
apps/backend/src/services/core/logger/logger.service.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class LoggerService {}
|
||||
18
apps/backend/src/services/core/mailer/mailer.service.spec.ts
Normal file
18
apps/backend/src/services/core/mailer/mailer.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MailerService } from './mailer.service';
|
||||
|
||||
describe('MailerService', () => {
|
||||
let service: MailerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [MailerService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MailerService>(MailerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
43
apps/backend/src/services/core/mailer/mailer.service.ts
Normal file
43
apps/backend/src/services/core/mailer/mailer.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import { SentMessageInfo, Options } from 'nodemailer/lib/smtp-transport';
|
||||
import { EnvService } from '../env/env.service';
|
||||
|
||||
@Injectable()
|
||||
export class MailerService {
|
||||
private readonly logger = new Logger('LoggerService');
|
||||
|
||||
private readonly transport: nodemailer.Transporter<SentMessageInfo, Options>;
|
||||
constructor(envService: EnvService) {
|
||||
const config = envService.config.mail;
|
||||
this.transport = nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: config.user,
|
||||
pass: config.password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async sendEmail(opts: {
|
||||
toAdresses: string[];
|
||||
subject: string;
|
||||
html: string;
|
||||
text?: string;
|
||||
}) {
|
||||
const res = await this.transport.sendMail({
|
||||
from: '"Cowsi" <info@cowsi.ch>',
|
||||
to: opts.toAdresses.join(','),
|
||||
subject: opts.subject,
|
||||
text: opts.text,
|
||||
html: opts.html,
|
||||
});
|
||||
if (res.rejected.length > 0) {
|
||||
this.logger.error('An issue occurred while sending email', res.rejected);
|
||||
} else {
|
||||
this.logger.log('Email sent successfuly');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
const MAIL_BASE = 'assets/ejs-templates/mail';
|
||||
|
||||
export interface BaseMailTempalteData {
|
||||
currentYear: number | string;
|
||||
}
|
||||
|
||||
export enum MailTemplateEnum {
|
||||
CreateAccount = 'create-account',
|
||||
}
|
||||
|
||||
export interface MailTemplateTypeMap extends Record<MailTemplateEnum, unknown> {
|
||||
[MailTemplateEnum.CreateAccount]: CreateAccountMailTemplateData;
|
||||
}
|
||||
|
||||
const MAIL_TEMPLATE_PATH_MAP = {
|
||||
[MailTemplateEnum.CreateAccount]: 'account-created.ejs',
|
||||
} as const satisfies Record<MailTemplateEnum, string>;
|
||||
|
||||
export interface CreateAccountMailTemplateData {
|
||||
title: string;
|
||||
bodyTitle: string;
|
||||
resetLink: string;
|
||||
}
|
||||
|
||||
export function getMailTemplatePath(template: MailTemplateEnum): string {
|
||||
const suffix = MAIL_TEMPLATE_PATH_MAP[template];
|
||||
return `${MAIL_BASE}/${suffix}`;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TemplateRendererService } from './template-renderer.service';
|
||||
|
||||
describe('TemplateRendererService', () => {
|
||||
let service: TemplateRendererService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [TemplateRendererService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TemplateRendererService>(TemplateRendererService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as ejs from 'ejs';
|
||||
import {
|
||||
BaseMailTempalteData,
|
||||
getMailTemplatePath,
|
||||
MailTemplateEnum,
|
||||
MailTemplateTypeMap,
|
||||
} from './mail-template-data';
|
||||
import * as dayjs from 'dayjs';
|
||||
|
||||
@Injectable()
|
||||
export class TemplateRendererService {
|
||||
renderMailTemplate<T extends MailTemplateEnum>(
|
||||
template: T,
|
||||
data: MailTemplateTypeMap[T],
|
||||
): Promise<string> {
|
||||
return this.render(getMailTemplatePath(template), data);
|
||||
}
|
||||
|
||||
private render(file: string, data: ejs.Data): Promise<string> {
|
||||
const base: BaseMailTempalteData = {
|
||||
currentYear: dayjs().year(),
|
||||
};
|
||||
return ejs.renderFile(file, {
|
||||
...data,
|
||||
...base,
|
||||
});
|
||||
}
|
||||
}
|
||||
23
documentation/bruno-rest-api/LocalAPI/Create user.bru
Normal file
23
documentation/bruno-rest-api/LocalAPI/Create user.bru
Normal file
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: Create user
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{BASE_URL}}/users
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"firstName": "Paul",
|
||||
"lastName": "Test",
|
||||
"email": "paul@coral.ch"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: asdf
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: localhost:5047
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -6,10 +6,17 @@ meta {
|
||||
|
||||
post {
|
||||
url: {{BASE_URL}}/auth/credentials
|
||||
body: none
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "paul@cowsi.ch",
|
||||
"password": "Password1234"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: reset password
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{BASE_URL}}/auth/reset
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"userId": "9",
|
||||
"resetToken": "baeaff51ad72152db8b55006e95ca605a375f4d5143b764bbe0d24a6dca02baf",
|
||||
"newPassword": "Password1234"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: hello
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{BASE_URL}}/hello
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
8
documentation/bruno-rest-api/LocalAPI/users/folder.bru
Normal file
8
documentation/bruno-rest-api/LocalAPI/users/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: users
|
||||
seq: 3
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
3
packages/common/src/models/auth/auth-token-payload.ts
Normal file
3
packages/common/src/models/auth/auth-token-payload.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface AuthTokenPayload {
|
||||
userId: string;
|
||||
}
|
||||
@@ -5,10 +5,4 @@ export const credentialAuthParser = z.object({
|
||||
password: z.string({ message: "invalid password" }),
|
||||
});
|
||||
|
||||
export const credentialAuthResponse = z.object({
|
||||
accessToken: z.string({ message: "invalid access tokens" }),
|
||||
});
|
||||
|
||||
export type CredentialAuth = z.infer<typeof credentialAuthParser>;
|
||||
|
||||
export type CredentialAuthResponse = z.infer<typeof credentialAuthResponse>;
|
||||
|
||||
3
packages/common/src/models/auth/index.ts
Normal file
3
packages/common/src/models/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./auth-token-payload";
|
||||
export * from "./credential-auth";
|
||||
export * from "./reset-auth";
|
||||
9
packages/common/src/models/auth/reset-auth.ts
Normal file
9
packages/common/src/models/auth/reset-auth.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import z from "zod";
|
||||
|
||||
export const resetAuthParser = z.object({
|
||||
userId: z.string(),
|
||||
resetToken: z.string(),
|
||||
newPassword: z.string(),
|
||||
});
|
||||
|
||||
export type ResetAuth = z.infer<typeof resetAuthParser>;
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./users";
|
||||
export * from "./lang";
|
||||
export * from "./auth";
|
||||
|
||||
6
packages/common/src/models/lang.ts
Normal file
6
packages/common/src/models/lang.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const SUPPORTED_LANGS = ["fr", "en"];
|
||||
export type SupportedLangs = (typeof SUPPORTED_LANGS)[number];
|
||||
|
||||
export function isSupportedLang(obj: unknown): obj is SupportedLangs {
|
||||
return (SUPPORTED_LANGS as unknown[]).includes(obj);
|
||||
}
|
||||
@@ -5,12 +5,22 @@ export enum UserTypeEnum {
|
||||
Professional = "professional",
|
||||
}
|
||||
|
||||
export const userParser = z.object({
|
||||
const userParser = z.object({
|
||||
id: z.string({ message: "inavlid user id" }),
|
||||
firstName: z.string({ message: "invalid user first name" }),
|
||||
lastName: z.string({ message: "invalid last name" }),
|
||||
email: z.string().email({ message: "invalid email" }),
|
||||
type: z.nativeEnum(UserTypeEnum, { message: "invalid user type" }),
|
||||
//type: z.nativeEnum(UserTypeEnum, { message: "invalid user type" }),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
deletedAt: z.date().optional(),
|
||||
});
|
||||
|
||||
export const createUserParser = userParser.pick({
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof userParser>;
|
||||
export type CreateUser = z.infer<typeof createUserParser>;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./common";
|
||||
export * from "./result";
|
||||
|
||||
32
packages/common/src/utils/result.ts
Normal file
32
packages/common/src/utils/result.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export type Result<S, E> = ResSuccess<S> | ResError<E>;
|
||||
|
||||
export const Result = {
|
||||
success<S>(data: S): Result<S, never> {
|
||||
return new ResSuccess(data);
|
||||
},
|
||||
error<E>(error: E): Result<never, E> {
|
||||
return new ResError(error);
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface BaseResult<S, E> {
|
||||
map<T>(f: (s: S) => T): Result<T, E>;
|
||||
}
|
||||
class ResSuccess<S> implements BaseResult<S, never> {
|
||||
readonly ok: true = true;
|
||||
|
||||
constructor(readonly data: S) {}
|
||||
|
||||
map<T>(f: (t: S) => T): Result<T, never> {
|
||||
return new ResSuccess(f(this.data));
|
||||
}
|
||||
}
|
||||
class ResError<E> implements BaseResult<never, E> {
|
||||
readonly ok: false = false;
|
||||
|
||||
constructor(readonly err: E) {}
|
||||
|
||||
map<T>(): Result<T, E> {
|
||||
return this as Result<T, E>;
|
||||
}
|
||||
}
|
||||
336
pnpm-lock.yaml
generated
336
pnpm-lock.yaml
generated
@@ -19,15 +19,54 @@ importers:
|
||||
'@nestjs/core':
|
||||
specifier: ^11.0.1
|
||||
version: 11.1.3(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/passport':
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||
'@nestjs/platform-express':
|
||||
specifier: ^11.0.1
|
||||
version: 11.1.3(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.10
|
||||
version: 9.0.10
|
||||
'@types/passport-local':
|
||||
specifier: ^1.0.38
|
||||
version: 1.0.38
|
||||
argon2:
|
||||
specifier: ^0.44.0
|
||||
version: 0.44.0
|
||||
cookie-parser:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
dotenv:
|
||||
specifier: ^17.2.1
|
||||
version: 17.2.1
|
||||
ejs:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.10
|
||||
i18next:
|
||||
specifier: ^25.3.2
|
||||
version: 25.3.2(typescript@5.7.3)
|
||||
i18next-fs-backend:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
kysely:
|
||||
specifier: ^0.28.3
|
||||
version: 0.28.3
|
||||
nodemailer:
|
||||
specifier: ^7.0.5
|
||||
version: 7.0.5
|
||||
passport:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
passport-local:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
pg:
|
||||
specifier: ^8.16.3
|
||||
version: 8.16.3
|
||||
@@ -62,6 +101,12 @@ importers:
|
||||
'@swc/core':
|
||||
specifier: ^1.10.7
|
||||
version: 1.12.11
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.9
|
||||
version: 1.4.9(@types/express@5.0.3)
|
||||
'@types/ejs':
|
||||
specifier: ^3.1.5
|
||||
version: 3.1.5
|
||||
'@types/express':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.3
|
||||
@@ -71,6 +116,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.10.7
|
||||
version: 22.16.3
|
||||
'@types/nodemailer':
|
||||
specifier: ^6.4.17
|
||||
version: 6.4.17
|
||||
'@types/pg':
|
||||
specifier: ^8.15.4
|
||||
version: 8.15.4
|
||||
@@ -324,6 +372,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.2':
|
||||
resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -347,6 +399,9 @@ packages:
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@epic-web/invariant@1.0.0':
|
||||
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0':
|
||||
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -788,6 +843,12 @@ packages:
|
||||
'@nestjs/websockets':
|
||||
optional: true
|
||||
|
||||
'@nestjs/passport@11.0.5':
|
||||
resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==}
|
||||
peerDependencies:
|
||||
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||
passport: ^0.5.0 || ^0.6.0 || ^0.7.0
|
||||
|
||||
'@nestjs/platform-express@11.1.3':
|
||||
resolution: {integrity: sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==}
|
||||
peerDependencies:
|
||||
@@ -836,6 +897,10 @@ packages:
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
|
||||
'@phc/format@1.0.0':
|
||||
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@pkgr/core@0.2.7':
|
||||
resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
@@ -983,9 +1048,17 @@ packages:
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cookie-parser@1.4.9':
|
||||
resolution: {integrity: sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==}
|
||||
peerDependencies:
|
||||
'@types/express': '*'
|
||||
|
||||
'@types/cookiejar@2.1.5':
|
||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||
|
||||
'@types/ejs@3.1.5':
|
||||
resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||
|
||||
@@ -1025,15 +1098,33 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||
|
||||
'@types/methods@1.1.4':
|
||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
'@types/node@22.16.3':
|
||||
resolution: {integrity: sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==}
|
||||
|
||||
'@types/nodemailer@6.4.17':
|
||||
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
|
||||
|
||||
'@types/passport-local@1.0.38':
|
||||
resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==}
|
||||
|
||||
'@types/passport-strategy@0.2.38':
|
||||
resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==}
|
||||
|
||||
'@types/passport@1.0.17':
|
||||
resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==}
|
||||
|
||||
'@types/pg@8.15.4':
|
||||
resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==}
|
||||
|
||||
@@ -1313,6 +1404,10 @@ packages:
|
||||
arg@4.1.3:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
|
||||
argon2@0.44.0:
|
||||
resolution: {integrity: sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
@@ -1408,6 +1503,9 @@ packages:
|
||||
buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -1579,6 +1677,13 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
@@ -1623,10 +1728,18 @@ packages:
|
||||
create-require@1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
|
||||
cross-env@10.0.0:
|
||||
resolution: {integrity: sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1712,6 +1825,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
@@ -2146,6 +2262,17 @@ packages:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
|
||||
i18next-fs-backend@2.6.0:
|
||||
resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==}
|
||||
|
||||
i18next@25.3.2:
|
||||
resolution: {integrity: sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2465,6 +2592,16 @@ packages:
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
|
||||
jwa@1.4.2:
|
||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
||||
|
||||
jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -2542,12 +2679,33 @@ packages:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lodash.includes@4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
|
||||
lodash.isboolean@3.0.3:
|
||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||
|
||||
lodash.isinteger@4.0.4:
|
||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||
|
||||
lodash.isnumber@3.0.3:
|
||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
|
||||
lodash.isstring@4.0.1:
|
||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||
|
||||
lodash.memoize@4.1.2:
|
||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash.once@4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
@@ -2701,15 +2859,27 @@ packages:
|
||||
node-abort-controller@3.1.1:
|
||||
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
||||
|
||||
node-addon-api@8.5.0:
|
||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-emoji@1.11.0:
|
||||
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
nodemailer@7.0.5:
|
||||
resolution: {integrity: sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2792,6 +2962,18 @@ packages:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
passport-local@1.0.0:
|
||||
resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
passport-strategy@1.0.0:
|
||||
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
passport@0.7.0:
|
||||
resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2819,6 +3001,9 @@ packages:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pause@0.0.1:
|
||||
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
|
||||
|
||||
peek-readable@5.4.2:
|
||||
resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -3065,11 +3250,6 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.1:
|
||||
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.2:
|
||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3470,6 +3650,10 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
utils-merge@1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
|
||||
@@ -3927,6 +4111,8 @@ snapshots:
|
||||
'@babel/core': 7.28.0
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/runtime@7.28.2': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -3959,6 +4145,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@epic-web/invariant@1.0.0': {}
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@1.21.7))':
|
||||
dependencies:
|
||||
eslint: 9.31.0(jiti@1.21.7)
|
||||
@@ -4484,6 +4672,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@nestjs/platform-express': 11.1.3(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)
|
||||
|
||||
'@nestjs/passport@11.0.5(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)':
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
passport: 0.7.0
|
||||
|
||||
'@nestjs/platform-express@11.1.3(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)':
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@@ -4548,6 +4741,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
'@phc/format@1.0.0': {}
|
||||
|
||||
'@pkgr/core@0.2.7': {}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
@@ -4683,8 +4878,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.16.3
|
||||
|
||||
'@types/cookie-parser@1.4.9(@types/express@5.0.3)':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
|
||||
'@types/cookiejar@2.1.5': {}
|
||||
|
||||
'@types/ejs@3.1.5': {}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
@@ -4735,14 +4936,40 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 22.16.3
|
||||
|
||||
'@types/methods@1.1.4': {}
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/node@22.16.3':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/nodemailer@6.4.17':
|
||||
dependencies:
|
||||
'@types/node': 22.16.3
|
||||
|
||||
'@types/passport-local@1.0.38':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
'@types/passport': 1.0.17
|
||||
'@types/passport-strategy': 0.2.38
|
||||
|
||||
'@types/passport-strategy@0.2.38':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
'@types/passport': 1.0.17
|
||||
|
||||
'@types/passport@1.0.17':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
|
||||
'@types/pg@8.15.4':
|
||||
dependencies:
|
||||
'@types/node': 22.16.3
|
||||
@@ -5105,6 +5332,13 @@ snapshots:
|
||||
|
||||
arg@4.1.3: {}
|
||||
|
||||
argon2@0.44.0:
|
||||
dependencies:
|
||||
'@phc/format': 1.0.0
|
||||
cross-env: 10.0.0
|
||||
node-addon-api: 8.5.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
@@ -5285,6 +5519,8 @@ snapshots:
|
||||
|
||||
buffer-crc32@0.2.13: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.7.1:
|
||||
@@ -5436,6 +5672,13 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.6
|
||||
|
||||
cookie-signature@1.0.6: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
@@ -5484,12 +5727,19 @@ snapshots:
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
cross-env@10.0.0:
|
||||
dependencies:
|
||||
'@epic-web/invariant': 1.0.0
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -5545,6 +5795,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
ejs@3.1.10:
|
||||
@@ -6045,6 +6299,14 @@ snapshots:
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
i18next-fs-backend@2.6.0: {}
|
||||
|
||||
i18next@25.3.2(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.2
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -6136,7 +6398,7 @@ snapshots:
|
||||
'@babel/parser': 7.28.0
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
semver: 7.7.1
|
||||
semver: 7.7.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6523,6 +6785,30 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
dependencies:
|
||||
jws: 3.2.2
|
||||
lodash.includes: 4.3.0
|
||||
lodash.isboolean: 3.0.3
|
||||
lodash.isinteger: 4.0.4
|
||||
lodash.isnumber: 3.0.3
|
||||
lodash.isplainobject: 4.0.6
|
||||
lodash.isstring: 4.0.1
|
||||
lodash.once: 4.1.1
|
||||
ms: 2.1.3
|
||||
semver: 7.7.2
|
||||
|
||||
jwa@1.4.2:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@3.2.2:
|
||||
dependencies:
|
||||
jwa: 1.4.2
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -6571,10 +6857,24 @@ snapshots:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lodash.includes@4.3.0: {}
|
||||
|
||||
lodash.isboolean@3.0.3: {}
|
||||
|
||||
lodash.isinteger@4.0.4: {}
|
||||
|
||||
lodash.isnumber@3.0.3: {}
|
||||
|
||||
lodash.isplainobject@4.0.6: {}
|
||||
|
||||
lodash.isstring@4.0.1: {}
|
||||
|
||||
lodash.memoize@4.1.2: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash.once@4.1.1: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
log-symbols@4.1.0:
|
||||
@@ -6695,14 +6995,20 @@ snapshots:
|
||||
|
||||
node-abort-controller@3.1.1: {}
|
||||
|
||||
node-addon-api@8.5.0: {}
|
||||
|
||||
node-emoji@1.11.0:
|
||||
dependencies:
|
||||
lodash: 4.17.21
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
node-int64@0.4.0: {}
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
nodemailer@7.0.5: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-url@8.0.2: {}
|
||||
@@ -6785,6 +7091,18 @@ snapshots:
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
passport-local@1.0.0:
|
||||
dependencies:
|
||||
passport-strategy: 1.0.0
|
||||
|
||||
passport-strategy@1.0.0: {}
|
||||
|
||||
passport@0.7.0:
|
||||
dependencies:
|
||||
passport-strategy: 1.0.0
|
||||
pause: 0.0.1
|
||||
utils-merge: 1.0.1
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
@@ -6802,6 +7120,8 @@ snapshots:
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pause@0.0.1: {}
|
||||
|
||||
peek-readable@5.4.2: {}
|
||||
|
||||
pend@1.2.0: {}
|
||||
@@ -7020,8 +7340,6 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.1: {}
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
send@1.2.0:
|
||||
@@ -7432,6 +7750,8 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
v8-compile-cache-lib@3.0.1: {}
|
||||
|
||||
v8-to-istanbul@9.3.0:
|
||||
|
||||
Reference in New Issue
Block a user