From d9dde39a209d771d987f9ecedac8532c15a3f5db Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sun, 23 Nov 2025 00:35:20 +0330 Subject: [PATCH] user address --- .../users/controllers/adminUser.controller.ts | 25 --------- .../controllers/public-user.controller.ts | 53 +++++++++++++++++++ .../users/controllers/user.controller.ts | 37 ++++++------- .../users/dto/create-user-address.dto.ts | 52 ++++++++++++++++++ .../users/entities/user-address.entity.ts | 36 +++++++++++++ src/modules/users/entities/user.entity.ts | 9 +++- src/modules/users/user.module.ts | 8 +-- src/modules/users/user.service.ts | 46 ++++++++++++++++ 8 files changed, 215 insertions(+), 51 deletions(-) delete mode 100644 src/modules/users/controllers/adminUser.controller.ts create mode 100644 src/modules/users/controllers/public-user.controller.ts create mode 100644 src/modules/users/dto/create-user-address.dto.ts create mode 100644 src/modules/users/entities/user-address.entity.ts diff --git a/src/modules/users/controllers/adminUser.controller.ts b/src/modules/users/controllers/adminUser.controller.ts deleted file mode 100644 index 20c597b..0000000 --- a/src/modules/users/controllers/adminUser.controller.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Controller, Get, UseGuards, Query, ValidationPipe } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiOkResponse } from '@nestjs/swagger'; -import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; -import { UserService } from '../user.service'; -import { FindUsersDto } from '../dto/find-user.dto'; -import { RestId } from 'src/common/decorators'; - -@UseGuards(AdminAuthGuard) -@ApiBearerAuth() -@ApiTags('Admin/User') -@Controller('admin/user') -export class AdminUserController { - constructor(private readonly userService: UserService) {} - - @ApiOperation({ summary: 'Get paginated list of users with filters' }) - @ApiOkResponse({ description: 'Paginated list of users' }) - @Get('/') - async findAll( - @Query(new ValidationPipe({ transform: true, whitelist: true })) - query: FindUsersDto, - @RestId() restId: string, - ) { - return this.userService.findAll(restId, query); - } -} diff --git a/src/modules/users/controllers/public-user.controller.ts b/src/modules/users/controllers/public-user.controller.ts new file mode 100644 index 0000000..2adacd1 --- /dev/null +++ b/src/modules/users/controllers/public-user.controller.ts @@ -0,0 +1,53 @@ +import { Controller, Get, UseGuards, Patch, Body, ValidationPipe, Post } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse } from '@nestjs/swagger'; +import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; +import { UserService } from '../user.service'; +import { UpdateUserDto } from '../dto/update-user.dto'; +import { CreateUserAddressDto } from '../dto/create-user-address.dto'; +import { UserId } from 'src/common/decorators/user-id.decorator'; + +@UseGuards(AuthGuard) +@ApiBearerAuth() +@ApiTags('User') +@Controller('user') +export class PublicUserController { + constructor(private readonly userService: UserService) {} + + @ApiOperation({ summary: 'Get the current authenticated user profile' }) + @Get('/me') + async getUser(@UserId() userId: string) { + const user = await this.userService.findById(userId); + return user; + } + + @ApiOperation({ summary: 'Update the authenticated user profile' }) + @ApiBody({ type: UpdateUserDto }) + @Patch('/') + async updateUser( + @UserId() userId: string, + @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto, + ) { + const user = await this.userService.updateUser(userId, dto); + return user; + } + + @ApiOperation({ summary: 'Get all addresses for the authenticated user' }) + @ApiOkResponse({ description: 'List of user addresses' }) + @Get('/addresses') + async getUserAddresses(@UserId() userId: string) { + const addresses = await this.userService.getUserAddresses(userId); + return addresses; + } + + @ApiOperation({ summary: 'Create a new address for the authenticated user' }) + @ApiBody({ type: CreateUserAddressDto }) + @ApiOkResponse({ description: 'Created user address' }) + @Post('/addresses') + async createUserAddress( + @UserId() userId: string, + @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: CreateUserAddressDto, + ) { + const address = await this.userService.createUserAddress(userId, dto); + return address; + } +} diff --git a/src/modules/users/controllers/user.controller.ts b/src/modules/users/controllers/user.controller.ts index e309bd5..880827c 100644 --- a/src/modules/users/controllers/user.controller.ts +++ b/src/modules/users/controllers/user.controller.ts @@ -1,32 +1,25 @@ -import { Controller, Get, UseGuards, Patch, Body, ValidationPipe } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody } from '@nestjs/swagger'; +import { Controller, Get, UseGuards, Query, ValidationPipe } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiOkResponse } from '@nestjs/swagger'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { UserService } from '../user.service'; -import { UpdateUserDto } from '../dto/update-user.dto'; -import { UserId } from 'src/common/decorators/user-id.decorator'; +import { FindUsersDto } from '../dto/find-user.dto'; +import { RestId } from 'src/common/decorators'; @UseGuards(AdminAuthGuard) @ApiBearerAuth() -@ApiTags('User') -@Controller('user') -export class UserController { +@ApiTags('Admin/User') +@Controller('admin/user') +export class UsersController { constructor(private readonly userService: UserService) {} - @ApiOperation({ summary: 'Get the current authenticated user profile' }) - @Get('/me') - async getUser(@UserId() userId: string) { - const user = await this.userService.findById(userId); - return user; - } - - @ApiOperation({ summary: 'Update the authenticated user profile' }) - @ApiBody({ type: UpdateUserDto }) - @Patch('/') - async updateUser( - @UserId() userId: string, - @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto, + @ApiOperation({ summary: 'Get paginated list of users with filters' }) + @ApiOkResponse({ description: 'Paginated list of users' }) + @Get('/') + async findAll( + @Query(new ValidationPipe({ transform: true, whitelist: true })) + query: FindUsersDto, + @RestId() restId: string, ) { - const user = await this.userService.updateUser(userId, dto); - return user; + return this.userService.findAll(restId, query); } } diff --git a/src/modules/users/dto/create-user-address.dto.ts b/src/modules/users/dto/create-user-address.dto.ts new file mode 100644 index 0000000..95a8893 --- /dev/null +++ b/src/modules/users/dto/create-user-address.dto.ts @@ -0,0 +1,52 @@ +import { IsString, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +/** + * DTO for creating a new user address. + */ +export class CreateUserAddressDto { + @ApiProperty({ description: 'Address title/label (e.g., "Home", "Work")', example: 'Home' }) + @IsString() + title!: string; + + @ApiProperty({ description: 'Street address', example: '123 Main Street' }) + @IsString() + address!: string; + + @ApiProperty({ description: 'City name', example: 'Tehran' }) + @IsString() + city!: string; + + @ApiPropertyOptional({ description: 'Province', example: 'Tehran Province' }) + @IsOptional() + @IsString() + province?: string; + + @ApiPropertyOptional({ description: 'Postal/ZIP code', example: '12345-6789' }) + @IsOptional() + @IsString() + postalCode?: string; + + @ApiProperty({ description: 'Latitude coordinate', example: 35.6892, minimum: -90, maximum: 90 }) + @IsNumber() + @Min(-90) + @Max(90) + latitude!: number; + + @ApiProperty({ description: 'Longitude coordinate', example: 51.3890, minimum: -180, maximum: 180 }) + @IsNumber() + @Min(-180) + @Max(180) + longitude!: number; + + @ApiPropertyOptional({ description: 'Contact phone for this address', example: '+989123456789' }) + @IsOptional() + @IsString() + phone?: string; + + @ApiPropertyOptional({ description: 'Set as default address', example: false, default: false }) + @IsOptional() + @IsBoolean() + isDefault?: boolean; +} + diff --git a/src/modules/users/entities/user-address.entity.ts b/src/modules/users/entities/user-address.entity.ts new file mode 100644 index 0000000..d2ae78c --- /dev/null +++ b/src/modules/users/entities/user-address.entity.ts @@ -0,0 +1,36 @@ +import { Entity, Property, ManyToOne } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { User } from './user.entity'; + +@Entity({ tableName: 'user_addresses' }) +export class UserAddress extends BaseEntity { + @ManyToOne(() => User) + user!: User; + + @Property() + title!: string; // e.g., "Home", "Work", "Office" + + @Property() + address!: string; + + @Property() + city!: string; + + @Property({ nullable: true }) + province?: string; + + @Property({ nullable: true }) + postalCode?: string | null = null; + + @Property({ type: 'float' }) + latitude!: number; + + @Property({ type: 'float' }) + longitude!: number; + + @Property({ nullable: true }) + phone?: string; // Contact phone for this address + + @Property({ default: false }) + isDefault: boolean = false; // Whether this is the default address +} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index d949c71..4c92726 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,6 +1,7 @@ -import { Entity, Property, OneToOne } from '@mikro-orm/core'; +import { Entity, Property, OneToOne, OneToMany, Collection, Cascade } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { UserAddress } from './user-address.entity'; @Entity({ tableName: 'users' }) export class User extends BaseEntity { @@ -36,4 +37,10 @@ export class User extends BaseEntity { @Property({ default: 0, type: 'int' }) points: number = 0; + + @OneToMany(() => UserAddress, address => address.user, { + cascade: [Cascade.ALL], + orphanRemoval: true, + }) + addresses = new Collection(this); } diff --git a/src/modules/users/user.module.ts b/src/modules/users/user.module.ts index c2f15f3..d8976fd 100644 --- a/src/modules/users/user.module.ts +++ b/src/modules/users/user.module.ts @@ -1,16 +1,18 @@ import { Module } from '@nestjs/common'; import { UserService } from './user.service'; -import { AdminUserController } from './controllers/adminUser.controller'; +import { UsersController } from './controllers/user.controller'; +import { PublicUserController } from './controllers/public-user.controller'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { User } from './entities/user.entity'; +import { UserAddress } from './entities/user-address.entity'; import { JwtModule } from '@nestjs/jwt'; import { UserRepository } from './repositories/user.repository'; import { RefreshToken } from './entities/refresh-token.entity'; @Module({ providers: [UserService, UserRepository], - controllers: [AdminUserController], - imports: [MikroOrmModule.forFeature([User, RefreshToken]), JwtModule], + controllers: [UsersController, PublicUserController], + imports: [MikroOrmModule.forFeature([User, UserAddress, RefreshToken]), JwtModule], exports: [UserService, UserRepository], }) export class UserModule {} diff --git a/src/modules/users/user.service.ts b/src/modules/users/user.service.ts index 661d90a..a583f17 100644 --- a/src/modules/users/user.service.ts +++ b/src/modules/users/user.service.ts @@ -1,9 +1,11 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { FilterQuery, RequiredEntityData } from '@mikro-orm/core'; import { User } from './entities/user.entity'; +import { UserAddress } from './entities/user-address.entity'; import { EntityManager } from '@mikro-orm/postgresql'; import { UpdateUserDto } from './dto/update-user.dto'; import { FindUsersDto } from './dto/find-user.dto'; +import { CreateUserAddressDto } from './dto/create-user-address.dto'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { UserRepository } from './repositories/user.repository'; @@ -137,4 +139,48 @@ export class UserService { }, }; } + + async getUserAddresses(userId: string): Promise { + const user = await this.userRepository.findOne({ id: userId }, { populate: ['addresses'] }); + if (!user) { + throw new NotFoundException(`User with ID ${userId} not found.`); + } + + // Load addresses if not already loaded + await user.addresses.loadItems(); + return user.addresses.getItems(); + } + + async createUserAddress(userId: string, dto: CreateUserAddressDto): Promise { + const user = await this.userRepository.findOne({ id: userId }); + if (!user) { + throw new NotFoundException(`User with ID ${userId} not found.`); + } + + // If setting as default, unset other default addresses + if (dto.isDefault) { + const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true }); + for (const address of existingAddresses) { + address.isDefault = false; + } + await this.em.flush(); + } + + // Create new address + const address = this.em.create(UserAddress, { + user, + title: dto.title, + address: dto.address, + city: dto.city, + province: dto.province, + postalCode: dto.postalCode, + latitude: dto.latitude, + longitude: dto.longitude, + phone: dto.phone, + isDefault: dto.isDefault ?? false, + }); + + await this.em.persistAndFlush(address); + return address; + } }