Skip to content

Commit a5c4d40

Browse files
authored
Merge pull request #112 from Open-Webtoon-Reader/staging
2 parents fb5cce8 + cd87fc3 commit a5c4d40

File tree

12 files changed

+442
-217
lines changed

12 files changed

+442
-217
lines changed

bun.lock

Lines changed: 179 additions & 175 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,43 @@
1313
},
1414
"dependencies": {
1515
"@fastify/helmet": "^13.0.1",
16-
"@fastify/static": "^8.1.1",
17-
"@nestjs/common": "^11.0.15",
16+
"@fastify/static": "^8.2.0",
17+
"@nestjs/common": "^11.1.3",
1818
"@nestjs/config": "^4.0.2",
19-
"@nestjs/cli": "^11.0.6",
20-
"@nestjs/core": "^11.0.15",
19+
"@nestjs/cli": "^11.0.7",
20+
"@nestjs/core": "^11.1.3",
2121
"@nestjs/jwt": "^11.0.0",
2222
"@nestjs/passport": "^11.0.5",
23-
"@nestjs/platform-fastify": "^11.0.15",
24-
"@nestjs/platform-socket.io": "^11.0.15",
25-
"@nestjs/schedule": "^5.0.1",
26-
"@nestjs/swagger": "^11.1.1",
23+
"@nestjs/platform-fastify": "^11.1.3",
24+
"@nestjs/platform-socket.io": "^11.1.3",
25+
"@nestjs/schedule": "^6.0.0",
26+
"@nestjs/swagger": "^11.2.0",
2727
"@nestjs/throttler": "^6.4.0",
28-
"@nestjs/websockets": "^11.0.15",
29-
"@prisma/client": "6.6.0",
30-
"axios": "^1.8.4",
28+
"@nestjs/websockets": "^11.1.3",
29+
"@prisma/client": "6.9.0",
30+
"axios": "^1.9.0",
3131
"class-transformer": "^0.5.1",
32-
"class-validator": "^0.14.1",
33-
"fastify": "5.2.2",
34-
"jsdom": "^26.0.0",
32+
"class-validator": "^0.14.2",
33+
"fastify": "5.3.3",
34+
"jsdom": "^26.1.0",
3535
"jszip": "^3.10.1",
3636
"minio": "^8.0.5",
3737
"passport-jwt": "^4.0.1",
38-
"sharp": "^0.34.1",
38+
"sharp": "^0.34.2",
3939
"socket.io": "^4.8.1",
4040
"swagger-themes": "^1.4.3",
4141
"uuid": "^11.1.0"
4242
},
4343
"devDependencies": {
4444
"@nestjs/schematics": "^11.0.5",
45-
"@stylistic/eslint-plugin": "^4.2.0",
46-
"@types/bun": "^1.2.9",
45+
"@stylistic/eslint-plugin": "^4.4.1",
46+
"@types/bun": "^1.2.15",
4747
"@types/jsdom": "^21.1.7",
48-
"@types/node": "^22.14.0",
48+
"@types/node": "^22.15.30",
4949
"@types/passport-jwt": "^4.0.1",
50-
"@typescript-eslint/parser": "^8.29.1",
51-
"eslint": "^9.24.0",
52-
"prisma": "^6.6.0",
50+
"@typescript-eslint/parser": "^8.33.1",
51+
"eslint": "^9.28.0",
52+
"prisma": "^6.9.0",
5353
"source-map-support": "^0.5.21",
5454
"typescript": "^5.8.3"
5555
},

src/modules/storage/storage.service.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,16 @@ export class StorageService{
3535
const fileName: string = this.getFileName(sum);
3636
this.logger.verbose(`Uploading file ${fileName}`);
3737
let file: BunFile | S3File;
38-
if(this.s3Client)
38+
if(this.s3Client){
3939
file = this.s3Client.file(fileName);
40-
else
40+
await file.write(data, {
41+
// @ts-ignore
42+
storageClass: process.env.S3_STORAGE_CLASS || "STANDARD",
43+
});
44+
}else{
4145
file = Bun.file(fileName);
42-
await file.write(data);
46+
await file.write(data);
47+
}
4348
return sum;
4449
}
4550

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {IsNotEmpty, IsOptional, IsString, Length} from "class-validator";
2+
3+
export class ChangePasswordDto{
4+
@IsString()
5+
@IsOptional()
6+
oldPassword?: string;
7+
8+
@IsNotEmpty()
9+
@IsString()
10+
@Length(8, 255)
11+
newPassword: string;
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {IsAlphanumeric, IsEmail, IsNotEmpty, IsString, Length} from "class-validator";
2+
3+
export class CreateUserDto{
4+
@IsString()
5+
@IsNotEmpty()
6+
@Length(3, 30)
7+
@IsAlphanumeric()
8+
username: string;
9+
10+
@IsString()
11+
@IsNotEmpty()
12+
@IsEmail()
13+
@Length(8, 255)
14+
email: string;
15+
16+
@IsString()
17+
@IsNotEmpty()
18+
@Length(8, 255)
19+
password: string;
20+
}

src/modules/users/users.controller.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, UseGuards} from "@nestjs/common";
1+
import {Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, UseGuards} from "@nestjs/common";
22
import {UserEntity} from "./models/entities/user.entity";
33
import {LoginPayload} from "./models/payloads/login.payload";
44
import {User} from "./decorators/user.decorator";
@@ -7,6 +7,7 @@ import {ApiBearerAuth} from "@nestjs/swagger";
77
import {UsersService} from "./users.service";
88
import {AuthGuard} from "@nestjs/passport";
99
import {EpisodeProgressionDto} from "./models/dto/episode-progression.dto";
10+
import {ChangePasswordDto} from "./models/dto/change-password.dto";
1011

1112
@Controller("user")
1213
export class UsersController{
@@ -34,11 +35,20 @@ export class UsersController{
3435
return avatars;
3536
}
3637

37-
@Post("avatar/:sum")
38+
@Patch("avatar/:sum")
3839
@UseGuards(AuthGuard("jwt"))
3940
@ApiBearerAuth()
41+
@HttpCode(HttpStatus.NO_CONTENT)
4042
async setAvatar(@User() user: UserEntity, @Param("sum") sum: string): Promise<void>{
41-
await this.usersService.setAvatar(user, sum);
43+
return this.usersService.setAvatar(user, sum);
44+
}
45+
46+
@Patch("password")
47+
@UseGuards(AuthGuard("jwt"))
48+
@ApiBearerAuth()
49+
@HttpCode(HttpStatus.NO_CONTENT)
50+
async changePassword(@User() user: UserEntity, @Body() changePasswordDto: ChangePasswordDto): Promise<void>{
51+
return this.usersService.changePassword(user, changePasswordDto);
4252
}
4353

4454
@Get("progression/webtoon/:webtoon_id")
@@ -92,17 +102,17 @@ export class UsersController{
92102
}
93103

94104
@Post("likes/webtoon/:webtoon_id")
95-
@HttpCode(HttpStatus.NO_CONTENT)
96105
@UseGuards(AuthGuard("jwt"))
97106
@ApiBearerAuth()
107+
@HttpCode(HttpStatus.NO_CONTENT)
98108
async likeWebtoon(@User() user: UserEntity, @Param("webtoon_id") webtoonId: number): Promise<void>{
99109
await this.usersService.likeWebtoon(user, webtoonId);
100110
}
101111

102112
@Delete("likes/webtoon/:webtoon_id")
103-
@HttpCode(HttpStatus.NO_CONTENT)
104113
@UseGuards(AuthGuard("jwt"))
105114
@ApiBearerAuth()
115+
@HttpCode(HttpStatus.NO_CONTENT)
106116
async unlikeWebtoon(@User() user: UserEntity, @Param("webtoon_id") webtoonId: number): Promise<void>{
107117
await this.usersService.unlikeWebtoon(user, webtoonId);
108118
}

src/modules/users/users.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ import {AdminJwtStrategy} from "./strategies/admin-jwt.strategy";
3232
JwtStrategy,
3333
AdminJwtStrategy,
3434
],
35+
exports: [UsersService],
3536
})
3637
export class UsersModule{}

src/modules/users/users.service.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import {BadRequestException, Injectable, NotFoundException, UnauthorizedException} from "@nestjs/common";
1+
import {
2+
BadRequestException,
3+
ConflictException,
4+
Injectable,
5+
NotFoundException,
6+
UnauthorizedException,
7+
} from "@nestjs/common";
28
import ImageTypes from "../webtoon/webtoon/models/enums/image-types";
39
import {LoginPayload} from "./models/payloads/login.payload";
410
import {UserEntity} from "./models/entities/user.entity";
@@ -7,6 +13,8 @@ import {MiscService} from "../misc/misc.service";
713
import {EpisodeProgressions, Images, Users} from "@prisma/client";
814
import {JwtService} from "@nestjs/jwt";
915
import {EpisodeProgressionPayload} from "./models/payloads/episode-progression.payload";
16+
import {CreateUserDto} from "./models/dto/create-user.dto";
17+
import {ChangePasswordDto} from "./models/dto/change-password.dto";
1018

1119
@Injectable()
1220
export class UsersService{
@@ -28,6 +36,70 @@ export class UsersService{
2836
});
2937
}
3038

39+
async createUser(userDto: CreateUserDto): Promise<UserEntity>{
40+
// Check if the user already exists
41+
const existingUser = await this.prismaService.users.findFirst({
42+
where: {
43+
OR: [
44+
{username: userDto.username},
45+
{email: userDto.email},
46+
],
47+
},
48+
});
49+
if(existingUser)
50+
throw new ConflictException("User with this username or email already exists");
51+
// Create the user
52+
const hashedPassword = this.miscService.hashPassword(userDto.password);
53+
const user = await this.prismaService.users.create({
54+
data: {
55+
id: Bun.randomUUIDv7(),
56+
username: userDto.username,
57+
email: userDto.email,
58+
password: hashedPassword,
59+
admin: false,
60+
jwt_id: this.miscService.generateRandomBytes(16),
61+
},
62+
include: {
63+
avatar: true,
64+
},
65+
});
66+
return this.toUserEntity(user, user.avatar?.sum);
67+
}
68+
69+
async changePassword(user: UserEntity, changePasswordDto: ChangePasswordDto): Promise<void>{
70+
// Check if the old password is valid
71+
if(changePasswordDto.oldPassword && !this.miscService.comparePassword(changePasswordDto.oldPassword, user.password))
72+
throw new UnauthorizedException("Invalid old password");
73+
// Check if the new password is valid
74+
if(changePasswordDto.newPassword.length < 8)
75+
throw new BadRequestException("New password must be at least 8 characters long");
76+
// Hash the new password
77+
const hashedPassword = this.miscService.hashPassword(changePasswordDto.newPassword);
78+
// Update the user's password
79+
await this.prismaService.users.update({
80+
where: {
81+
id: user.id,
82+
},
83+
data: {
84+
password: hashedPassword,
85+
},
86+
});
87+
}
88+
89+
async changeUserPassword(userId: string, newPassword: string): Promise<void>{
90+
// Hash the new password
91+
const hashedPassword = this.miscService.hashPassword(newPassword);
92+
// Update the user's password
93+
await this.prismaService.users.update({
94+
where: {
95+
id: userId,
96+
},
97+
data: {
98+
password: hashedPassword,
99+
},
100+
});
101+
}
102+
31103
async getAvailableAvatars(): Promise<string[]>{
32104
const webtoons: any[] = await this.prismaService.webtoons.findMany({
33105
include: {
@@ -80,6 +152,33 @@ export class UsersService{
80152
return this.toUserEntity(user, user.avatar?.sum);
81153
}
82154

155+
async getUsers(): Promise<UserEntity[]>{
156+
const users = await this.prismaService.users.findMany({
157+
include: {
158+
avatar: true,
159+
},
160+
});
161+
if(!users.length)
162+
return [];
163+
return users.map((user): UserEntity => this.toUserEntity(user, user.avatar?.sum));
164+
}
165+
166+
async deleteUserById(userId: string): Promise<void>{
167+
const user = await this.prismaService.users.findUnique({
168+
where: {
169+
id: userId,
170+
},
171+
});
172+
if(!user)
173+
throw new NotFoundException("User not found");
174+
// Delete the user
175+
await this.prismaService.users.delete({
176+
where: {
177+
id: userId,
178+
},
179+
});
180+
}
181+
83182
async login(usernameOrEmail: string, password: string): Promise<LoginPayload>{
84183
const user = await this.prismaService.users.findFirst({
85184
where: {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
Body,
3+
ConflictException,
4+
Controller,
5+
Delete,
6+
Get,
7+
HttpCode,
8+
Param,
9+
Patch,
10+
Post,
11+
UseGuards,
12+
} from "@nestjs/common";
13+
import {ApiBearerAuth, ApiTags} from "@nestjs/swagger";
14+
import {AuthGuard} from "@nestjs/passport";
15+
import {UsersService} from "../../users/users.service";
16+
import {UserEntity} from "../../users/models/entities/user.entity";
17+
import {CreateUserDto} from "../../users/models/dto/create-user.dto";
18+
import {HttpStatusCode} from "axios";
19+
import {User} from "../../users/decorators/user.decorator";
20+
import {ChangePasswordDto} from "../../users/models/dto/change-password.dto";
21+
22+
@Controller("admin/users")
23+
@ApiTags("Admin users")
24+
@UseGuards(AuthGuard("admin-jwt"))
25+
export class AdminUsersController{
26+
constructor(
27+
private readonly usersService: UsersService,
28+
){}
29+
30+
@Get("")
31+
@ApiBearerAuth()
32+
async getUsers(): Promise<UserEntity[]>{
33+
return this.usersService.getUsers();
34+
}
35+
36+
@Post("new")
37+
@ApiBearerAuth()
38+
async createUser(@Body() user: CreateUserDto): Promise<UserEntity>{
39+
return this.usersService.createUser(user);
40+
}
41+
42+
@Delete(":id")
43+
@ApiBearerAuth()
44+
@HttpCode(HttpStatusCode.NoContent)
45+
async deleteUser(@User() user: UserEntity, @Param("id") userId: string): Promise<void>{
46+
if(user.id === userId)
47+
throw new ConflictException("You cannot delete your own account");
48+
return this.usersService.deleteUserById(userId);
49+
}
50+
51+
@Patch(":id/password")
52+
@ApiBearerAuth()
53+
@HttpCode(HttpStatusCode.NoContent)
54+
async setUserPassword(@User() user: UserEntity, @Body() changePasswordDto: ChangePasswordDto, @Param("id") userId: string): Promise<void>{
55+
if(user.id === userId)
56+
throw new ConflictException("You cannot change your own password through this endpoint. Use the user profile endpoint instead.");
57+
return this.usersService.changeUserPassword(userId, changePasswordDto.newPassword);
58+
}
59+
}

src/modules/webtoon/admin/admin.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {AddWebtoonToQueueDto} from "./models/dto/add-webtoon-to-queue.dto";
55
import {ApiBearerAuth, ApiResponse, ApiTags} from "@nestjs/swagger";
66
import {AuthGuard} from "@nestjs/passport";
77
import {HttpStatusCode} from "axios";
8+
import {UsersService} from "../../users/users.service";
89

910
@Controller("admin")
1011
@ApiTags("Admin")
1112
@UseGuards(AuthGuard("admin-jwt"))
1213
export class AdminController{
1314
constructor(
1415
private readonly downloadManagerService: DownloadManagerService,
16+
private readonly usersService: UsersService,
1517
){}
1618

1719
@Post("queue")

0 commit comments

Comments
 (0)