This commit is contained in:
parent
b4eb949a3e
commit
418a9e2c7a
17 changed files with 105 additions and 78 deletions
2
build.ts
2
build.ts
|
@ -1,4 +1,4 @@
|
||||||
// @ts-ignore
|
// @ts-expect-error bun build process
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
entrypoints: ['./src/app.ts'],
|
entrypoints: ['./src/app.ts'],
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
|
|
|
@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: ['eslint.config.mjs'],
|
ignores: ['eslint.config.mjs', 'dist/', 'node_modules/'],
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
@ -29,7 +29,7 @@ export default tseslint.config(
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-floating-promises': 'warn',
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { ApiTags } from '@nestjs/swagger';
|
||||||
@UseInterceptors(CacheInterceptor)
|
@UseInterceptors(CacheInterceptor)
|
||||||
export class AppController {
|
export class AppController {
|
||||||
@Get('version')
|
@Get('version')
|
||||||
async getVersion(): Promise<VersionEntity> {
|
getVersion(): VersionEntity {
|
||||||
return {
|
return {
|
||||||
version: process.env.npm_package_version,
|
version: process.env.npm_package_version,
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,10 +24,14 @@ import KeyvRedis from '@keyv/redis';
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
CacheModule.registerAsync({
|
CacheModule.registerAsync({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
useFactory: async (): Promise<any> => {
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
const redisUrl: string | undefined = process.env.REDIS_URL;
|
useFactory: async () => {
|
||||||
return {
|
return {
|
||||||
stores: [redisUrl ? new KeyvRedis(redisUrl) : new CacheableMemory()],
|
stores: [
|
||||||
|
process.env.REDIS_URL
|
||||||
|
? new KeyvRedis(process.env.REDIS_URL)
|
||||||
|
: new CacheableMemory(),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
31
src/app.ts
31
src/app.ts
|
@ -13,6 +13,8 @@ import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ConsoleLogger, Logger } from '@nestjs/common';
|
import { ConsoleLogger, Logger } from '@nestjs/common';
|
||||||
import * as process from 'process';
|
import * as process from 'process';
|
||||||
|
import { FastifyReply } from 'fastify/types/reply';
|
||||||
|
import { FastifyRequest } from 'fastify/types/request';
|
||||||
|
|
||||||
const logger: Logger = new Logger('App');
|
const logger: Logger = new Logger('App');
|
||||||
|
|
||||||
|
@ -50,17 +52,20 @@ async function loadServer(server: NestFastifyApplication) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
server.use(new LoggerMiddleware().use);
|
const loggerMiddleware = new LoggerMiddleware();
|
||||||
await server.register(fastifyMultipart as any);
|
const loggerMiddlewareUse = (
|
||||||
await server.register(
|
req: FastifyRequest['raw'],
|
||||||
fastifyHelmet as any,
|
res: FastifyReply['raw'],
|
||||||
{
|
next: () => void,
|
||||||
contentSecurityPolicy: false,
|
) => loggerMiddleware.use(req, res, next);
|
||||||
crossOriginEmbedderPolicy: false,
|
server.use(loggerMiddlewareUse);
|
||||||
crossOriginOpenerPolicy: false,
|
await server.register(fastifyMultipart);
|
||||||
crossOriginResourcePolicy: false,
|
await server.register(fastifyHelmet, {
|
||||||
} as any,
|
contentSecurityPolicy: false,
|
||||||
);
|
crossOriginEmbedderPolicy: false,
|
||||||
|
crossOriginOpenerPolicy: false,
|
||||||
|
crossOriginResourcePolicy: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Swagger
|
// Swagger
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
|
@ -88,4 +93,6 @@ async function loadServer(server: NestFastifyApplication) {
|
||||||
server.useGlobalPipes(new CustomValidationPipe());
|
server.useGlobalPipes(new CustomValidationPipe());
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@ export class LoggerMiddleware implements NestMiddleware {
|
||||||
|
|
||||||
use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
|
use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
(res as any).on('finish', () => {
|
res.on('finish', () => {
|
||||||
const path = req.url;
|
const path = req.url;
|
||||||
try {
|
try {
|
||||||
const protocol = LoggerMiddleware.getProtocol(req);
|
const protocol = LoggerMiddleware.getProtocol(req);
|
||||||
|
@ -15,7 +15,7 @@ export class LoggerMiddleware implements NestMiddleware {
|
||||||
if (method === 'OPTIONS') return;
|
if (method === 'OPTIONS') return;
|
||||||
const statusCode = res.statusCode;
|
const statusCode = res.statusCode;
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
const resSize: any = res.getHeader('Content-Length') || '0';
|
const resSize = res.getHeader('Content-Length').toString() || '0';
|
||||||
const intResSize = parseInt(resSize);
|
const intResSize = parseInt(resSize);
|
||||||
LoggerMiddleware.logger.log(
|
LoggerMiddleware.logger.log(
|
||||||
`${protocol} ${method} ${path} ${statusCode} ${duration}ms ${intResSize}`,
|
`${protocol} ${method} ${path} ${statusCode} ${duration}ms ${intResSize}`,
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import { Controller, Delete, NotImplementedException } from '@nestjs/common';
|
import { Controller, Delete, UseGuards } from '@nestjs/common';
|
||||||
|
import { UserEntity } from './models/entities/user.entity';
|
||||||
|
import { User } from './decorators/user.decorator';
|
||||||
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor() {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
@Delete('logout/all')
|
@Delete('logout/all')
|
||||||
logoutAll() {
|
@UseGuards(JwtAuthGuard)
|
||||||
throw new NotImplementedException();
|
@ApiBearerAuth()
|
||||||
|
async logoutAll(@User() user: UserEntity) {
|
||||||
|
await this.authService.invalidateTokens(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,17 @@ import { RegisterService } from './register.service';
|
||||||
import { UsersModule } from '../../../modules/users/users.module';
|
import { UsersModule } from '../../../modules/users/users.module';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AuthJwtStrategy } from './strategies/auth-jwt.strategy';
|
import { AuthJwtStrategy } from './strategies/auth-jwt.strategy';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AuthController, LoginController, RegisterController],
|
controllers: [AuthController, LoginController, RegisterController],
|
||||||
providers: [JwtStrategy, AuthJwtStrategy, LoginService, RegisterService],
|
providers: [
|
||||||
|
JwtStrategy,
|
||||||
|
AuthJwtStrategy,
|
||||||
|
LoginService,
|
||||||
|
RegisterService,
|
||||||
|
AuthService,
|
||||||
|
],
|
||||||
imports: [
|
imports: [
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
|
23
src/common/modules/auth/auth.service.ts
Normal file
23
src/common/modules/auth/auth.service.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { CipherService } from '../helper/cipher.service';
|
||||||
|
import { PrismaService } from '../helper/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { UserEntity } from './models/entities/user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly cipherService: CipherService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async invalidateTokens(user: UserEntity): Promise<void> {
|
||||||
|
await this.prismaService.users.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
token_id: this.cipherService.generateRandomBytes(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
12
src/common/modules/auth/decorators/user.decorator.ts
Normal file
12
src/common/modules/auth/decorators/user.decorator.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { UserEntity } from '../models/entities/user.entity';
|
||||||
|
|
||||||
|
export const User = createParamDecorator(
|
||||||
|
(_: unknown, ctx: ExecutionContext): UserEntity => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const request: any = ctx.switchToHttp().getRequest();
|
||||||
|
if (request.user.user) return request.user.user as UserEntity;
|
||||||
|
return request.user as UserEntity;
|
||||||
|
},
|
||||||
|
);
|
|
@ -1,10 +1,7 @@
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
HttpCode,
|
|
||||||
NotImplementedException,
|
|
||||||
Post,
|
Post,
|
||||||
Req,
|
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
@ -16,6 +13,7 @@ import { LoginService } from './login.service';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { UsersService } from '../../../modules/users/users.service';
|
import { UsersService } from '../../../modules/users/users.service';
|
||||||
|
import { User } from './decorators/user.decorator';
|
||||||
|
|
||||||
@Controller('auth/login')
|
@Controller('auth/login')
|
||||||
@ApiTags('Auth')
|
@ApiTags('Auth')
|
||||||
|
@ -60,35 +58,15 @@ export class LoginController {
|
||||||
@Post('callback')
|
@Post('callback')
|
||||||
@UseGuards(AuthGuard('auth-jwt'))
|
@UseGuards(AuthGuard('auth-jwt'))
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
loginCallback(@Req() req: any): Promise<LoginPayload> {
|
loginCallback(@User() user: UserEntity): LoginPayload {
|
||||||
// TODO: Fix this line
|
|
||||||
req = req.user;
|
|
||||||
if (req.scope === JwtScope.MAGIC) {
|
|
||||||
// Check for 2FA/Passkey
|
|
||||||
const token: string = this.loginService.generateToken(
|
|
||||||
req.user.id,
|
|
||||||
req.user.tokenId,
|
|
||||||
JwtScope.USAGE,
|
|
||||||
);
|
|
||||||
return new LoginPayload({
|
|
||||||
user: req.user,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const authToken: string = this.loginService.generateToken(
|
const authToken: string = this.loginService.generateToken(
|
||||||
req.user.id,
|
user.id,
|
||||||
req.user.tokenId,
|
user.tokenId,
|
||||||
JwtScope.USAGE,
|
JwtScope.USAGE,
|
||||||
);
|
);
|
||||||
return new LoginPayload({
|
return new LoginPayload({
|
||||||
user: req.user,
|
user,
|
||||||
token: authToken,
|
token: authToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('passkey')
|
|
||||||
loginPasskey() {
|
|
||||||
// Generate JWT token
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { UserEntity } from './models/entities/user.entity';
|
import { UserEntity } from './models/entities/user.entity';
|
||||||
import { CipherService } from '../helper/cipher.service';
|
import { CipherService } from '../helper/cipher.service';
|
||||||
import { PrismaService } from '../helper/prisma.service';
|
import { PrismaService } from '../helper/prisma.service';
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
import {
|
import { ConflictException, Injectable } from '@nestjs/common';
|
||||||
ConflictException,
|
|
||||||
Injectable,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../helper/prisma.service';
|
import { PrismaService } from '../helper/prisma.service';
|
||||||
import { CipherService } from '../helper/cipher.service';
|
import { CipherService } from '../helper/cipher.service';
|
||||||
import {
|
import { Users } from '@prisma/client';
|
||||||
EmailVerifications,
|
|
||||||
Passkeys,
|
|
||||||
TwoFactorAuth,
|
|
||||||
Users,
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { UserEntity } from './models/entities/user.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RegisterService {
|
export class RegisterService {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class AuthJwtStrategy extends PassportStrategy(Strategy, 'auth-jwt') {
|
||||||
throw new UnauthorizedException('Invalid token');
|
throw new UnauthorizedException('Invalid token');
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
scope: payload.scope,
|
scope: JwtScope.USAGE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ export class CipherService {
|
||||||
decipher.setAuthTag(tag);
|
decipher.setAuthTag(tag);
|
||||||
try {
|
try {
|
||||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||||
} catch (_) {
|
} catch {
|
||||||
throw new Error('Decryption failed');
|
throw new Error('Decryption failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,13 +142,13 @@ export class CipherService {
|
||||||
// Asymmetric functions
|
// Asymmetric functions
|
||||||
generateKeyPair(
|
generateKeyPair(
|
||||||
modulusLength = 4096,
|
modulusLength = 4096,
|
||||||
privateEncryptionKey = null,
|
privateEncryptionKey: string | null = null,
|
||||||
): crypto.KeyPairSyncResult<string, string> {
|
): crypto.KeyPairSyncResult<string, string> {
|
||||||
if (!privateEncryptionKey)
|
if (!privateEncryptionKey)
|
||||||
console.warn(
|
console.warn(
|
||||||
'No private encryption key provided, the private key will not be encrypted',
|
'No private encryption key provided, the private key will not be encrypted',
|
||||||
);
|
);
|
||||||
let options = undefined;
|
let options: { cipher: string; passphrase: string } | undefined = undefined;
|
||||||
if (privateEncryptionKey) {
|
if (privateEncryptionKey) {
|
||||||
options = {
|
options = {
|
||||||
cipher: 'aes-256-cbc',
|
cipher: 'aes-256-cbc',
|
||||||
|
@ -185,7 +185,7 @@ export class CipherService {
|
||||||
decryptAsymmetric(
|
decryptAsymmetric(
|
||||||
encryptedContent: string,
|
encryptedContent: string,
|
||||||
privateKey: string | Buffer,
|
privateKey: string | Buffer,
|
||||||
privateEncryptionKey = undefined,
|
privateEncryptionKey: string | undefined = undefined,
|
||||||
): string {
|
): string {
|
||||||
const buffer = Buffer.from(encryptedContent, 'base64');
|
const buffer = Buffer.from(encryptedContent, 'base64');
|
||||||
if (!privateEncryptionKey)
|
if (!privateEncryptionKey)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../common/modules/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/modules/auth/guards/jwt-auth.guard';
|
||||||
import { UserEntity } from '../../common/modules/auth/models/entities/user.entity';
|
import { UserEntity } from '../../common/modules/auth/models/entities/user.entity';
|
||||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { User } from 'src/common/modules/auth/decorators/user.decorator';
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
|
@ -10,7 +11,7 @@ export class UsersController {
|
||||||
@Get('me')
|
@Get('me')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
getMyself(@Req() req: any): UserEntity {
|
getMyself(@User() user: UserEntity): UserEntity {
|
||||||
return req.user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue