Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[백한결] 식당 예약 API #1

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
10,192 changes: 10,192 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion template/package/package.typeorm.json → package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,25 @@
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.1.13",
"@nestjs/typeorm": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.7",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"joi": "^17.13.0",
"mysql2": "^3.6.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"ts-mockito": "^2.6.1",
Expand All @@ -40,7 +52,7 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.2.7",
"@types/express": "^4.17.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
Expand Down
Empty file modified setup.sh
100644 → 100755
Empty file.
17 changes: 16 additions & 1 deletion template/source/typeorm/app.module.ts → src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { addTransactionalDataSource } from 'typeorm-transactional';
import { DataSource } from 'typeorm';
import { RestaurantModule } from './restaurant/restaurant.module';
import * as Joi from 'joi'
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
validationSchema: Joi.object({
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(),
}),
}),
TypeOrmModule.forRootAsync({
useFactory() {
return {
Expand All @@ -17,6 +28,7 @@ import { DataSource } from 'typeorm';
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
synchronize: process.env.DB_SYNC === 'true',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
timezone: 'Z',
};
},
Expand All @@ -28,6 +40,9 @@ import { DataSource } from 'typeorm';
return addTransactionalDataSource(new DataSource(options));
},
}),
RestaurantModule,
AuthModule,
UserModule,
],
controllers: [],
providers: [],
Expand Down
27 changes: 27 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Body, Controller, Post, Req, Res, UseGuards } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { CreateUserResponseDto } from "src/user/dto/create-user-response.dto";
import RequestWithUser from "./interfaces/requestWithUser.interface";
import { LocalAuthGuard } from "./local/localAuthentication.guard";
import { Response } from "express";

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) { }

@Post('/register')
async register(@Body() dto: CreateUserResponseDto) {
return this.authService.register(dto);
}

@UseGuards(LocalAuthGuard)
@Post('/login')
async login(@Req() request: RequestWithUser, @Res() response: Response) {
const { user } = request;
const cookie = this.authService.getCookieWithJwtToken(user.id);
response.setHeader('Set-Cookie', cookie);
user.password = undefined;

return response.send(user);
}
}
31 changes: 31 additions & 0 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Module } from "@nestjs/common";
import { PassportModule } from "@nestjs/passport";
import { UserModule } from "src/user/user.module";
import { AuthService } from "./auth.service";
import { LocalStrategy } from "./local/local.strategy";
import { AuthController } from "./auth.controller";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { JwtStrategy } from "./jwt/jwt.strategy";

@Module({
imports: [
UserModule,
PassportModule,
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`,
},
}),
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController],
})

export class AuthModule {}
62 changes: 62 additions & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { compare, hash } from "bcrypt";
import { CreateUserResponseDto } from "src/user/dto/create-user-response.dto";
import { UserService } from "src/user/user.service";
import TokenPayload from "./interfaces/tokenPayload.interface";

@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) { }

public async register(dto: CreateUserResponseDto) {
const hashedPassword = await hash(dto.password, 10);
try {
const createdUser = await this.userService.create({
...dto,
password: hashedPassword,
});
createdUser.password = undefined;

return createdUser;
} catch (error) {
throw new HttpException(
'알 수 없는 오류가 발생하였습니다.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

public async getAuthenticatedUser(email: string, plainTextPassword: string) {
try {
const user = await this.userService.getByEmail(email);
await this.verifyPassword(plainTextPassword, user.password);
user.password = undefined;
return user;
} catch (error) {
throw new HttpException('잘못된 인증 정보입니다.', HttpStatus.BAD_REQUEST);
}
}

private async verifyPassword(plainTextPassword: string, hashedPassword: string) {
const isPasswordMatching = await compare(
plainTextPassword,
hashedPassword
);
if (!isPasswordMatching) {
throw new HttpException('잘못된 인증 정보입니다.', HttpStatus.BAD_REQUEST);
}
}

public getCookieWithJwtToken(userId: number) {
const payload: TokenPayload = { userId };
const token = this.jwtService.sign(payload);

return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get('JWT_EXPIRATION_TIME')}`;
}
}
7 changes: 7 additions & 0 deletions src/auth/interfaces/requestWithUser.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { User } from "src/user/entities/user.entity";

interface RequestWithUser extends Request {
user: User;
}

export default RequestWithUser;
5 changes: 5 additions & 0 deletions src/auth/interfaces/tokenPayload.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface TokenPayload {
userId: number;
}

export default TokenPayload;
28 changes: 28 additions & 0 deletions src/auth/jwt/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { Request } from "express";
import { ExtractJwt, Strategy } from "passport-jwt";
import { UserService } from "src/user/user.service";
import TokenPayload from "../interfaces/tokenPayload.interface";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly userService: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
return request?.cookies?.Authentication;
},
]),
secretOrKey: configService.get('JWT_SECRET'),
});
}

async validate(payload: TokenPayload) {
return this.userService.getById(payload.userId);
}
}
5 changes: 5 additions & 0 deletions src/auth/jwt/jwtAuthentication.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export default class JwtAuthenticationGuard extends AuthGuard('jwt') {}
18 changes: 18 additions & 0 deletions src/auth/local/local.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { AuthService } from "../auth.service";
import { User } from "src/user/entities/user.entity";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
usernameField: 'email',
});
}

async validate(email: string, password: string): Promise<User> {
return this.authService.getAuthenticatedUser(email, password);
}
}
5 changes: 5 additions & 0 deletions src/auth/local/localAuthentication.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
12 changes: 12 additions & 0 deletions src/common/dto/pagination.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Type } from 'class-transformer';
import { IsInt } from 'class-validator';

export class PaginationDto {
@Type(() => Number)
@IsInt()
page: number = 1;

@Type(() => Number)
@IsInt()
limit: number = 10;
}
24 changes: 24 additions & 0 deletions src/common/util/pagination.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export class PaginationResult<T> {
data: T[];
page: number;
limit: number;
totalCount: number;
totalPage: number;

constructor(data: T[], page: number, limit: number, totalCount: number) {
this.data = data;
this.page = page;
this.limit = limit;
this.totalCount = totalCount;
this.totalPage = Math.ceil(totalCount / limit);
}
}

export function createPaginationResult<T>(
data: T[],
page: number,
limit: number,
totalCount: number
): PaginationResult<T> {
return new PaginationResult(data, page, limit, totalCount);
}
14 changes: 14 additions & 0 deletions template/source/typeorm/main.ts → src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { initializeTransactionalContext } from 'typeorm-transactional';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
initializeTransactionalContext();

const app = await NestFactory.create(AppModule);

const configService = app.get(ConfigService);
app.use(cookieParser());

app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
// TODO: 프로그램 구현
await app.listen(process.env.PORT || 8000);

Expand Down
26 changes: 26 additions & 0 deletions src/restaurant/dto/request/create-restaurant-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IsNotEmpty } from 'class-validator';
import { RestaurantCategory } from '../../entities/restaurant-category.enum';
import { Image } from '../../entities/image.entity';

export class CreateRestaurantRequestDto {
@IsNotEmpty()
name: string;

@IsNotEmpty()
description: string;

@IsNotEmpty()
category: RestaurantCategory;

@IsNotEmpty()
address: string;

@IsNotEmpty()
phoneNumber: string;

@IsNotEmpty()
logoImage: string;

@IsNotEmpty()
images: Image[];
}
23 changes: 23 additions & 0 deletions src/restaurant/dto/request/reservation-restaurant-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IsEmail, IsNotEmpty, IsString, IsInt, Min, MaxLength } from 'class-validator';

export class ReservationRestaurantRequestDto {
@IsNotEmpty()
@IsString()
name: string;

@IsNotEmpty()
@IsInt()
@Min(1)
numberOfPeople: number;

@IsNotEmpty()
@IsString()
phoneNumber: string;

@IsEmail()
email: string;

@IsString()
@MaxLength(500)
request?: string;
}
Loading