Skip to content

Commit

Permalink
Merge branch 'stage' of https://github.com/mash-up-kr/VitaminC_server
Browse files Browse the repository at this point in the history
…into feat/private
  • Loading branch information
Ho-s committed Sep 6, 2024
2 parents 08c349a + a976810 commit 44580f4
Show file tree
Hide file tree
Showing 21 changed files with 267 additions and 26 deletions.
2 changes: 1 addition & 1 deletion src/entities/invite-link.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class InviteLink {
@ManyToOne(() => GroupMap)
map: GroupMap;

@Property({ type: 'string', default: UserMapRole.WRITE })
@Property({ type: 'string', default: UserMapRole.READ })
@Enum({ items: [UserMapRole.ADMIN, UserMapRole.READ, UserMapRole.WRITE] })
mapRole: UserMapRoleValueType;

Expand Down
4 changes: 2 additions & 2 deletions src/entities/place-for-map.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export class PlaceForMap {
@ManyToMany(() => Tag, 'placeForMap', { owner: true })
tags = new Collection<Tag>(this);

@ManyToOne(() => User)
createdBy: User;
@ManyToOne(() => User, { nullable: true })
createdBy: User | null;

@Property()
createdAt: Date = new Date();
Expand Down
3 changes: 3 additions & 0 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export class User {
@Property({ type: 'string', default: null })
nickname: string | null = null;

@Property({ type: 'string', default: null })
profileImage: string | null = null;

@Property()
kakaoAccessToken: string;

Expand Down
15 changes: 15 additions & 0 deletions src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,21 @@ export class UserNotInMapException extends ExceptionOf.USER(
'지도에 참여하지 않은 사용자입니다.' as const,
) {}

export class UserMapRoleBadRequestException extends ExceptionOf.USER(
HttpStatus.BAD_REQUEST,
'어드민으로 변경할 수 없습니다.' as const,
) {}

export class UserMapNotFoundException extends ExceptionOf.USER(
HttpStatus.NOT_FOUND,
'해당 유저는 해당 지도의 멤버가 아닙니다.' as const,
) {}

export class UserMapRoleCannotMineException extends ExceptionOf.USER(
HttpStatus.BAD_REQUEST,
'자신의 권한은 변경할 수 없습니다.' as const,
) {}

export class MapNotFoundException extends ExceptionOf.USER(
HttpStatus.NOT_FOUND,
'존재하지 않는 지도입니다.' as const,
Expand All @@ -63,3 +73,8 @@ export class PlaceForMapConflictException extends ExceptionOf.USER(
HttpStatus.CONFLICT,
'이미 생성된 지도입니다.' as const,
) {}

export class PlaceNotMineException extends ExceptionOf.USER(
HttpStatus.FORBIDDEN,
'내가 등록한 장소만 삭제할 수 있습니다.' as const,
) {}
8 changes: 7 additions & 1 deletion src/invite-link/invite-link.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
InviteLink,
InviteLinkRepository,
User,
UserMapRoleValueType,
} from 'src/entities';
import {
InviteLinkInvalidException,
Expand All @@ -26,7 +27,11 @@ export class InviteLinkService {
private readonly utilService: UtilService,
) {}

async create(mapId: string, by: User): Promise<InviteLink> {
async create(
mapId: string,
role: UserMapRoleValueType,
by: User,
): Promise<InviteLink> {
const map = await this.mapRepository.findOne({ id: mapId });
if (map == null) {
throw new MapNotFoundException();
Expand All @@ -40,6 +45,7 @@ export class InviteLinkService {
inviteLink.token = token;
inviteLink.createdBy = by;
inviteLink.map = map;
inviteLink.mapRole = role;
inviteLink.expiresAt = new Date(expiration);

await this.inviteLinkRepository.persistAndFlush(inviteLink);
Expand Down
15 changes: 15 additions & 0 deletions src/map/dtos/create-invite-link.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';

import { IsEnum } from 'class-validator';

import { UserMapRole, UserMapRoleValueType } from 'src/entities/index';

export class CreateInviteLinkDto {
@IsEnum([UserMapRole.WRITE, UserMapRole.READ])
@ApiProperty({
enum: [UserMapRole.WRITE, UserMapRole.READ], // Admin 초대 불가
default: UserMapRole.READ,
nullable: true,
})
mapRole: UserMapRoleValueType = UserMapRole.READ;
}
2 changes: 1 addition & 1 deletion src/map/dtos/invite-link-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class InviteLinkResponseDto {
token: string;

@ApiProperty({
enum: [UserMapRole.ADMIN, UserMapRole.READ, UserMapRole.WRITE],
enum: UserMapRole,
})
mapRole: UserMapRoleValueType;

Expand Down
6 changes: 6 additions & 0 deletions src/map/dtos/kick-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';

export class KickUserDto {
@ApiProperty()
userId: number;
}
10 changes: 9 additions & 1 deletion src/map/dtos/update-map.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';

import { IsOptional } from 'class-validator';

import { GroupMap } from 'src/entities';
import { GroupMap, UserMapRole, UserMapRoleValueType } from 'src/entities';

export class UpdateMapDto implements Partial<GroupMap> {
@ApiProperty({ required: false })
Expand All @@ -17,3 +17,11 @@ export class UpdateMapDto implements Partial<GroupMap> {
@IsOptional()
isPublic?: boolean;
}

export class UpdateUserRoleInMapDto {
@ApiProperty({ enum: [UserMapRole.READ, UserMapRole.WRITE] })
role: UserMapRoleValueType;

@ApiProperty()
userId: number;
}
43 changes: 40 additions & 3 deletions src/map/map.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@nestjs/swagger';

import { CheckInviteLinkResponseDto } from 'src/map/dtos/check-invite-link-response.dto';
import { CreateInviteLinkDto } from 'src/map/dtos/create-invite-link.dto';
import { CreateTagDto } from 'src/map/dtos/create-tag.dto';
import { TagResponseDto } from 'src/map/dtos/tag-response.dto';

Expand All @@ -29,9 +30,10 @@ import { GroupMap, InviteLink, User, UserMapRole, UserRole } from '../entities';
import { InviteLinkService } from '../invite-link/invite-link.service';
import { CreateMapDto } from './dtos/create-map.dto';
import { InviteLinkResponseDto } from './dtos/invite-link-response.dto';
import { KickUserDto } from './dtos/kick-user.dto';
import { MapItemForUserDto } from './dtos/map-item-for-user.dto';
import { MapResponseDto, PublicMapResponseDto } from './dtos/map-response.dto';
import { UpdateMapDto } from './dtos/update-map.dto';
import { UpdateMapDto, UpdateUserRoleInMapDto } from './dtos/update-map.dto';
import { ArrayElement, MapService, publicMapOrder } from './map.service';

@ApiTags('maps')
Expand Down Expand Up @@ -110,6 +112,22 @@ export class MapController {
return dto;
}

@Patch('roles/:id/:userId')
@ApiOperation({
summary: '지도 멤버의 권한을 변경합니다.',
})
@ApiOkResponse({ type: MapResponseDto })
@ApiBearerAuth()
@UseMapRoleGuard([UserMapRole.ADMIN])
@UseAuthGuard([UserRole.USER])
async updateRole(
@Param('id') id: string,
@Body() body: UpdateUserRoleInMapDto,
@CurrentUser() user: User,
) {
await this.mapService.updateRole(id, body.userId, body.role, user);
}

@Delete(':id')
@ApiOkResponse({ type: Number })
@ApiExcludeEndpoint()
Expand All @@ -130,11 +148,27 @@ export class MapController {
async createInviteLink(
@CurrentUser() user: User,
@Param('id') id: string,
@Body() body: CreateInviteLinkDto,
): Promise<InviteLinkResponseDto> {
const entity: InviteLink = await this.inviteLinkService.create(id, user);
const entity: InviteLink = await this.inviteLinkService.create(
id,
body.mapRole,
user,
);
return new InviteLinkResponseDto(entity);
}

@Post('kick/:id')
@ApiOperation({
summary: '지도에서 유저 추방',
})
@ApiBearerAuth()
@UseMapRoleGuard([UserMapRole.ADMIN])
@UseAuthGuard([UserRole.USER])
async kickUser(@Param('id') id: string, @Body() body: KickUserDto) {
await this.mapService.kickUser(id, body.userId);
}

@Get(':id/tag')
@ApiOperation({
summary: '기본 태그와 지도에 저장된 태그를 조회합니다.',
Expand All @@ -154,6 +188,7 @@ export class MapController {
@ApiBearerAuth()
@ApiResponse({ type: TagResponseDto })
@ApiBearerAuth()
@UseMapRoleGuard([UserMapRole.ADMIN, UserMapRole.WRITE])
@UseAuthGuard([UserRole.USER])
createTag(@Param('id') id: string, @Body() createTagDto: CreateTagDto) {
return this.mapService.createTag(id, createTagDto);
Expand All @@ -164,6 +199,7 @@ export class MapController {
summary: '맛집 저장시 사용할 태그를 삭제합니다.',
})
@ApiBearerAuth()
@UseMapRoleGuard([UserMapRole.ADMIN, UserMapRole.WRITE])
@UseAuthGuard([UserRole.USER])
removeTag(@Param('id') id: string, @Param('name') name: string) {
return this.mapService.removeTag(id, name);
Expand All @@ -186,7 +222,8 @@ export class MapController {
inviteLink.map,
);

const inviteLinkResponseDto = new InviteLinkResponseDto(inviteLink);
const inviteLinkResponseDto: InviteLinkResponseDto =
new InviteLinkResponseDto(inviteLink);
return new CheckInviteLinkResponseDto(
map,
inviteLinkResponseDto,
Expand Down
50 changes: 50 additions & 0 deletions src/map/map.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
TagNotFoundException,
UserMapConflictException,
UserMapNotFoundException,
UserMapRoleBadRequestException,
UserMapRoleCannotMineException,
} from 'src/exceptions';
import { CreateTagDto } from 'src/map/dtos/create-tag.dto';
import { TagResponseDto } from 'src/map/dtos/tag-response.dto';
Expand Down Expand Up @@ -283,4 +285,52 @@ export class MapService {
}
});
}

async kickUser(mapId: string, userId: number) {
const userMap: UserMap = await this.userMapRepository.findOne({
user: { id: userId },
map: { id: mapId },
});
if (!userMap) {
throw new UserMapNotFoundException();
}
const placeMap = await this.placeForMapRepository.find({
map: { id: mapId },
createdBy: { id: userId },
});

if (placeMap.length) {
placeMap.forEach((place) => {
place.createdBy = null;
});
await this.placeForMapRepository.flush();
}

await this.userMapRepository.removeAndFlush(userMap);
}

async updateRole(
mapId: string,
userId: number,
role: UserMapRoleValueType,
me: User,
) {
const userMap: UserMap = await this.userMapRepository.findOne({
user: { id: userId },
map: { id: mapId },
});
if (!userMap) {
throw new UserMapNotFoundException();
}
if (role === UserMapRole.ADMIN) {
throw new UserMapRoleBadRequestException();
}

if (userId === me.id) {
throw new UserMapRoleCannotMineException();
}

userMap.role = role;
await this.userMapRepository.persistAndFlush(userMap);
}
}
15 changes: 13 additions & 2 deletions src/migrations/.snapshot-korrk-stage.json
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,16 @@
"default": "null",
"mappedType": "string"
},
"profile_image": {
"name": "profile_image",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"default": "null",
"mappedType": "string"
},
"kakao_access_token": {
"name": "kakao_access_token",
"type": "varchar(255)",
Expand Down Expand Up @@ -828,7 +838,7 @@
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"nullable": true,
"mappedType": "integer"
},
"created_at": {
Expand Down Expand Up @@ -888,6 +898,7 @@
"localTableName": "public.place_for_map",
"referencedColumnNames": ["id"],
"referencedTableName": "public.user",
"deleteRule": "set null",
"updateRule": "cascade"
}
},
Expand Down Expand Up @@ -1031,7 +1042,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'WRITE'",
"default": "'READ'",
"mappedType": "string"
},
"expires_at": {
Expand Down
27 changes: 27 additions & 0 deletions src/migrations/Migration20240904080344.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Migration } from '@mikro-orm/migrations';

export class Migration20240904080344 extends Migration {
async up(): Promise<void> {
this.addSql(
'alter table "user" add column "profile_image" varchar(255) null;',
);

this.addSql(
'alter table "invite_link" alter column "map_role" type varchar(255) using ("map_role"::varchar(255));',
);
this.addSql(
'alter table "invite_link" alter column "map_role" set default \'READ\';',
);
}

async down(): Promise<void> {
this.addSql('alter table "user" drop column "profile_image";');

this.addSql(
'alter table "invite_link" alter column "map_role" type varchar(255) using ("map_role"::varchar(255));',
);
this.addSql(
'alter table "invite_link" alter column "map_role" set default \'WRITE\';',
);
}
}
Loading

0 comments on commit 44580f4

Please sign in to comment.