feat: added biome on pre-commit

This commit is contained in:
Hackntosh 2024-02-03 14:41:32 +00:00
parent 6f7aafef81
commit 66681673bc
41 changed files with 2530 additions and 772 deletions

View file

@ -1,5 +1,5 @@
if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') { if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
process.exit(0) process.exit(0);
} }
const husky = (await import('husky')).default const husky = (await import("husky")).default;
console.log(husky()) console.log(husky());

1
.husky/pre-commit Normal file
View file

@ -0,0 +1 @@
lint-staged

24
.swcrc
View file

@ -1,14 +1,14 @@
{ {
"$schema": "https://json.schemastore.org/swcrc", "$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true, "sourceMaps": true,
"jsc": { "jsc": {
"target": "esnext", "target": "esnext",
"parser": { "parser": {
"syntax": "typescript", "syntax": "typescript",
"decorators": true, "decorators": true,
"dynamicImport": true "dynamicImport": true
}, },
"baseUrl": "./" "baseUrl": "./"
}, },
"minify": false "minify": false
} }

View file

@ -1,5 +1,3 @@
{ {
"recommendations": [ "recommendations": ["biomejs.biome"]
"biomejs.biome"
]
} }

View file

@ -1,3 +1,3 @@
module.exports = { module.exports = {
extends: ['@commitlint/config-conventional'], extends: ["@commitlint/config-conventional"],
}; };

View file

@ -1,10 +1,10 @@
{ {
"$schema": "https://json.schemastore.org/nest-cli", "$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true, "deleteOutDir": true,
"builder": "swc", "builder": "swc",
"typeCheck": true "typeCheck": true
} }
} }

1753
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -61,6 +61,8 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.5.3", "@biomejs/biome": "1.5.3",
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
@ -81,6 +83,7 @@
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"husky": "^9.0.7", "husky": "^9.0.7",
"jest": "^29.5.0", "jest": "^29.5.0",
"lint-staged": "^15.2.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^6.3.3", "supertest": "^6.3.3",
@ -108,5 +111,8 @@
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
},
"lint-staged": {
"**.{js|ts|json}": "biome check --apply --no-errors-on-unmatched"
} }
} }

View file

@ -1,56 +1,56 @@
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { UserModule } from "./users/users.module"; import { ConfigModule } from "@nestjs/config";
import { APP_GUARD, APP_PIPE } from "@nestjs/core"; import { APP_GUARD, APP_PIPE } from "@nestjs/core";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { S3Module } from "nestjs-s3";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
import { ZodValidationPipe } from "nestjs-zod"; import { ZodValidationPipe } from "nestjs-zod";
import { AuthModule } from "./auth/auth.module"; import { AuthModule } from "./auth/auth.module";
import { ConfigModule } from "@nestjs/config";
import { JwtAuthGuard } from "./auth/jwt-auth.guard"; import { JwtAuthGuard } from "./auth/jwt-auth.guard";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
import { KweeksModule } from "./kweeks/kweeks.module"; import { KweeksModule } from "./kweeks/kweeks.module";
import { FastifyMulterModule } from "@nest-lab/fastify-multer"; import { UserModule } from "./users/users.module";
import { S3Module } from "nestjs-s3";
@Module({ @Module({
imports: [ imports: [
UserModule, UserModule,
AuthModule, AuthModule,
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
}), }),
ThrottlerModule.forRoot({ ThrottlerModule.forRoot({
throttlers: [{ limit: 10, ttl: 60000 }], throttlers: [{ limit: 10, ttl: 60000 }],
storage: new ThrottlerStorageRedisService( storage: new ThrottlerStorageRedisService(
`redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`, `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
), ),
}), }),
KweeksModule, KweeksModule,
FastifyMulterModule, FastifyMulterModule,
S3Module.forRoot({ S3Module.forRoot({
config: { config: {
credentials: { credentials: {
accessKeyId: process.env.MINIO_ROOT_USER, // CHANGE WHEN PRODUCTION TO S3 accessKeyId: process.env.MINIO_ROOT_USER, // CHANGE WHEN PRODUCTION TO S3
secretAccessKey: process.env.MINIO_ROOT_PASSWORD, secretAccessKey: process.env.MINIO_ROOT_PASSWORD,
}, },
region: "us-east-1", region: "us-east-1",
endpoint: process.env.MINIO_ENDPOINT, endpoint: process.env.MINIO_ENDPOINT,
forcePathStyle: true, forcePathStyle: true,
}, },
}), }),
], ],
providers: [ providers: [
{ {
provide: APP_PIPE, provide: APP_PIPE,
useClass: ZodValidationPipe, useClass: ZodValidationPipe,
}, },
{ {
provide: APP_GUARD, provide: APP_GUARD,
useClass: ThrottlerGuard, useClass: ThrottlerGuard,
}, },
{ {
provide: APP_GUARD, provide: APP_GUARD,
useClass: JwtAuthGuard, useClass: JwtAuthGuard,
}, },
], ],
}) })
export class AppModule {} export class AppModule {}

View file

@ -1,35 +1,35 @@
import { import {
Controller, Body,
Request, Controller,
Post, HttpCode,
UseGuards, Post,
Body, Request,
HttpCode, UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
import { import {
ApiOkResponse, ApiOkResponse,
ApiOperation, ApiOperation,
ApiTags, ApiTags,
ApiUnauthorizedResponse, ApiUnauthorizedResponse,
} from "@nestjs/swagger"; } from "@nestjs/swagger";
import { LocalAuthGuard } from "./local-auth.guard"; import { Public } from "src/public.decorator";
import { AuthService } from "./auth.service"; import { AuthService } from "./auth.service";
import { LoginUserDTO } from "./dto/login.dto"; import { LoginUserDTO } from "./dto/login.dto";
import { Public } from "src/public.decorator"; import { LocalAuthGuard } from "./local-auth.guard";
@ApiTags("Auth") @ApiTags("Auth")
@Controller("auth") @Controller("auth")
export class AuthController { export class AuthController {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
@Public() @Public()
@UseGuards(LocalAuthGuard) @UseGuards(LocalAuthGuard)
@Post("/") @Post("/")
@ApiOperation({ summary: "Authenticates a user" }) @ApiOperation({ summary: "Authenticates a user" })
@ApiOkResponse({ status: 200, description: "Authenticated successfully" }) @ApiOkResponse({ status: 200, description: "Authenticated successfully" })
@ApiUnauthorizedResponse({ description: "Wrong username or password" }) @ApiUnauthorizedResponse({ description: "Wrong username or password" })
@HttpCode(200) @HttpCode(200)
async login(@Request() req, @Body() _: LoginUserDTO) { async login(@Request() req, @Body() _: LoginUserDTO) {
return this.authService.login(req.user); return this.authService.login(req.user);
} }
} }

View file

@ -1,22 +1,22 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service"; import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport"; import { PassportModule } from "@nestjs/passport";
import { LocalStrategy } from "./local.strategy";
import { UserModule } from "src/users/users.module"; import { UserModule } from "src/users/users.module";
import { AuthController } from "./auth.controller"; import { AuthController } from "./auth.controller";
import { JwtModule } from "@nestjs/jwt"; import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt.strategy"; import { JwtStrategy } from "./jwt.strategy";
import { LocalStrategy } from "./local.strategy";
@Module({ @Module({
controllers: [AuthController], controllers: [AuthController],
imports: [ imports: [
UserModule, UserModule,
PassportModule, PassportModule,
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_ACCESS_SECRET, secret: process.env.JWT_ACCESS_SECRET,
signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens
}), }),
], ],
providers: [AuthService, LocalStrategy, JwtStrategy], providers: [AuthService, LocalStrategy, JwtStrategy],
}) })
export class AuthModule {} export class AuthModule {}

View file

@ -1,46 +1,46 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { UserService } from "src/users/users.service"; import { JwtService } from "@nestjs/jwt";
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt";
import { UserModel } from "src/users/models/user.model"; import { UserModel } from "src/users/models/user.model";
import { JwtService } from "@nestjs/jwt"; import { UserService } from "src/users/users.service";
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
private userService: UserService, private userService: UserService,
private jwtService: JwtService, private jwtService: JwtService,
) {} ) {}
async validateUser( async validateUser(
username: string, username: string,
password: string, password: string,
): Promise<UserModel | null> { ): Promise<UserModel | null> {
const user = await this.userService.auth_search(username); const user = await this.userService.auth_search(username);
if (user === undefined) { if (user === undefined) {
return null; return null;
} }
const validation = await bcrypt.compare(password, user.password); const validation = await bcrypt.compare(password, user.password);
if (user && validation) { if (user && validation) {
const { password, ...result } = user; const { password, ...result } = user;
return result; return result;
} }
return null; return null;
} }
async login(user: UserModel): Promise<{ token: string }> { async login(user: UserModel): Promise<{ token: string }> {
const payload = { const payload = {
displayName: user.displayName, displayName: user.displayName,
username: user.username, username: user.username,
profileImage: user.profileImage, profileImage: user.profileImage,
sub: user.id, sub: user.id,
}; };
return { return {
token: this.jwtService.sign(payload), token: this.jwtService.sign(payload),
}; };
} }
} }

View file

@ -2,28 +2,28 @@ import { createZodDto } from "nestjs-zod";
import { z } from "nestjs-zod/z"; import { z } from "nestjs-zod/z";
export const LoginUserSchema = z export const LoginUserSchema = z
.object({ .object({
username: z username: z
.string({ .string({
required_error: "Username is required", required_error: "Username is required",
}) })
.regex( .regex(
/^[a-zA-Z0-9_.]{5,15}$/, /^[a-zA-Z0-9_.]{5,15}$/,
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters", "The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
) )
.toLowerCase(), .toLowerCase(),
password: z password: z
.password({ .password({
required_error: "Password is required", required_error: "Password is required",
}) })
.min(8) .min(8)
.max(32) .max(32)
.atLeastOne("digit") .atLeastOne("digit")
.atLeastOne("uppercase") .atLeastOne("uppercase")
.atLeastOne("lowercase") .atLeastOne("lowercase")
.atLeastOne("special") .atLeastOne("special")
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace .transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
}) })
.required(); .required();
export class LoginUserDTO extends createZodDto(LoginUserSchema) {} export class LoginUserDTO extends createZodDto(LoginUserSchema) {}

View file

@ -6,20 +6,20 @@ import { IS_PUBLIC_KEY } from "src/public.decorator";
@Injectable() @Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") { export class JwtAuthGuard extends AuthGuard("jwt") {
constructor(private reflector: Reflector) { constructor(private reflector: Reflector) {
super(); super();
} }
canActivate( canActivate(
context: ExecutionContext, context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> { ): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]); ]);
if (isPublic) { if (isPublic) {
return true; return true;
} }
return super.canActivate(context); return super.canActivate(context);
} }
} }

View file

@ -3,26 +3,26 @@ import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt"; import { ExtractJwt, Strategy } from "passport-jwt";
type Payload = { type Payload = {
displayName: string; displayName: string;
username: string; username: string;
sub: string; sub: string;
}; };
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() { constructor() {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: process.env.JWT_ACCESS_SECRET, secretOrKey: process.env.JWT_ACCESS_SECRET,
}); });
} }
async validate(payload: Payload) { async validate(payload: Payload) {
return { return {
displayName: payload.displayName, displayName: payload.displayName,
username: payload.username, username: payload.username,
id: payload.sub, id: payload.sub,
}; };
} }
} }

View file

@ -1,20 +1,20 @@
import { Injectable, UnauthorizedException } from "@nestjs/common"; import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local"; import { Strategy } from "passport-local";
import { AuthService } from "./auth.service";
import { UserModel } from "src/users/models/user.model"; import { UserModel } from "src/users/models/user.model";
import { AuthService } from "./auth.service";
@Injectable() @Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) { export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) { constructor(private authService: AuthService) {
super(); super();
} }
async validate(username: string, password: string): Promise<UserModel> { async validate(username: string, password: string): Promise<UserModel> {
const user = await this.authService.validateUser(username, password); const user = await this.authService.validateUser(username, password);
if (!user) { if (!user) {
throw new UnauthorizedException("Wrong username or password"); throw new UnauthorizedException("Wrong username or password");
} }
return user; return user;
} }
} }

View file

@ -2,10 +2,10 @@ import { createZodDto } from "nestjs-zod";
import { z } from "nestjs-zod/z"; import { z } from "nestjs-zod/z";
export const CreateKweekSchema = z export const CreateKweekSchema = z
.object({ .object({
content: z.string({ required_error: "Kweek content is required" }).max(300), content: z.string({ required_error: "Kweek content is required" }).max(300),
files: z.array(z.object({})), files: z.array(z.object({})),
}) })
.required(); .required();
export class CreateKweekDTO extends createZodDto(CreateKweekSchema) {} export class CreateKweekDTO extends createZodDto(CreateKweekSchema) {}

View file

@ -1,4 +1,4 @@
import { PartialType } from '@nestjs/swagger'; import { PartialType } from "@nestjs/swagger";
import { CreateKweekDto } from './create-kweek.dto'; import { CreateKweekDto } from "./create-kweek.dto";
export class UpdateKweekDto extends PartialType(CreateKweekDto) {} export class UpdateKweekDto extends PartialType(CreateKweekDto) {}

View file

@ -37,7 +37,7 @@ export class KweeksController {
@UploadedFiles() attachments: File, @UploadedFiles() attachments: File,
@Request() req, @Request() req,
) { ) {
return this.kweeksService.create(createKweekDto); return this.kweeksService.create(createKweekDto);
} }
@Public() @Public()

View file

@ -1,11 +1,11 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { KweeksService } from "./kweeks.service";
import { KweeksController } from "./kweeks.controller";
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from "src/prisma/prisma.module";
import { KweeksController } from "./kweeks.controller";
import { KweeksService } from "./kweeks.service";
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],
controllers: [KweeksController], controllers: [KweeksController],
providers: [KweeksService], providers: [KweeksService],
}) })
export class KweeksModule {} export class KweeksModule {}

View file

@ -1,24 +1,24 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateKweekDTO } from "./dto/create-kweek.dto"; import { CreateKweekDTO } from "./dto/create-kweek.dto";
import { UpdateKweekDto } from "./dto/update-kweek.dto"; import { UpdateKweekDto } from "./dto/update-kweek.dto";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable() @Injectable()
export class KweeksService { export class KweeksService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
create(createKweekDto: CreateKweekDTO) { create(createKweekDto: CreateKweekDTO) {
return "This action adds a new kweek"; return "This action adds a new kweek";
} }
findOne(id: number) { findOne(id: number) {
return `This action returns a #${id} kweek`; return `This action returns a #${id} kweek`;
} }
update(id: number, updateKweekDto: UpdateKweekDto) { update(id: number, updateKweekDto: UpdateKweekDto) {
return `This action updates a #${id} kweek`; return `This action updates a #${id} kweek`;
} }
remove(id: number) { remove(id: number) {
return `This action removes a #${id} kweek`; return `This action removes a #${id} kweek`;
} }
} }

View file

@ -1,51 +1,51 @@
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";
import { patchNestJsSwagger } from "nestjs-zod";
import {
FastifyAdapter,
NestFastifyApplication,
} from "@nestjs/platform-fastify";
import * as helmet from "@fastify/helmet"; import * as helmet from "@fastify/helmet";
import { NestFactory } from "@nestjs/core";
import {
FastifyAdapter,
NestFastifyApplication,
} from "@nestjs/platform-fastify";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { patchNestJsSwagger } from "nestjs-zod";
import { AppModule } from "./app.module";
// TODO: File Upload (Posts and User Profile Image) // TODO: File Upload (Posts and User Profile Image)
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestFastifyApplication>(
AppModule, AppModule,
new FastifyAdapter({ logger: true }), new FastifyAdapter({ logger: true }),
); );
patchNestJsSwagger(); patchNestJsSwagger();
app.enableCors(); app.enableCors();
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle("Project Knedita") .setTitle("Project Knedita")
.setDescription("An open-source social media") .setDescription("An open-source social media")
.setVersion("1.0") .setVersion("1.0")
.addBearerAuth( .addBearerAuth(
{ {
type: "http", type: "http",
scheme: "bearer", scheme: "bearer",
bearerFormat: "JWT", bearerFormat: "JWT",
name: "JWT", name: "JWT",
description: "Enter JWT Token", description: "Enter JWT Token",
in: "header", in: "header",
}, },
"JWT", "JWT",
) )
.addTag("Auth") .addTag("Auth")
.addTag("Kweeks") .addTag("Kweeks")
.addTag("Users") .addTag("Users")
.build(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("/", app, document); SwaggerModule.setup("/", app, document);
await app.register(helmet); await app.register(helmet);
await app.listen(process.env.SERVER_PORT, process.env.SERVER_HOST); await app.listen(process.env.SERVER_PORT, process.env.SERVER_HOST);
} }
bootstrap(); bootstrap();

View file

@ -2,7 +2,7 @@ import { Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service"; import { PrismaService } from "./prisma.service";
@Module({ @Module({
providers: [PrismaService], providers: [PrismaService],
exports: [PrismaService], exports: [PrismaService],
}) })
export class PrismaModule {} export class PrismaModule {}

View file

@ -3,16 +3,16 @@ import { Prisma, PrismaClient } from "@prisma/client";
@Injectable() @Injectable()
export class PrismaService export class PrismaService
extends PrismaClient<Prisma.PrismaClientOptions, "beforeExit"> extends PrismaClient<Prisma.PrismaClientOptions, "beforeExit">
implements OnModuleInit implements OnModuleInit
{ {
async onModuleInit() { async onModuleInit() {
await this.$connect(); await this.$connect();
} }
async enableShutdownHooks(app: INestApplication) { async enableShutdownHooks(app: INestApplication) {
this.$on("beforeExit", async () => { this.$on("beforeExit", async () => {
await app.close(); await app.close();
}); });
} }
} }

View file

@ -2,33 +2,33 @@ import { createZodDto } from "nestjs-zod";
import { z } from "nestjs-zod/z"; import { z } from "nestjs-zod/z";
export const CreateUserSchema = z export const CreateUserSchema = z
.object({ .object({
username: z username: z
.string({ .string({
required_error: "Username is required", required_error: "Username is required",
}) })
.regex( .regex(
/^[a-zA-Z0-9_.]{5,15}$/, /^[a-zA-Z0-9_.]{5,15}$/,
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters", "The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
) )
.toLowerCase(), .toLowerCase(),
email: z email: z
.string({ .string({
required_error: "Email is required", required_error: "Email is required",
}) })
.email("Invalid email"), .email("Invalid email"),
password: z password: z
.password({ .password({
required_error: "Password is required", required_error: "Password is required",
}) })
.min(8) .min(8)
.max(32) .max(32)
.atLeastOne("digit") .atLeastOne("digit")
.atLeastOne("uppercase") .atLeastOne("uppercase")
.atLeastOne("lowercase") .atLeastOne("lowercase")
.atLeastOne("special") .atLeastOne("special")
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace .transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
}) })
.required(); .required();
export class CreateUserDTO extends createZodDto(CreateUserSchema) {} export class CreateUserDTO extends createZodDto(CreateUserSchema) {}

View file

@ -2,13 +2,13 @@ import { createZodDto } from "nestjs-zod";
import { z } from "nestjs-zod/z"; import { z } from "nestjs-zod/z";
export const UpdateEmailSchema = z export const UpdateEmailSchema = z
.object({ .object({
email: z email: z
.string({ .string({
required_error: "Email is required", required_error: "Email is required",
}) })
.email("Invalid email"), .email("Invalid email"),
}) })
.required(); .required();
export class UpdateEmailDTO extends createZodDto(UpdateEmailSchema) {} export class UpdateEmailDTO extends createZodDto(UpdateEmailSchema) {}

View file

@ -2,19 +2,19 @@ import { createZodDto } from "nestjs-zod";
import { z } from "nestjs-zod/z"; import { z } from "nestjs-zod/z";
export const UpdateNameSchema = z export const UpdateNameSchema = z
.object({ .object({
username: z username: z
.string() .string()
.regex( .regex(
/^[a-zA-Z0-9_.]{5,15}$/, /^[a-zA-Z0-9_.]{5,15}$/,
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters", "The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
) )
.toLowerCase() .toLowerCase()
.describe("New username - optional") .describe("New username - optional")
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
displayName: z.string({ required_error: "Display name is required" }), displayName: z.string({ required_error: "Display name is required" }),
}) })
.required(); .required();
export class UpdateNameDTO extends createZodDto(UpdateNameSchema) {} export class UpdateNameDTO extends createZodDto(UpdateNameSchema) {}

View file

@ -4,30 +4,30 @@ import { z } from "nestjs-zod/z";
// TODO: see if it can be refactored // TODO: see if it can be refactored
export const UpdatePasswordSchema = z export const UpdatePasswordSchema = z
.object({ .object({
old_password: z old_password: z
.password({ .password({
required_error: "Password is required", required_error: "Password is required",
}) })
.min(8) .min(8)
.max(32) .max(32)
.atLeastOne("digit") .atLeastOne("digit")
.atLeastOne("uppercase") .atLeastOne("uppercase")
.atLeastOne("lowercase") .atLeastOne("lowercase")
.atLeastOne("special") .atLeastOne("special")
.transform((value) => value.replace(/\s+/g, "")), .transform((value) => value.replace(/\s+/g, "")),
new_password: z new_password: z
.password({ .password({
required_error: "Password is required", required_error: "Password is required",
}) })
.min(8) .min(8)
.max(32) .max(32)
.atLeastOne("digit") .atLeastOne("digit")
.atLeastOne("uppercase") .atLeastOne("uppercase")
.atLeastOne("lowercase") .atLeastOne("lowercase")
.atLeastOne("special") .atLeastOne("special")
.transform((value) => value.replace(/\s+/g, "")), .transform((value) => value.replace(/\s+/g, "")),
}) })
.required(); .required();
export class UpdatePasswordDTO extends createZodDto(UpdatePasswordSchema) {} export class UpdatePasswordDTO extends createZodDto(UpdatePasswordSchema) {}

View file

@ -2,21 +2,21 @@ import { createZodDto } from "nestjs-zod";
import { z } from "nestjs-zod/z"; import { z } from "nestjs-zod/z";
export const UserSchema = z export const UserSchema = z
.object({ .object({
id: z.string().uuid(), id: z.string().uuid(),
displayName: z.string().optional(), displayName: z.string().optional(),
username: z.string(), username: z.string(),
email: z.string().email(), email: z.string().email(),
password: z.password(), password: z.password(),
kweeks: z.array(z.object({})).optional(), kweeks: z.array(z.object({})).optional(),
profileImage: z.string().url().optional(), profileImage: z.string().url().optional(),
likedKweeks: z.array(z.object({})).optional(), likedKweeks: z.array(z.object({})).optional(),
likedComments: z.array(z.object({})).optional(), likedComments: z.array(z.object({})).optional(),
followers: z.number(), followers: z.number(),
following: z.number(), following: z.number(),
kweeksComments: z.array(z.object({})).optional(), kweeksComments: z.array(z.object({})).optional(),
createdAt: z.date(), createdAt: z.date(),
}) })
.required(); .required();
export class UserModel extends createZodDto(UserSchema) {} export class UserModel extends createZodDto(UserSchema) {}

View file

@ -5,34 +5,34 @@ import sharp from "sharp";
@Injectable() @Injectable()
export class S3Service { export class S3Service {
constructor(@InjectS3() private readonly s3: S3) {} constructor(@InjectS3() private readonly s3: S3) {}
/** /**
* Returns the image url if the upload to minio was successful. * Returns the image url if the upload to minio was successful.
*/ */
async uploadImageToMinio(userID: string, buffer: Buffer): Promise<string> { async uploadImageToMinio(userID: string, buffer: Buffer): Promise<string> {
const compressedBuffer = await sharp(buffer) const compressedBuffer = await sharp(buffer)
.resize(200, 200) .resize(200, 200)
.webp({ quality: 70 }) .webp({ quality: 70 })
.toBuffer(); .toBuffer();
const uploadParams: PutObjectCommandInput = { const uploadParams: PutObjectCommandInput = {
Bucket: process.env.MINIO_DEFAULT_BUCKETS, Bucket: process.env.MINIO_DEFAULT_BUCKETS,
Key: `profile_images/${userID}.webp`, Key: `profile_images/${userID}.webp`,
Body: compressedBuffer, Body: compressedBuffer,
ContentType: "image/webp", ContentType: "image/webp",
ContentDisposition: "inline", ContentDisposition: "inline",
ACL: "public-read", ACL: "public-read",
}; };
const { ETag } = await this.s3.send(new PutObjectCommand(uploadParams)); const { ETag } = await this.s3.send(new PutObjectCommand(uploadParams));
if (ETag !== null) { if (ETag !== null) {
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`; return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`;
} }
throw new InternalServerErrorException( throw new InternalServerErrorException(
"Failed to upload the profile image", "Failed to upload the profile image",
); );
} }
} }

View file

@ -1,14 +1,14 @@
const UploadImageSchema = { const UploadImageSchema = {
required: true, required: true,
schema: { schema: {
type: "object", type: "object",
properties: { properties: {
image: { image: {
type: "string", type: "string",
format: "binary", format: "binary",
}, },
}, },
}, },
}; };
export default UploadImageSchema; export default UploadImageSchema;

View file

@ -1,5 +1,5 @@
export type User = { export type User = {
displayName: string; displayName: string;
username: string; username: string;
id: string; id: string;
}; };

View file

@ -1,129 +1,129 @@
import { File, FileInterceptor } from "@nest-lab/fastify-multer";
import { import {
Body, Body,
Controller, Controller,
Delete, Delete,
Get, Get,
HttpCode, HttpCode,
Param, Param,
Patch, Patch,
Post, Post,
Request, Request,
UploadedFile, UploadedFile,
UseInterceptors, UseInterceptors,
} from "@nestjs/common"; } from "@nestjs/common";
import { import {
ApiBadRequestResponse, ApiBadRequestResponse,
ApiBearerAuth, ApiBearerAuth,
ApiBody, ApiBody,
ApiConsumes, ApiConsumes,
ApiCreatedResponse, ApiCreatedResponse,
ApiNotFoundResponse, ApiNotFoundResponse,
ApiOperation, ApiOperation,
ApiTags, ApiTags,
ApiUnauthorizedResponse, ApiUnauthorizedResponse,
} from "@nestjs/swagger"; } from "@nestjs/swagger";
import { UserService } from "./users.service";
import { CreateUserDTO } from "./dto/create-user.dto";
import { Public } from "src/public.decorator"; import { Public } from "src/public.decorator";
import { UpdateNameDTO } from "./dto/update-name.dto";
import { User } from "./types/user.type";
import { UpdateEmailDTO } from "./dto/update-email.dto";
import { UpdatePasswordDTO } from "./dto/update-password.dto";
import { File, FileInterceptor } from "@nest-lab/fastify-multer";
import { BufferValidator } from "src/validators/buffer-validator.pipe"; import { BufferValidator } from "src/validators/buffer-validator.pipe";
import UploadImageSchema from "./schemas/upload-image.schema";
import UploadImageValidator from "src/validators/upload-image.validator"; import UploadImageValidator from "src/validators/upload-image.validator";
import { CreateUserDTO } from "./dto/create-user.dto";
import { UpdateEmailDTO } from "./dto/update-email.dto";
import { UpdateNameDTO } from "./dto/update-name.dto";
import { UpdatePasswordDTO } from "./dto/update-password.dto";
import UploadImageSchema from "./schemas/upload-image.schema";
import { User } from "./types/user.type";
import { UserService } from "./users.service";
@ApiTags("Users") @ApiTags("Users")
@Controller("users") @Controller("users")
export class UserController { export class UserController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}
// POST // POST
@Public() @Public()
@Post() @Post()
@ApiOperation({ summary: "Creates a new account" }) @ApiOperation({ summary: "Creates a new account" })
@ApiCreatedResponse({ description: "Account created successfully" }) @ApiCreatedResponse({ description: "Account created successfully" })
@ApiBadRequestResponse({ @ApiBadRequestResponse({
description: description:
"Missing field / Invalid username / Invalid email / Weak password", "Missing field / Invalid username / Invalid email / Weak password",
}) })
create(@Body() createUserDTO: CreateUserDTO) { create(@Body() createUserDTO: CreateUserDTO) {
return this.userService.create(createUserDTO); return this.userService.create(createUserDTO);
} }
// GET // GET
@Get("/profile") @Get("/profile")
@ApiOperation({ summary: "Returns information about the logged user" }) @ApiOperation({ summary: "Returns information about the logged user" })
@ApiBearerAuth("JWT") @ApiBearerAuth("JWT")
@ApiUnauthorizedResponse({ @ApiUnauthorizedResponse({
description: "Not authenticated / Invalid JWT Token", description: "Not authenticated / Invalid JWT Token",
}) })
me(@Request() req) { me(@Request() req) {
return req.user; return req.user;
} }
@Public() @Public()
@Get(":username") @Get(":username")
@ApiOperation({ summary: "Returns information about a user" }) @ApiOperation({ summary: "Returns information about a user" })
@ApiNotFoundResponse({ description: "User not found" }) @ApiNotFoundResponse({ description: "User not found" })
@HttpCode(200) @HttpCode(200)
info(@Param("username") username: string) { info(@Param("username") username: string) {
return this.userService.info(username); return this.userService.info(username);
} }
// PATCH // PATCH
@Patch() @Patch()
@ApiOperation({ @ApiOperation({
summary: "Updates the username or display name of a logged user", summary: "Updates the username or display name of a logged user",
}) })
@ApiBearerAuth("JWT") @ApiBearerAuth("JWT")
updateName(@Body() { displayName, username }: UpdateNameDTO, @Request() req) { updateName(@Body() { displayName, username }: UpdateNameDTO, @Request() req) {
return this.userService.updateName(req.user as User, username, displayName); return this.userService.updateName(req.user as User, username, displayName);
} }
@Patch("/email") @Patch("/email")
@ApiOperation({ summary: "Updates the email of a logged user" }) @ApiOperation({ summary: "Updates the email of a logged user" })
@ApiBearerAuth("JWT") @ApiBearerAuth("JWT")
updateEmail(@Body() body: UpdateEmailDTO, @Request() req) { updateEmail(@Body() body: UpdateEmailDTO, @Request() req) {
return this.userService.updateEmail(req.user as User, body.email); return this.userService.updateEmail(req.user as User, body.email);
} }
@Patch("/password") @Patch("/password")
@ApiOperation({ summary: "Updates the password of a logged user" }) @ApiOperation({ summary: "Updates the password of a logged user" })
@ApiBearerAuth("JWT") @ApiBearerAuth("JWT")
updatePassword( updatePassword(
@Body() { old_password, new_password }: UpdatePasswordDTO, @Body() { old_password, new_password }: UpdatePasswordDTO,
@Request() req, @Request() req,
) { ) {
return this.userService.updatePassword( return this.userService.updatePassword(
req.user as User, req.user as User,
old_password, old_password,
new_password, new_password,
); );
} }
@Patch("/image") @Patch("/image")
@ApiOperation({ @ApiOperation({
summary: "Add a profile image", summary: "Add a profile image",
}) })
@ApiBearerAuth("JWT") @ApiBearerAuth("JWT")
@UseInterceptors(FileInterceptor("image")) @UseInterceptors(FileInterceptor("image"))
@ApiConsumes("multipart/form-data") @ApiConsumes("multipart/form-data")
@ApiBody(UploadImageSchema) @ApiBody(UploadImageSchema)
uploadProfileImage( uploadProfileImage(
@UploadedFile( @UploadedFile(
UploadImageValidator, UploadImageValidator,
new BufferValidator(), // Magic number validation new BufferValidator(), // Magic number validation
) )
image: File, image: File,
@Request() req, @Request() req,
) { ) {
return this.userService.uploadImage(req.user, image); return this.userService.uploadImage(req.user, image);
} }
// DELETE // DELETE
@Delete() @Delete()
@ApiOperation({ summary: "Deletes the account of a logged user" }) @ApiOperation({ summary: "Deletes the account of a logged user" })
@ApiBearerAuth("JWT") @ApiBearerAuth("JWT")
remove() {} remove() {}
} }

View file

@ -1,13 +1,13 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { UserController } from "./users.controller";
import { UserService } from "./users.service";
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from "src/prisma/prisma.module";
import { S3Service } from "./s3.service"; import { S3Service } from "./s3.service";
import { UserController } from "./users.controller";
import { UserService } from "./users.service";
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService, S3Service], providers: [UserService, S3Service],
exports: [UserService], exports: [UserService],
}) })
export class UserModule {} export class UserModule {}

View file

@ -1,221 +1,221 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { CreateUserDTO } from "./dto/create-user.dto";
import { PrismaService } from "src/prisma/prisma.service";
import { UserModel } from "./models/user.model";
import * as bcrypt from "bcrypt";
import { User } from "./types/user.type";
import { File } from "@nest-lab/fastify-multer"; import { File } from "@nest-lab/fastify-multer";
import {
BadRequestException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import * as bcrypt from "bcrypt";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateUserDTO } from "./dto/create-user.dto";
import { UserModel } from "./models/user.model";
import { S3Service } from "./s3.service"; import { S3Service } from "./s3.service";
import { User } from "./types/user.type";
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly s3: S3Service, private readonly s3: S3Service,
) {} ) {}
async auth_search(username: string): Promise<UserModel> { async auth_search(username: string): Promise<UserModel> {
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { where: {
username, username,
}, },
select: { select: {
id: true, id: true,
profileImage: true, profileImage: true,
displayName: true, displayName: true,
username: true, username: true,
password: true, password: true,
}, },
}); });
if (user == null) { if (user == null) {
return undefined; return undefined;
} }
return user; return user;
} }
async info(username: string): Promise<UserModel> { async info(username: string): Promise<UserModel> {
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { username }, where: { username },
select: { select: {
id: true, id: true,
profileImage: true, profileImage: true,
displayName: true, displayName: true,
username: true, username: true,
createdAt: true, createdAt: true,
followers: true, followers: true,
following: true, following: true,
kweeks: { kweeks: {
select: { select: {
id: true, id: true,
content: true, content: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
}, },
}, },
}, },
}); });
if (user === null) { if (user === null) {
throw new NotFoundException("User not found"); throw new NotFoundException("User not found");
} }
return { return {
...user, ...user,
followers: user.followers.length, followers: user.followers.length,
following: user.following.length, following: user.following.length,
}; };
} }
async create({ async create({
username, username,
email, email,
password, password,
}: CreateUserDTO): Promise< }: CreateUserDTO): Promise<
Pick<UserModel, "displayName" | "username" | "createdAt"> Pick<UserModel, "displayName" | "username" | "createdAt">
> { > {
if ((await this.prisma.user.findFirst({ where: { username } })) != null) { if ((await this.prisma.user.findFirst({ where: { username } })) != null) {
throw new BadRequestException("Username already in use"); throw new BadRequestException("Username already in use");
} }
if ((await this.prisma.user.findFirst({ where: { email } })) != null) { if ((await this.prisma.user.findFirst({ where: { email } })) != null) {
throw new BadRequestException("Email already in use"); throw new BadRequestException("Email already in use");
} }
// Password encryption // Password encryption
const salt = await bcrypt.genSalt(15); const salt = await bcrypt.genSalt(15);
const hash = await bcrypt.hash(password, salt); const hash = await bcrypt.hash(password, salt);
const user = await this.prisma.user.create({ const user = await this.prisma.user.create({
data: { data: {
username, username,
email, email,
password: hash, password: hash,
}, },
select: { select: {
displayName: true, displayName: true,
username: true, username: true,
createdAt: true, createdAt: true,
}, },
}); });
return user; return user;
} }
async updateEmail( async updateEmail(
loggedUser: User, loggedUser: User,
email: string, email: string,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { id: loggedUser.id }, where: { id: loggedUser.id },
}); });
if (email !== undefined && email.trim() !== user.email) { if (email !== undefined && email.trim() !== user.email) {
const isAlreadyInUse = await this.prisma.user.findFirst({ const isAlreadyInUse = await this.prisma.user.findFirst({
where: { email }, where: { email },
}); });
if (isAlreadyInUse != null && isAlreadyInUse.email !== user.email) { if (isAlreadyInUse != null && isAlreadyInUse.email !== user.email) {
throw new BadRequestException("Email already in use"); throw new BadRequestException("Email already in use");
} }
await this.prisma.user.update({ await this.prisma.user.update({
where: { where: {
id: loggedUser.id, id: loggedUser.id,
}, },
data: { data: {
email: email ?? user.email, email: email ?? user.email,
}, },
}); });
return { message: "Email updated successfully" }; return { message: "Email updated successfully" };
} }
} }
async updateName( async updateName(
loggedUser: User, loggedUser: User,
username: string | undefined, username: string | undefined,
displayName: string, displayName: string,
): Promise<Pick<User, "username" | "displayName">> { ): Promise<Pick<User, "username" | "displayName">> {
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { id: loggedUser.id }, where: { id: loggedUser.id },
}); });
if (username !== undefined && username.trim() !== user.username) { if (username !== undefined && username.trim() !== user.username) {
const isAlreadyInUse = await this.prisma.user.findFirst({ const isAlreadyInUse = await this.prisma.user.findFirst({
where: { username }, where: { username },
}); });
if (isAlreadyInUse != null && isAlreadyInUse.username !== user.username) { if (isAlreadyInUse != null && isAlreadyInUse.username !== user.username) {
throw new BadRequestException("Username already in use"); throw new BadRequestException("Username already in use");
} }
} }
return await this.prisma.user.update({ return await this.prisma.user.update({
where: { where: {
id: loggedUser.id, id: loggedUser.id,
}, },
data: { data: {
displayName, displayName,
username: username ?? user.username, username: username ?? user.username,
}, },
select: { select: {
displayName: true, displayName: true,
username: true, username: true,
}, },
}); });
} }
async updatePassword( async updatePassword(
loggedUser: User, loggedUser: User,
old_password: string, old_password: string,
new_password: string, new_password: string,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const id = loggedUser.id; const id = loggedUser.id;
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { id }, where: { id },
}); });
const validatePassword = await bcrypt.compare(old_password, user.password); const validatePassword = await bcrypt.compare(old_password, user.password);
if (!validatePassword) { if (!validatePassword) {
throw new BadRequestException("Wrong password"); throw new BadRequestException("Wrong password");
} }
const salt = await bcrypt.genSalt(15); const salt = await bcrypt.genSalt(15);
const hash = await bcrypt.hash(new_password, salt); const hash = await bcrypt.hash(new_password, salt);
await this.prisma.user.update({ await this.prisma.user.update({
where: { where: {
id, id,
}, },
data: { data: {
password: hash, password: hash,
}, },
}); });
return { message: "Password updated successfully" }; return { message: "Password updated successfully" };
} }
async uploadImage(authenticatedUser: User, image: File) { async uploadImage(authenticatedUser: User, image: File) {
const url = await this.s3.uploadImageToMinio( const url = await this.s3.uploadImageToMinio(
authenticatedUser.id, authenticatedUser.id,
image.buffer, image.buffer,
); );
return await this.prisma.user.update({ return await this.prisma.user.update({
where: { where: {
id: authenticatedUser.id, id: authenticatedUser.id,
}, },
data: { data: {
profileImage: url, profileImage: url,
}, },
select: { select: {
profileImage: true, profileImage: true,
}, },
}); });
} }
} }

View file

@ -6,20 +6,20 @@ import { BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
*/ */
@Injectable() @Injectable()
export class BufferValidator implements PipeTransform { export class BufferValidator implements PipeTransform {
async transform(value: File) { async transform(value: File) {
const { fileTypeFromBuffer } = await (eval( const { fileTypeFromBuffer } = await (eval(
'import("file-type")', 'import("file-type")',
) as Promise<typeof import("file-type")>); ) as Promise<typeof import("file-type")>);
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"]; const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
const Type = await fileTypeFromBuffer(value.buffer); const Type = await fileTypeFromBuffer(value.buffer);
if (!Type || !ALLOWED_MIMES.includes(Type.mime)) { if (!Type || !ALLOWED_MIMES.includes(Type.mime)) {
throw new BadRequestException( throw new BadRequestException(
"Invalid file type. Should be jpeg, png or webp", "Invalid file type. Should be jpeg, png or webp",
); );
} }
return value; return value;
} }
} }

View file

@ -1,17 +1,17 @@
import { import {
FileTypeValidator, FileTypeValidator,
MaxFileSizeValidator, MaxFileSizeValidator,
ParseFilePipe, ParseFilePipe,
} from "@nestjs/common"; } from "@nestjs/common";
const UploadImageValidator = new ParseFilePipe({ const UploadImageValidator = new ParseFilePipe({
validators: [ validators: [
new MaxFileSizeValidator({ new MaxFileSizeValidator({
maxSize: 15 * 1024 * 1024, maxSize: 15 * 1024 * 1024,
message: "File too big. Max 1MB.", message: "File too big. Max 1MB.",
}), }),
new FileTypeValidator({ fileType: /^image\/(jpeg|png|webp)$/ }), // File extension validation new FileTypeValidator({ fileType: /^image\/(jpeg|png|webp)$/ }), // File extension validation
], ],
}); });
export default UploadImageValidator; export default UploadImageValidator;

View file

@ -1,24 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from "@nestjs/common";
import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from "@nestjs/testing";
import * as request from 'supertest'; import * as request from "supertest";
import { AppModule } from './../src/app.module'; import { AppModule } from "./../src/app.module";
describe('AppController (e2e)', () => { describe("AppController (e2e)", () => {
let app: INestApplication; let app: INestApplication;
beforeEach(async () => { beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({ const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule], imports: [AppModule],
}).compile(); }).compile();
app = moduleFixture.createNestApplication(); app = moduleFixture.createNestApplication();
await app.init(); await app.init();
}); });
it('/ (GET)', () => { it("/ (GET)", () => {
return request(app.getHttpServer()) return request(app.getHttpServer())
.get('/') .get("/")
.expect(200) .expect(200)
.expect('Hello World!'); .expect("Hello World!");
}); });
}); });

View file

@ -1,9 +1,9 @@
{ {
"moduleFileExtensions": ["js", "json", "ts"], "moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".", "rootDir": ".",
"testEnvironment": "node", "testEnvironment": "node",
"testRegex": ".e2e-spec.ts$", "testRegex": ".e2e-spec.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
} }
} }

View file

@ -1,4 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
} }