refactor: changed the project structure
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { AttachmentService } from './attachment.service';
|
||||
import { CreateAttachmentDto } from './dto/create-attachment.dto';
|
||||
import { UpdateAttachmentDto } from './dto/update-attachment.dto';
|
||||
|
||||
@Controller('attachment')
|
||||
export class AttachmentController {
|
||||
constructor(private readonly attachmentService: AttachmentService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() createAttachmentDto: CreateAttachmentDto) {
|
||||
return this.attachmentService.create(createAttachmentDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.attachmentService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.attachmentService.findOneOrFail(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateAttachmentDto: UpdateAttachmentDto) {
|
||||
return this.attachmentService.update(id, updateAttachmentDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.attachmentService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AttachmentService } from './attachment.service';
|
||||
import { AttachmentController } from './attachment.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { AttachmentRepository } from './repositories/attachment.repository';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Attachment])],
|
||||
controllers: [AttachmentController],
|
||||
providers: [AttachmentService, AttachmentRepository],
|
||||
})
|
||||
export class AttachmentModule {}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateAttachmentDto } from './dto/create-attachment.dto';
|
||||
import { UpdateAttachmentDto } from './dto/update-attachment.dto';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { AttachmentRepository } from './repositories/attachment.repository';
|
||||
import { AttachmentMessages } from 'src/common/enums/messages.enum';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentService {
|
||||
constructor(private readonly attachmentRepository: AttachmentRepository) {}
|
||||
|
||||
async create(createAttachmentDto: CreateAttachmentDto): Promise<Attachment> {
|
||||
const attachment = this.attachmentRepository.create(createAttachmentDto);
|
||||
await this.attachmentRepository.save(attachment);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Attachment[]> {
|
||||
return await this.attachmentRepository.find();
|
||||
}
|
||||
|
||||
async findOneOrFail(id: string): Promise<Attachment> {
|
||||
const attachment = await this.attachmentRepository.findOne({where: {id}});
|
||||
if(!attachment) throw new NotFoundException(AttachmentMessages.ATTACHMENT_NOT_FOUND);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
async update(id: string, updateAttachmentDto: UpdateAttachmentDto): Promise<Attachment> {
|
||||
const attachment = await this.findOneOrFail(id);
|
||||
Object.assign(attachment, updateAttachmentDto);
|
||||
return await this.attachmentRepository.save(attachment);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<Attachment> {
|
||||
const attachment = await this.findOneOrFail(id);
|
||||
await this.attachmentRepository.remove(attachment);
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum, IsNotEmpty, IsString } from "class-validator";
|
||||
import { AttachmentType } from "../enums/attachment-type.enum";
|
||||
|
||||
export class CreateAttachmentDto {
|
||||
@ApiProperty({
|
||||
example: 'فایل شکایت',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: AttachmentType.IMAGE,
|
||||
})
|
||||
@IsEnum(AttachmentType)
|
||||
@IsNotEmpty()
|
||||
type: AttachmentType;
|
||||
|
||||
@ApiProperty({
|
||||
example: '/images/file.pdf'
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
filepath: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateAttachmentDto } from './create-attachment.dto';
|
||||
|
||||
export class UpdateAttachmentDto extends PartialType(CreateAttachmentDto) {}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { BaseEntity } from "src/common/entities/base.entity";
|
||||
import { Column, Entity, OneToOne } from "typeorm";
|
||||
import { AttachmentType } from "../enums/attachment-type.enum";
|
||||
import { ComplaintAttachment } from "src/modules/complaint/entities/complaint-attachment.entity";
|
||||
import { ReplyAttachment } from "src/modules/reply/entities/reply-attachment.entity";
|
||||
|
||||
@Entity('attachments')
|
||||
export class Attachment extends BaseEntity{
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: AttachmentType,
|
||||
})
|
||||
type: AttachmentType;
|
||||
|
||||
@Column({
|
||||
type: 'text'
|
||||
})
|
||||
filepath: string;
|
||||
|
||||
@OneToOne(() => ComplaintAttachment, (ca) => ca.attachment)
|
||||
complaint: ComplaintAttachment;
|
||||
|
||||
@OneToOne(() => ReplyAttachment, (ra) => ra.attachment)
|
||||
reply: ReplyAttachment;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum AttachmentType {
|
||||
FILE = "file",
|
||||
LINK = "link",
|
||||
IMAGE = "image",
|
||||
VIDEO = "video",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Repository } from "typeorm";
|
||||
import { Attachment } from "../entities/attachment.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentRepository extends Repository<Attachment> {
|
||||
constructor(@InjectRepository(Attachment) attachmentRepository: Repository<Attachment>) {
|
||||
super(attachmentRepository.target, attachmentRepository.manager, attachmentRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, Matches } from "class-validator";
|
||||
|
||||
export class LoginCredentialDto {
|
||||
@ApiProperty({example: '09123456789'})
|
||||
@IsString()
|
||||
@Matches(/^09\d{9}$/)
|
||||
phoneNumber: string;
|
||||
|
||||
@ApiProperty({example: 'Mystrongpassword@47'})
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Matches } from 'class-validator';
|
||||
|
||||
export class SendOtpDto {
|
||||
@ApiProperty({
|
||||
example: '09123456789',
|
||||
})
|
||||
@Matches(/^09\d{9}$/, {
|
||||
message: 'Phone number must be a valid Iranian mobile number.',
|
||||
})
|
||||
phoneNumber!: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, Matches, Length } from 'class-validator';
|
||||
|
||||
export class VerifyOtpDto {
|
||||
@ApiProperty({
|
||||
example: '09123456789',
|
||||
})
|
||||
@Matches(/^09\d{9}$/)
|
||||
phoneNumber: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '123456',
|
||||
})
|
||||
@IsString()
|
||||
@Length(6, 6)
|
||||
code: string;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
import { SendOtpDto } from './DTO/send-otp.dto';
|
||||
import { LoginCredentialDto } from './DTO/login-credential.dto';
|
||||
import { VerifyOtpDto } from './DTO/verify-otp.dto';
|
||||
import { AuthGuard } from 'src/common/decorators/auth-guard.decorator';
|
||||
import type { IRequest } from './interfaces/IRequest';
|
||||
import { UserDec } from 'src/common/decorators/user.decorator';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('send-otp')
|
||||
@ApiOperation({
|
||||
summary: 'Send OTP to a phone number',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Sent Otp Successfully!' })
|
||||
@ApiResponse({ status: 400, description: 'Bad Request from User!' })
|
||||
async sendOtp(@Body() dto: SendOtpDto) {
|
||||
return this.authService.sendOTP(dto.phoneNumber);
|
||||
}
|
||||
|
||||
@Post('verify-otp')
|
||||
@ApiOperation({
|
||||
summary: 'Verify OTP.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Verified Otp Successfully!' })
|
||||
@ApiResponse({ status: 400, description: 'Bad Request from the user' })
|
||||
async verifyOtp(@Body() dto: VerifyOtpDto) {
|
||||
return this.authService.verifyOTP(dto);
|
||||
}
|
||||
|
||||
@Post('check-credential')
|
||||
@ApiOperation({ summary: 'Check Login Credentials. If Valid Sends OTP' })
|
||||
@ApiResponse({ status: 200, description: 'Valid Credentials!' })
|
||||
@ApiResponse({ status: 400, description: 'Credentials Invalid!' })
|
||||
async checkLoginCredential(@Body() dto: LoginCredentialDto) {
|
||||
return this.authService.checkLoginCredentials(dto);
|
||||
}
|
||||
|
||||
@Post('verify-login')
|
||||
@ApiOperation({ summary: 'verifies OTP and then login' })
|
||||
@ApiResponse({ status: 200, description: 'User Logged in successfully!' })
|
||||
@ApiResponse({ status: 400, description: 'Bad Request From The User' })
|
||||
async verifyOtpAndLogin(@Body() verifyOtpDto: VerifyOtpDto) {
|
||||
return this.authService.verifyOtpAndLogin(verifyOtpDto);
|
||||
}
|
||||
|
||||
@AuthGuard()
|
||||
@Get('profile')
|
||||
profile(@Req() req: IRequest, @UserDec('id') userId: string) {
|
||||
return req.user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { OTP } from './entities/otp.entity';
|
||||
import { OtpRepository } from './repositories/otp.repository';
|
||||
import { UsersModule } from 'src/modules/users/users.module';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([OTP]),
|
||||
PassportModule,
|
||||
JwtModule.register({}),
|
||||
UtilsModule,
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, OtpRepository, JwtStrategy],
|
||||
exports: [JwtModule, PassportModule]
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { ISmsConfigs } from 'src/config/sms.config';
|
||||
import { SMS_CONFIG } from '../utils/constants';
|
||||
import { SmsService } from '../utils/providers/sms.service';
|
||||
import { OtpRepository } from './repositories/otp.repository';
|
||||
import { OtpStatus } from './enums/otp-status.enum';
|
||||
import { VerifyOtpDto } from './DTO/verify-otp.dto';
|
||||
import { OTPMessages, UserMessages } from 'src/common/enums/messages.enum';
|
||||
import { LoginCredentialDto } from './DTO/login-credential.dto';
|
||||
import { UsersService } from 'src/modules/users/users.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import ms from 'ms';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@Inject(SMS_CONFIG) private smsConfig: ISmsConfigs,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly OtpRepository: OtpRepository,
|
||||
private readonly userService: UsersService,
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async sendOTP(phone: string) {
|
||||
const otp = this.generateOtp();
|
||||
|
||||
try {
|
||||
await this.smsService.sendSms({
|
||||
Mobile: phone,
|
||||
TemplateId: this.smsConfig.SMS_PATTERN_OTP,
|
||||
Parameters: [
|
||||
{
|
||||
name: 'code',
|
||||
value: otp,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await this.OtpRepository.update(
|
||||
{ phoneNumber: phone, status: OtpStatus.PENDING },
|
||||
{ status: OtpStatus.EXPIRED },
|
||||
);
|
||||
|
||||
await this.OtpRepository.save(
|
||||
this.OtpRepository.create({
|
||||
phoneNumber: phone,
|
||||
code: otp,
|
||||
expirationDate: new Date(Date.now() + 2 * 60 * 1000),
|
||||
status: OtpStatus.PENDING,
|
||||
numberOfTries: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
return { otp };
|
||||
} catch (error) {
|
||||
if (this.configService.get('NODE_ENV') !== 'production') {
|
||||
return { otp };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async verifyOTP(dto: VerifyOtpDto) {
|
||||
const otp = await this.OtpRepository.findOne({
|
||||
where: { phoneNumber: dto.phoneNumber, status: OtpStatus.PENDING },
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
if (!otp) {
|
||||
throw new BadRequestException(OTPMessages.OTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (otp.expirationDate < new Date()) {
|
||||
otp.status = OtpStatus.EXPIRED;
|
||||
|
||||
await this.OtpRepository.save(otp);
|
||||
|
||||
throw new BadRequestException(OTPMessages.OTP_EXPIRED);
|
||||
}
|
||||
|
||||
if (otp.code !== dto.code) {
|
||||
otp.numberOfTries++;
|
||||
|
||||
if (otp.numberOfTries >= 3) {
|
||||
otp.status = OtpStatus.EXPIRED;
|
||||
|
||||
await this.OtpRepository.save(otp);
|
||||
|
||||
throw new BadRequestException(OTPMessages.OTP_TOO_MANY_TRIES);
|
||||
}
|
||||
|
||||
await this.OtpRepository.save(otp);
|
||||
|
||||
throw new BadRequestException(OTPMessages.OTP_INVALID);
|
||||
}
|
||||
|
||||
otp.status = OtpStatus.VERIFIED;
|
||||
|
||||
await this.OtpRepository.save(otp);
|
||||
}
|
||||
|
||||
private generateOtp(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
|
||||
async checkLoginCredentials(credential: LoginCredentialDto): Promise<void> {
|
||||
const { phoneNumber, password } = credential;
|
||||
|
||||
const user = await this.userService.findOneByPhoneOrFail(phoneNumber);
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new BadRequestException(UserMessages.USER_PASSWORD_INVALID);
|
||||
}
|
||||
|
||||
await this.sendOTP(phoneNumber);
|
||||
}
|
||||
|
||||
async verifyOtpAndLogin(dto: VerifyOtpDto) {
|
||||
await this.verifyOTP(dto);
|
||||
|
||||
const user = await this.userService.findUserWithRolesByPhoneOrFail(
|
||||
dto.phoneNumber,
|
||||
);
|
||||
|
||||
const roles = user.roles.map((r) => r.role);
|
||||
|
||||
const payload = {
|
||||
id: user.id,
|
||||
phone: user.phone,
|
||||
roles,
|
||||
};
|
||||
|
||||
const accessToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
|
||||
expiresIn: this.configService.getOrThrow<string>(
|
||||
'JWT_ACCESS_EXPIRES',
|
||||
) as ms.StringValue,
|
||||
});
|
||||
|
||||
const refreshToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
|
||||
expiresIn: this.configService.getOrThrow<string>(
|
||||
'JWT_REFRESH_EXPIRES',
|
||||
) as ms.StringValue,
|
||||
});
|
||||
|
||||
await this.userService.updateRefreshToken(user.id, refreshToken);
|
||||
|
||||
return {
|
||||
user: {
|
||||
userId: user.id,
|
||||
username: user.firstName + ' ' + user.lastName,
|
||||
userPhone: user.phone,
|
||||
userEmail: user.email,
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Column, Entity } from 'typeorm';
|
||||
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||
import { OtpStatus } from '../enums/otp-status.enum';
|
||||
|
||||
@Entity('otps')
|
||||
export class OTP extends BaseEntity {
|
||||
@Column({ type: 'varchar', length: 6 })
|
||||
code: string;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
expirationDate: Date;
|
||||
|
||||
@Column({type: 'enum', enum: OtpStatus, default: OtpStatus.PENDING})
|
||||
status: OtpStatus;
|
||||
|
||||
@Column({ type: 'smallint', default: 0 })
|
||||
numberOfTries: number;
|
||||
|
||||
@Column({
|
||||
length: 11,
|
||||
})
|
||||
phoneNumber: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum OtpStatus {
|
||||
PENDING = 'PENDING',
|
||||
VERIFIED = 'VERIFIED',
|
||||
EXPIRED = 'EXPIRED'
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SKIP_AUTH_KEY } from 'src/common/decorators/skip-auth.decorator';
|
||||
import { AuthMessages } from 'src/common/enums/messages.enum';
|
||||
import { JWT_STRATEGY_NAME } from 'src/constants';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const skipAuth = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (skipAuth) return true;
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: unknown, user: any, _info: unknown) {
|
||||
if (err || !user)
|
||||
throw new UnauthorizedException(AuthMessages.TOKEN_EXPIRED_OR_INVALID);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ITokenPayload } from "./IToken-payload";
|
||||
import { IUserIpAndHeaders } from "./IUserIpAndHeader";
|
||||
|
||||
export interface IRequest {
|
||||
user?: ITokenPayload,
|
||||
ip_header?: IUserIpAndHeaders
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RoleType } from "src/modules/users/enums/user-role.enum";
|
||||
|
||||
export interface ITokenPayload {
|
||||
id: string;
|
||||
phone: string;
|
||||
roles: RoleType[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface IUserIpAndHeaders {
|
||||
ip: string;
|
||||
headers: Record<string, string>;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Repository } from 'typeorm';
|
||||
import { OTP } from '../entities/otp.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
export class OtpRepository extends Repository<OTP> {
|
||||
constructor(@InjectRepository(OTP) otpRepository: Repository<OTP>) {
|
||||
super(
|
||||
otpRepository.target,
|
||||
otpRepository.manager,
|
||||
otpRepository.queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy, ExtractJwt } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JWT_STRATEGY_NAME } from 'src/constants';
|
||||
import { ITokenPayload } from '../interfaces/IToken-payload';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: ITokenPayload) {
|
||||
return {
|
||||
id: payload.id,
|
||||
phone: payload.phone,
|
||||
roles: payload.roles
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { ComplaintService } from './complaint.service';
|
||||
import { CreateComplaintDto } from './dto/create-complaint.dto';
|
||||
import { UpdateComplaintDto } from './dto/update-complaint.dto';
|
||||
|
||||
@Controller('complaint')
|
||||
export class ComplaintController {
|
||||
constructor(private readonly complaintService: ComplaintService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() createComplaintDto: CreateComplaintDto) {
|
||||
return this.complaintService.create(createComplaintDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.complaintService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.complaintService.findOneOrFail(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateComplaintDto: UpdateComplaintDto) {
|
||||
return this.complaintService.update(id, updateComplaintDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.complaintService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ComplaintService } from './complaint.service';
|
||||
import { ComplaintController } from './complaint.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Complaint } from './entities/complaint.entity';
|
||||
import { ComplaintRepository } from './repositories/complaint.repository';
|
||||
import { ComplaintAttachment } from './entities/complaint-attachment.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Complaint, ComplaintAttachment])],
|
||||
controllers: [ComplaintController],
|
||||
providers: [ComplaintService, ComplaintRepository],
|
||||
})
|
||||
export class ComplaintModule {}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateComplaintDto } from './dto/create-complaint.dto';
|
||||
import { UpdateComplaintDto } from './dto/update-complaint.dto';
|
||||
import { ComplaintRepository } from './repositories/complaint.repository';
|
||||
import { Complaint } from './entities/complaint.entity';
|
||||
import { ComplaintMessages } from 'src/common/enums/messages.enum';
|
||||
|
||||
@Injectable()
|
||||
export class ComplaintService {
|
||||
constructor(private readonly complaintRepository: ComplaintRepository) {}
|
||||
|
||||
async create(createComplaintDto: CreateComplaintDto): Promise<Complaint> {
|
||||
const complaint = this.complaintRepository.create(createComplaintDto);
|
||||
await this.complaintRepository.save(complaint);
|
||||
return complaint;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Complaint[]> {
|
||||
return this.complaintRepository.find();
|
||||
}
|
||||
|
||||
async findOneOrFail(id: string): Promise<Complaint> {
|
||||
const complaint = await this.complaintRepository.findOne({ where: { id } });
|
||||
if (!complaint)
|
||||
throw new NotFoundException(ComplaintMessages.COMPLAINT_NOT_FOUND);
|
||||
|
||||
return complaint;
|
||||
}
|
||||
|
||||
async update(id: string, updateComplaintDto: UpdateComplaintDto): Promise<Complaint> {
|
||||
const complaint = await this.findOneOrFail(id);
|
||||
Object.assign(complaint, updateComplaintDto);
|
||||
return this.complaintRepository.save(complaint);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<Complaint> {
|
||||
const complaint = await this.findOneOrFail(id);
|
||||
await this.complaintRepository.remove(complaint);
|
||||
return complaint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum, IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
import { ComplaintStatus } from "../enums/complaint.enum";
|
||||
|
||||
export class CreateComplaintDto {
|
||||
@ApiProperty({
|
||||
example: 'شکایت من',
|
||||
type: 'string'
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'شکایت من',
|
||||
type: 'string'
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: ComplaintStatus.WAITING_FOR_RESPONSE,
|
||||
enum: ComplaintStatus
|
||||
})
|
||||
@IsEnum(ComplaintStatus)
|
||||
status: ComplaintStatus;
|
||||
|
||||
@ApiProperty({example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', description: 'Id of the User that made the complaint'})
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', description: 'Id of the Orgain the complaint is about.'})
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
organId: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateComplaintDto } from './create-complaint.dto';
|
||||
|
||||
export class UpdateComplaintDto extends PartialType(CreateComplaintDto) {}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { BaseEntity } from "src/common/entities/base.entity";
|
||||
import { Entity, JoinColumn, ManyToOne, OneToOne, Unique } from "typeorm";
|
||||
import { Complaint } from "./complaint.entity";
|
||||
import { Attachment } from "src/modules/attachment/entities/attachment.entity";
|
||||
|
||||
@Entity('complaint_attachments')
|
||||
@Unique(['attachment'])
|
||||
export class ComplaintAttachment extends BaseEntity {
|
||||
@ManyToOne(() => Complaint, (complaint) => complaint.attachments)
|
||||
complaint: Complaint;
|
||||
|
||||
@OneToOne(() => Attachment, (a) => a.complaint, {
|
||||
cascade: true,
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
attachment: Attachment;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne } from 'typeorm';
|
||||
import { ComplaintStatus } from '../enums/complaint.enum';
|
||||
import { User } from 'src/modules/users/entities/user.entity';
|
||||
import { Organ } from 'src/modules/organ/entities/organ.entity';
|
||||
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||
import { ComplaintAttachment } from './complaint-attachment.entity';
|
||||
import { Reply } from 'src/modules/reply/entities/reply.entity';
|
||||
|
||||
@Entity('complaints')
|
||||
export class Complaint extends BaseEntity {
|
||||
@Column({
|
||||
type: 'text',
|
||||
})
|
||||
message: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
})
|
||||
subject: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ComplaintStatus,
|
||||
default: ComplaintStatus.WAITING_FOR_RESPONSE,
|
||||
})
|
||||
status: ComplaintStatus;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.complaints, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({
|
||||
name: 'userId',
|
||||
})
|
||||
complainant: User;
|
||||
|
||||
@ManyToOne(() => Organ, (organ) => organ.complaints, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn({
|
||||
name: 'organId'
|
||||
})
|
||||
organ: Organ;
|
||||
|
||||
@OneToMany(() => ComplaintAttachment, (ca) => ca.complaint)
|
||||
attachments: ComplaintAttachment[];
|
||||
|
||||
@OneToMany(() => Reply, (reply) => reply.complaint)
|
||||
messages: Reply[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum ComplaintStatus {
|
||||
WAITING_FOR_RESPONSE = 'waiting_for_response',
|
||||
ANSWERED = 'answered'
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Complaint } from '../entities/complaint.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class ComplaintRepository extends Repository<Complaint> {
|
||||
constructor(
|
||||
@InjectRepository(Complaint) complaintRepository: ComplaintRepository,
|
||||
) {
|
||||
super(
|
||||
complaintRepository.target,
|
||||
complaintRepository.manager,
|
||||
complaintRepository.queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
export class CreateOrganDto {
|
||||
@ApiProperty({example: 'استانداری', type: 'string'})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({example: '/images/logos/xyz.jpg', type: 'string'})
|
||||
@IsString()
|
||||
logo: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateOrganDto } from './create-organ.dto';
|
||||
|
||||
export class UpdateOrganDto extends PartialType(CreateOrganDto) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||
import { Complaint } from 'src/modules/complaint/entities/complaint.entity';
|
||||
import { Column, Entity, OneToMany } from 'typeorm';
|
||||
|
||||
@Entity('organs')
|
||||
export class Organ extends BaseEntity {
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
})
|
||||
logo: string;
|
||||
|
||||
@OneToMany(() => Complaint, (complaint) => complaint.organ)
|
||||
complaints: Complaint[];
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { OrganService } from './organ.service';
|
||||
import { CreateOrganDto } from './dto/create-organ.dto';
|
||||
import { UpdateOrganDto } from './dto/update-organ.dto';
|
||||
|
||||
@Controller('organ')
|
||||
export class OrganController {
|
||||
constructor(private readonly organService: OrganService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() createOrganDto: CreateOrganDto) {
|
||||
return this.organService.create(createOrganDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.organService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.organService.findOneOrFail(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateOrganDto: UpdateOrganDto) {
|
||||
return this.organService.update(id, updateOrganDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.organService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrganService } from './organ.service';
|
||||
import { OrganController } from './organ.controller';
|
||||
import { OrganRepository } from './repositories/organ.repository';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Organ } from './entities/organ.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Organ])
|
||||
],
|
||||
controllers: [OrganController],
|
||||
providers: [OrganService, OrganRepository],
|
||||
})
|
||||
export class OrganModule {}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateOrganDto } from './dto/create-organ.dto';
|
||||
import { UpdateOrganDto } from './dto/update-organ.dto';
|
||||
import { OrganRepository } from './repositories/organ.repository';
|
||||
import { OrganMessages } from 'src/common/enums/messages.enum';
|
||||
import { Organ } from './entities/organ.entity';
|
||||
|
||||
@Injectable()
|
||||
export class OrganService {
|
||||
constructor(private readonly organRepository: OrganRepository) {}
|
||||
|
||||
async create(createOrganDto: CreateOrganDto) {
|
||||
const organ = this.organRepository.create(createOrganDto);
|
||||
await this.organRepository.save(organ);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Organ[]> {
|
||||
return await this.organRepository.find();
|
||||
}
|
||||
|
||||
async findOneOrFail(id: string): Promise<Organ> {
|
||||
const organ = await this.organRepository.findOne({where: {id}});
|
||||
if(!organ) throw new NotFoundException(OrganMessages.ORGAN_NOT_FOUND);
|
||||
|
||||
return organ;
|
||||
}
|
||||
|
||||
async update(id: string, updateOrganDto: UpdateOrganDto) {
|
||||
const organ = await this.findOneOrFail(id);
|
||||
|
||||
Object.assign(organ, updateOrganDto);
|
||||
return await this.organRepository.save(organ);
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const organ = await this.findOneOrFail(id);
|
||||
await this.organRepository.remove(organ);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Repository } from "typeorm";
|
||||
import { Organ } from "../entities/organ.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OrganRepository extends Repository<Organ> {
|
||||
constructor(@InjectRepository(Organ) organRepository: Repository<Organ>) {
|
||||
super(organRepository.target, organRepository.manager, organRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
|
||||
export class CreateReplyDto {
|
||||
@ApiProperty({
|
||||
description: 'text content of the reply'
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
content: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Id of the sender user'
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
senderId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Id of the receiver user'
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
receiverId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Id of the Complaint'
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
complaintId: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateReplyDto } from './create-reply.dto';
|
||||
|
||||
export class UpdateReplyDto extends PartialType(CreateReplyDto) {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BaseEntity } from "src/common/entities/base.entity";
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from "typeorm";
|
||||
import { Reply } from "./reply.entity";
|
||||
import { Attachment } from "src/modules/attachment/entities/attachment.entity";
|
||||
|
||||
@Entity('reply_attachments')
|
||||
export class ReplyAttachment extends BaseEntity {
|
||||
@ManyToOne(() => Reply, (rp) => rp.attachments, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
reply: Reply;
|
||||
|
||||
@OneToOne(() => Attachment, (attch) => attch.reply, {
|
||||
cascade: true,
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
attachment: Attachment;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Attachment } from "src/modules/attachment/entities/attachment.entity";
|
||||
import { BaseEntity } from "src/common/entities/base.entity";
|
||||
import { Complaint } from "src/modules/complaint/entities/complaint.entity";
|
||||
import { User } from "src/modules/users/entities/user.entity";
|
||||
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
|
||||
import { ReplyAttachment } from "./reply-attachment.entity";
|
||||
|
||||
@Entity('replies')
|
||||
export class Reply extends BaseEntity {
|
||||
@Column({
|
||||
type: 'text'
|
||||
})
|
||||
content: string; // the text or message written.
|
||||
|
||||
@ManyToOne(() => User)
|
||||
sender: User;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
receiver: User;
|
||||
|
||||
@ManyToOne(() => Complaint, (complaint) => complaint.messages)
|
||||
complaint: Complaint;
|
||||
|
||||
@OneToMany(() => ReplyAttachment, (ra) => ra.reply)
|
||||
attachments: ReplyAttachment;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { ReplyService } from './reply.service';
|
||||
import { CreateReplyDto } from './dto/create-reply.dto';
|
||||
import { UpdateReplyDto } from './dto/update-reply.dto';
|
||||
|
||||
@Controller('reply')
|
||||
export class ReplyController {
|
||||
constructor(private readonly replyService: ReplyService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() createReplyDto: CreateReplyDto) {
|
||||
return this.replyService.create(createReplyDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.replyService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.replyService.findOneOrFail(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateReplyDto: UpdateReplyDto) {
|
||||
return this.replyService.update(id, updateReplyDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.replyService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ReplyService } from './reply.service';
|
||||
import { ReplyController } from './reply.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Reply } from './entities/reply.entity';
|
||||
import { ReplyAttachment } from './entities/reply-attachment.entity';
|
||||
import { ReplyRepository } from './repositories/reply.repository';
|
||||
import { ReplyAttachmentRepository } from './repositories/reply-attachment.repository';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Reply, ReplyAttachment])],
|
||||
controllers: [ReplyController],
|
||||
providers: [ReplyService, ReplyRepository, ReplyAttachmentRepository],
|
||||
})
|
||||
export class ReplyModule {}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateReplyDto } from './dto/create-reply.dto';
|
||||
import { UpdateReplyDto } from './dto/update-reply.dto';
|
||||
import { ReplyRepository } from './repositories/reply.repository';
|
||||
// import { ReplyAttachmentRepository } from './repositories/reply-attachment.repository';
|
||||
import { Reply } from './entities/reply.entity';
|
||||
import { ReplyMessages } from 'src/common/enums/messages.enum';
|
||||
|
||||
@Injectable()
|
||||
export class ReplyService {
|
||||
constructor(
|
||||
private readonly replyRepository: ReplyRepository,
|
||||
// private readonly replyAttachmentRepository: ReplyAttachmentRepository,
|
||||
) {}
|
||||
|
||||
async create(createReplyDto: CreateReplyDto): Promise<Reply> {
|
||||
const reply = this.replyRepository.create(createReplyDto);
|
||||
await this.replyRepository.save(reply);
|
||||
return reply;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Reply[]> {
|
||||
return await this.replyRepository.find();
|
||||
}
|
||||
|
||||
async findOneOrFail(id: string): Promise<Reply> {
|
||||
const reply = await this.replyRepository.findOne({where: {id}});
|
||||
if(!reply) throw new NotFoundException(ReplyMessages.REPLY_NOT_FOUND);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
async update(id: string, updateReplyDto: UpdateReplyDto): Promise<Reply> {
|
||||
const reply = await this.findOneOrFail(id);
|
||||
Object.assign(reply, updateReplyDto);
|
||||
return reply;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<Reply> {
|
||||
const reply = await this.findOneOrFail(id);
|
||||
await this.replyRepository.remove(reply);
|
||||
return reply;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Repository } from "typeorm";
|
||||
import { ReplyAttachment } from "../entities/reply-attachment.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
|
||||
@Injectable()
|
||||
export class ReplyAttachmentRepository extends Repository<ReplyAttachment> {
|
||||
constructor(@InjectRepository(ReplyAttachment) replyAttachmentRepository: Repository<ReplyAttachment>) {
|
||||
super(replyAttachmentRepository.target, replyAttachmentRepository.manager, replyAttachmentRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Repository } from "typeorm";
|
||||
import { Reply } from "../entities/reply.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ReplyRepository extends Repository<Reply> {
|
||||
constructor(@InjectRepository(Reply) replyRepository: Repository<Reply>) {
|
||||
super(replyRepository.target, replyRepository.manager, replyRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
import { RoleType } from "../enums/user-role.enum";
|
||||
|
||||
export class CreateUserRoleDto {
|
||||
@ApiProperty({
|
||||
enum: RoleType,
|
||||
example: RoleType.NORMAL_USER
|
||||
})
|
||||
@IsEnum(RoleType)
|
||||
role: RoleType;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty({ example: 'Mahdi' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(20)
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty({ example: 'Rafie' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty({ example: 'mahdirafie@gmail.com' })
|
||||
@IsEmail()
|
||||
@MaxLength(100)
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: '09123456789' })
|
||||
@Matches(/^09\d{9}$/, {
|
||||
message: 'Phone number must be a valid Iranian mobile number.',
|
||||
})
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({ example: 'MahdiPassword@5!' })
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { BaseEntity } from "src/common/entities/base.entity";
|
||||
import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
|
||||
import { RoleType } from "../enums/user-role.enum";
|
||||
import { User } from "./user.entity";
|
||||
|
||||
@Entity('user_roles')
|
||||
@Unique(['user', 'role'])
|
||||
export class UserRole extends BaseEntity {
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: RoleType,
|
||||
default: RoleType.NORMAL_USER
|
||||
})
|
||||
role: RoleType;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.roles, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Entity, Column, ManyToMany, JoinTable, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { UserRole } from './user-role.entity';
|
||||
import { Complaint } from 'src/modules/complaint/entities/complaint.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User extends BaseEntity {
|
||||
@Column({ type: 'varchar', length: 20 })
|
||||
firstName: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20 })
|
||||
lastName: string;
|
||||
|
||||
@Column({ unique: true, type: 'varchar', length: 100 })
|
||||
email: string;
|
||||
|
||||
@Column({ unique: true, type: 'varchar', length: 11 })
|
||||
phone: string;
|
||||
|
||||
@Column({})
|
||||
@Exclude()
|
||||
password: string;
|
||||
|
||||
@Column({
|
||||
name: 'refresh_token',
|
||||
nullable: true,
|
||||
select: false,
|
||||
})
|
||||
@Exclude()
|
||||
refreshToken: string;
|
||||
|
||||
@OneToMany(() => UserRole, (userRole) => userRole.user, {
|
||||
cascade: true
|
||||
})
|
||||
roles: UserRole[];
|
||||
|
||||
@OneToMany(() => Complaint, (complaint) => complaint.complainant, {
|
||||
cascade: true
|
||||
})
|
||||
complaints: Complaint[];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum RoleType {
|
||||
NORMAL_USER = 'normal_user',
|
||||
ADMIN = 'admin',
|
||||
EXPERT = 'expert'
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserRole } from '../entities/user-role.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserRoleRepository extends Repository<UserRole> {
|
||||
constructor(
|
||||
@InjectRepository(UserRole) userRoleRepository: Repository<UserRole>,
|
||||
) {
|
||||
super(
|
||||
userRoleRepository.target,
|
||||
userRoleRepository.manager,
|
||||
userRoleRepository.queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from '../entities/user.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository extends Repository<User> {
|
||||
constructor(@InjectRepository(User) userRepository: Repository<User>) {
|
||||
super(
|
||||
userRepository.target,
|
||||
userRepository.manager,
|
||||
userRepository.queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
findUserWithRolesByPhone(phone: string): Promise<User | null> {
|
||||
return this.createQueryBuilder('users')
|
||||
.leftJoinAndSelect('users.roles', 'roles')
|
||||
.where("users.phone = :phone", {phone})
|
||||
.select(
|
||||
[
|
||||
'users',
|
||||
'roles.id',
|
||||
'roles.role'
|
||||
]
|
||||
).getOne();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { CreateUserDto } from './DTO/create-user.dto';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserRoleDto } from './DTO/create-user-role.dto';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
|
||||
constructor(
|
||||
private readonly userService: UsersService
|
||||
) {}
|
||||
|
||||
@Post('signup')
|
||||
@ApiOperation({summary: 'Create a new user!'})
|
||||
@ApiResponse({status: 201, description: 'User created successfully!'})
|
||||
@ApiResponse({status: 400, description: 'Bad Request from User!'})
|
||||
signupUser(@Body() body: CreateUserDto): Promise<User> {
|
||||
return this.userService.createUser(body);
|
||||
}
|
||||
|
||||
@Post(':userId/roles')
|
||||
createRole(
|
||||
@Param('userId') userId: string,
|
||||
@Body() userRoleDto: CreateUserRoleDto
|
||||
) {
|
||||
return this.userService.createUserRole(userId, userRoleDto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserRoleRepository } from './repositories/user-role.repository';
|
||||
import { UserRole } from './entities/user-role.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, UserRole])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService, UserRepository, UserRoleRepository],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { User } from './entities/user.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { CreateUserDto } from './DTO/create-user.dto';
|
||||
import { UserMessages, UserRoleMessages } from 'src/common/enums/messages.enum';
|
||||
import { UserRole } from './entities/user-role.entity';
|
||||
import { CreateUserRoleDto } from './DTO/create-user-role.dto';
|
||||
import { UserRoleRepository } from './repositories/user-role.repository';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly userRoleRepository: UserRoleRepository,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ where: { id } });
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(UserMessages.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findOneByPhoneOrFail(phone: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { phone },
|
||||
relations: {
|
||||
roles: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(UserMessages.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findUserWithRolesByPhoneOrFail(phone: string): Promise<User> {
|
||||
const user = await this.userRepository.findUserWithRolesByPhone(phone);
|
||||
|
||||
if (!user)
|
||||
throw new NotFoundException(UserMessages.USER_NOT_FOUND);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateRefreshToken(id: string, newRefreshToken: string) {
|
||||
const user = await this.findOneOrFail(id);
|
||||
|
||||
user.refreshToken = newRefreshToken;
|
||||
await this.userRepository.save(user);
|
||||
}
|
||||
|
||||
async createUser(userDto: CreateUserDto): Promise<User> {
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: [
|
||||
{
|
||||
phone: userDto.phone,
|
||||
},
|
||||
{
|
||||
email: userDto.email,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (existingUser)
|
||||
throw new BadRequestException(UserMessages.USER_ALREADY_EXISTS);
|
||||
|
||||
const hashedPassword: string = await bcrypt.hash(userDto.password, 10);
|
||||
|
||||
const user = this.userRepository.create({
|
||||
...userDto,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
await this.userRepository.save(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUserRole(
|
||||
userId: string,
|
||||
userRoleDto: CreateUserRoleDto,
|
||||
): Promise<UserRole> {
|
||||
const existingRole = await this.userRoleRepository.findOne({
|
||||
where: {
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
role: userRoleDto.role,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRole)
|
||||
throw new BadRequestException(UserRoleMessages.USER_ROLE_ALREADY_EXISTS);
|
||||
|
||||
const userRole = this.userRoleRepository.create({
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
role: userRoleDto.role,
|
||||
});
|
||||
|
||||
await this.userRoleRepository.save(userRole);
|
||||
|
||||
return userRole;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const SMS_CONFIG = "SMS_CONFIG";
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface SmsParameter {
|
||||
name: SmsTemplateParameter;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SmsRequest {
|
||||
Mobile: string;
|
||||
TemplateId: string;
|
||||
Parameters: SmsParameter[];
|
||||
}
|
||||
|
||||
export interface SmsResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface SmsSendResponse extends SmsResponse {
|
||||
data: {
|
||||
MessageId: number;
|
||||
Cost: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type SmsTemplateParameter = string;
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
BadGatewayException,
|
||||
Inject,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { SMS_CONFIG } from '../constants';
|
||||
import type { ISmsConfigs } from 'src/config/sms.config';
|
||||
import { catchError, firstValueFrom } from 'rxjs';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SmsRequest, SmsSendResponse } from '../interfaces/ISms';
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
private readonly logger = new Logger(SmsService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(SMS_CONFIG) private smsConfigs: ISmsConfigs,
|
||||
private readonly httpService: HttpService,
|
||||
) {}
|
||||
|
||||
async sendSms(body: SmsRequest): Promise<SmsSendResponse> {
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<SmsSendResponse>(
|
||||
`${this.smsConfigs.API_URL}/send/verify`,
|
||||
body,
|
||||
{
|
||||
headers: {
|
||||
'X-API-KEY': this.smsConfigs.API_KEY,
|
||||
},
|
||||
timeout: 5000,
|
||||
},
|
||||
)
|
||||
.pipe(
|
||||
catchError((error: AxiosError) => {
|
||||
this.logger.error(
|
||||
'Error while calling SMS provider.',
|
||||
error.stack,
|
||||
);
|
||||
|
||||
throw new InternalServerErrorException(
|
||||
'Failed to communicate with SMS provider.',
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
'Failed to send SMS.',
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
|
||||
throw new InternalServerErrorException('Failed to send SMS.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SmsService } from './providers/sms.service';
|
||||
import { smsConfigsProvider } from 'src/config/sms.config';
|
||||
import { SMS_CONFIG } from './constants';
|
||||
|
||||
@Module({
|
||||
providers: [SmsService, smsConfigsProvider],
|
||||
exports: [SMS_CONFIG, SmsService],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
Reference in New Issue
Block a user