Skip to content

Commit

Permalink
Merge pull request #27 from PBTP/feature/chat
Browse files Browse the repository at this point in the history
DMVM-177 feature: 채팅
  • Loading branch information
emibgo2 authored Aug 13, 2024
2 parents e0d188d + 9c24209 commit 7231d3c
Show file tree
Hide file tree
Showing 65 changed files with 1,715 additions and 269 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/swagger": "^7.3.1",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.3.10",
"@ssut/nestjs-sqs": "^2.2.0",
"@types/socket.io": "^3.0.2",
"aws-sdk": "^2.1623.0",
"axios": "^1.7.2",
"class-transformer": "^0.5.1",
Expand All @@ -42,6 +45,8 @@
"pg": "^8.11.5",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"socket.io-redis": "^6.1.1",
"tsid-ts": "^0.0.9",
"typeorm": "^0.3.20",
"typeorm-naming-strategies": "^4.1.0"
},
Expand Down
3 changes: 3 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { LoggerModule } from './config/logger/logger.module';
import { SystemAlarmModule } from './system/system.alarm.module';
import { ImageModule } from './common/image/image.module';
import { CloudModule } from './common/cloud/cloud.module';
import { ChatModule } from './chat/chat.module';

@Module({
imports: [
Expand All @@ -34,6 +35,7 @@ import { CloudModule } from './common/cloud/cloud.module';
username: datasource.username,
password: datasource.password,
database: datasource.database,
logging: datasource.logging,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
namingStrategy: new SnakeNamingStrategy(),
Expand All @@ -60,6 +62,7 @@ import { CloudModule } from './common/cloud/cloud.module';
AuthModule,
ImageModule,
CloudModule,
ChatModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
54 changes: 45 additions & 9 deletions src/auth/application/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { Injectable } from '@nestjs/common';
import { AuthDto } from '../presentation/auth.dto';
import { CustomerService } from 'src/customer/application/customer.service';
import { Customer } from 'src/schemas/customers.entity';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { CacheService } from '../../common/cache/cache.service';
import { CustomerDto } from '../../customer/presentation/customer.dto';
import { UnauthorizedException } from '@nestjs/common/exceptions';
import { DriverService } from '../../driver/application/driver.service';
import { BusinessService } from '../../business/application/business.service';

@Injectable()
export class AuthService {
private readonly accessTokenOption: JwtSignOptions;
private readonly refreshTokenOption: JwtSignOptions;
private readonly accessTokenStrategy: string;
private readonly userServices = {};

constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly cacheService: CacheService,
private readonly customerService: CustomerService,
private readonly driverService: DriverService,
private readonly businessService: BusinessService,
) {
this.accessTokenOption = {
secret: this.configService.get<string>('jwt/access/secret'),
Expand All @@ -32,21 +37,33 @@ export class AuthService {
this.accessTokenStrategy = this.configService.get<string>(
'jwt/access/strategy',
);

this.userServices['customer'] = customerService;
this.userServices['driver'] = driverService;
this.userServices['business'] = businessService;
}

async login(dto: CustomerDto): Promise<AuthDto> {
async login(dto: CustomerDto): Promise<CustomerDto> {
let customer: Customer = await this.customerService.findOne(dto);
customer = customer ?? (await this.customerService.create(dto));

const accessToken = this.jwtService.sign(
{ tokenType: 'access', subject: customer.customerId },
{
tokenType: 'access',
subject: customer.customerId,
userType: 'customer',
},
this.accessTokenOption,
);

await this.saveAccessToken(customer, accessToken);

const refreshToken = this.jwtService.sign(
{ tokenType: 'refresh', subject: customer.customerId },
{
tokenType: 'refresh',
subject: customer.customerId,
userType: 'customer',
},
this.refreshTokenOption,
);

Expand All @@ -67,22 +84,30 @@ export class AuthService {
};
}

async tokenRefresh(request: Request): Promise<AuthDto> {
async tokenRefresh(request: Request): Promise<CustomerDto> {
const token = request.headers['authorization'].replace('Bearer ', '');

const payload = this.jwtService.decode(token);

const customer: Customer = await this.customerService.findOne({
customerId: payload.subject,
userId: payload.subject,
});

const accessToken = this.jwtService.sign(
{ tokenType: 'access', subject: customer.customerId },
{
tokenType: 'access',
subject: customer.customerId,
userType: 'customer',
},
this.accessTokenOption,
);

const refreshToken = this.jwtService.sign(
{ tokenType: 'refresh', subject: customer.customerId },
{
tokenType: 'refresh',
subject: customer.customerId,
userType: 'customer',
},
this.refreshTokenOption,
);

Expand Down Expand Up @@ -132,7 +157,18 @@ export class AuthService {
);
}

async decodeToken(token: string): Promise<any> {
async decode(token: string): Promise<any> {
return await this.jwtService.decode(token);
}

async getUser(token: string): Promise<any> {
const payload = await this.jwtService.verify(token);
if (!payload) {
throw new UnauthorizedException();
}

return await this.userServices[payload.userType].findOne({
userId: payload.subject,
});
}
}
4 changes: 4 additions & 0 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { JwtAccessStrategy } from './application/jwt-access.strategy';
import { JwtRefreshStrategy } from './application/jwt-refresh.strategy';
import { PassportModule } from '@nestjs/passport';
import { CacheModule } from '../common/cache/cache.module';
import { DriverModule } from '../driver/driver.module';
import { BusinessModule } from '../business/business.module';

@Global()
@Module({
Expand All @@ -20,7 +22,9 @@ import { CacheModule } from '../common/cache/cache.module';
}),
PassportModule.register({ defaultStrategy: 'access' }),
CacheModule,
DriverModule,
CustomerModule,
BusinessModule,
],
controllers: [AuthController],
providers: [AuthService, JwtAccessStrategy, JwtRefreshStrategy],
Expand Down
8 changes: 7 additions & 1 deletion src/auth/decorator/auth.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
applyDecorators,
createParamDecorator,
ExecutionContext,
HttpCode,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { Customer } from '../../schemas/customers.entity';
Expand All @@ -15,13 +17,17 @@ export const CurrentCustomer = createParamDecorator(
},
);

export function Auth(authGuardType: string = 'access') {
export function Auth(
httpStatusCode: HttpStatus | number = HttpStatus.OK,
authGuardType: string = 'access',
) {
return applyDecorators(
UseGuards(AuthGuard(authGuardType)),
ApiBearerAuth(),
ApiUnauthorizedResponse({
description:
'Unauthorized / Access Token이 만료되었거나 잘못되었습니다. 토큰을 갱신하세요',
}),
HttpCode(httpStatusCode),
);
}
6 changes: 3 additions & 3 deletions src/auth/presentation/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Post, Body, Req } from '@nestjs/common';
import { Body, Controller, HttpStatus, Post, Req } from '@nestjs/common';
import { AuthService } from '../application/auth.service';
import { AuthDto } from './auth.dto';
import {
Expand Down Expand Up @@ -40,9 +40,9 @@ export class AuthController {
description:
'Unauthorized / 요청한 Refresh Token에 해당하는 고객이 없습니다.',
})
@Auth('refresh')
@Auth(HttpStatus.CREATED, 'refresh')
@Post('/refresh')
async refresh(@Req() req: Request): Promise<AuthDto> {
async refresh(@Req() req: Request): Promise<CustomerDto> {
return await this.authService.tokenRefresh(req);
}
}
9 changes: 6 additions & 3 deletions src/auth/presentation/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { CustomerDto } from 'src/customer/presentation/customer.dto';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
import { UserDto } from './user.dto';

export class AuthDto extends CustomerDto {
export class AuthDto extends UserDto {
@ApiProperty({
description: 'Access Token',
type: String,
})
accessToken: string;
@IsOptional()
accessToken?: string;

@ApiProperty({
description: 'Refresh Token',
type: String,
})
@IsOptional()
refreshToken: string;
}
17 changes: 17 additions & 0 deletions src/auth/presentation/user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
import { Group } from '../../common/validation/validation.data';

export class UserDto {
@IsNotEmpty({ groups: [Group.create] })
@IsOptional()
userType?: UserType;

@IsNotEmpty({ groups: [Group.create] })
@IsOptional()
userId?: number;
uuid?: string;
name?: string;
}

// 고객, 업체, 기사 공통 사용 DTO
export type UserType = 'customer' | 'driver' | 'business';
5 changes: 5 additions & 0 deletions src/auth/user.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UserDto } from './presentation/user.dto';

export interface IUserService {
findOne(dto: Partial<UserDto>): Promise<UserDto>;
}
30 changes: 30 additions & 0 deletions src/business/application/business.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IUserService } from '../../auth/user.interface';
import { UserDto } from '../../auth/presentation/user.dto';
import { Business } from '../../schemas/business.entity';

@Injectable()
export class BusinessService implements IUserService {
constructor(
@InjectRepository(Business)
private readonly businessRepository: Repository<Business>,
) {}

async signUp(dto: Business): Promise<Business> {
const newDriver = this.businessRepository.create(dto);
return await this.businessRepository.save(newDriver);
}

async findOne(dto: Partial<UserDto>): Promise<Business> {
const where = {};

dto.userId && (where['driverId'] = dto.userId);
dto.uuid && (where['uuid'] = dto.uuid);

return await this.businessRepository.findOne({
where: where,
});
}
}
11 changes: 11 additions & 0 deletions src/business/business.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BusinessService } from './application/business.service';
import { Business } from '../schemas/business.entity';

@Module({
imports: [TypeOrmModule.forFeature([Business])],
exports: [BusinessService],
providers: [BusinessService],
})
export class BusinessModule {}
59 changes: 59 additions & 0 deletions src/chat/application/business-chat.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BusinessChatRoom } from '../../schemas/business-chat-room.entity';
import { IChatService } from './chat.interface';
import { UserDto } from '../../auth/presentation/user.dto';
import { plainToInstance as toDto } from 'class-transformer';
import { ChatRoomDto } from '../presentation/chat.dto';

@Injectable()
export class BusinessChatService implements IChatService {
private readonly logger = new Logger(BusinessChatService.name);

constructor(
@InjectRepository(BusinessChatRoom)
private readonly businessChatRoomRepository: Repository<BusinessChatRoom>,
) {}

async exitsUserRoom(user: UserDto, chatRoomId: number): Promise<boolean> {
return await this.businessChatRoomRepository.exists({
where: { businessId: user.userId, chatRoomId },
});
}

async findChatRooms(user: UserDto): Promise<ChatRoomDto[]> {
const businessChatRooms = await this.businessChatRoomRepository.find({
where: { businessId: user.userId },
});

return await Promise.all(
businessChatRooms.map((room) => room.chatRoom),
).then((rooms) => {
return rooms.map((room) => {
return toDto(ChatRoomDto, room);
});
});
}

async getChatRoomById(
businessId: number,
chatRoomId: number,
): Promise<BusinessChatRoom> {
return await this.businessChatRoomRepository.findOne({
where: { businessId, chatRoomId },
});
}

async createChatRoom(dto: ChatRoomDto): Promise<ChatRoomDto> {
const newRoom = this.businessChatRoomRepository.create({
chatRoomId: dto.chatRoomId,
businessId: dto.inviteUser.userId,
});

return await this.businessChatRoomRepository.save(newRoom).then((room) => {
this.logger.log(`Business Chat room created: ${room.chatRoomId}`);
return toDto(ChatRoomDto, room.chatRoom);
});
}
}
8 changes: 8 additions & 0 deletions src/chat/application/chat.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { UserDto } from '../../auth/presentation/user.dto';
import { ChatRoomDto } from '../presentation/chat.dto';

export interface IChatService {
exitsUserRoom(user: UserDto, chatRoomId: number): Promise<boolean>;
findChatRooms(user: UserDto): Promise<ChatRoomDto[]>;
createChatRoom(chatRoomDto: ChatRoomDto): Promise<ChatRoomDto>;
}
Loading

0 comments on commit 7231d3c

Please sign in to comment.