mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 09:31:16 +00:00
feat: added biome on pre-commit
This commit is contained in:
parent
6f7aafef81
commit
66681673bc
41 changed files with 2530 additions and 772 deletions
|
@ -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
1
.husky/pre-commit
Normal file
|
@ -0,0 +1 @@
|
||||||
|
lint-staged
|
26
.swcrc
26
.swcrc
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,5 +1,3 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["biomejs.biome"]
|
||||||
"biomejs.biome"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['@commitlint/config-conventional'],
|
extends: ["@commitlint/config-conventional"],
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
1753
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,4 +92,4 @@ model Notifications {
|
||||||
enum NotificationType {
|
enum NotificationType {
|
||||||
WARNING
|
WARNING
|
||||||
INFO
|
INFO
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
74
src/main.ts
74
src/main.ts
|
@ -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();
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export type User = {
|
export type User = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
username: string;
|
username: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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() {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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!");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue