TIL

Node.js 백오피스 팀 프로젝트 (4)

황민도 2024. 6. 24. 05:09

내일배움캠프 스파르타 코딩클럽

 

< routers. index.js >

import express from 'express';
import { authRouter } from './auth.router.js';
import { bookingRouter } from './bookings.router.js';
import { bookmarkRouter } from './bookmarks.router.js';
import { petsitterRouter } from './petsitters.router.js';
import { reviewRouter } from './reviews.router.js';
import { usersRouter } from './users.router.js';

const apiRouter = express.Router();

apiRouter.use('/auth', authRouter);
apiRouter.use('/users', usersRouter);
apiRouter.use('/petsitters', petsitterRouter);
apiRouter.use('/bookings', bookingRouter);
apiRouter.use('/bookmarks', bookmarkRouter);
apiRouter.use('/reviews', reviewRouter);

export { apiRouter };

 

< routers. auth.router.js >

import express from 'express';
import { prisma } from '../utils/prisma.utils.js';
import { UsersRepository } from '../repositories/users.repository.js';
import { PetsitterRepository } from '../repositories/petsitters.repository.js';
import { AuthService } from '../services/auth.service.js';
import { AuthController } from '../controllers/auth.controller.js';
import { signUpValidator } from '../middlewares/validators/sign-up-validator.middleware.js';
import { signInValidator } from '../middlewares/validators/sign-in-validator.middleware.js';
import { requireRefreshToken } from '../middlewares/require-refresh-token.middleware.js';

const authRouter = express.Router();

const usersRepository = new UsersRepository(prisma);
const petsitterRepository = new PetsitterRepository(prisma);
const authService = new AuthService(usersRepository, petsitterRepository);
const authController = new AuthController(authService);

// 회원가입
authRouter.post('/sign-up', signUpValidator, authController.signUp);

// 로그인
authRouter.post('/sign-in', signInValidator, authController.signIn);


//펫시터 회원가입
authRouter.post(
  '/petsitters/sign-up',
  signUpValidator,
  authController.signUpPetsitter
);

//펫시터 로그인
authRouter.post(
  '/petsitters/sign-in',
  signInValidator,
  authController.signInPetsitter
);


// 로그아웃
authRouter.post('/sign-out', requireRefreshToken, authController.signOut);

// 토큰 재발급
authRouter.post('/renew-tokens', requireRefreshToken, authController.renewTokens);

export { authRouter };

 

< controllers. auth.controller.js >

import { MESSAGES } from '../constants/message.constant.js';
import { HTTP_STATUS } from '../constants/http-status.constant.js';

export class AuthController {
  constructor(authService) {
    this.authService = authService;
  }

  // 회원가입
  signUp = async (req, res, next) => {
    try {
      const { email, password, name, introduce, profileImage } = req.body;

      const data = await this.authService.signUp({
        email,
        password,
        name,
        introduce,
        profileImage,
      });

      return res.status(HTTP_STATUS.CREATED).json({
        status: HTTP_STATUS.CREATED,
        message: MESSAGES.AUTH.SIGN_UP.SUCCEED,
        data,
      });
    } catch (error) {
      next(error);
    }
  };

  // 로그인
  signIn = async (req, res, next) => {
    try {
      const { email, password } = req.body;

      const user = await this.authService.signIn({
        email,
        password,
      });

      const { accessToken, refreshToken } =
        await this.authService.createAccessAndRefreshToken({
          id: user.id,
          role: 'user',
        });

      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.AUTH.SIGN_IN.SUCCEED,
        accessToken,
        refreshToken,
      });
    } catch (error) {
      next(error);
    }
  };

 
  //펫시터 회원가입
  signUpPetsitter = async (req, res, next) => {
    try {
      const { email, password, name, experience, introduce, profileImage } =
        req.body;

      const data = await this.authService.createPetsitter({
        email,
        password,
        name,
        experience: experience ? Number(experience) : undefined,
        introduce,
        profileImage,
      });

      res
        .status(HTTP_STATUS.CREATED)
        .json({ message: '펫시터로 회원가입 했습니다.', data: data.id });
    } catch (error) {
      next(error);
    }
  };

  //펫시터 로그인
  signInPetsitter = async (req, res, next) => {
    try {
      const { email, password } = req.body;

      const petsitter = await this.authService.signInPetsitter({
        email,
        password,
      });

      const { accessToken, refreshToken } =
        await this.authService.createAccessAndRefreshToken({
          id: petsitter.id,
          role: 'petsitter',
        });

      res
        .status(HTTP_STATUS.OK)
        .json({ result: true, accessToken, refreshToken });
      return;
    } catch (error) {
      next(error);
    }
  };

  // 로그아웃
  signOut = async (req, res, next) => {
    try {
      const user = req.user;

      await this.authService.signOut({
        id: user.id,
        hashedRefreshToken: 'nodata',
      });

      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.AUTH.SIGN_OUT.SUCCEED,
        data: { id: user.id },
      });
    } catch (error) {
      next(error);
    }
  };

  // 토큰 재발급
  renewTokens = async (req, res, next) => {
    try {
      const user = req.user;
      const id = user.id;
      const role = user.role;

      console.log(id, role)

      const { accessToken, refreshToken } =
        await this.authService.createAccessAndRefreshToken({
          id,
          role,
        });

      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.AUTH.SIGN_IN.TOKEN,
        accessToken,
        refreshToken,
      });
    } catch (error) {
      next(error);
    }
  };
}

 

< services. auth.service.js >

import { MESSAGES } from '../constants/message.constant.js';
import { HttpError } from '../errors/http.error.js';
import { HASH_SALT_ROUNDS } from '../constants/auth.constant.js';
import bcrypt from 'bcrypt';
import { createAccessToken, createRefreshToken } from '../utils/tokens.js';

export class AuthService {
  constructor(usersRepository, petsitterRepository) {
    this.usersRepository = usersRepository;
    this.petsitterRepository = petsitterRepository;
  }

  // 회원가입
  signUp = async ({ email, password, name, introduce, profileImage }) => {
    const findOneEmail = await this.usersRepository.findOneEmail(email);

    if (findOneEmail) {
      throw new HttpError.Conflict(MESSAGES.AUTH.COMMON.EMAIL.DUPLICATED);
    }
    const hashedPassword = bcrypt.hashSync(password, HASH_SALT_ROUNDS);
    const { password: _pw, ...data } = await this.usersRepository.createUsers({
      email,
      hashedPassword,
      name,
      introduce,
      profileImage,
    });

    return data;
  };

  // 로그인
  signIn = async ({ email, password }) => {
    const user = await this.usersRepository.findOneEmail(email);
    const passwordRef = user && bcrypt.compareSync(password, user.password);

    if (!passwordRef) {
      throw new HttpError.Unauthorized(MESSAGES.AUTH.SIGN_IN.UNAUTHORIZED);
    }

    return user;
  };

  // 펫시터 회원가입
  createPetsitter = async ({
    email,
    password,
    name,
    experience,
    introduce,
    profileImage,
  }) => {
    const existingPetsitter =
      await this.petsitterRepository.findPetsitterByEmail({
        email,
      });

    if (existingPetsitter) {
      throw new HttpError.Conflict('이미 가입된 이메일입니다.');
    }

    const hashedPassword = await bcrypt.hash(password, HASH_SALT_ROUNDS);

    const user = await this.petsitterRepository.createPetsitter({
      email,
      password: hashedPassword,
      name,
      experience,
      introduce,
      profileImage,
    });

    return user;
  };

  // 펫시터 로그인
  signInPetsitter = async ({ email, password }) => {
    const petsitter = await this.petsitterRepository.findPetsitterByEmail({
      email,
    });

    const decodedPassword = petsitter
      ? await bcrypt.compare(password, petsitter.password)
      : null;

    if (!petsitter || !decodedPassword) {
      throw new HttpError.Unauthorized('인증 정보가 유효하지 않습니다.');
    }

    return petsitter;
  };

  // 로그아웃
  signOut = async ({ id, hashedRefreshToken }) => {
    await this.usersRepository.refreshTokenUpdate({ id, hashedRefreshToken });
  };

  //액세스토큰, 리프레시 토큰 발급
  createAccessAndRefreshToken = async ({ id, role }) => {
    const accessToken = createAccessToken({ id, role });
    const refreshToken = createRefreshToken({ id, role });

    if (role === 'user') {
      const hashedRefreshToken = bcrypt.hashSync(
        refreshToken,
        HASH_SALT_ROUNDS
      );
      await this.usersRepository.refreshTokenUpdate({ id, hashedRefreshToken });
    }

    return { accessToken, refreshToken };
  };
}

 

< repositories. users.repository.js >

export class UsersRepository {
  constructor(prisma) {
    this.prisma = prisma;
  }

  createUsers = async ({
    email,
    hashedPassword,
    name,
    introduce,
    profileImage,
  }) => {
    const user = await this.prisma.user.create({
      data: {
        email,
        password: hashedPassword,
        name,
        introduce,
        profileImage,
      },
    });

    await this.prisma.refreshToken.create({
      data: {
        refreshToken: null,
        user: {
          connect: { id: user.id },
        },
      },
    });

    return user;
  };

  findOneEmail = async (email) => {
    const data = await this.prisma.user.findUnique({
      where: { email },
    });

    return data;
  };

  findOneId = async (id) => {
    const data = await this.prisma.user.findUnique({
      where: { id },
    });
    const result = {
      id: data.id,
      email: data.email,
      name: data.name,
      introduce: data.introduce,
      profileImage: data.profileImage,
      createdAt: data.createdAt,
      updatedAt: data.updatedAt,
    };

    return result;
  };

  findOneRefreshTokenId = async (id) => {
    const data = await this.prisma.refreshToken.findUnique({
      where: { userId: id },
    });
    return data;
  };

  getProfile = async (id) => {
    const data = await this.prisma.user.findUnique({
      where: { id },
      select: {
        id: true,
        email: true,
        name: true,
        introduce: true,
        profileImage: true,
        createdAt: true,
        updatedAt: true,
      },
    });
    return data;
  };
  // 비밀번호를 포함한 사용자 정보 가져오기
  getProfileWithPassword = async (userId) => {
    const userData = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        email: true,
        name: true,
        introduce: true,
        profileImage: true,
        password: true,
        createdAt: true,
        updatedAt: true,
      },
    });
    return userData;
  };

  updateUser = async (userId, newPassword, name, introduce, profileImage) => {
    return await this.prisma.user.update({
      where: { id: userId },
      data: { password: newPassword, name, introduce, profileImage },
    });
  };
  refreshTokenUpdate = async ({ id, hashedRefreshToken }) => {
    if (hashedRefreshToken === 'nodata') {
      await this.prisma.refreshToken.update({
        where: {
          userId: id,
        },
        data: {
          refreshToken: null,
        },
      });
    } else {
      await this.prisma.refreshToken.update({
        where: {
          userId: id,
        },
        data: {
          refreshToken: hashedRefreshToken,
        },
      });
    }
  };
}

 

< routers. users.router.js >

import express from 'express';
import UsersController from '../controllers/users.controllers.js';
import UsersService from '../services/users.service.js';
import { UsersRepository } from '../repositories/users.repository.js';
import { updateProfileValidator } from '../middlewares/validators/update-profile-validator.middleware.js';
import { requireAccessToken } from '../middlewares/require-access-token.middleware.js';
import { prisma } from '../utils/prisma.utils.js';
import { upload } from '../middlewares/upload-image.js';
import { requireRoles } from '../middlewares/require-roles.middleware.js';

const usersRouter = express.Router();
const usersRepository = new UsersRepository(prisma);
const usersService = new UsersService(usersRepository);
const usersController = new UsersController(usersService);

usersRouter.get(
  '/mypage',
  requireAccessToken,
  requireRoles(['user']),
  usersController.readMe
);
usersRouter.patch(
  '/mypage',
  requireAccessToken,
  requireRoles(['user']),
  upload,
  updateProfileValidator,
  usersController.updateUsers
);

export { usersRouter };

 

< controllers. users.controllers.js >

import { HTTP_STATUS } from '../constants/http-status.constant.js';
// User의 컨트롤러(Controller)역할을 하는 클래스
export default class UsersController {
  constructor(usersService) {
    this.usersService = usersService;
  }
  // 프로필 조회
  readMe = async (req, res, next) => {
    try {
      const userId = req.user.id;
      const userInfo = await this.usersService.getUserInfoByUserId(userId);
      res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: '해당 유저를 성공적으로 찾아 왔습니다.',
        data: userInfo,
      });
    } catch (err) {
      next(err);
    }
  };
  //프로필 업데이트
  updateUsers = async (req, res, next) => {
    try {
      const userId = req.user.id;
      const {
        password,
        newPassword,
        newPasswordConfirm,
        name,
        introduce,
        profileImage,
      } = req.body;

      // 사용자 정보 업데이트 서비스 호출
      const updatedUser = await this.usersService.updateUser(
        userId,
        password,
        newPassword,
        newPasswordConfirm,
        name,
        introduce,
        profileImage
      );

      // 프로필 이미지 처리
      if (req.file) {
        userData.profileImage = req.file.path;
      }
      res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: '사용자 정보를 성공적으로 업데이트했습니다.',
        data: updatedUser,
      });
    } catch (err) {
      next(err);
    }
  };
}

 

< services. users.service.js >

import bcrypt from 'bcrypt';
import { HttpError } from '../errors/http.error.js';
import { HASH_SALT_ROUNDS } from '../constants/auth.constant.js';
export default class UsersService {
  constructor(usersRepository) {
    this.usersRepository = usersRepository;
  }

  getUserInfoByUserId = async (userId) => {
    const userInfo = await this.usersRepository.getProfile(userId);
    if (!userInfo) {
      throw new HttpError.NotFound('사용자를 찾을 수 없습니다.');
    }
    return userInfo;
  };

  updateUser = async (
    userId,
    password,
    newPassword,
    newPasswordConfirm,
    name,
    introduce,
    profileImage
  ) => {
    const existedUser =
      await this.usersRepository.getProfileWithPassword(userId);
    if (!existedUser) {
      throw new HttpError.NotFound('사용자를 찾을 수 없습니다.');
    }
    // 비밀번호 확인 및 해싱
    if (password) {
      console.log(password);
      console.log(existedUser.password);
      console.log(password === existedUser.password);
      const dbPasswordMatch = await bcrypt.compare(
        password,
        existedUser.password
      );
      if (!dbPasswordMatch) {
        throw new HttpError.BadRequest('현재 비밀번호가 일치하지 않습니다.');
      }
      if (newPassword !== newPasswordConfirm) {
        throw new HttpError.BadRequest(
          '새 비밀번호와 비밀번호 확인이 일치하지 않습니다.'
        );
      }
    }
    const hashedPassword = await bcrypt.hash(newPassword, HASH_SALT_ROUNDS);

    const updatedUser = await this.usersRepository.updateUser(
      userId,
      hashedPassword,
      name,
      introduce,
      profileImage
    );
    return {
      id: updatedUser.id,
      email: updatedUser.email,
      name: updatedUser.name,
      introduce: updatedUser.introduce,
      profileImage: updatedUser.profileImage,
      createdAt: updatedUser.createdAt,
      updatedAt: updatedUser.updatedAt,
    };
  };
}