Carpool(React Native & Express & apollo federation & Mariadb, Mongodb) - 4. auth-service(1)

yellow_note·2021년 12월 14일
0
post-thumbnail

#1 auth-service

auth-service는 유저의 로그인, 회원가입 등과 같이 유저의 정보를 다루는 서비스입니다. 따라서 보안에 필요한 authentication, authorization과 같은 기능 그리고 비밀번호의 암호화등의 다양한 방법을 활용이 필요합니다. 우선 유저의 데이터를 저장하기 위한 관계 테이블은 다음과 같습니다.


Carpool 서비스에서는 총 2가지의 유저 종류가 필요합니다. 운전자와 탑승객이죠. 때문에 유저 데이터베이스에는 유저의 기본정보를 담기위한 유저 테이블, 운전자 확인을 위한 운전면허증 테이블이 필요합니다. 이를 바탕으로 데이터 모델을 구성하도록 하겠습니다.

#2 auth-service api

Graphql을 위한 Query, Mutation, Input, Type, Enum 명세서입니다.

1) 로그인

로그인에 대한 흐름도입니다. 로그인을 요청하면 리졸버 레이어에서 요청 리소스를 바탕으로 타당한 데이터인지 판단한 후 user_id의 값을 클라이언트에게 반환합니다.

2) 회원가입

회원가입에 대한 흐름도입니다. 회원가입 요청 시 리졸버 레이어에서 해당 리소스가 타당한 리소스인지 판단한 후 결과 코드를 클라이언트에게 반환합니다.

3) 이메일 체크

회원가입 시 고유한 이메일인지 체크해주는 api입니다. 이메일 값을 입력하면 이 이메일 값을 전송하여 타당한 이메일인지 결과 코드를 반환합니다.

4) 닉네임 체크

회원가입 시 고유한 닉네임인지 체크해주는 api입니다. 닉네임 값을 입력하면 이 닉네임 값을 전송하여 타당한 닉네임인지 결과 코드를 반환합니다.

5) 회원 수정

회원 수정에 대한 흐름도입니다. 회원 수정 요청 시 gateway에서 타당한 토큰인지 판단한 후 타당하다면 service로 리소스를 전송한 후 비즈니스 로직을 수행합니다. 이후 이에 대한 결과 코드를 반환합니다.

6) 유저 정보 불러오기

유저 정보를 불러오는 흐름도입니다. 유저 데이터 요청 시 gateway에서 이 토큰이 타당한지에 대한 여부를 확인합니다. 타당하다면 이 토큰에서 user_id를 추출한 후 서비스로 전송합니다. 서비스에서는 이 user_id를 매개로 유저 리소스를 클라이언트에게 반환합니다.

#3 프로젝트 작성

이를 바탕으로 auth-service를 작성해보도록 하겠습니다.

Carpool디렉토리에 다음의 명령어로 apollo + express + typescript 환경을 만들도록 하겠습니다.

mkdir auth-service
cd auth-service
npm init
mkdir src
touch src/index.ts
npm install @apollo/subgraph @graphql-tools/merge apollo-server-core apollo-server-express bcrypt cors express graphql@15.8.0 jsonwebtoken nodemon mongoose uuid tsc-watch winston winston-daily-rotate-file @types/jsonwebtoken @types/bcrypt

npm install -D @types/express typescript

다음의 명령어로 tsconfig.json 파일을 생성하도록 하겠습니다.

npx tsc --init
  • tsconfig.json
{
  "compilerOptions": {
    "target": "es5",            // import를 지원하는 es5
    "module": "CommonJS",
    "outDir": "build/",         // 빌드 경로
    "esModuleInterop": true,
    "sourceMap": true,
    "resolveJsonModule": true                           
  },
  "include": ["src/**/*"],
}

필요한 라이브러리를 작성했으면 /auth-service 디렉토리 바로 아래에 config 폴더를 생성하여 환경변수들을 관리하도록 하겠습니다.

  • auth-service/config/env.variable.ts
const PORT: number = 6000;

const MONGO_DEV_URI: string = "mongodb://127.0.0.1:27017/CARPOOL_AUTH";
const MONGO_URI: string = "mongodb://mongo:27017/CARPOOL_AUTH"

const SECRET_KEY: string = "asw8*@1ssmxa";

export {
    PORT,
    MONGO_DEV_URI,
    MONGO_URI,
    SECRET_KEY
};

이 설정 파일에는 auth-service가 작동하기 위한 포트번호, mongodb url, jwt를 위한 key값을 관리하도록 하겠습니다.

이어서 constants라는 디렉토리를 만들어 후에 사용할 상태 결과값들을 만들도록 하겠습니다.

  • ./src/constants/result.code.ts
// auth-service status code - 1000 ~ 1100
// common code - 2000 ~ 3000

const UNIQUE_EMAIL: number = 1000;
const UNIQUE_NICKNAME: number = 1001;
const FOUND_USER: number = 1002;
const SIGNED_IN: number = 1003;
const SIGNED_UP: number = 1004;
const UPDATED_PASSWORD: number = 1005;
const UPDATED_NICKNAME: number = 1006;
const UPDATED_LICENSE: number = 1007;
const DELTED_USER: number = 1008;
const LOGOUTED: number = 1009;
const NOT_FOUND_USER: number = 1010;
const NOT_MATCHED_PASSWORD: number = 1011;
const DUPLICATED_EMAIL: number = 1012;
const DUPLICATED_NICKNAME: number = 1013;
const FAILED_TO_CREATE_USER: number = 1014;
const FAILED_TO_UPDATE_PASSWORD: number = 1015;
const FAILED_TO_UPDATE_NICKNAME: number = 1016;
const FAILED_TO_UPDATE_LICENSE: number = 1017;
const FAILED_TO_DELETE_USER: number = 1018;

const DATABASE_ERROR: number = 2000;
const EXCEPTION_ERROR: number = 2001;
const UN_AUTHENTICATION: number = 2003;

export {
    UNIQUE_EMAIL,
    SIGNED_IN,
    FOUND_USER,
    UNIQUE_NICKNAME,
    SIGNED_UP,
    UPDATED_PASSWORD,
    UPDATED_NICKNAME,
    UPDATED_LICENSE,
    DELTED_USER,
    LOGOUTED,
    NOT_FOUND_USER,
    NOT_MATCHED_PASSWORD,
    DUPLICATED_EMAIL,
    DUPLICATED_NICKNAME,
    FAILED_TO_CREATE_USER,
    FAILED_TO_UPDATE_PASSWORD,
    FAILED_TO_UPDATE_NICKNAME,
    FAILED_TO_UPDATE_LICENSE,
    FAILED_TO_DELETE_USER,
    DATABASE_ERROR,
    EXCEPTION_ERROR,
    UN_AUTHENTICATION,
};

상태값을 작성했으니 graphql을 위한 typeDefs들을 작성해보도록 하겠습니다.

  • ./src/typeDefs/enum.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    enum Role {
        PASSENGER,
        DRIVER
    }
`;

export default typeDefs;

gql은 gql내부에 있는 코드들을 graphql을 위한 코드로 정의시켜주는 라이브러리입니다. 이 gql을 이용하여 내부에 enum이라는 키워드로 Role이라는 열거형 함수를 정의하여 유저의 역할 값을 관리하도록 하겠습니다.

  • ./src/typeDefs/input.ts
import  { gql } from 'apollo-server-express';

const typeDefs = gql`
    input GetRiderInfoInput {
        riderId: String!
    } 

    input CheckEmailInput {
        email: String!
    }

    input CheckNicknameInput {
        nickname: String!
    }

    input LoginUserInput {
        email: String!,
        password: String!
    }

    input RegisterUserInput {
        email: String!,
        password: String!,
        nickname: String!,
        license: LicenseInput
    }

    input ModifyUserInput {
        password: String,
        nickname: String,
        license: LicenseInput
    }

    input DeleteUserInput {
        isDelete: Boolean!
    }

    input LicenseInput {
        birthDate: String,
        name: String,
        licNumber: String
    }
`;

export default typeDefs;

input 키워드는 일전에 작성했던 nest, spring의 dto객체랑 같은 역할을 하는 키워드라 생각할 수 있습니다. client에서 server로 데이터 요청, 수정등의 crud를 수행하기 위해 입력되는 값들을 정의하는 gql입니다.
여기서 LicenseInput을 잠깐 주목해볼 수 있겠는데요. graphql에서 기본적으로 제공하는 데이터 타입은 string, int, float, boolean, id 총 5가지를 제공하고 있습니다. 이 기본 타입들을 이용하여 license라는 데이터 타입을 만들어 사용하도록 하겠습니다.

  • ./src/typeDefs/type.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    type User @key(fields: "user_id") {
        email: String!,
        password: String!
        nickname: String!
        createdAt: String!
        role: Role!
        license: License
        userId: ID!
        isDelete: Boolean!
    }
    
    type License {
        birthDate: String!,
        name: String!,
        licNumber: String!
    }

    type Response {
        code: Int!,
        message: String!,
        payload: AuthPayload!
    }

    union AuthPayload = User | String | Int;
`;

export default typeDefs;

type 키워드를 이용하여 스키마를 작성한 파일입니다. #1의 스키마 표를 참고하여 스키마를 정의했습니다.

그러면 이를 이용해서 query, mutation 파일을 작성해보도록 하겠습니다.

  • ./src/typeDefs/query.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    type Query {
        getUser: Response! 
        getRiderInfo(getRiderInfoInput: GetRiderInfoInput): Response!
        checkEmail(checkEmailInput: CheckEmailInput!): Response!
        checkNickname(checkNicknameInput: CheckNicknameInput!): Response!
    }
`;

export default typeDefs;

graphql에서 query는 일반적인 rest기반의 http 메서드 중 get 메서드에 해당합니다. 즉, 데이터 불러오기를 이용할 때 query에 메서드를 정의하여 이를 활용할 수 있죠. 정의한 메서드를 살펴 보겠습니다.

1) getUser: 유저의 정보를 불러오는 메서드입니다. 로그인 시 헤더에 있는 jwt를 디코드하여 여기에 포함되어 있는 user_id값을 이용하여 유저의 정보를 불러올 수 있고, 마이 페이지를 이용할 때 user_id로 유저의 정보를 불러오는 등 다양한 부분에서 활용됩니다.

2) getRiderInfo: 운전자라면 자신의 아이디를 이용하여 유저의 정보를 불러와 카풀 데이터에 합할 때 사용됩니다.

3) checkEmail: 회원가입 시 기재한 email의 값이 중복 값인지 판별해주는 메서드입니다.

4) checkNickname: 회원가입, 회원수정 시 기재한 nickname의 값이 중복 값인지 판별해주는 메서드입니다.

  • ./src/typeDefs/mutation.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    type Mutation {
        loginUser(loginUserInput: LoginInput!): Response!
        logoutUser: Response!
        register(registerUserInput: RegisterInput!): Response!
        modifyUser(modifyUserInput: ModifyInput!): Response!
        deleteUser(deleteUserInput: DeleteInput!): Response!
    }
`;

export default typeDefs;

mutation은 rest의 http 메서드들 중 post, put, delete의 기능을 수행합니다. 즉, 데이터의 변경이라던지 저장과 같은 기능을 수행할 수 있죠. mutation 키워드를 이용하여 작성한 메서드를 살펴 보겠습니다.

1) loginUser: 앞서 정의한 input을 이용하여 로그인을 수행하는 메서드입니다. email, password를 입력받아 데이터베이스에 해당 데이터가 존재하는지 확인 후 user_id를 이용하여 json-web-token을 발행하죠.

2) registerUser: 회원가입을 위한 메서드입니다. 회원가입을 위한 리소스를 전달받아 데이터베이스에 저장합니다.

3) logoutUser: 로그아웃을 위한 메서드입니다.

4) modifyUser: 회원수정을 위한 메서드입니다. input 자체는 password, license, nickname을 한번에 입력받을 수 있도록 했으나 front 부분에서 이를 이용할 때 비밀번호 변경, 운전자 등록, 닉네임 변경과 같이 독립적인 기능을 수행하도록 만들 예정입니다.

5) deleteUser: 회원탈퇴를 위한 메서드입니다. 실제적으로 delete가 수행되는 것이 아니라 is_delete 데이터에 true값을 부여합니다. 즉, 실질적으로 애플리케이션을 이용하는 유저는 is_delete값이 false인 유저들이 되는 것이죠.

graphql을 위한 typeDefs들을 정의했으니 이를 한 데 모아줄 index 파일을 작성하겠습니다.

  • ./src/typeDefs/index.ts
import queries from './query';
import mutations from './mutation';
import types from './type';
import inputs from './input';
import enums from './enum';
import { mergeTypeDefs } from '@graphql-tools/merge';

const typeDefs = mergeTypeDefs([
    queries,
    mutations,
    types,
    inputs,
    enums
]);

export { typeDefs };

resolver를 작성하기전에 모델 interface를 작성하도록 하겠습니다.

  • ./src/interface/user.interface.ts
import { ILicense } from "./license.interface";

interface IUser {
    email: string,
    password: string,
    nickname: string,
    createdAt: string,
    role: string,
    userId: string,
    isDelete: boolean,
    license: ILicense
};

export { IUser }; 
  • ./src/interface/license.interface.ts
interface ILicense {
    birthDate: string,
    name: string,
    licNumber: string
};

export { ILicense };

이어서 graphql의 input을 변환하기위한 dto를 만들도록 하겠습니다.

  • ./src/dto/get.rider.info.dto.ts
interface GetRiderInfoDto {
    riderId: string
};

export { GetRiderInfoDto };
  • ./src/dto/check.email.dto.ts
interface CheckEmailDto {
    email: string
};

export { CheckEmailDto };
  • ./src/dto/check.nickname.dto.ts
interface CheckNicknameDto {
    nickname: string
};

export { CheckNicknameDto }
  • ./src/dto/login.user.dto.ts
interface LoginUserDto {
    email: string,
    password: string
};

export { LoginUserDto };
  • ./src/dto/register.user.dto.ts
import { ILicense } from "../interfaces/license.interface";

interface RegisterUserDto {
    email: string,
    password: string,
    nickname: string,
    license?: ILicense
};

export { RegisterUserDto };
  • ./src/dto/modify.user.dto.ts
import { ILicense } from "../interfaces/license.interface";

interface ModifyUserDto {
    password?: string,
    nickname?: string,
    license?: ILicense
};

export { ModifyUserDto };
  • ./src/dto/delete.user.dto.ts
interface DeleteUserDto {
    isDelete: boolean
};

export { DeleteUserDto };
  • ./src/dto/response.dto.ts
import { IUser } from "../interfaces/user.interface";

interface ResponseDto {
    code: number,
    message: string,
    payload: IUser | string | number
};

export { ResponseDto };

dto 인터페이스들을 작성했으니 이를 바탕으로 resolver를 작성하도록 하겠습니다.

  • ./src/resolvers/resolvers.ts
import { CheckEmailDto } from "../dto/check.email.dto";
import { CheckNicknameDto } from "../dto/check.nickname.dto";
import { DeleteUserDto } from "../dto/delete.user.dto";
import { GetRiderInfoDto } from "../dto/get.rider.info.dto";
import { LoginUserDto } from "../dto/login.user.dto";
import { ModifyUserDto } from "../dto/modify.user.dto";
import { RegisterUserDto } from "../dto/register.user.dto";
import { ResponseDto } from "../dto/response.dto";
import { authService } from "../services/auth.service";

const resolvers = {
    Query: {
        getUser: async (context: any): Promise<ResponseDto> => {
            return await authService.getUser(context);
        },
        getRiderInfo: async (
            context: any,
            args: any
        ): Promise<ResponseDto> => {
            const getRiderInfo: GetRiderInfoDto = args.getRiderInfo;

            return await authService.getRiderInfo(
                context, 
                getRiderInfo
            );
        },
        checkEmail: async (args: any): Promise<ResponseDto> => {
            const checkEmailDto: CheckEmailDto = args.checkEmailDto;

            return await authService.checkEmail(checkEmailDto);
        },
        checkNickname: async (args: any): Promise<ResponseDto> => {
            const checkNicknameDto: CheckNicknameDto = args.checkNicknameDto;

            return await authService.checkNickname(checkNicknameDto);
        },
    },
    Mutation: {
        loginUser: async (args: any): Promise<ResponseDto> => {
            const loginUserDto: LoginUserDto = args.loginUserDto;

            return await authService.loginUser(loginUserDto);
        },
        logout: async (context: any): Promise<ResponseDto> => {
            return await authService.logoutUser(context);
        }, 
        register: async (args: any): Promise<ResponseDto> => {
            const registerUserDto: RegisterUserDto = args.registerUserDto;

            return await authService.registerUser(registerUserDto);
        },
        modifyUser: async (
            context: any,
            args: any
        ): Promise<ResponseDto> => {
            const modifyUserDto: ModifyUserDto = args.modifyUserDto;

            return await authService.modifyUser(
                context, 
                modifyUserDto
            );
        },
        deleteUser: async (
            context: any,
            args: any
        ): Promise<ResponseDto> => {
            const deleteUserDto: DeleteUserDto = args.deleteUserDto;

            return await authService.deleteUser(
                context, 
                deleteUserDto
            );
        }
    },
};

export { resolvers };

앞서 작성한 mutation이라던지 query는 일종의 인터페이스라고 한다면 resolver는 이 인터페이스 객체들에 실질적인 코드를 구현하는 객체입니다. 저는 상기 코드처럼 resolver객체를 작성하였고, resolver에서는 context, args와 같은 변수들을 서비스 모듈에 넘겨주고, 실질적인 비즈니스 로직은 서비스 모듈을 따로 만들어 구현하였습니다.

서비스 모듈을 작성하기 전에 서비스 모듈에서 사용될 유틸 모듈, 몽고 디비를 위한 스키마를 작성하도록 하겠습니다.

  • ./src/utils/jwt.utils.ts
import jwt from 'jsonwebtoken';
import { SECRET_KEY } from '../config/env.variable';

class JwtUtils {
    constructor() {}

    public async generator(userId: string): Promise<string> {
        return jwt.sign({
            exp: Math.floor(Date.now() / 1000) + (60 *60),
            data: userId
        }, SECRET_KEY);
    }

    public async verify(token: string): Promise<any> {
        return jwt.verify(token.split('Bearer ')[1],
                          SECRET_KEY);
    }
}

const jwtUtils = new JwtUtils();

export { 
    jwtUtils,
    JwtUtils
};

jsonwebtoken 라이브러리를 활용하여 토큰의 유효 기간을 1시간으로 설정하고, user_id 데이터를 섞어 토큰 값을 생성하는 유틸입니다.

  • ./src/util/password.utils.ts
import bcrypt from 'bcrypt';

class PasswordUtils {
    constructor() {}

    public async generator(password: string): Promise<string> {
        return await bcrypt.hash(password, 10);
    }

    public async checker(
        origin_password: string,
        encrypted_password: string
    ): Promise<boolean> {
        return await bcrypt.compare(
            origin_password,
            encrypted_password
        );
    }
}

const passwordUtils = new PasswordUtils();

export { 
    passwordUtils,
    PasswordUtils
};

bcyrpt 라이브러리를 이용하여 비밀번호를 해싱 시켜줄 수 있는 generator, 원본 비밀번호와 해싱 비밀번호를 비교하여 검증해주는 checker를 가지고 있는 모듈 유틸입니다.

mongoose클라이언트와 Mongoose를 위한 모델을 작성해보도록 하겠습니다.

  • ./src/models/mongoose.client.ts
import mongoose from "mongoose";
import { MONGO_DEV_URI } from "../config/env.variable";

mongoose.connect(MONGO_DEV_URI)
        .then(response => {
            console.log("Successfully connected to mongodb");
        });

export { mongoose }; 
  • ./src/models/license.model.ts
import mongoose from 'mongoose';
import { ILicense } from '../interfaces/license.interface';

const licenseSchema = new mongoose.Schema<ILicense>({
    birthDate: {
        type: String,
        required: true
    },
    name: {
        type: String,
        required: true
    },
    licNumber: {
        type: String,
        required: true
    },
});

export { licenseSchema };

운전면허 데이터를 위한 스키마입니다.

  • ./src/models/user.model.ts
import { ILicense } from "./license.interface";

interface IUser {
    email: string,
    password: string,
    nickname: string,
    createdAt: string,
    role: string,
    userId: string,
    isDelete: boolean,
    license: ILicense
};

export { IUser }; 

license 스키마를 불러와 유저 스키마에서 하나의 데이터 타입으로 활용하도록 하겠습니다.

로깅을 위한 모듈을 작성하도록 하겠습니다.

  • ./src/utils/logger.ts
import winston from 'winston';
import winstonDaily from 'winston-daily-rotate-file';

const logformat = winston.format.printf(info => {
    return `${info.timestamp} ${info.level}: ${info.message}`
});

const logger = winston.createLogger({
    format: winston.format.combine(
        winston.format.timestamp({ format: 'YYYY-MM-DD HH:MM:SS'}),
        logformat
    ),
    level: 'debug',
    transports: [
        new winstonDaily({
            level: 'info',
            datePattern: 'YYYY-MM-DD',
            dirname: 'logs',
            filename: `%DATE%.log`,
            maxFiles: 30
        }),
        new winstonDaily({
            level: 'error',
            datePattern: 'YYYY-MM-DD',
            dirname: 'logs',
            filename: `%DATE%.error.log`,
            maxFiles: 30
        })
    ]
});

logger.add(new winston.transports.Console({
    format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
    )
}));

export { logger };

이 로깅 모듈은 날짜별로 logs 디렉토리에 logger 정보를 저장해주는 모듈입니다.

에러를
작성된 유틸, 로깅 모듈, 스키마를 이용하여 최종적으로 서비스, 레포지토리 클래스 그리고 의존성 모듈을 작성해보겠습니다.

  • ./src/modules/service.dependency.module.ts
import { ErrorHandler } from "../error/error.handler";
import { AuthRepository } from "../repository/auth.repository";
import { JwtUtils } from "../utils/jwt.utils";
import { PasswordUtils } from '../utils/password.utils';

type AuthServiceDependency = {
    authRepository: AuthRepository,
    jwtUtils: JwtUtils,
    passwordUtils: PasswordUtils,
    errorHandler: ErrorHandler
};

export { AuthServiceDependency };

의존성 모듈을 만든 이유는 의존성 주입을 위함인데 의존성을 필요로 하는 클래스들을 각 파일에서 객체로 생성하여 의존성 모듈에 주입하는 방식으로 진행했습니다. 서비스 클래스에서 레포지토리를 new로 생성하여 결합도를 높이는 방식보다는 레포지토리에서 new로 생성된 객체를 받아와서 의존성 모듈에 등록하는 방법이 결합도를 낮출 수 있기 때문입니다.

  • ./src/services/auth.service.ts
import { DATABASE_ERROR, DUPLICATED_EMAIL, DUPLICATED_NICKNAME, EXCEPTION_ERROR, FAILED_TO_CREATE_USER, FAILED_TO_DELETE_USER, FAILED_TO_UPDATE_LICENSE, FAILED_TO_UPDATE_NICKNAME, FAILED_TO_UPDATE_PASSWORD, LOGOUTED, NOT_FOUND_USER, NOT_MATCHED_PASSWORD, SIGNED_IN } from "../constants/result.code";
import { LoginUserDto } from "../dto/login.user.dto";
import { ResponseDto } from "../dto/response.dto";
import { AuthServiceDependency } from "../modules/service.dependency.module";
import { authRepository } from "../repository/auth.repository";
import { jwtUtils } from "../utils/jwt.utils";
import { passwordUtils } from '../utils/password.utils';
import { errorHandler } from '../error/error.handler';
import { logger } from "../utils/logger";
import { GetRiderInfoDto } from "../dto/get.rider.info.dto";
import { CheckEmailDto } from "../dto/check.email.dto";
import { CheckNicknameDto } from "../dto/check.nickname.dto";
import { RegisterUserDto } from "../dto/register.user.dto";
import { IUser } from "../interfaces/user.interface";
import { v4 } from 'uuid';
import { DRIVER, PASSENGER } from "../models/license.role";
import { ModifyUserDto } from "../dto/modify.user.dto";
import { DeleteUserDto } from "../dto/delete.user.dto";

class AuthService {
    constructor(private authServiceDependency: AuthServiceDependency) {}

    public async loginUser(loginUserDto: LoginUserDto): Promise<ResponseDto> {
        const {
            email,
            password
        } = loginUserDto;

        try {
            const loginUserResponse: ResponseDto = await this.authServiceDependency.authRepository.findUserByEmail(email);

            if(
                loginUserResponse.code === DATABASE_ERROR || 
                loginUserResponse.code === NOT_FOUND_USER ||
                loginUserResponse.code === EXCEPTION_ERROR
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's loginUser",
                    loginUserResponse.message,
                    loginUserResponse.code
                );
            }

            if(!(await this.authServiceDependency.passwordUtils.checker(password, loginUserResponse.payload.password))) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's loginUser",
                    "Not matched password",
                    NOT_MATCHED_PASSWORD
                );
            }

            const accessToken: string = await this.authServiceDependency.jwtUtils.generator(loginUserResponse.payload.userId);

            logger.info("AuthService's loginUser: " + accessToken);

            return {
                code: SIGNED_IN,
                message: "Token is generated",
                payload: accessToken
            };
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's loginUser",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async getUser(context: any): Promise<ResponseDto> {
        try {
            const userId: string = (await this.authServiceDependency.jwtUtils.verify(context.userId)).data;

            if(!userId) {
                this.authServiceDependency.errorHandler.authenticationError("AuthService's getUser");
            }

            const getUserResponse: ResponseDto = await this.authServiceDependency.authRepository.findUserByUserId(userId);

            if(
                getUserResponse.code === NOT_FOUND_USER || 
                getUserResponse.code === DATABASE_ERROR ||
                getUserResponse.code === EXCEPTION_ERROR
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's getUser",
                    getUserResponse.message,
                    getUserResponse.code
                );
            }

            logger.info("AuthServices' getUser: " + getUserResponse.payload.toString());

            return getUserResponse;
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's getUser",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async getRiderInfo(
        context: any,
        getRiderInfo: GetRiderInfoDto    
    ): Promise<ResponseDto> {
        const { riderId } = getRiderInfo;

        try {
            const userId: string = (await this.authServiceDependency.jwtUtils.verify(context.userId)).data;

            if(!userId) {
                this.authServiceDependency.errorHandler.authenticationError("AuthService's getRiderInfo");
            }

            const getRiderInfoResponse: ResponseDto = await this.authServiceDependency.authRepository.findUserByUserId(riderId);

            if(
                getRiderInfoResponse.code === NOT_FOUND_USER || 
                getRiderInfoResponse.code === DATABASE_ERROR ||
                getRiderInfoResponse.code === EXCEPTION_ERROR
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthServices's getRiderInfo",
                    getRiderInfoResponse.message,
                    getRiderInfoResponse.code
                );
            }

            logger.info("AuthService's getRiderInfo: " + getRiderInfoResponse.payload.toString());

            return getRiderInfoResponse;
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's getRiderInfo",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async checkEmail(checkEmailDto: CheckEmailDto): Promise<ResponseDto> {
        const { email } = checkEmailDto;

        try {
            const checkEmailResponse: ResponseDto = await this.authServiceDependency.authRepository.isExistEmail(email);

            if(
                checkEmailResponse.code === DUPLICATED_EMAIL ||
                checkEmailResponse.code === DATABASE_ERROR ||
                checkEmailResponse.code === EXCEPTION_ERROR
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's checkEmail",
                    checkEmailResponse.message,
                    checkEmailResponse.code
                );
            }

            logger.info("AuthService's checkEmail" + checkEmailResponse.message);
            
            return checkEmailResponse;
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's checkEmail",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async checkNickname(checkNicknameDto: CheckNicknameDto): Promise<ResponseDto> {
        const { nickname } = checkNicknameDto;

        try {
            const checkNicknameResponse: ResponseDto = await this.authServiceDependency.authRepository.isExistNickname(nickname);

            if(
                checkNicknameResponse.code === DUPLICATED_NICKNAME ||
                checkNicknameResponse.code === DATABASE_ERROR ||
                checkNicknameResponse.code === EXCEPTION_ERROR
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's checkEmail",
                    checkNicknameResponse.message,
                    checkNicknameResponse.code
                );
            }

            logger.info("AuthService's checkEmail: " + checkNicknameResponse.message);

            return checkNicknameResponse;
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's checkNickname",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async registerUser(registerUserDto: RegisterUserDto): Promise<ResponseDto> {
        const {
            email,
            password,
            nickname,
            license
        } = registerUserDto;

        try {
            const userEntity: IUser = license ? {
                email,
                password: (await this.authServiceDependency.passwordUtils.generator(password)),
                nickname,
                createdAt: new Date().toISOString(),
                role: DRIVER,
                license,
                userId: v4(),
                isDelete: false
            }: {
                email,
                password: (await this.authServiceDependency.passwordUtils.generator(password)),
                nickname,
                createdAt: new Date().toISOString(),
                role: PASSENGER,
                license: null,
                userId: v4(),
                isDelete: false
            };
            const saveUserResponse: ResponseDto = await this.authServiceDependency.authRepository.createUser(userEntity);

            if(
                saveUserResponse.code === FAILED_TO_CREATE_USER ||
                saveUserResponse.code === DATABASE_ERROR ||
                saveUserResponse.code === EXCEPTION_ERROR
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's registerUser",
                    saveUserResponse.message,
                    saveUserResponse.code
                );
            }

            logger.info("AuthService's registerUser: " + saveUserResponse.payload.toString());

            return saveUserResponse;
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's registerUser",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async modifyUser(
        context: any,
        modifyUserDto: ModifyUserDto
    ): Promise<ResponseDto> {
        const {
            password,
            nickname,
            license
        } = modifyUserDto;

        try {
            const userId: string = (await this.authServiceDependency.jwtUtils.verify(context.userId)).data;

            if(!userId) {
                this.authServiceDependency.errorHandler.authenticationError("AuthService's modifyUser");
            }

            let modifyUserResponse: ResponseDto;

            if(password) {
                modifyUserResponse = await this.authServiceDependency.authRepository.modifyPasswordByUserId(
                    userId,
                    password
                );
            }

            if(nickname) {
                modifyUserResponse = await this.authServiceDependency.authRepository.modifyNicknameByUserId(
                    userId,
                    nickname
                );
            }

            if(license) {
                modifyUserResponse = await this.authServiceDependency.authRepository.modifyLicenseByUserId(
                    userId,
                    license
                );
            }

            if(
                modifyUserResponse.code === FAILED_TO_UPDATE_LICENSE ||
                modifyUserResponse.code === FAILED_TO_UPDATE_PASSWORD ||
                modifyUserResponse.code === FAILED_TO_UPDATE_NICKNAME ||
                modifyUserResponse.code === DATABASE_ERROR ||
                modifyUserResponse.code === EXCEPTION_ERROR
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's modifyUser",
                    modifyUserResponse.message,
                    modifyUserResponse.code
                );
            }

            logger.info("AuthService's modifyUser: " + modifyUserResponse.payload.toString());

            return modifyUserResponse;
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's modifyUser",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async deleteUser(
        context: any,
        deleteUserDto: DeleteUserDto
    ) {
        const { isDelete } = deleteUserDto;

        try {
            const userId: string = (await this.authServiceDependency.jwtUtils.verify(context.userId)).data;

            if(!userId) {
                this.authServiceDependency.errorHandler.authenticationError("AuthService's deleteUser");
            }

            const deleteUserResponse: ResponseDto = await this.authServiceDependency.authRepository.deleteUser(
                userId,
                isDelete
            );

            if(
                deleteUserResponse.code === FAILED_TO_DELETE_USER ||
                deleteUserResponse.code === DATABASE_ERROR ||
                deleteUserResponse.code === EXCEPTION_ERROR    
            ) {
                this.authServiceDependency.errorHandler.error(
                    "AuthService's deleteUser",
                    deleteUserResponse.message,
                    deleteUserResponse.code
                );
            }
        
            logger.info("AuthService's deleteUser: " + deleteUserResponse.payload);

            return deleteUserResponse;
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's deleteUser",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }

    public async logoutUser(context: any): Promise<ResponseDto> {
        try {
            const userId: string = (await this.authServiceDependency.jwtUtils.verify(context.userId)).data;

            if(!userId) {
                this.authServiceDependency.errorHandler.authenticationError("AuthService's logoutUser");
            }

            return {
                code: LOGOUTED,
                message: "Successfully logouted!",
                payload: null
            };
        } catch(error) {
            this.authServiceDependency.errorHandler.error(
                "AuthService's logoutUser",
                error.toString(),
                EXCEPTION_ERROR
            );
        }
    }
}

const authService = new AuthService({
    authRepository,
    jwtUtils,
    passwordUtils,
    errorHandler
});

export { 
    authService,
    AuthService
};

메서드 위주로 서비스 모듈을 살펴 보겠습니다.

1) loginUser: 우선 email을 매개로 하여 유저 데이터가 존재하는지 확인합니다. 존재하지 않는다면 에러 메시지를 반환하고, 존재한다면 password.util의 checker 메서드를 호출하여 비밀번호를 검증합니다. 이 과정을 수행하고 나면 최종적으로 jwt.generator를 호출하여 user_id값을 담아 토큰을 생성하여 반환합니다.

2) getUser: 유저의 데이터를 가져오는 메서드입니다. 인자로 전달받은 user_id를 받아와 유저 데이터를 가져오고, 데이터의 존재 유무를 파악한 후 응답 데이터를 반환합니다.

3) getRiderInfo: 라이더 아이디 값을 매개로 유저의 정보를 가져오는 메서드입니다.

4) checkEmail: 이메일의 중복 여부를 확인해주는 메서드입니다. email값을 exists 메서드 인자에 넣어 해당 데이터가 존재하면 EXIST_NICKNAME 코드를 그렇지 않다면 NOT_EXIST_NICKNAME를 반환합니다.

5) checkNickname: 닉네임의 중복 여부를 확인해주는 메서드입니다. nickname값을 exists 메서드 인자에 넣어 데이터가 존재한다면 EXIST_NICKNAME 코드를 그렇지 않다면 NOT_EXIST_NICKNAME 코드를 반환합니다.

6) registerUser: 회원가입 리소스를 받아와서 리소스에 운전면허 데이터의 유무를 파악합니다. 운전면허 데이터가 존재한다면 DRIVER 역할을, 존재하지 않는다면 PASSENGER 역할을 부여합니다. 그리고 데이터를 저장하고 저장이 수행된다면 SUCCESS 코드를 반환합니다.

7) modifyUser: 유저의 데이터를 수정하는 메서드입니다. 조건문으로 케이스를 분기하여 비밀번호 변경, 닉네임 변경, 운전면허등록 총 3가지의 독립적인 기능을 수행하도록 합니다.

8) deleteUser: is_delete의 값을 true로 만들어 유저의 상태를 바꾸어 주는 메서드입니다. 이를 이용하여 유저를 호출하는 메서드에 is_delete: false라는 옵션을 넣어 사용이 가능한 유저들만 호출할 수 있도록 합니다.

9) logoutUser: 해당 메서드를 정상적으로 거치게 되면 로그아웃 코드를 반환합니다.

서비스 모듈이 완성되었습니다.

  • ./src/repository/auth.repository.ts
import { DATABASE_ERROR, DELTED_USER, DUPLICATED_EMAIL, DUPLICATED_NICKNAME, EXCEPTION_ERROR, FAILED_TO_CREATE_USER, FOUND_USER, SIGNED_UP, UNIQUE_EMAIL, UNIQUE_NICKNAME, UPDATED_LICENSE, UPDATED_NICKNAME, UPDATED_PASSWORD } from "../constants/result.code";
import { ResponseDto } from "../dto/response.dto";
import { ILicense } from "../interfaces/license.interface";
import { IUser } from "../interfaces/user.interface";
import { DRIVER } from "../models/license.role";
import { User } from "../models/user.model";

class AuthRepository {
    constructor() {}

    public async findUserByEmail(email: string): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.findOne({
                email,
                isDelete: false
            }).then((result: IUser) => {
                return {
                    code: FOUND_USER,
                    message: "Found user!",
                    payload: result
                };  
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }

    public async findUserByUserId(userId: string): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.findOne({
                userId,
                isDelete: false
            }).then((result: IUser) => {
                return {
                    code: FOUND_USER,
                    message: "Found User!",
                    payload: result
                };
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }

    public async isExistEmail(email: string): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.exists({
                email,
                isDelete: false
            }).then(result => {
                if(result) {
                    return {
                        code: DUPLICATED_EMAIL,
                        message: "This email has been existed!",
                        payload: null
                    };
                }

                return {
                    code: UNIQUE_EMAIL,
                    message: "This email is unique!",
                    payload: null
                };
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }

    public async isExistNickname(nickname: string): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.exists({
                nickname,
                isDelete: false
            }).then(result => {
                if(result) {
                    return {
                        code: DUPLICATED_NICKNAME,
                        message: "This nickname has been existed",
                        payload: null
                    };
                }

                return {
                    code: UNIQUE_NICKNAME,
                    message: "This nickname is unique",
                    payload: null
                };
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            }
        }
    }

    public async createUser(userEntity: IUser): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await new User(userEntity).save()
                                                                       .then(result => {
                                                                           return {
                                                                               code: SIGNED_UP,
                                                                               message: "Successfully save entity in database",
                                                                               payload: result
                                                                           };
                                                                       })
                                                                       .catch(error => {
                                                                           return {
                                                                               code: DATABASE_ERROR,
                                                                               message: error,
                                                                               payload: null
                                                                           };
                                                                       });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }

    public async modifyPasswordByUserId(
        userId: string,
        password: string
    ): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.findOneAndUpdate(
                {
                    userId,
                    isDelete: false
                },
                { $set: { password } },
                { new: true }
            ).then(result => {
                return {
                    code: UPDATED_PASSWORD,
                    message: "Successfully updated password",
                    payload: result
                };
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }

    public async modifyNicknameByUserId(
        userId: string,
        nickname: string
    ): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.findOneAndUpdate(
                {
                    userId,
                    isDelete: false
                }, 
                { $set: { nickname } },
                { new: true }
            ).then(result => {
                return {
                    code: UPDATED_NICKNAME,
                    message: "Successfully updated nickname",
                    payload: result
                };
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }

    public async modifyLicenseByUserId(
        userId: string,
        license: ILicense
    ): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.findOneAndUpdate(
                {
                    userId,
                    isDelete: false
                },
                { 
                    $set: { 
                        license,
                        role: DRIVER 
                    }
                }
            ).then(result => {
                return {
                    code: UPDATED_LICENSE,
                    message: "Successfully update license",
                    payload: result
                };
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }

    public async deleteUser(
        userId: string,
        isDelete: boolean
    ): Promise<ResponseDto> {
        try {
            const responseDto: ResponseDto = await User.findOneAndUpdate(
                { userId },
                { $set: { isDelete } },
                { new: true }
            ).then(result => {
                return {
                    code: DELTED_USER,
                    message: "Successfully delete user!",
                    payload: result
                };
            }).catch(error => {
                return {
                    code: DATABASE_ERROR,
                    message: error.toString(),
                    payload: null
                };
            });

            return responseDto;
        } catch(error) {
            return {
                code: EXCEPTION_ERROR,
                message: error.toString(),
                payload: null
            };
        }
    }
}

const authRepository = new AuthRepository();

export { 
    authRepository,
    AuthRepository
};

1) findUserByEmail: 이메일을 매개로 유저 데이터를 찾습니다.

2) findUserByUserId: 유저 아이디를 매개로 유저 데이터를 찾습니다.

3) isExistEmail: email을 매개로 이메일이 존재하는지 확인합니다.

4) isExistNickname: nickname을 매개로 닉네임이 존재하는지 확인합니다.

5) createUser: registerUser 서비스에서 전달받은 userEntity를 데이터베이스에 저장합니다.

6) modifyPasswordByUserId: 조건문으로 분기되어 패스워드를 변경할 경우 해당 메서드에서 패스워드를 변경합니다.

7) modifyNicknameByUserId: 조건문으로 분기되어 닉네임을 수정할 경우 해당 메서드에서 닉네임을 변경합니다.

8) modifyLicenseByUserId: 조건문으로 분기되어 운전면허증을 수정할 경우 해당 메서드에서 운전면허를 수정합니다.

9) deleteUser: 실제 유저 데이터를 삭제하지 않고, 유저의 isDelete값을 true로 전환합니다.

레포지토리 클래스가 완성되었습니다.

마지막으로 서버를 구동시키키 위한 코드를 작성하도록 하겠습니다.

  • ./src/index.js
import { buildSubgraphSchema } from '@apollo/subgraph';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-express';
import express from 'express';
import http from 'http';
import cors from 'cors';
import { PORT } from './config/env.variable';
import mongoose from 'mongoose';
import { typeDefs } from './typeDefs';
import { resolvers } from './resolvers/resolver';

class Server {
    constructor() {}

    public async start(
        typeDefs,
        resolvers
    ) {
        const app: express.Application = express();
        const httpServer = http.createServer(app);
        const server: ApolloServer = new ApolloServer({
            schema: buildSubgraphSchema([{
                typeDefs,
                resolvers
            }]),
            context: ({ req }) => {
                const token: string | string[] | null = req.headers.token ? req.headers.token : null;
                
                return token;
            },
            plugins: [ApolloServerPluginDrainHttpServer({ httpServer })]
        });

        app.use(cors());

        await server.start();

        server.applyMiddleware({
            app,
            path: '/auth-service',
            cors: false
        });

        httpServer.listen({ port: PORT });

        mongoose;

        console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
    }
}

const server = async (
    typeDefs,
    resolvers
) => await new Server().start(
    typeDefs,
    resolvers
);

server(
    typeDefs,
    resolvers
);

Carpool 프로젝트는 apollo federation의 gateway, subgraph를 이용하는 아키텍쳐 구조를 가지고 있습니다. 따라서 서비스들은 subgraph로써 작동합니다. 그리고 모든 서버는 express를 기반으로 하는 apollo-server 인스턴스입니다.

여기까지 auth-service를 작성해보았고 다음 글에서 gateway를 만들어 subgraph와 연결한 후 테스트를 진행해보도록 하겠습니다.

0개의 댓글