Skip to content

Commit

Permalink
Merge pull request #37 from BookJamm/feat/35-report-user
Browse files Browse the repository at this point in the history
✨ [Feat]: 사용자 신고 API 구현
  • Loading branch information
eomgerm authored Jan 5, 2024
2 parents f6296ec + 0f152e9 commit 0b336f7
Show file tree
Hide file tree
Showing 19 changed files with 207 additions and 41 deletions.
5 changes: 2 additions & 3 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import { BaseResponse } from 'src/global/base/base-response';
import { ExtractPayload } from 'src/global/decorator/extract-payload.decorator';
import { ExtractToken } from 'src/global/decorator/extract-token.decorator';
import { AuthService } from './auth.service';
import { AppleOAuthRequest } from './dto/apple-oauth-request.dto';
import { AppleOAuthResponse } from './dto/apple-oauth-response.dto';
import { EmailCheckRequest } from './dto/email-check-request.dto';
import { EmailCheckResponse } from './dto/email-check-response.dto';
import { JwtResponse } from './dto/jwt-response.dto';
import { KakaoOAuthRequest } from './dto/kakao-oauth-request.dto';
import { KakaoOAuthResponse } from './dto/kakao-oauth-response.dto';
import { LoginRequest } from './dto/login-request.dto';
import { JwtAuthGuard } from './guard/auth.guard';
import { AppleOAuthRequest } from './dto/apple-oauth-request.dto';
import { AppleOAuthResponse } from './dto/apple-oauth-response.dto';
import { AuthRequestValidationPipe } from 'src/global/validation/pipe/auth-request-validation.pipe';

@Controller('api/auth')
@ApiTags('auth')
Expand Down
16 changes: 8 additions & 8 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@ import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import axios from 'axios';
import { Builder } from 'builder-pattern';
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';
import { JwtHeader, jwtDecode } from 'jwt-decode';
import { BaseException } from 'src/global/base/base-exception';
import { Password } from 'src/user/entity/password';
import { User } from 'src/user/entity/user.entity';
import { SocialType } from 'src/user/enum/social-type';
import { UserResponseCode } from 'src/user/exception/user-response-code';
import { ObjectId, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { AppleOAuthResponse } from './dto/apple-oauth-response.dto';
import { JwtResponse } from './dto/jwt-response.dto';
import { KakaoOAuthResponse } from './dto/kakao-oauth-response.dto';
import { AuthResponseCode } from './exception/auth-respone-code';
import { JwtClaim } from './types/jwt-claim';
import { KakaoUser } from './types/kakao-user';
import { AppleOAuthResponse } from './dto/apple-oauth-response.dto';
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';
import { JwtHeader, jwtDecode } from 'jwt-decode';
import {
APPLE_AUTH_TOKEN_URL,
APPLE_ISSUER,
APP_BUNDLE_ID,
AppleAuthKeys,
AppleJWTPayload,
APP_BUNDLE_ID,
} from './types/apple-user';
import { JwtClaim } from './types/jwt-claim';
import { KakaoUser } from './types/kakao-user';

@Injectable()
export class AuthService {
Expand Down
18 changes: 18 additions & 0 deletions src/global/validation/pipe/user-exists-validation.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
import { BaseException } from 'src/global/base/base-exception';
import { UserResponseCode } from 'src/user/exception/user-response-code';
import { UserService } from 'src/user/user.service';

@Injectable()
export class UserExistsValidationPipe implements PipeTransform {
constructor(private readonly userService: UserService) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async transform(value: number, metadata: ArgumentMetadata) {
const isValid = await this.userService.isUserExists(value);
if (!isValid) {
throw BaseException.of(UserResponseCode.USER_NOT_FOUND);
}

return value;
}
}
3 changes: 2 additions & 1 deletion src/place-review/place-review.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Activity } from 'src/activity/entity/activity.entity';
import { S3Service } from 'src/aws/s3/s3.service';
import { Place } from 'src/place/entity/place.entity';
import { UserReport } from 'src/user/entity/user-report.entity';
import { User } from 'src/user/entity/user.entity';
import { UserModule } from 'src/user/user.module';
import { PlaceReviewImage } from './entity/place-review-image.entity';
Expand All @@ -13,7 +14,7 @@ import { PlaceReviewService } from './place-review.service';

@Module({
imports: [
TypeOrmModule.forFeature([PlaceReview, PlaceReviewImage, User, Place, Activity]),
TypeOrmModule.forFeature([PlaceReview, PlaceReviewImage, User, Place, Activity, UserReport]),
UserModule,
],
controllers: [PlaceReviewController],
Expand Down
18 changes: 16 additions & 2 deletions src/place-review/place-review.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { S3Service } from 'src/aws/s3/s3.service';
import { BaseException } from 'src/global/base/base-exception';
import { CreatePlaceReviewRequest } from 'src/place/dto/request/create-place-review-request.dto';
import { Place } from 'src/place/entity/place.entity';
import { UserReport } from 'src/user/entity/user-report.entity';
import { User } from 'src/user/entity/user.entity';
import { LessThan, Repository } from 'typeorm';
import { In, LessThan, Not, Repository } from 'typeorm';
import { PlaceReview } from './entity/place-review.entity';
import { PlaceReviewResponseCode } from './exception/place-review-response-code';
import { PlaceReviewConverter } from './place-review.converter';
Expand All @@ -25,6 +26,8 @@ export class PlaceReviewService {
private readonly placeReviewRepository: Repository<PlaceReview>,
@InjectRepository(Activity)
private readonly activityRepository: Repository<Activity>,
@InjectRepository(UserReport)
private readonly userReportRepository: Repository<UserReport>,
private readonly s3Service: S3Service,
) {}
async create(
Expand Down Expand Up @@ -79,8 +82,19 @@ export class PlaceReviewService {
}

async findPlaceReviews(userId: number, placeId: number, last: number): Promise<PlaceReview[]> {
const userReports = await this.userReportRepository.find({
where: { reporter: { userId } },
relations: { targetUser: true },
});

// TODO: 신고한 리뷰도 안 보이게 해야함

const reviews = await this.placeReviewRepository.find({
where: { place: { placeId }, reviewId: last ? LessThan(last) : null },
where: {
place: { placeId },
reviewId: last ? LessThan(last) : null,
author: { userId: Not(In(userReports.map(report => report.targetUser.userId))) },
},
relations: ['author', 'images'],
});

Expand Down
6 changes: 0 additions & 6 deletions src/user/dto/follow-response.dto.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/user/dto/reqeust/report-user-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { UserReportReason } from 'src/user/enum/user-report-reason';

export class ReportUserReqeust {
@ApiProperty({ description: '신고 사유', enum: Object.keys(UserReportReason) })
@IsNotEmpty({ message: '신고 사유는 필수입니다.' })
@IsEnum(UserReportReason, {
message: `신고 사유는 ${Object.keys(UserReportReason)} 중 하나입니다.`,
})
readonly reason: string;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator';
import { IsEmailAvailable } from 'src/global/validation/decorator/is-email-available.decorator';
import { Password } from '../entity/password';
import { Password } from '../../entity/password';

export class SignUpRequest {
@ApiProperty({
Expand Down
8 changes: 8 additions & 0 deletions src/user/dto/response/report-user-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';

export class ReportUserResponse {
@ApiProperty({ description: '신고 당한 사용자의 아이디', example: 1 })
reportedUserId: number;
@ApiProperty({ description: '신고 시간', example: new Date() })
reportedAt: Date;
}
File renamed without changes.
File renamed without changes.
20 changes: 20 additions & 0 deletions src/user/entity/user-report.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { BaseEntity } from 'src/global/base/base.entity';
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from './user.entity';

@Entity('user_reports')
export class UserReport extends BaseEntity {
@PrimaryGeneratedColumn()
reportId: number;

@Column()
reason: string;

@ManyToOne(() => User, user => user.userId)
@JoinColumn({ name: 'reporter_id', referencedColumnName: 'userId' })
reporter: User;

@ManyToOne(() => User, user => user.userId)
@JoinColumn({ name: 'target_user_id', referencedColumnName: 'userId' })
targetUser: User;
}
7 changes: 7 additions & 0 deletions src/user/entity/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PlaceReview } from 'src/place-review/entity/place-review.entity';
import { Column, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { SocialType } from '../enum/social-type';
import { Password } from './password';
import { UserReport } from './user-report.entity';

@Entity('users')
export class User extends BaseEntity {
Expand Down Expand Up @@ -35,4 +36,10 @@ export class User extends BaseEntity {

@OneToMany(() => PlaceReview, review => review.author)
reviews: PlaceReview[];

@OneToMany(() => UserReport, report => report.reporter)
reports: UserReport[];

@OneToMany(() => UserReport, report => report.targetUser)
targetedReports: UserReport[];
}
5 changes: 5 additions & 0 deletions src/user/enum/user-report-reason.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum UserReportReason {
SCAM = 'SCAM',
INAPPROPRIATE = 'INAPPROPRIATE',
COMMERCIAL = 'COMMERCIAL',
}
11 changes: 8 additions & 3 deletions src/user/exception/user-response-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ export const UserResponseCode = {
INVALID_PASSWORD_PATTERN: new ResponseCode(
HttpStatus.BAD_REQUEST,
'USER_001',
'비밀번호 형식이 아닙니',
'비밀번호 형식이 아닙니다',
),
USER_NOT_FOUND: new ResponseCode(HttpStatus.NOT_FOUND, 'USER_002', '해당 유저가 없습니다.'),
FOLLOW_MYSELF: new ResponseCode(
REPORT_SELF: new ResponseCode(
HttpStatus.BAD_REQUEST,
'USER_003',
'나를 팔로우 할 수 없습니다.',
'나 자신을 신고할 수 없습니다.',
),
ALREADY_REPORTED: new ResponseCode(
HttpStatus.BAD_REQUEST,
'USER_004',
'이미 신고된 사용자입니다.',
),
};
40 changes: 35 additions & 5 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { Body, Controller, Global, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import {
Body,
Controller,
Param,
Post,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiCreatedResponse,
Expand All @@ -10,14 +19,19 @@ import {
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/guard/auth.guard';
import { BaseResponse } from 'src/global/base/base-response';
import { GlobalResponseCode } from 'src/global/base/global-respose-code';
import { SignUpRequest } from './dto/sign-up-request.dto';
import { SignUpResponse } from './dto/sign-up-response.dto';
import { UserConverter } from './user.converter';
import { UserService } from './user.service';
import { ExtractPayload } from 'src/global/decorator/extract-payload.decorator';
import { UserExistsValidationPipe } from 'src/global/validation/pipe/user-exists-validation.pipe';
import { FindingPasswordRequest } from './dto/finding-password-request.dto';
import { FindingPasswordResponse } from './dto/finding-password-response.dto';
import { ReportUserReqeust } from './dto/reqeust/report-user-request.dto';
import { SignUpRequest } from './dto/reqeust/sign-up-request.dto';
import { ReportUserResponse } from './dto/response/report-user-response.dto';
import { SignUpResponse } from './dto/response/sign-up-response.dto';
import { UserConverter } from './user.converter';
import { UserService } from './user.service';

@Controller('api/users')
@ApiTags('users')
Expand Down Expand Up @@ -60,6 +74,22 @@ export class UserController {
return BaseResponse.of(UserConverter.toSignUpResponse(newUser), GlobalResponseCode.CREATED);
}

@Post(':targetUserId/report')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: '사용자 신고 API',
})
@ApiBearerAuth()
@ApiCreatedResponse({ type: ReportUserResponse, description: '사용자 신고 성공' })
async reportUser(
@Param('targetUserId', UserExistsValidationPipe) userId: number,
@Body() request: ReportUserReqeust,
@ExtractPayload() reporterId: number,
): Promise<BaseResponse<ReportUserResponse>> {
const userReport = await this.userService.reportUser(request, reporterId, userId);
return BaseResponse.of(UserConverter.toReportUserResponse(userReport));
}

@Post('finding-password')
@ApiOperation({
summary: '이메일로 비밀번호 찾기',
Expand Down
25 changes: 22 additions & 3 deletions src/user/user.converter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Builder } from 'builder-pattern';
import { S3Service } from 'src/aws/s3/s3.service';
import { SignUpRequest } from './dto/sign-up-request.dto';
import { SignUpResponse } from './dto/sign-up-response.dto';
import { FindingPasswordResponse } from './dto/finding-password-response.dto';
import { ReportUserReqeust } from './dto/reqeust/report-user-request.dto';
import { SignUpRequest } from './dto/reqeust/sign-up-request.dto';
import { ReportUserResponse } from './dto/response/report-user-response.dto';
import { SignUpResponse } from './dto/response/sign-up-response.dto';
import { Password } from './entity/password';
import { UserReport } from './entity/user-report.entity';
import { User } from './entity/user.entity';
import { FindingPasswordResponse } from './dto/finding-password-response.dto';
import { UserReportReason } from './enum/user-report-reason';

@Injectable()
export class UserConverter implements OnModuleInit {
Expand Down Expand Up @@ -35,6 +39,21 @@ export class UserConverter implements OnModuleInit {
return Builder(SignUpResponse).userId(user.userId).build();
}

public static toUserReport(reqeust: ReportUserReqeust, reporter: User, targetUser: User) {
return Builder(UserReport)
.reason(UserReportReason[reqeust.reason])
.reporter(reporter)
.targetUser(targetUser)
.build();
}

public static toReportUserResponse(userReport: UserReport) {
return Builder(ReportUserResponse)
.reportedAt(userReport.createdAt)
.reportedUserId(userReport.reporter.userId)
.build();
}

public static toFindingPasswordResponse(isPasswordSended: boolean): FindingPasswordResponse {
return Builder(FindingPasswordResponse).isPasswordSended(isPasswordSended).build();
}
Expand Down
7 changes: 5 additions & 2 deletions src/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { S3Service } from 'src/aws/s3/s3.service';
import { MailService } from 'src/global/mail.service';
import { UserExistsValidationPipe } from 'src/global/validation/pipe/user-exists-validation.pipe';
import { UserReport } from './entity/user-report.entity';
import { User } from './entity/user.entity';
import { UserController } from './user.controller';
import { UserConverter } from './user.converter';
import { UserService } from './user.service';

@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService, S3Service, UserConverter],
imports: [TypeOrmModule.forFeature([User, UserReport])],
providers: [UserService, S3Service, MailService, UserConverter, UserExistsValidationPipe],
controllers: [UserController],
exports: [UserService],
})
Expand Down
Loading

0 comments on commit 0b336f7

Please sign in to comment.