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') {
|
||||
process.exit(0)
|
||||
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
||||
process.exit(0);
|
||||
}
|
||||
const husky = (await import('husky')).default
|
||||
console.log(husky())
|
||||
const husky = (await import("husky")).default;
|
||||
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",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"minify": false
|
||||
}
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"minify": false
|
||||
}
|
||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,5 +1,3 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"biomejs.biome"
|
||||
]
|
||||
}
|
||||
"recommendations": ["biomejs.biome"]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"builder": "swc",
|
||||
"typeCheck": true
|
||||
}
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"builder": "swc",
|
||||
"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": {
|
||||
"@biomejs/biome": "1.5.3",
|
||||
"@commitlint/cli": "^18.6.0",
|
||||
"@commitlint/config-conventional": "^18.6.0",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
|
@ -81,6 +83,7 @@
|
|||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"husky": "^9.0.7",
|
||||
"jest": "^29.5.0",
|
||||
"lint-staged": "^15.2.1",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
|
@ -108,5 +111,8 @@
|
|||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**.{js|ts|json}": "biome check --apply --no-errors-on-unmatched"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,4 +92,4 @@ model Notifications {
|
|||
enum NotificationType {
|
||||
WARNING
|
||||
INFO
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,56 @@
|
|||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||
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 { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { S3Module } from "nestjs-s3";
|
||||
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
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 { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||
import { S3Module } from "nestjs-s3";
|
||||
import { UserModule } from "./users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
AuthModule,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
ThrottlerModule.forRoot({
|
||||
throttlers: [{ limit: 10, ttl: 60000 }],
|
||||
storage: new ThrottlerStorageRedisService(
|
||||
`redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
|
||||
),
|
||||
}),
|
||||
KweeksModule,
|
||||
FastifyMulterModule,
|
||||
S3Module.forRoot({
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.MINIO_ROOT_USER, // CHANGE WHEN PRODUCTION TO S3
|
||||
secretAccessKey: process.env.MINIO_ROOT_PASSWORD,
|
||||
},
|
||||
region: "us-east-1",
|
||||
endpoint: process.env.MINIO_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useClass: ZodValidationPipe,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
UserModule,
|
||||
AuthModule,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
ThrottlerModule.forRoot({
|
||||
throttlers: [{ limit: 10, ttl: 60000 }],
|
||||
storage: new ThrottlerStorageRedisService(
|
||||
`redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
|
||||
),
|
||||
}),
|
||||
KweeksModule,
|
||||
FastifyMulterModule,
|
||||
S3Module.forRoot({
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.MINIO_ROOT_USER, // CHANGE WHEN PRODUCTION TO S3
|
||||
secretAccessKey: process.env.MINIO_ROOT_PASSWORD,
|
||||
},
|
||||
region: "us-east-1",
|
||||
endpoint: process.env.MINIO_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useClass: ZodValidationPipe,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
import {
|
||||
Controller,
|
||||
Request,
|
||||
Post,
|
||||
UseGuards,
|
||||
Body,
|
||||
HttpCode,
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
Post,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { LocalAuthGuard } from "./local-auth.guard";
|
||||
import { Public } from "src/public.decorator";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { LoginUserDTO } from "./dto/login.dto";
|
||||
import { Public } from "src/public.decorator";
|
||||
import { LocalAuthGuard } from "./local-auth.guard";
|
||||
|
||||
@ApiTags("Auth")
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Authenticates a user" })
|
||||
@ApiOkResponse({ status: 200, description: "Authenticated successfully" })
|
||||
@ApiUnauthorizedResponse({ description: "Wrong username or password" })
|
||||
@HttpCode(200)
|
||||
async login(@Request() req, @Body() _: LoginUserDTO) {
|
||||
return this.authService.login(req.user);
|
||||
}
|
||||
@Public()
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Authenticates a user" })
|
||||
@ApiOkResponse({ status: 200, description: "Authenticated successfully" })
|
||||
@ApiUnauthorizedResponse({ description: "Wrong username or password" })
|
||||
@HttpCode(200)
|
||||
async login(@Request() req, @Body() _: LoginUserDTO) {
|
||||
return this.authService.login(req.user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { LocalStrategy } from "./local.strategy";
|
||||
import { UserModule } from "src/users/users.module";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtStrategy } from "./jwt.strategy";
|
||||
import { LocalStrategy } from "./local.strategy";
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
imports: [
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_ACCESS_SECRET,
|
||||
signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
imports: [
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_ACCESS_SECRET,
|
||||
signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { UserService } from "src/users/users.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { UserModel } from "src/users/models/user.model";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { UserService } from "src/users/users.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async validateUser(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<UserModel | null> {
|
||||
const user = await this.userService.auth_search(username);
|
||||
async validateUser(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<UserModel | null> {
|
||||
const user = await this.userService.auth_search(username);
|
||||
|
||||
if (user === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (user === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validation = await bcrypt.compare(password, user.password);
|
||||
const validation = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (user && validation) {
|
||||
const { password, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
if (user && validation) {
|
||||
const { password, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(user: UserModel): Promise<{ token: string }> {
|
||||
const payload = {
|
||||
displayName: user.displayName,
|
||||
username: user.username,
|
||||
profileImage: user.profileImage,
|
||||
sub: user.id,
|
||||
};
|
||||
async login(user: UserModel): Promise<{ token: string }> {
|
||||
const payload = {
|
||||
displayName: user.displayName,
|
||||
username: user.username,
|
||||
profileImage: user.profileImage,
|
||||
sub: user.id,
|
||||
};
|
||||
|
||||
return {
|
||||
token: this.jwtService.sign(payload),
|
||||
};
|
||||
}
|
||||
return {
|
||||
token: this.jwtService.sign(payload),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,28 +2,28 @@ import { createZodDto } from "nestjs-zod";
|
|||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const LoginUserSchema = z
|
||||
.object({
|
||||
username: z
|
||||
.string({
|
||||
required_error: "Username is required",
|
||||
})
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||
)
|
||||
.toLowerCase(),
|
||||
password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||
})
|
||||
.required();
|
||||
.object({
|
||||
username: z
|
||||
.string({
|
||||
required_error: "Username is required",
|
||||
})
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||
)
|
||||
.toLowerCase(),
|
||||
password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||
})
|
||||
.required();
|
||||
|
||||
export class LoginUserDTO extends createZodDto(LoginUserSchema) {}
|
||||
|
|
|
@ -6,20 +6,20 @@ import { IS_PUBLIC_KEY } from "src/public.decorator";
|
|||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
return super.canActivate(context);
|
||||
}
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,26 +3,26 @@ import { PassportStrategy } from "@nestjs/passport";
|
|||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
|
||||
type Payload = {
|
||||
displayName: string;
|
||||
username: string;
|
||||
sub: string;
|
||||
displayName: string;
|
||||
username: string;
|
||||
sub: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: process.env.JWT_ACCESS_SECRET,
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: process.env.JWT_ACCESS_SECRET,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: Payload) {
|
||||
return {
|
||||
displayName: payload.displayName,
|
||||
username: payload.username,
|
||||
id: payload.sub,
|
||||
};
|
||||
}
|
||||
async validate(payload: Payload) {
|
||||
return {
|
||||
displayName: payload.displayName,
|
||||
username: payload.username,
|
||||
id: payload.sub,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Strategy } from "passport-local";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { UserModel } from "src/users/models/user.model";
|
||||
import { AuthService } from "./auth.service";
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
constructor(private authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async validate(username: string, password: string): Promise<UserModel> {
|
||||
const user = await this.authService.validateUser(username, password);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("Wrong username or password");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
async validate(username: string, password: string): Promise<UserModel> {
|
||||
const user = await this.authService.validateUser(username, password);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("Wrong username or password");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ import { createZodDto } from "nestjs-zod";
|
|||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const CreateKweekSchema = z
|
||||
.object({
|
||||
content: z.string({ required_error: "Kweek content is required" }).max(300),
|
||||
files: z.array(z.object({})),
|
||||
})
|
||||
.required();
|
||||
.object({
|
||||
content: z.string({ required_error: "Kweek content is required" }).max(300),
|
||||
files: z.array(z.object({})),
|
||||
})
|
||||
.required();
|
||||
|
||||
export class CreateKweekDTO extends createZodDto(CreateKweekSchema) {}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateKweekDto } from './create-kweek.dto';
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateKweekDto } from "./create-kweek.dto";
|
||||
|
||||
export class UpdateKweekDto extends PartialType(CreateKweekDto) {}
|
||||
|
|
|
@ -37,7 +37,7 @@ export class KweeksController {
|
|||
@UploadedFiles() attachments: File,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.kweeksService.create(createKweekDto);
|
||||
return this.kweeksService.create(createKweekDto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { KweeksService } from "./kweeks.service";
|
||||
import { KweeksController } from "./kweeks.controller";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { KweeksController } from "./kweeks.controller";
|
||||
import { KweeksService } from "./kweeks.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [KweeksController],
|
||||
providers: [KweeksService],
|
||||
imports: [PrismaModule],
|
||||
controllers: [KweeksController],
|
||||
providers: [KweeksService],
|
||||
})
|
||||
export class KweeksModule {}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { CreateKweekDTO } from "./dto/create-kweek.dto";
|
||||
import { UpdateKweekDto } from "./dto/update-kweek.dto";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class KweeksService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
create(createKweekDto: CreateKweekDTO) {
|
||||
return "This action adds a new kweek";
|
||||
}
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
create(createKweekDto: CreateKweekDTO) {
|
||||
return "This action adds a new kweek";
|
||||
}
|
||||
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} kweek`;
|
||||
}
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} kweek`;
|
||||
}
|
||||
|
||||
update(id: number, updateKweekDto: UpdateKweekDto) {
|
||||
return `This action updates a #${id} kweek`;
|
||||
}
|
||||
update(id: number, updateKweekDto: UpdateKweekDto) {
|
||||
return `This action updates a #${id} kweek`;
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} kweek`;
|
||||
}
|
||||
remove(id: number) {
|
||||
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 { 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)
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({ logger: true }),
|
||||
);
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({ logger: true }),
|
||||
);
|
||||
|
||||
patchNestJsSwagger();
|
||||
patchNestJsSwagger();
|
||||
|
||||
app.enableCors();
|
||||
app.enableCors();
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle("Project Knedita")
|
||||
.setDescription("An open-source social media")
|
||||
.setVersion("1.0")
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
name: "JWT",
|
||||
description: "Enter JWT Token",
|
||||
in: "header",
|
||||
},
|
||||
"JWT",
|
||||
)
|
||||
.addTag("Auth")
|
||||
.addTag("Kweeks")
|
||||
.addTag("Users")
|
||||
.build();
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle("Project Knedita")
|
||||
.setDescription("An open-source social media")
|
||||
.setVersion("1.0")
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
name: "JWT",
|
||||
description: "Enter JWT Token",
|
||||
in: "header",
|
||||
},
|
||||
"JWT",
|
||||
)
|
||||
.addTag("Auth")
|
||||
.addTag("Kweeks")
|
||||
.addTag("Users")
|
||||
.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();
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Module } from "@nestjs/common";
|
|||
import { PrismaService } from "./prisma.service";
|
||||
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
|
|
|
@ -3,16 +3,16 @@ import { Prisma, PrismaClient } from "@prisma/client";
|
|||
|
||||
@Injectable()
|
||||
export class PrismaService
|
||||
extends PrismaClient<Prisma.PrismaClientOptions, "beforeExit">
|
||||
implements OnModuleInit
|
||||
extends PrismaClient<Prisma.PrismaClientOptions, "beforeExit">
|
||||
implements OnModuleInit
|
||||
{
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async enableShutdownHooks(app: INestApplication) {
|
||||
this.$on("beforeExit", async () => {
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
async enableShutdownHooks(app: INestApplication) {
|
||||
this.$on("beforeExit", async () => {
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,33 +2,33 @@ import { createZodDto } from "nestjs-zod";
|
|||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const CreateUserSchema = z
|
||||
.object({
|
||||
username: z
|
||||
.string({
|
||||
required_error: "Username is required",
|
||||
})
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||
)
|
||||
.toLowerCase(),
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Invalid email"),
|
||||
password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||
})
|
||||
.required();
|
||||
.object({
|
||||
username: z
|
||||
.string({
|
||||
required_error: "Username is required",
|
||||
})
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||
)
|
||||
.toLowerCase(),
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Invalid email"),
|
||||
password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||
})
|
||||
.required();
|
||||
|
||||
export class CreateUserDTO extends createZodDto(CreateUserSchema) {}
|
||||
|
|
|
@ -2,13 +2,13 @@ import { createZodDto } from "nestjs-zod";
|
|||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const UpdateEmailSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Invalid email"),
|
||||
})
|
||||
.required();
|
||||
.object({
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Invalid email"),
|
||||
})
|
||||
.required();
|
||||
|
||||
export class UpdateEmailDTO extends createZodDto(UpdateEmailSchema) {}
|
||||
|
|
|
@ -2,19 +2,19 @@ import { createZodDto } from "nestjs-zod";
|
|||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const UpdateNameSchema = z
|
||||
.object({
|
||||
username: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||
)
|
||||
.toLowerCase()
|
||||
.describe("New username - optional")
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
displayName: z.string({ required_error: "Display name is required" }),
|
||||
})
|
||||
.required();
|
||||
.object({
|
||||
username: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||
)
|
||||
.toLowerCase()
|
||||
.describe("New username - optional")
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
displayName: z.string({ required_error: "Display name is required" }),
|
||||
})
|
||||
.required();
|
||||
|
||||
export class UpdateNameDTO extends createZodDto(UpdateNameSchema) {}
|
||||
|
|
|
@ -4,30 +4,30 @@ import { z } from "nestjs-zod/z";
|
|||
// TODO: see if it can be refactored
|
||||
|
||||
export const UpdatePasswordSchema = z
|
||||
.object({
|
||||
old_password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")),
|
||||
new_password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")),
|
||||
})
|
||||
.required();
|
||||
.object({
|
||||
old_password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")),
|
||||
new_password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")),
|
||||
})
|
||||
.required();
|
||||
|
||||
export class UpdatePasswordDTO extends createZodDto(UpdatePasswordSchema) {}
|
||||
|
|
|
@ -2,21 +2,21 @@ import { createZodDto } from "nestjs-zod";
|
|||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const UserSchema = z
|
||||
.object({
|
||||
id: z.string().uuid(),
|
||||
displayName: z.string().optional(),
|
||||
username: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.password(),
|
||||
kweeks: z.array(z.object({})).optional(),
|
||||
profileImage: z.string().url().optional(),
|
||||
likedKweeks: z.array(z.object({})).optional(),
|
||||
likedComments: z.array(z.object({})).optional(),
|
||||
followers: z.number(),
|
||||
following: z.number(),
|
||||
kweeksComments: z.array(z.object({})).optional(),
|
||||
createdAt: z.date(),
|
||||
})
|
||||
.required();
|
||||
.object({
|
||||
id: z.string().uuid(),
|
||||
displayName: z.string().optional(),
|
||||
username: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.password(),
|
||||
kweeks: z.array(z.object({})).optional(),
|
||||
profileImage: z.string().url().optional(),
|
||||
likedKweeks: z.array(z.object({})).optional(),
|
||||
likedComments: z.array(z.object({})).optional(),
|
||||
followers: z.number(),
|
||||
following: z.number(),
|
||||
kweeksComments: z.array(z.object({})).optional(),
|
||||
createdAt: z.date(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export class UserModel extends createZodDto(UserSchema) {}
|
||||
|
|
|
@ -5,34 +5,34 @@ import sharp from "sharp";
|
|||
|
||||
@Injectable()
|
||||
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.
|
||||
*/
|
||||
async uploadImageToMinio(userID: string, buffer: Buffer): Promise<string> {
|
||||
const compressedBuffer = await sharp(buffer)
|
||||
.resize(200, 200)
|
||||
.webp({ quality: 70 })
|
||||
.toBuffer();
|
||||
/**
|
||||
* Returns the image url if the upload to minio was successful.
|
||||
*/
|
||||
async uploadImageToMinio(userID: string, buffer: Buffer): Promise<string> {
|
||||
const compressedBuffer = await sharp(buffer)
|
||||
.resize(200, 200)
|
||||
.webp({ quality: 70 })
|
||||
.toBuffer();
|
||||
|
||||
const uploadParams: PutObjectCommandInput = {
|
||||
Bucket: process.env.MINIO_DEFAULT_BUCKETS,
|
||||
Key: `profile_images/${userID}.webp`,
|
||||
Body: compressedBuffer,
|
||||
ContentType: "image/webp",
|
||||
ContentDisposition: "inline",
|
||||
ACL: "public-read",
|
||||
};
|
||||
const uploadParams: PutObjectCommandInput = {
|
||||
Bucket: process.env.MINIO_DEFAULT_BUCKETS,
|
||||
Key: `profile_images/${userID}.webp`,
|
||||
Body: compressedBuffer,
|
||||
ContentType: "image/webp",
|
||||
ContentDisposition: "inline",
|
||||
ACL: "public-read",
|
||||
};
|
||||
|
||||
const { ETag } = await this.s3.send(new PutObjectCommand(uploadParams));
|
||||
const { ETag } = await this.s3.send(new PutObjectCommand(uploadParams));
|
||||
|
||||
if (ETag !== null) {
|
||||
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`;
|
||||
}
|
||||
if (ETag !== null) {
|
||||
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to upload the profile image",
|
||||
);
|
||||
}
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to upload the profile image",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
const UploadImageSchema = {
|
||||
required: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
image: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
image: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default UploadImageSchema;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export type User = {
|
||||
displayName: string;
|
||||
username: string;
|
||||
id: string;
|
||||
displayName: string;
|
||||
username: string;
|
||||
id: string;
|
||||
};
|
||||
|
|
|
@ -1,129 +1,129 @@
|
|||
import { File, FileInterceptor } from "@nest-lab/fastify-multer";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Request,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Request,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
ApiCreatedResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
ApiCreatedResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { UserService } from "./users.service";
|
||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
||||
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 UploadImageSchema from "./schemas/upload-image.schema";
|
||||
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")
|
||||
@Controller("users")
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
// POST
|
||||
@Public()
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Creates a new account" })
|
||||
@ApiCreatedResponse({ description: "Account created successfully" })
|
||||
@ApiBadRequestResponse({
|
||||
description:
|
||||
"Missing field / Invalid username / Invalid email / Weak password",
|
||||
})
|
||||
create(@Body() createUserDTO: CreateUserDTO) {
|
||||
return this.userService.create(createUserDTO);
|
||||
}
|
||||
constructor(private readonly userService: UserService) {}
|
||||
// POST
|
||||
@Public()
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Creates a new account" })
|
||||
@ApiCreatedResponse({ description: "Account created successfully" })
|
||||
@ApiBadRequestResponse({
|
||||
description:
|
||||
"Missing field / Invalid username / Invalid email / Weak password",
|
||||
})
|
||||
create(@Body() createUserDTO: CreateUserDTO) {
|
||||
return this.userService.create(createUserDTO);
|
||||
}
|
||||
|
||||
// GET
|
||||
@Get("/profile")
|
||||
@ApiOperation({ summary: "Returns information about the logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
@ApiUnauthorizedResponse({
|
||||
description: "Not authenticated / Invalid JWT Token",
|
||||
})
|
||||
me(@Request() req) {
|
||||
return req.user;
|
||||
}
|
||||
// GET
|
||||
@Get("/profile")
|
||||
@ApiOperation({ summary: "Returns information about the logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
@ApiUnauthorizedResponse({
|
||||
description: "Not authenticated / Invalid JWT Token",
|
||||
})
|
||||
me(@Request() req) {
|
||||
return req.user;
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(":username")
|
||||
@ApiOperation({ summary: "Returns information about a user" })
|
||||
@ApiNotFoundResponse({ description: "User not found" })
|
||||
@HttpCode(200)
|
||||
info(@Param("username") username: string) {
|
||||
return this.userService.info(username);
|
||||
}
|
||||
@Public()
|
||||
@Get(":username")
|
||||
@ApiOperation({ summary: "Returns information about a user" })
|
||||
@ApiNotFoundResponse({ description: "User not found" })
|
||||
@HttpCode(200)
|
||||
info(@Param("username") username: string) {
|
||||
return this.userService.info(username);
|
||||
}
|
||||
|
||||
// PATCH
|
||||
@Patch()
|
||||
@ApiOperation({
|
||||
summary: "Updates the username or display name of a logged user",
|
||||
})
|
||||
@ApiBearerAuth("JWT")
|
||||
updateName(@Body() { displayName, username }: UpdateNameDTO, @Request() req) {
|
||||
return this.userService.updateName(req.user as User, username, displayName);
|
||||
}
|
||||
// PATCH
|
||||
@Patch()
|
||||
@ApiOperation({
|
||||
summary: "Updates the username or display name of a logged user",
|
||||
})
|
||||
@ApiBearerAuth("JWT")
|
||||
updateName(@Body() { displayName, username }: UpdateNameDTO, @Request() req) {
|
||||
return this.userService.updateName(req.user as User, username, displayName);
|
||||
}
|
||||
|
||||
@Patch("/email")
|
||||
@ApiOperation({ summary: "Updates the email of a logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
updateEmail(@Body() body: UpdateEmailDTO, @Request() req) {
|
||||
return this.userService.updateEmail(req.user as User, body.email);
|
||||
}
|
||||
@Patch("/email")
|
||||
@ApiOperation({ summary: "Updates the email of a logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
updateEmail(@Body() body: UpdateEmailDTO, @Request() req) {
|
||||
return this.userService.updateEmail(req.user as User, body.email);
|
||||
}
|
||||
|
||||
@Patch("/password")
|
||||
@ApiOperation({ summary: "Updates the password of a logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
updatePassword(
|
||||
@Body() { old_password, new_password }: UpdatePasswordDTO,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.userService.updatePassword(
|
||||
req.user as User,
|
||||
old_password,
|
||||
new_password,
|
||||
);
|
||||
}
|
||||
@Patch("/password")
|
||||
@ApiOperation({ summary: "Updates the password of a logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
updatePassword(
|
||||
@Body() { old_password, new_password }: UpdatePasswordDTO,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.userService.updatePassword(
|
||||
req.user as User,
|
||||
old_password,
|
||||
new_password,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch("/image")
|
||||
@ApiOperation({
|
||||
summary: "Add a profile image",
|
||||
})
|
||||
@ApiBearerAuth("JWT")
|
||||
@UseInterceptors(FileInterceptor("image"))
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiBody(UploadImageSchema)
|
||||
uploadProfileImage(
|
||||
@UploadedFile(
|
||||
UploadImageValidator,
|
||||
new BufferValidator(), // Magic number validation
|
||||
)
|
||||
image: File,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.userService.uploadImage(req.user, image);
|
||||
}
|
||||
@Patch("/image")
|
||||
@ApiOperation({
|
||||
summary: "Add a profile image",
|
||||
})
|
||||
@ApiBearerAuth("JWT")
|
||||
@UseInterceptors(FileInterceptor("image"))
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiBody(UploadImageSchema)
|
||||
uploadProfileImage(
|
||||
@UploadedFile(
|
||||
UploadImageValidator,
|
||||
new BufferValidator(), // Magic number validation
|
||||
)
|
||||
image: File,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.userService.uploadImage(req.user, image);
|
||||
}
|
||||
|
||||
// DELETE
|
||||
@Delete()
|
||||
@ApiOperation({ summary: "Deletes the account of a logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
remove() {}
|
||||
// DELETE
|
||||
@Delete()
|
||||
@ApiOperation({ summary: "Deletes the account of a logged user" })
|
||||
@ApiBearerAuth("JWT")
|
||||
remove() {}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { UserController } from "./users.controller";
|
||||
import { UserService } from "./users.service";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { S3Service } from "./s3.service";
|
||||
import { UserController } from "./users.controller";
|
||||
import { UserService } from "./users.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService, S3Service],
|
||||
exports: [UserService],
|
||||
imports: [PrismaModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService, S3Service],
|
||||
exports: [UserService],
|
||||
})
|
||||
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 {
|
||||
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 { User } from "./types/user.type";
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly s3: S3Service,
|
||||
) {}
|
||||
async auth_search(username: string): Promise<UserModel> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
profileImage: true,
|
||||
displayName: true,
|
||||
username: true,
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly s3: S3Service,
|
||||
) {}
|
||||
async auth_search(username: string): Promise<UserModel> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
profileImage: true,
|
||||
displayName: true,
|
||||
username: true,
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (user == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async info(username: string): Promise<UserModel> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { username },
|
||||
select: {
|
||||
id: true,
|
||||
profileImage: true,
|
||||
displayName: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
followers: true,
|
||||
following: true,
|
||||
kweeks: {
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
async info(username: string): Promise<UserModel> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { username },
|
||||
select: {
|
||||
id: true,
|
||||
profileImage: true,
|
||||
displayName: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
followers: true,
|
||||
following: true,
|
||||
kweeks: {
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
if (user === null) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
followers: user.followers.length,
|
||||
following: user.following.length,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...user,
|
||||
followers: user.followers.length,
|
||||
following: user.following.length,
|
||||
};
|
||||
}
|
||||
|
||||
async create({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
}: CreateUserDTO): Promise<
|
||||
Pick<UserModel, "displayName" | "username" | "createdAt">
|
||||
> {
|
||||
if ((await this.prisma.user.findFirst({ where: { username } })) != null) {
|
||||
throw new BadRequestException("Username already in use");
|
||||
}
|
||||
async create({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
}: CreateUserDTO): Promise<
|
||||
Pick<UserModel, "displayName" | "username" | "createdAt">
|
||||
> {
|
||||
if ((await this.prisma.user.findFirst({ where: { username } })) != null) {
|
||||
throw new BadRequestException("Username already in use");
|
||||
}
|
||||
|
||||
if ((await this.prisma.user.findFirst({ where: { email } })) != null) {
|
||||
throw new BadRequestException("Email already in use");
|
||||
}
|
||||
if ((await this.prisma.user.findFirst({ where: { email } })) != null) {
|
||||
throw new BadRequestException("Email already in use");
|
||||
}
|
||||
|
||||
// Password encryption
|
||||
const salt = await bcrypt.genSalt(15);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
// Password encryption
|
||||
const salt = await bcrypt.genSalt(15);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hash,
|
||||
},
|
||||
select: {
|
||||
displayName: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hash,
|
||||
},
|
||||
select: {
|
||||
displayName: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateEmail(
|
||||
loggedUser: User,
|
||||
email: string,
|
||||
): Promise<{ message: string }> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id: loggedUser.id },
|
||||
});
|
||||
async updateEmail(
|
||||
loggedUser: User,
|
||||
email: string,
|
||||
): Promise<{ message: string }> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id: loggedUser.id },
|
||||
});
|
||||
|
||||
if (email !== undefined && email.trim() !== user.email) {
|
||||
const isAlreadyInUse = await this.prisma.user.findFirst({
|
||||
where: { email },
|
||||
});
|
||||
if (isAlreadyInUse != null && isAlreadyInUse.email !== user.email) {
|
||||
throw new BadRequestException("Email already in use");
|
||||
}
|
||||
if (email !== undefined && email.trim() !== user.email) {
|
||||
const isAlreadyInUse = await this.prisma.user.findFirst({
|
||||
where: { email },
|
||||
});
|
||||
if (isAlreadyInUse != null && isAlreadyInUse.email !== user.email) {
|
||||
throw new BadRequestException("Email already in use");
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: {
|
||||
id: loggedUser.id,
|
||||
},
|
||||
data: {
|
||||
email: email ?? user.email,
|
||||
},
|
||||
});
|
||||
await this.prisma.user.update({
|
||||
where: {
|
||||
id: loggedUser.id,
|
||||
},
|
||||
data: {
|
||||
email: email ?? user.email,
|
||||
},
|
||||
});
|
||||
|
||||
return { message: "Email updated successfully" };
|
||||
}
|
||||
}
|
||||
return { message: "Email updated successfully" };
|
||||
}
|
||||
}
|
||||
|
||||
async updateName(
|
||||
loggedUser: User,
|
||||
username: string | undefined,
|
||||
displayName: string,
|
||||
): Promise<Pick<User, "username" | "displayName">> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id: loggedUser.id },
|
||||
});
|
||||
async updateName(
|
||||
loggedUser: User,
|
||||
username: string | undefined,
|
||||
displayName: string,
|
||||
): Promise<Pick<User, "username" | "displayName">> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id: loggedUser.id },
|
||||
});
|
||||
|
||||
if (username !== undefined && username.trim() !== user.username) {
|
||||
const isAlreadyInUse = await this.prisma.user.findFirst({
|
||||
where: { username },
|
||||
});
|
||||
if (isAlreadyInUse != null && isAlreadyInUse.username !== user.username) {
|
||||
throw new BadRequestException("Username already in use");
|
||||
}
|
||||
}
|
||||
if (username !== undefined && username.trim() !== user.username) {
|
||||
const isAlreadyInUse = await this.prisma.user.findFirst({
|
||||
where: { username },
|
||||
});
|
||||
if (isAlreadyInUse != null && isAlreadyInUse.username !== user.username) {
|
||||
throw new BadRequestException("Username already in use");
|
||||
}
|
||||
}
|
||||
|
||||
return await this.prisma.user.update({
|
||||
where: {
|
||||
id: loggedUser.id,
|
||||
},
|
||||
data: {
|
||||
displayName,
|
||||
username: username ?? user.username,
|
||||
},
|
||||
select: {
|
||||
displayName: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return await this.prisma.user.update({
|
||||
where: {
|
||||
id: loggedUser.id,
|
||||
},
|
||||
data: {
|
||||
displayName,
|
||||
username: username ?? user.username,
|
||||
},
|
||||
select: {
|
||||
displayName: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updatePassword(
|
||||
loggedUser: User,
|
||||
old_password: string,
|
||||
new_password: string,
|
||||
): Promise<{ message: string }> {
|
||||
const id = loggedUser.id;
|
||||
async updatePassword(
|
||||
loggedUser: User,
|
||||
old_password: string,
|
||||
new_password: string,
|
||||
): Promise<{ message: string }> {
|
||||
const id = loggedUser.id;
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id },
|
||||
});
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
const validatePassword = await bcrypt.compare(old_password, user.password);
|
||||
const validatePassword = await bcrypt.compare(old_password, user.password);
|
||||
|
||||
if (!validatePassword) {
|
||||
throw new BadRequestException("Wrong password");
|
||||
}
|
||||
if (!validatePassword) {
|
||||
throw new BadRequestException("Wrong password");
|
||||
}
|
||||
|
||||
const salt = await bcrypt.genSalt(15);
|
||||
const hash = await bcrypt.hash(new_password, salt);
|
||||
const salt = await bcrypt.genSalt(15);
|
||||
const hash = await bcrypt.hash(new_password, salt);
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
password: hash,
|
||||
},
|
||||
});
|
||||
await this.prisma.user.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
password: hash,
|
||||
},
|
||||
});
|
||||
|
||||
return { message: "Password updated successfully" };
|
||||
}
|
||||
return { message: "Password updated successfully" };
|
||||
}
|
||||
|
||||
async uploadImage(authenticatedUser: User, image: File) {
|
||||
const url = await this.s3.uploadImageToMinio(
|
||||
authenticatedUser.id,
|
||||
image.buffer,
|
||||
);
|
||||
async uploadImage(authenticatedUser: User, image: File) {
|
||||
const url = await this.s3.uploadImageToMinio(
|
||||
authenticatedUser.id,
|
||||
image.buffer,
|
||||
);
|
||||
|
||||
return await this.prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
profileImage: url,
|
||||
},
|
||||
select: {
|
||||
profileImage: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return await this.prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
profileImage: url,
|
||||
},
|
||||
select: {
|
||||
profileImage: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,20 +6,20 @@ import { BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
|
|||
*/
|
||||
@Injectable()
|
||||
export class BufferValidator implements PipeTransform {
|
||||
async transform(value: File) {
|
||||
const { fileTypeFromBuffer } = await (eval(
|
||||
'import("file-type")',
|
||||
) as Promise<typeof import("file-type")>);
|
||||
async transform(value: File) {
|
||||
const { fileTypeFromBuffer } = await (eval(
|
||||
'import("file-type")',
|
||||
) as Promise<typeof import("file-type")>);
|
||||
|
||||
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
|
||||
const Type = await fileTypeFromBuffer(value.buffer);
|
||||
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
|
||||
const Type = await fileTypeFromBuffer(value.buffer);
|
||||
|
||||
if (!Type || !ALLOWED_MIMES.includes(Type.mime)) {
|
||||
throw new BadRequestException(
|
||||
"Invalid file type. Should be jpeg, png or webp",
|
||||
);
|
||||
}
|
||||
if (!Type || !ALLOWED_MIMES.includes(Type.mime)) {
|
||||
throw new BadRequestException(
|
||||
"Invalid file type. Should be jpeg, png or webp",
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {
|
||||
FileTypeValidator,
|
||||
MaxFileSizeValidator,
|
||||
ParseFilePipe,
|
||||
FileTypeValidator,
|
||||
MaxFileSizeValidator,
|
||||
ParseFilePipe,
|
||||
} from "@nestjs/common";
|
||||
|
||||
const UploadImageValidator = new ParseFilePipe({
|
||||
validators: [
|
||||
new MaxFileSizeValidator({
|
||||
maxSize: 15 * 1024 * 1024,
|
||||
message: "File too big. Max 1MB.",
|
||||
}),
|
||||
new FileTypeValidator({ fileType: /^image\/(jpeg|png|webp)$/ }), // File extension validation
|
||||
],
|
||||
validators: [
|
||||
new MaxFileSizeValidator({
|
||||
maxSize: 15 * 1024 * 1024,
|
||||
message: "File too big. Max 1MB.",
|
||||
}),
|
||||
new FileTypeValidator({ fileType: /^image\/(jpeg|png|webp)$/ }), // File extension validation
|
||||
],
|
||||
});
|
||||
|
||||
export default UploadImageValidator;
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from './../src/app.module';
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { AppModule } from "./../src/app.module";
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
describe("AppController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
it("/ (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/")
|
||||
.expect(200)
|
||||
.expect("Hello World!");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue