Skip to content

Commit 900b2d5

Browse files
feat: implement user management module and client administration interface
1 parent 5f3a052 commit 900b2d5

18 files changed

Lines changed: 468 additions & 207 deletions

File tree

backend/package-lock.json

Lines changed: 0 additions & 181 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/src/modules/gallery/infrastructure/repositories/gallery.repository.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class GalleryRepository {
1818
const docs = await this.galleryModel.find().exec();
1919
const doc = docs.map((doc) => this.toDomain(doc));
2020
console.log('DOCS: ', doc);
21-
21+
2222
return doc;
2323
}
2424

@@ -54,13 +54,13 @@ export class GalleryRepository {
5454
private toDomain(doc: GalleryDocument): Gallery {
5555
const { _id, title, imageUrl, category, tags, createdAt } = doc as any;
5656
console.log('createdAT: ', createdAt);
57-
57+
5858
return new Gallery(
5959
_id.toString(),
6060
title,
6161
imageUrl,
6262
category,
63-
tags,
63+
tags,
6464
createdAt,
6565
);
6666
}

backend/src/modules/user/application/user.service.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@nestjs/common';
2+
import * as bcrypt from 'bcrypt';
23
import { User } from '../domain/user.entity';
34
import { UserRepository } from '../infrastructure/repositories/user.repository';
45

@@ -36,8 +37,18 @@ export class UserService {
3637
return user;
3738
}
3839

39-
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
40-
return await this.userRepository.create(user);
40+
async create(
41+
user: Omit<User, 'id' | 'createdAt'> & { password?: string },
42+
): Promise<User> {
43+
const payload = { ...user };
44+
if (payload.password) {
45+
const salt = await bcrypt.genSalt();
46+
payload.passwordHash = await bcrypt.hash(payload.password, salt);
47+
delete payload.password;
48+
}
49+
return await this.userRepository.create(
50+
payload as Omit<User, 'id' | 'createdAt'>,
51+
);
4152
}
4253

4354
async findOne(id: string): Promise<User> {
@@ -48,8 +59,20 @@ export class UserService {
4859
return user;
4960
}
5061

51-
async update(id: string, updateUserDto: Partial<User>): Promise<User> {
52-
const updatedUser = await this.userRepository.update(id, updateUserDto);
62+
async update(
63+
id: string,
64+
updateUserDto: Partial<User> & { password?: string },
65+
): Promise<User> {
66+
const payload = { ...updateUserDto };
67+
if (payload.password) {
68+
const salt = await bcrypt.genSalt();
69+
payload.passwordHash = await bcrypt.hash(payload.password, salt);
70+
delete payload.password;
71+
}
72+
const updatedUser = await this.userRepository.update(
73+
id,
74+
payload as Partial<User>,
75+
);
5376
if (!updatedUser) {
5477
throw new Error(`User with ID ${id} not found`);
5578
}

backend/src/modules/user/infrastructure/schemas/user.schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export class UserSchemaEntity {
1717
@Prop({ required: true })
1818
firstName: string;
1919

20-
@Prop({ required: true })
20+
@Prop({ required: false })
2121
lastName: string;
2222

23-
@Prop({ required: true })
23+
@Prop({ required: false })
2424
username: string;
2525

2626
@Prop({ required: false })

backend/src/modules/user/presentation/dto/create-user.dto.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,35 @@ import {
99
} from 'class-validator';
1010

1111
export class CreateUserDto {
12+
@IsOptional()
1213
@IsNumber()
13-
@IsNotEmpty()
14-
telegramId: number;
14+
telegramId?: number;
15+
16+
@IsOptional()
17+
@IsString()
18+
email?: string;
1519

1620
@IsString()
1721
@IsNotEmpty()
1822
firstName: string;
1923

24+
@IsOptional()
2025
@IsString()
21-
@IsNotEmpty()
2226
lastName?: string;
2327

28+
@IsOptional()
2429
@IsString()
25-
@IsNotEmpty()
2630
username?: string;
2731

28-
@IsUrl()
29-
@IsNotEmpty()
32+
@IsOptional()
33+
@IsString()
3034
photoUrl?: string;
3135

36+
@IsOptional()
3237
@IsEnum(['user', 'admin'])
3338
role?: 'user' | 'admin';
39+
40+
@IsOptional()
41+
@IsString()
42+
password?: string;
3443
}

backend/src/modules/user/presentation/user.controller.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,46 @@ import {
77
Param,
88
Delete,
99
Req,
10+
UseInterceptors,
11+
UploadedFile,
1012
} from '@nestjs/common';
1113
import { UserService } from '../application/user.service';
1214
import { User } from '@modules/user';
1315
import { CreateUserDto } from './dto/create-user.dto';
1416
import { UpdateUserDto } from './dto/update-user.dto';
1517
import type { AuthenticatedRequest } from '@common/interfaces/authenticated-request.interface';
18+
import { FileInterceptor } from '@nestjs/platform-express';
19+
import { diskStorage } from 'multer';
20+
import { extname } from 'path';
21+
22+
const multerOptions = {
23+
storage: diskStorage({
24+
destination: './uploads/users',
25+
filename: (req, file, callback) => {
26+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
27+
const ext = extname(file.originalname);
28+
callback(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
29+
},
30+
}),
31+
};
1632

1733
@Controller('users')
1834
export class UserController {
1935
constructor(private readonly userService: UserService) {}
2036

2137
@Post()
22-
async create(@Body() createUserDto: CreateUserDto): Promise<User> {
23-
// Mapping DTO to Domain Entity (simple casting for now, or use a mapper)
24-
const user = createUserDto as unknown as Omit<User, 'id' | 'createdAt'>;
25-
return this.userService.create(user);
38+
@UseInterceptors(FileInterceptor('file', multerOptions))
39+
async create(
40+
@Body() createUserDto: CreateUserDto,
41+
@UploadedFile() file: Express.Multer.File,
42+
): Promise<User> {
43+
const userData: Record<string, any> = { ...createUserDto };
44+
if (file) {
45+
userData['photoUrl'] = `/uploads/users/${file.filename}`;
46+
}
47+
return this.userService.create(
48+
userData as Omit<User, 'id' | 'createdAt'> & { password?: string },
49+
);
2650
}
2751

2852
@Get()
@@ -36,14 +60,17 @@ export class UserController {
3660
}
3761

3862
@Put(':id')
63+
@UseInterceptors(FileInterceptor('file', multerOptions))
3964
async update(
4065
@Param('id') id: string,
4166
@Body() updateUserDto: UpdateUserDto,
67+
@UploadedFile() file: Express.Multer.File,
4268
): Promise<User> {
43-
return this.userService.update(
44-
id,
45-
updateUserDto as unknown as Partial<User>,
46-
);
69+
const userData: Record<string, any> = { ...updateUserDto };
70+
if (file) {
71+
userData['photoUrl'] = `/uploads/users/${file.filename}`;
72+
}
73+
return this.userService.update(id, userData);
4774
}
4875

4976
@Delete(':id')

0 commit comments

Comments
 (0)