diff --git a/.github/workflows/server_jest.yml b/.github/workflows/server_jest.yml new file mode 100644 index 00000000..3dd095a6 --- /dev/null +++ b/.github/workflows/server_jest.yml @@ -0,0 +1,66 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Server Jest + +on: + push: + branches: [ "main", "dev","dev-be" ] + paths: 'server/**' + pull_request: + branches: [ "main", "dev","dev-be" ] + paths: 'server/**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + # 해당 저장소의 코드를 가져옵니다. + - name: Checkout + uses: actions/checkout@v2 + + # Node 18 버전을 사용합니다. + - name: Install node + uses: actions/setup-node@v2 + with: + node-version: '18' + cache: 'npm' + + # yarn을 설치합니다. + - name: Install Yarn + run: npm install yarn + + # 설치된 yarn을 통해 패키지를 설치합니다. + - name: Install dependencies + run: yarn install + + # 테스트 수행과 그 테스트 결과를 xml파일로 생성합니다. + - name: Run tests + run: yarn server-test | tee ./coverage.txt + + # 테스트 결과를 담은 xml 파일을 레포트로 변환합니다. + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() # run this step even if previous step failed + with: + name: test-results + path: server/junit.xml + fail-on-error: 'false' + reporter: jest-junit # Format of test results + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Jest Coverage Comment + uses: MishaKav/jest-coverage-comment@main + with: + coverage-path: ./coverage.txt + coverage-summary-path: ./server/coverage/coverage-final.json + junitxml-path: ./server/junit.xml + - name: build 실패 시 Slack 알림 + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + author_name: 백엔드 빌드 실패 알림 + fields: repo, message, commit, author, action, eventName, ref, workflow, job, took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_FAIL_WEBHOOK_URL }} + if: failure() \ No newline at end of file diff --git a/server/__mock__/community.mock.ts b/server/__mock__/community.mock.ts new file mode 100644 index 00000000..f631960e --- /dev/null +++ b/server/__mock__/community.mock.ts @@ -0,0 +1,8 @@ +import { CreateCommunityDto } from '@api/src/community/dto/create-community.dto'; + +export const communityDto1 = { + name: 'asnity commu', + managerId: '63734af9e62b37012c73e399', + description: 'test description', + profileUrl: 'test profileUrl', +} as CreateCommunityDto; diff --git a/server/apps/api/src/api.module.ts b/server/apps/api/src/api.module.ts index ecc0fc45..fefba2ab 100644 --- a/server/apps/api/src/api.module.ts +++ b/server/apps/api/src/api.module.ts @@ -9,9 +9,6 @@ import { UserModule } from './user/user.module'; import * as winston from 'winston'; import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston'; import { AuthModule } from './auth/auth.module'; -import { APP_INTERCEPTOR } from '@nestjs/core'; -import { SentryInterceptor } from '../../webhook.interceptor'; -import { RavenInterceptor, RavenModule } from 'nest-raven'; @Module({ imports: [ diff --git a/server/apps/api/src/auth/dto/index.ts b/server/apps/api/src/auth/dto/index.ts index 0ee0429b..aa80f504 100644 --- a/server/apps/api/src/auth/dto/index.ts +++ b/server/apps/api/src/auth/dto/index.ts @@ -1 +1,2 @@ -export * from './auth.dto'; +export * from './sign-in.dto'; +export * from './sign-up.dto'; diff --git a/server/apps/api/src/auth/dto/sign-in.dto.ts b/server/apps/api/src/auth/dto/sign-in.dto.ts new file mode 100644 index 00000000..f23a0ee1 --- /dev/null +++ b/server/apps/api/src/auth/dto/sign-in.dto.ts @@ -0,0 +1,11 @@ +import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; + +export class SignInDto { + @IsEmail() + @IsNotEmpty() + id: string; + + @IsString() + @MinLength(8) + password: string; +} diff --git a/server/apps/api/src/auth/dto/auth.dto.ts b/server/apps/api/src/auth/dto/sign-up.dto.ts similarity index 67% rename from server/apps/api/src/auth/dto/auth.dto.ts rename to server/apps/api/src/auth/dto/sign-up.dto.ts index 87a5fdcd..66bda45e 100644 --- a/server/apps/api/src/auth/dto/auth.dto.ts +++ b/server/apps/api/src/auth/dto/sign-up.dto.ts @@ -1,15 +1,5 @@ import { IsEmail, IsNotEmpty, IsString, Length, MinLength } from 'class-validator'; -export class SignInDto { - @IsEmail() - @IsNotEmpty() - id: string; - - @IsString() - @MinLength(8) - password: string; -} - export class SignUpDto { @IsEmail() @IsNotEmpty() diff --git a/server/apps/api/src/auth/strategy/jwt-access.strategy.ts b/server/apps/api/src/auth/strategy/jwt-access.strategy.ts index e890d211..e82bd868 100644 --- a/server/apps/api/src/auth/strategy/jwt-access.strategy.ts +++ b/server/apps/api/src/auth/strategy/jwt-access.strategy.ts @@ -19,6 +19,6 @@ export class JwtAccessStrategy extends PassportStrategy(Strategy, 'jwt-access-to throw new ForbiddenException('잘못된 요청입니다.'); } delete user.password; - return user; + return { ...user, _id: user._id.toString() }; } } diff --git a/server/apps/api/src/channel/channel.controller.ts b/server/apps/api/src/channel/channel.controller.ts index fff4d422..6fa995b6 100644 --- a/server/apps/api/src/channel/channel.controller.ts +++ b/server/apps/api/src/channel/channel.controller.ts @@ -1,4 +1,27 @@ -import { Controller } from '@nestjs/common'; +import { Body, Controller, Inject, LoggerService, Post, Req, UseGuards } from '@nestjs/common'; +import { ChannelService } from '@api/src/channel/channel.service'; +import { JwtAccessGuard } from '@api/src/auth/guard'; +import { CreateChannelDto } from '@api/src/channel/dto'; +import { responseForm } from '@utils/responseForm'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -@Controller('channel') -export class ChannelController {} +@Controller('api/channel') +export class ChannelController { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService, + private channelService: ChannelService, + ) {} + + @Post() + @UseGuards(JwtAccessGuard) + async createChannel(@Body() createChannelDto: CreateChannelDto, @Req() req: any) { + const _id = req.user._id; + try { + await this.channelService.createChannel({ ...createChannelDto, managerId: _id }); + return responseForm(200, { message: '채널 생성 성공!' }); + } catch (error) { + this.logger.error(JSON.stringify(error.response)); + throw error; + } + } +} diff --git a/server/apps/api/src/channel/channel.module.ts b/server/apps/api/src/channel/channel.module.ts index fe632c18..6dd53985 100644 --- a/server/apps/api/src/channel/channel.module.ts +++ b/server/apps/api/src/channel/channel.module.ts @@ -1,9 +1,22 @@ import { Module } from '@nestjs/common'; import { ChannelController } from './channel.controller'; import { ChannelService } from './channel.service'; +import { MongooseModule } from '@nestjs/mongoose'; +import { ChannelRepository } from '@repository/channel.repository'; +import { ChannelSchema } from '@schemas/channel.schema'; +import { Channel } from 'diagnostics_channel'; +import { CommunityRepository } from '@repository/community.repository'; +import { CommunityModule } from '@community/community.module'; +import { UserRepository } from '@repository/user.repository'; +import { UserModule } from '@user/user.module'; @Module({ + imports: [ + MongooseModule.forFeature([{ name: Channel.name, schema: ChannelSchema }]), + CommunityModule, + UserModule, + ], controllers: [ChannelController], - providers: [ChannelService] + providers: [ChannelService, ChannelRepository, CommunityRepository, UserRepository], }) export class ChannelModule {} diff --git a/server/apps/api/src/channel/channel.service.ts b/server/apps/api/src/channel/channel.service.ts index fc8cd3a7..dd1dadc6 100644 --- a/server/apps/api/src/channel/channel.service.ts +++ b/server/apps/api/src/channel/channel.service.ts @@ -1,4 +1,39 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { ChannelRepository } from '@repository/channel.repository'; +import { CreateChannelDto } from '@api/src/channel/dto'; +import { CommunityRepository } from '@repository/community.repository'; +import { UserRepository } from '@repository/user.repository'; +import { addChannelToUserForm } from '@utils/addObjectForm'; @Injectable() -export class ChannelService {} +export class ChannelService { + constructor( + private readonly channelRepository: ChannelRepository, + private readonly communityRepository: CommunityRepository, + private readonly userRepository: UserRepository, + ) {} + + async createChannel(createChannelDto: CreateChannelDto) { + // 자신이 속한 커뮤니티 찾기 + try { + const community = await this.communityRepository.findById(createChannelDto.communityId); + + // 채널 생성 + const channel = await this.channelRepository.create({ + ...createChannelDto, + users: [createChannelDto.managerId], + }); + + // 커뮤니티 목록에 채널 업데이트 + await this.communityRepository.addArrAtArr({ _id: community.id }, 'channels', [ + channel._id.toString(), + ]); + + // 유저 목록에 자신이 속한 채널 업데이트 + const newChannel = addChannelToUserForm(community._id, channel._id); + await this.userRepository.updateObject({ _id: createChannelDto.managerId }, newChannel); + } catch (error) { + throw new BadRequestException('채널 생성 중 오류 발생!'); + } + } +} diff --git a/server/apps/api/src/channel/dto/create-channel.dto.ts b/server/apps/api/src/channel/dto/create-channel.dto.ts new file mode 100644 index 00000000..95d020d5 --- /dev/null +++ b/server/apps/api/src/channel/dto/create-channel.dto.ts @@ -0,0 +1,37 @@ +import { IsBoolean, IsNotEmpty, IsOptional, IsString, Length } from 'class-validator'; +import { Transform } from 'class-transformer'; + +export class CreateChannelDto { + @IsString() + @IsNotEmpty() + communityId: string; + + @IsString() + @Length(2, 20) + @IsNotEmpty() + name: string; + + @IsString() + @IsOptional() + managerId: string; + + @IsString() + @IsOptional() + description: string; + + @IsString() + @IsOptional() + profileUrl: string; + + @IsString() + @IsNotEmpty() + type: 'Channel' | 'DM'; + + @IsBoolean() + @IsNotEmpty() + @Transform(({ value }) => value === 'true') + isPrivate: boolean; + + @IsOptional() + users: string[]; +} diff --git a/server/apps/api/src/channel/dto/index.ts b/server/apps/api/src/channel/dto/index.ts new file mode 100644 index 00000000..a21d4e9f --- /dev/null +++ b/server/apps/api/src/channel/dto/index.ts @@ -0,0 +1 @@ +export * from './create-channel.dto'; diff --git a/server/apps/api/src/community/community.controller.ts b/server/apps/api/src/community/community.controller.ts index a169f161..7fc975fc 100644 --- a/server/apps/api/src/community/community.controller.ts +++ b/server/apps/api/src/community/community.controller.ts @@ -1,4 +1,104 @@ -import { Controller } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Inject, + LoggerService, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { CommunityService } from '@api/src/community/community.service'; +import { responseForm } from '@utils/responseForm'; +import { JwtAccessGuard } from '@api/src/auth/guard'; +import { + CreateCommunityDto, + AppendUsersToCommunityDto, + ModifyCommunityDto, + DeleteCommunityDto, +} from './dto'; -@Controller('community') -export class CommunityController {} +@Controller('api/community') +export class CommunityController { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService, + private communityService: CommunityService, + ) {} + + @Get() + @UseGuards(JwtAccessGuard) + async getCommunities(@Req() req: any) { + try { + const result = await this.communityService.getCommunities(req.user._doc); + return responseForm(200, result); + } catch (error) { + this.logger.error(JSON.stringify(error.response)); + throw error; + } + } + + @Post() + @UseGuards(JwtAccessGuard) + async crateCommunity(@Body() createCommunityDto: CreateCommunityDto, @Req() req: any) { + try { + const _id = req.user._id; + const result = await this.communityService.createCommunity({ + managerId: _id, + ...createCommunityDto, + }); + return responseForm(200, result); + } catch (error) { + this.logger.error(JSON.stringify(error.response)); + throw error; + } + } + + @Post('participants') + @UseGuards(JwtAccessGuard) + async appendParticipantsToCommunity( + @Body() appendUsersToCommunityDto: AppendUsersToCommunityDto, + @Req() req: any, + ) { + try { + await this.communityService.appendParticipantsToCommunity( + req.user, + appendUsersToCommunityDto, + ); + return responseForm(200, { message: '커뮤니티 사용자 추가 완료' }); + } catch (error) { + this.logger.error(JSON.stringify(error.response)); + throw error; + } + } + + @Patch('settings') + @UseGuards(JwtAccessGuard) + async modifyCommunitySetting(@Body() modifyCommunityDto: ModifyCommunityDto, @Req() req: any) { + try { + const _id = req.user._id; + await this.communityService.modifyCommunity({ ...modifyCommunityDto, managerId: _id }); + return responseForm(200, { message: '커뮤니티 정보 수정 완료' }); + } catch (error) { + this.logger.error(JSON.stringify(error.response)); + throw error; + } + } + + @Delete(':_id') + @UseGuards(JwtAccessGuard) + async deleteCommunity(@Param('_id') community_id: string, @Req() req: any) { + try { + const managerId = req.user._id; + const deleteCommunityDto: DeleteCommunityDto = { managerId, community_id }; + await this.communityService.deleteCommunity(deleteCommunityDto); + return responseForm(200, { message: '커뮤니티 삭제 성공' }); + } catch (error) { + this.logger.error(JSON.stringify(error.response)); + throw error; + } + } +} diff --git a/server/apps/api/src/community/community.module.ts b/server/apps/api/src/community/community.module.ts index d4fb4995..474dda22 100644 --- a/server/apps/api/src/community/community.module.ts +++ b/server/apps/api/src/community/community.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { CommunityController } from './community.controller'; import { CommunityService } from './community.service'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Community, CommunitySchema } from '@schemas/community.schema'; +import { CommunityRepository } from '@repository/community.repository'; +import { UserRepository } from '@repository/user.repository'; +import { UserModule } from '@user/user.module'; @Module({ + imports: [ + MongooseModule.forFeature([{ name: Community.name, schema: CommunitySchema }]), + UserModule, + ], controllers: [CommunityController], - providers: [CommunityService], + providers: [CommunityService, CommunityRepository, UserRepository], + exports: [MongooseModule], }) export class CommunityModule {} diff --git a/server/apps/api/src/community/community.service.ts b/server/apps/api/src/community/community.service.ts index 4cd3bcc7..1e0fc2d0 100644 --- a/server/apps/api/src/community/community.service.ts +++ b/server/apps/api/src/community/community.service.ts @@ -1,4 +1,164 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { CommunityRepository } from '@repository/community.repository'; +import { UserRepository } from '@repository/user.repository'; +import { + CreateCommunityDto, + AppendUsersToCommunityDto, + ModifyCommunityDto, + DeleteCommunityDto, +} from './dto'; +import { IsUserInCommunity, makeCommunityObj } from '@community/helper'; +import { communityInUser } from '@user/dto/community-in-user.dto'; @Injectable() -export class CommunityService {} +export class CommunityService { + constructor( + private readonly communityRepository: CommunityRepository, + private readonly userRepository: UserRepository, + ) {} + + async getCommunities(user2) { + const user = await this.userRepository.findById('637f2abb146636e4082885b1'); + const infos = []; + await Promise.all( + Array.from(user.communities.values()).map(async (userCommunity) => { + const { _id, channels } = userCommunity as communityInUser; + const community = await this.communityRepository.findById(_id); + if (!community) { + throw new BadRequestException('해당하는 커뮤니티의 _id가 올바르지 않습니다.'); + } + const result = Array.from(channels.keys()).filter( + (channel_id: string) => !community.channels.includes(channel_id), + ); + if (result.length > 0) { + console.log(result); + throw new BadRequestException('커뮤니티에 없는 비정상적인 채널이 존재합니다.'); + } + const info = {}; + info[_id] = []; + await Promise.all( + Array.from(channels.keys()).map(async (channelId) => { + const lastRead = channels.get(channelId); + console.log(lastRead); + // TODO: soft delete이면 조건 다시 설정 + // const channel = await this.channelRepository.findById(channelId); + // if (!channel) { + // throw new BadRequestException('존재하지 않는 채널입니다.'); + // } + // info[_id][channelId] = lastRead < channel.updatedAt; + // console.log(info[_id]); + }), + ); + console.log(info); + + infos.push(info); + }), + ); + return infos; + } + async createCommunity(createCommunityDto: CreateCommunityDto) { + const community = await this.communityRepository.create({ + ...createCommunityDto, + users: [createCommunityDto.managerId], + }); + const newCommunity = makeCommunityObj(community._id.toString()); + await this.userRepository.updateObject({ _id: createCommunityDto.managerId }, newCommunity); + return community; + } + + async appendParticipantsToCommunity( + reqUser, + appendUsersToCommunityDto: AppendUsersToCommunityDto, + ) { + const communityId = appendUsersToCommunityDto.community_id; + if (!IsUserInCommunity(reqUser, communityId)) { + throw new BadRequestException(`커뮤니티에 속하지 않는 사용자는 요청할 수 없습니다.`); + } + const newCommunity = makeCommunityObj(communityId); + await Promise.all( + // 사용자 document 검증 (올바른 사용자인지, 해당 사용자가 이미 커뮤니티에 참여하고 있는건 아닌지) + appendUsersToCommunityDto.users.map(async (user_id) => { + const user = await this.userRepository.findById(user_id); + if (!user) { + throw new BadRequestException( + `커뮤니티에 추가를 요청한 사용자 _id(${user_id})가 올바르지 않습니다.`, + ); + } else if (IsUserInCommunity(user, communityId)) { + throw new BadRequestException(`이미 커뮤니티에 추가된 사용자 입니다.`); + } + }), + ); + await Promise.all( + // 사용자 document 검증이 끝난 후 update + appendUsersToCommunityDto.users.map(async (user_id) => { + await this.userRepository.updateObject({ _id: user_id }, newCommunity); + }), + ); + const community = await this.communityRepository.addArrAtArr( + { _id: appendUsersToCommunityDto.community_id }, + 'users', + appendUsersToCommunityDto.users, + ); + if (!community) { + await Promise.all( + // 사용자 document에서 다시 삭제 + appendUsersToCommunityDto.users.map((user_id) => { + this.userRepository.deleteObject({ _id: user_id }, newCommunity); + }), + ); + throw new BadRequestException('해당하는 커뮤니티의 _id가 올바르지 않습니다.'); + } + } + + async modifyCommunity(modifyCommunityDto: ModifyCommunityDto) { + // TODO : refactoring을 findAndUpdate로 해서 매니저 id, deletedAt인지 바로 검증이랑 동시에 하도록.. + const community = await this.verfiyCommunity(modifyCommunityDto.community_id); + if (community.managerId != modifyCommunityDto.managerId) { + throw new BadRequestException('사용자의 커뮤니티 수정 권한이 없습니다.'); + } + const { managerId, community_id, ...updateField } = modifyCommunityDto; + // TODO: 꼭 기다려줘야하는지 생각해보기 + return await this.communityRepository.updateOne({ _id: community_id }, updateField); + } + + async deleteCommunity(deleteCommunityDto: DeleteCommunityDto) { + const updateField = { deletedAt: new Date() }; + let community = await this.communityRepository.findAndUpdateOne( + { + _id: deleteCommunityDto.community_id, + managerId: deleteCommunityDto.managerId, + deletedAt: { $exists: false }, + }, + updateField, + ); + if (!community) { + community = await this.verfiyCommunity(deleteCommunityDto.community_id); + if (community.managerId != deleteCommunityDto.managerId) { + throw new BadRequestException('사용자의 커뮤니티 수정 권한이 없습니다.'); + } else if (community.deletedAt) { + throw new BadRequestException('이미 삭제된 커뮤니티입니다.'); + } + } + await Promise.all( + community.users.map((user_id) => + this.deleteCommunityAtUserDocument(user_id, community._id.toString()), + ), + ); + } + async verfiyCommunity(community_id: string) { + const community = await this.communityRepository.findOne({ _id: community_id }); + if (!community) { + throw new BadRequestException('해당하는 커뮤니티의 _id가 올바르지 않습니다.'); + } + return community; + } + + deleteCommunityAtUserDocument(user_id: string, community_id: string) { + this.userRepository.deleteElementAtArr( + { _id: user_id }, + { + communities: [community_id], + }, + ); + } +} diff --git a/server/apps/api/src/community/dto/append-participants-to-community.dto.ts b/server/apps/api/src/community/dto/append-participants-to-community.dto.ts new file mode 100644 index 00000000..fb6b8ab7 --- /dev/null +++ b/server/apps/api/src/community/dto/append-participants-to-community.dto.ts @@ -0,0 +1,23 @@ +import { + ArrayNotEmpty, + IsArray, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; + +export class AppendUsersToCommunityDto { + @IsNotEmpty() + @IsString() + community_id: string; + + @IsOptional() + @IsString() + requestUser_id: string; + + @IsNotEmpty() + @IsArray() + @ArrayNotEmpty() + users: string[]; +} diff --git a/server/apps/api/src/community/dto/create-community.dto.ts b/server/apps/api/src/community/dto/create-community.dto.ts new file mode 100644 index 00000000..7d7d173d --- /dev/null +++ b/server/apps/api/src/community/dto/create-community.dto.ts @@ -0,0 +1,22 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateCommunityDto { + @IsNotEmpty() + @IsString() + name: string; + + @IsOptional() + @IsString() + managerId: string; + + @IsOptional() + @IsString() + description: string; + + @IsOptional() + @IsString() + profileUrl: string; + + @IsOptional() + users: string[]; +} diff --git a/server/apps/api/src/community/dto/delete-community.dto.ts b/server/apps/api/src/community/dto/delete-community.dto.ts new file mode 100644 index 00000000..0ad34141 --- /dev/null +++ b/server/apps/api/src/community/dto/delete-community.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteCommunityDto { + @IsNotEmpty() + @IsString() + community_id: string; + + @IsNotEmpty() + @IsString() + managerId: string; +} diff --git a/server/apps/api/src/community/dto/index.ts b/server/apps/api/src/community/dto/index.ts new file mode 100644 index 00000000..a50668c8 --- /dev/null +++ b/server/apps/api/src/community/dto/index.ts @@ -0,0 +1,4 @@ +export * from './append-participants-to-community.dto'; +export * from './create-community.dto'; +export * from './modify-community.dto'; +export * from './delete-community.dto'; diff --git a/server/apps/api/src/community/dto/modify-community.dto.ts b/server/apps/api/src/community/dto/modify-community.dto.ts new file mode 100644 index 00000000..18a5a292 --- /dev/null +++ b/server/apps/api/src/community/dto/modify-community.dto.ts @@ -0,0 +1,23 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class ModifyCommunityDto { + @IsNotEmpty() + @IsString() + community_id: string; + + @IsOptional() + @IsString() + name: string; + + @IsOptional() + @IsString() + managerId: string; + + @IsOptional() + @IsString() + description: string; + + @IsOptional() + @IsString() + profileUrl: string; +} diff --git a/server/apps/api/src/community/helper/checkUserIsInCommunity.ts b/server/apps/api/src/community/helper/checkUserIsInCommunity.ts new file mode 100644 index 00000000..eb279d47 --- /dev/null +++ b/server/apps/api/src/community/helper/checkUserIsInCommunity.ts @@ -0,0 +1,2 @@ +export const IsUserInCommunity = (user, communityId) => + (user.communities ?? false) && Array.from(user.communities.keys()).includes(communityId); diff --git a/server/apps/api/src/community/helper/index.ts b/server/apps/api/src/community/helper/index.ts new file mode 100644 index 00000000..cd9c8465 --- /dev/null +++ b/server/apps/api/src/community/helper/index.ts @@ -0,0 +1,2 @@ +export * from './checkUserIsInCommunity'; +export * from './makeCommunityObj'; diff --git a/server/apps/api/src/community/helper/makeCommunityObj.ts b/server/apps/api/src/community/helper/makeCommunityObj.ts new file mode 100644 index 00000000..108db276 --- /dev/null +++ b/server/apps/api/src/community/helper/makeCommunityObj.ts @@ -0,0 +1,8 @@ +export const makeCommunityObj = (community_id: string) => { + const newCommunity = {}; + newCommunity[`communities.${community_id}`] = { + _id: community_id, + channels: {}, + }; + return newCommunity; +}; diff --git a/server/apps/api/src/main.ts b/server/apps/api/src/main.ts index 22661db8..8e5e4cdb 100644 --- a/server/apps/api/src/main.ts +++ b/server/apps/api/src/main.ts @@ -8,7 +8,11 @@ import * as cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(ApiModule); - app.enableCors(); + app.enableCors({ + origin: true, + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + credentials: true, + }); if (process.env.NODE_ENV == 'prod') { Sentry.init({ dsn: process.env.SENTRY_DSN, diff --git a/server/apps/api/src/user/dto/community-in-user.dto.ts b/server/apps/api/src/user/dto/community-in-user.dto.ts new file mode 100644 index 00000000..07a69aee --- /dev/null +++ b/server/apps/api/src/user/dto/community-in-user.dto.ts @@ -0,0 +1,4 @@ +export interface communityInUser { + _id: string; + channels?: Map; +} diff --git a/server/apps/api/src/user/dto/index.ts b/server/apps/api/src/user/dto/index.ts new file mode 100644 index 00000000..c9dac182 --- /dev/null +++ b/server/apps/api/src/user/dto/index.ts @@ -0,0 +1,4 @@ +export * from './modify-user.dto'; +export * from './create-user.dto'; +export * from './follower.dto'; +export * from './user-basic-info.dto'; diff --git a/server/apps/api/src/user/user.controller.ts b/server/apps/api/src/user/user.controller.ts index 5d24caed..d077262a 100644 --- a/server/apps/api/src/user/user.controller.ts +++ b/server/apps/api/src/user/user.controller.ts @@ -8,15 +8,16 @@ import { Param, Patch, Post, + Req, Res, + UseGuards, } from '@nestjs/common'; import { UserService } from './user.service'; -import { FollowerDto } from '@user/dto/follower.dto'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { responseForm } from '@utils/responseForm'; import { ObjectIdValidationPipe } from '@custom_pipe/mongodbObjectIdValidation.pipe'; -import { ModifyUserDto } from '@user/dto/modify-user.dto'; -import { Response } from 'express'; +import { JwtAccessGuard } from '@api/src/auth/guard'; +import { FollowerDto, ModifyUserDto } from './dto'; @Controller('api/user') export class UserController { @@ -25,33 +26,25 @@ export class UserController { private userService: UserService, ) {} - // @Get() - // getUsers() { - // const createUserDto: CreateUserDto = { id: 'mj', pw: 'mjpw' }; - // this.userService.createUser(createUserDto); - // return 'hello user'; - // } - @Post('following/:id') - async addFollowing(@Param('id', ObjectIdValidationPipe) id: string) { + @UseGuards(JwtAccessGuard) + async addFollowing(@Param('id', ObjectIdValidationPipe) id: string, @Req() req: any) { try { - const myId = '63786b635d4f08bbe0c940de'; - // TODO: Request Header에서 access token으로 현재 사용자 알아내기 + const myId = req.user._id; const addFollowingDto: FollowerDto = { myId, followId: id }; const result = await this.userService.toggleFollowing(addFollowingDto); return responseForm(200, result); } catch (error) { this.logger.error(JSON.stringify(error.response)); - // res.status(400).json(error.response); throw error; } } @Get('followers') - async getFollowers() { + @UseGuards(JwtAccessGuard) + async getFollowers(@Req() req: any) { try { - const _id = '63786b635d4f08bbe0c940dc'; - // TODO: Request Header에서 access token으로 현재 사용자 알아내기 + const _id = req.user._id; const result = await this.userService.getRelatedUsers(_id, 'followers'); return responseForm(200, { followers: result }); } catch (error) { @@ -61,10 +54,10 @@ export class UserController { } @Get('followings') - async getFollowings() { + @UseGuards(JwtAccessGuard) + async getFollowings(@Req() req: any) { try { - const _id = '63739b643969101c3fec884'; - // TODO: Request Header에서 access token으로 현재 사용자 알아내기 + const _id = req.user._id; const result = await this.userService.getRelatedUsers(_id, 'followings'); return responseForm(200, { followings: result }); } catch (error) { @@ -77,7 +70,7 @@ export class UserController { async getUser(@Param('id') id: string) { try { const result = await this.userService.getUser(id); - return responseForm(200, { result }); + return responseForm(200, { users: result }); } catch (error) { this.logger.error(JSON.stringify(error.response)); throw error; @@ -85,10 +78,10 @@ export class UserController { } @Patch('settings') - async modifyUserSetting(@Body() modifyUserDto: ModifyUserDto) { + @UseGuards(JwtAccessGuard) + async modifyUserSetting(@Body() modifyUserDto: ModifyUserDto, @Req() req: any) { try { - const _id = '63786b635d4f08bbe0c940de'; - // TODO: Request Header에서 access token으로 현재 사용자 알아내기 + const _id = req.user._id; await this.userService.modifyUser({ ...modifyUserDto, _id }); return responseForm(200, {}); } catch (error) { @@ -96,9 +89,4 @@ export class UserController { throw error; } } - - // @Post() - // createUser(@Body() createUserDto: CreateUserDto) { - // this.usersService.createUser(createUserDto); - // } } diff --git a/server/apps/api/src/user/user.module.ts b/server/apps/api/src/user/user.module.ts index 7a7310b2..8726018d 100644 --- a/server/apps/api/src/user/user.module.ts +++ b/server/apps/api/src/user/user.module.ts @@ -2,15 +2,14 @@ import { Module } from '@nestjs/common'; import { UserController } from '@user/user.controller'; import { UserService } from '@user/user.service'; import { MongooseModule } from '@nestjs/mongoose'; -import { WinstonModule } from 'nest-winston'; import { User, UserSchema } from '@schemas/user.schema'; import { AuthModule } from '../auth/auth.module'; import { UserRepository } from '@repository/user.repository'; -import { AuthController } from '../auth/auth.controller'; @Module({ imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), AuthModule], controllers: [UserController], providers: [UserService, UserRepository], + exports: [MongooseModule], }) export class UserModule {} diff --git a/server/apps/api/src/user/user.service.ts b/server/apps/api/src/user/user.service.ts index 32508f9b..2526a47c 100644 --- a/server/apps/api/src/user/user.service.ts +++ b/server/apps/api/src/user/user.service.ts @@ -1,8 +1,6 @@ import { BadRequestException, ConflictException, Injectable, Param } from '@nestjs/common'; -import { FollowerDto } from '@user/dto/follower.dto'; import { UserRepository } from '@repository/user.repository'; -import { getUserBasicInfo } from '@user/dto/user-basic-info.dto'; -import { ModifyUserDto } from '@user/dto/modify-user.dto'; +import { FollowerDto, ModifyUserDto, getUserBasicInfo } from './dto'; @Injectable() export class UserService { diff --git a/server/apps/api/test/community.service.spec.ts b/server/apps/api/test/community.service.spec.ts new file mode 100644 index 00000000..36870c3c --- /dev/null +++ b/server/apps/api/test/community.service.spec.ts @@ -0,0 +1,54 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getModelToken } from '@nestjs/mongoose'; +import * as _ from 'lodash'; +import { CommunityService } from '@api/src/community/community.service'; +import { Community } from '@schemas/community.schema'; +import { CommunityRepository } from '@repository/community.repository'; +import { user1 } from '@mock/user.mock'; +import { UserRepository } from '@repository/user.repository'; +import { communityDto1 } from '@mock/community.mock'; +import { UserModule } from '@user/user.module'; +import { User } from '@schemas/user.schema'; + +describe('[Community Service]', () => { + let communityService: CommunityService; + let communityRepository: CommunityRepository; + let userRepository: UserRepository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CommunityService, + CommunityRepository, + UserRepository, + { + provide: getModelToken(Community.name), + useFactory: () => {}, + }, + { + provide: getModelToken(User.name), + useFactory: () => {}, + }, + ], + }).compile(); + + communityService = module.get(CommunityService); + communityRepository = module.get(CommunityRepository); + userRepository = module.get(UserRepository); + }); + + it('should be defined', () => { + expect(communityService).toBeDefined(); + }); + + describe('[createCommunity] 커뮤니티 생성', () => { + it('커뮤니티 생성 정상 동작', async () => { + const community1 = _.cloneDeep(user1); + community1.users = [user1._id]; + jest.spyOn(userRepository, 'findById').mockResolvedValue(user1); + jest.spyOn(communityRepository, 'create').mockResolvedValue(community1); + const result = await communityService.createCommunity(communityDto1); + expect(result).toEqual(community1); + }); + }); +}); diff --git a/server/dao/repository/channel.repository.ts b/server/dao/repository/channel.repository.ts new file mode 100644 index 00000000..fd182f90 --- /dev/null +++ b/server/dao/repository/channel.repository.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { Channel, ChannelDocument } from '@schemas/channel.schema'; +import { CreateChannelDto } from '@api/src/channel/dto'; + +@Injectable() +export class ChannelRepository { + constructor(@InjectModel(Channel.name) private channelModel: Model) {} + + async create(createChannelDto: CreateChannelDto) { + return await this.channelModel.create(createChannelDto); + } + + async findOne(condition: any) { + return await this.channelModel.findOne(condition); + } +} diff --git a/server/dao/repository/community.repository.ts b/server/dao/repository/community.repository.ts new file mode 100644 index 00000000..5376e0f5 --- /dev/null +++ b/server/dao/repository/community.repository.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { Community, CommunityDocument } from '@schemas/community.schema'; +import { CreateCommunityDto } from '@api/src/community/dto/create-community.dto'; + +@Injectable() +export class CommunityRepository { + constructor(@InjectModel(Community.name) private communityModel: Model) {} + + async create(createCommunityDto: CreateCommunityDto) { + const result = await this.communityModel.create(createCommunityDto); + return (result as any)._doc; + } + + async findById(_id: string) { + return await this.communityModel.findById(_id); + } + + async addArrAtArr(filter, attribute, appendArr) { + const addArr = {}; + addArr[attribute] = { $each: appendArr }; + return await this.communityModel.findOneAndUpdate(filter, { $addToSet: addArr }, { new: true }); + // console.log('pass'); + } + + async findOne(condition: any) { + return await this.communityModel.findOne(condition); + } + + async updateOne(filter, updateField) { + await this.communityModel.updateOne(filter, updateField); + } + + async findAndUpdateOne(filter, updateField) { + return await this.communityModel.findOneAndUpdate(filter, updateField, { new: true }); + } +} diff --git a/server/dao/repository/user.repository.ts b/server/dao/repository/user.repository.ts index 60d99b9d..66e7536d 100644 --- a/server/dao/repository/user.repository.ts +++ b/server/dao/repository/user.repository.ts @@ -33,7 +33,31 @@ export class UserRepository { await this.userModel.updateOne(filter, { $push: appendElement }); } + async updateObject(filter, appendElement) { + return await this.userModel.updateOne(filter, { $set: appendElement }); + } + + async deleteObject(filter, appendElement) { + await this.userModel.updateOne(filter, { $unset: appendElement }); + } + async deleteElementAtArr(filter, removeElement) { await this.userModel.updateOne(filter, { $pullAll: removeElement }); } + + async deleteElementAtArr2(_id, removeElement) { + await this.userModel.findByIdAndUpdate(_id, { $pullAll: removeElement }, { new: true }); + } + + async addArrAtArr(filter, attribute, appendArr) { + const addArr = {}; + addArr[attribute] = { $each: appendArr }; + return await this.userModel.findByIdAndUpdate(filter, { $addToSet: addArr }, { new: true }); + } + + // async set(filter, obj) { + // const user = new User(); + // + // this.userModel.find(filter).communities.set(); + // } } diff --git a/server/dao/schemas/channel.schema.ts b/server/dao/schemas/channel.schema.ts index 605b3d0d..5d77e202 100644 --- a/server/dao/schemas/channel.schema.ts +++ b/server/dao/schemas/channel.schema.ts @@ -1,30 +1,28 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; +import mongoose, { Document } from 'mongoose'; import { CHANNEL_TYPE } from '@utils/def'; -import { IsBoolean, IsIn, IsNotEmpty, IsString } from 'class-validator'; +import { IsBoolean, IsIn, IsNotEmpty, IsString, Length } from 'class-validator'; export type ChannelDocument = Channel & Document; -@Schema() +@Schema({ timestamps: true }) export class Channel { @Prop({ required: true, - unique: true, }) @IsString() @IsNotEmpty() + @Length(2, 20) name: string; @Prop({ required: true, - unique: true, }) @IsString() communityId: string; @Prop({ required: true, - unique: true, }) @IsString() @IsNotEmpty() @@ -53,5 +51,8 @@ export class Channel { @Prop() @IsString() chatLists: string[]; + + @Prop({ type: mongoose.Schema.Types.Date }) + deletedAt: Date; } export const ChannelSchema = SchemaFactory.createForClass(Channel); diff --git a/server/dao/schemas/community.schema.ts b/server/dao/schemas/community.schema.ts index 87b231f8..e6fe5806 100644 --- a/server/dao/schemas/community.schema.ts +++ b/server/dao/schemas/community.schema.ts @@ -4,7 +4,7 @@ import { IsNotEmpty, IsString } from 'class-validator'; export type CommunityDocument = Community & Document; -@Schema() +@Schema({ timestamps: true }) export class Community { @Prop() @IsString() @@ -13,7 +13,6 @@ export class Community { @Prop({ required: true, - unique: true, }) @IsString() @IsNotEmpty() @@ -27,12 +26,6 @@ export class Community { @IsString() profileUrl: string; - @Prop({ default: new Date(), type: mongoose.Schema.Types.Date }) - createdAt: Date; - - @Prop({ default: new Date(), type: mongoose.Schema.Types.Date }) - updatedAt: Date; - @Prop({ type: mongoose.Schema.Types.Date }) deletedAt: Date; diff --git a/server/dao/schemas/user.schema.ts b/server/dao/schemas/user.schema.ts index b7ec5881..9c0597f3 100644 --- a/server/dao/schemas/user.schema.ts +++ b/server/dao/schemas/user.schema.ts @@ -1,11 +1,11 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose'; import { IsIn, IsString } from 'class-validator'; import mongoose, { Document } from 'mongoose'; import { STATUS } from '@utils/def'; export type UserDocument = User & Document; -@Schema() +@Schema({ timestamps: true }) export class User { @Prop() name: string; @@ -49,12 +49,6 @@ export class User { @Prop({ default: new Date(), type: mongoose.Schema.Types.Date }) lastLogin: Date; - @Prop({ default: new Date(), type: mongoose.Schema.Types.Date }) - createdAt: Date; - - @Prop({ default: new Date(), type: mongoose.Schema.Types.Date }) - updatedAt: Date; - @Prop({ type: mongoose.Schema.Types.Date }) deletedAt: Date; @@ -66,7 +60,13 @@ export class User { @IsString() followers: string[]; - @Prop() - communities: string[]; + @Prop( + raw({ + type: Map, + of: new mongoose.Schema({ _id: { type: String }, channels: { type: Map, of: Date } }), + }), + ) + communities; } + export const UserSchema = SchemaFactory.createForClass(User); diff --git a/server/tsconfig.json b/server/tsconfig.json index b43f558a..28fdce23 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -23,6 +23,7 @@ "@utils/*": ["utils/*"], "@custom_pipe/*": ["custom_pipe/*"], "@user/*": ["apps/api/src/user/*"], + "@community/*": ["apps/api/src/community/*"], "@api/*": ["apps/api/*"], "@mock/*": ["__mock__/*"], } diff --git a/server/utils/addObjectForm.ts b/server/utils/addObjectForm.ts new file mode 100644 index 00000000..1514c0ed --- /dev/null +++ b/server/utils/addObjectForm.ts @@ -0,0 +1,6 @@ +export function addChannelToUserForm(communityId, channelId) { + const newChannel = {}; + newChannel[`communities.${communityId.toString()}.channels.${channelId.toString()}`] = new Date(); + + return newChannel; +} diff --git a/yarn.lock b/yarn.lock index e4da8cb0..f9ae5416 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2529,6 +2529,14 @@ multer "1.4.4-lts.1" tslib "2.4.1" +"@nestjs/platform-socket.io@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/platform-socket.io/-/platform-socket.io-9.2.0.tgz#5194a13d4ef5c70b32b2bcc64379e07674ddf0ab" + integrity sha512-ttxXtqHV3Cpk5AfZOxfE8urILV5oLBpG21vdyqUHiL0YDuhHdc2tBz5GKSYAfsWefmVeQQiBAV9dqaa23Rf0nQ== + dependencies: + socket.io "4.5.3" + tslib "2.4.1" + "@nestjs/schematics@^9.0.0": version "9.0.3" resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.0.3.tgz#175218350fb3829c9a903e980046a11950310e24" @@ -2547,6 +2555,15 @@ dependencies: tslib "2.4.1" +"@nestjs/websockets@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/websockets/-/websockets-9.2.0.tgz#cbe8d446eff653d9c63234ef396ccc1ea031e875" + integrity sha512-AbG4eN9p9O6QmNSOWsk0lrA+CtHkrdDkogcl1sGyTrg+LRd6IUlkaTu9fFK9Hl6o7bs2ieGgDmxAvl+Xd156Aw== + dependencies: + iterare "1.2.1" + object-hash "3.0.0" + tslib "2.4.1" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" @@ -2782,6 +2799,11 @@ "@types/node" ">=12.0.0" axios "^0.21.4" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@tanstack/match-sorter-utils@8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz#895f407813254a46082a6bbafad9b39b943dc834" @@ -2964,6 +2986,11 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== +"@types/cors@^2.8.12": + version "2.8.12" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== + "@types/debug@^4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" @@ -3125,7 +3152,7 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@>=12.0.0", "@types/node@>=8.9.0": +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.0.0", "@types/node@>=8.9.0": version "18.11.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== @@ -3242,6 +3269,13 @@ dependencies: "@types/node" "*" +"@types/socket.io@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-3.0.2.tgz#606c9639e3f93bb8454cba8f5f0a283d47917759" + integrity sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ== + dependencies: + socket.io "*" + "@types/sockjs@^0.3.33": version "0.3.33" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" @@ -4069,6 +4103,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -4650,7 +4689,7 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookie@^0.4.1, cookie@^0.4.2: +cookie@^0.4.1, cookie@^0.4.2, cookie@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== @@ -4689,7 +4728,7 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cors@2.8.5: +cors@2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -4899,7 +4938,7 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -5242,6 +5281,27 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-parser@~5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0" + integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== + +engine.io@~6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f" + integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.2.3" + enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0, enhanced-resolve@^5.7.0: version "5.10.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" @@ -10040,6 +10100,31 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +socket.io-adapter@~2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" + integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== + +socket.io-parser@~4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5" + integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@*, socket.io@4.5.3, socket.io@^4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.3.tgz#44dffea48d7f5aa41df4a66377c386b953bc521c" + integrity sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + debug "~4.3.2" + engine.io "~6.2.0" + socket.io-adapter "~2.4.0" + socket.io-parser "~4.2.0" + sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" @@ -11309,6 +11394,11 @@ ws@^5.2.0: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + xml-name-validator@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"