Nodejs: Signin, Login, User Information

2023. 11. 22. 00:23Javascript Node.js

Signin and Login code 

// ./routers/auth.router.js

import { Router } from 'express';
import db from '../models/index.cjs';
import bcrypt from 'bcrypt';
import {
  PASSWORD_HASH_SALT_ROUNDS,
  JWT_ACCESS_TOKEN_SECRET,
  JWT_ACCESS_TOKEN_EXPIRES_IN,
} from '../constants/security.constant.js';
import jwt from 'jsonwebtoken';
const { Users } = db;

const authRouter = Router();

// 회원가입 /api/auth/siginup
authRouter.post('/signup', async (req, res) => {
  try {
    const { email, password, passwordConfirm, name } = req.body;

    if (!email) {
      return res.status(400).json({
        success: false,
        message: '이메일 형식이 맞지 않습니다.',
      });
    }
    if (!password) {
      return res.status(400).json({
        success: false,
        message: '비밀번호 입력이 필요합니다.',
      });
    }
    if (!passwordConfirm) {
      return res.status(400).json({
        success: false,
        message: '비밀번호 확인 입력이 필요합니다.',
      });
    }
    if (!name) {
      return res.status(400).json({
        success: false,
        message: '이름입력이 필요합니다.',
      });
    }
    if (password !== passwordConfirm) {
      return res.status(400).json({
        success: false,
        message: '입력한 비밀번호가 서로 일치하지 않습니다.',
      });
    }

    if (password.length < 6) {
      return res.status(400).json({
        success: false,
        message: '비밀번호는 최소 6자리 이상입니다.',
      });
    }

    let emailValidationRegex = new RegExp('[a-z0-9._]+@[a-z]+.[a-z]{2,3}');

    const isValidEmail = emailValidationRegex.test(email);
    if (!isValidEmail) {
      return res.status(400).json({
        success: false,
        message: '올바른 이메일 형식이 아닙니다.',
      });
    }

    const existedUser = await Users.findOne({ where: { email } });

    if (existedUser) {
      return res.status(400).json({
        success: false,
        message: '이미 가입된 이메일 입니다.',
      });
    }
    const hashedPassword = bcrypt.hashSync(password, PASSWORD_HASH_SALT_ROUNDS);

    const newUser = (
      await Users.create({ email, password: hashedPassword, name })
    ).toJSON();
    delete newUser.password;

    return res.status(201).json({
      success: true,
      message: '회원가입에 성공했습니다.',
      data: newUser,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      success: true,
      message: '예상치 못한 에러가 발생하였습니다. 관리자에게 문의하세요',
    });
  }
});

// 로그인
authRouter.post('/signin', async (req, res) => {
  try {
    const { email, password } = req.body;

    if (!email) {
      return res.status(400).json({
        success: false,
        message: '이메일 입력이 필요합니다.',
      });
    }
    if (!password) {
      return res.status(400).json({
        success: false,
        message: '비밀번호 입력이 필요합니다.',
      });
    }

    const user = (await Users.findOne({ where: { email } }))?.toJSON();
    const hashedPassword = user?.password;
    // isPsswordMatched 의 bcrypt.compareSync(password, hashedPassword); 값이 같으면 true를 반환
    // 반대로 반대 값이 나오면 false 즉 같지 않다는 의미
    const isPasswordMatched = bcrypt.compareSync(password, hashedPassword);

    const isCorrectUser = user && isPasswordMatched;

    if (!isCorrectUser) {
      return res.status(401).json({
        success: false,
        message: '일치하는 인증 정보가 없습니다.',
      });
    }
    // accessToken 발급
    // 회원가입시 사용했던 id를 userid이름으로 변경
    // jwt.sign()메서드는 별도의 promise를 반환하지 않고 바로 string을 반환하기 때문에 await을 붙일 필요가 없다.
    const accessToken = jwt.sign(
      { /* payload값 */ userId: user.id },
      /* secret key */ JWT_ACCESS_TOKEN_SECRET,
      { /* options-시간설정 */ expiresIn: JWT_ACCESS_TOKEN_EXPIRES_IN },
    );

    return res.status(200).json({
      success: true,
      message: '로그인에 성공했습니다.',
      data: { accessToken },
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      success: true,
      message: '예상치 못한 에러가 발생하였습니다. 관리자에게 문의하세요',
    });
  }
});

export { authRouter };

 

User Infromation code

// ./middleware/need-signin.middleware.js

import jwt from 'jsonwebtoken';
import { JWT_ACCESS_TOKEN_SECRET } from '../constants/security.constant.js';
import db from '../models/index.cjs';
const { Users } = db;

export const needSignIn = async (req, res, next) => {
  try {
    const authorizationHeader = req.headers.authorization;
    // 인증 정보가 아예 없는 경우
    if (!authorizationHeader) {
      return res.status(400).json({
        success: true,
        message: '인증정보가 없습니다.',
      });
    }

    // JWT 기본적인 형태 -> Authorization: Bearer <token>
    const [tokenType, accessToken] = authorizationHeader?.split(' ');
    // 토큰형식이 일치하지 않는 경우
    if (tokenType !== 'Bearer') {
      return res.status(400).json({
        success: true,
        message: '지원하지 않는 인증 방식입니다.',
      });
    }

    // AccessToken이 존재하지 않는 경우
    if (!accessToken) {
      return res.status(400).json({
        success: true,
        message: 'AccessToken이 없습니다.',
      });
    }

    const decodedPayload = jwt.verify(
      accessToken,
      JWT_ACCESS_TOKEN_SECRET + '1',
    );
    const { userId } = decodedPayload;
    console.log({ decodedPayload });

    // 일치하는 userId가 없는 경우
    const user = (await Users.findByPk(userId)).toJSON();

    if (!user) {
      return res.status(400).json({
        success: true,
        message: '존재하지 않는 사용자 입니다.',
      });
    }

    delete user.password;
    res.locals.user = user;

    next();
  } catch (error) {
    console.error(error);

    let statusCode = 500;
    let errorMessage = '';

    // switch case 로 분기처리
    switch (error.message) {
      // JWT 유효기간이 지난 경우
      case 'jwt expired':
        statusCode = 401;
        errorMessage = '인증 정보 유효기간이 지났습니다.';
        break;
      // 검증에 실패한 경우
      case 'invalid signature':
        statusCode = 401;
        errorMessage = '유효하지 않는 인증정보입니다.';
        break;
      default:
        statusCode = 500;
        errorMessage =
          '예상치 못한 에러가 발생하였습니다. 관리자에게 문의하세요';
        break;
    }

    return res.status(statusCode).json({
      success: true,
      message: errorMessage,
    });
  }
};

 

Associated modules

// ./models/index.cjs

// Sequelize ORM 구조
// sequelize는 ORM(Object-Relational Mapping)로 분류
// ORM이란 객체와 관계형 데이터베이스의 관계를 매핑 해주는 도구이다.
'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
// config 파일을 .cjs로 변경한 점이 특이점이다. 이유는 현재 프로젝트는 model type으로 진행
const config = require(__dirname + '/../config/config.cjs')[env];
const db = {};

let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(
    config.database,
    config.username,
    config.password,
    config,
  );
}

fs.readdirSync(__dirname)
  .filter((file) => {
    return (
      file.indexOf('.') !== 0 &&
      file !== basename &&
      file.slice(-4) === '.cjs' &&
      file.indexOf('.test.js') === -1
    );
  })
  .forEach((file) => {
    const model = require(path.join(__dirname, file))(
      sequelize,
      Sequelize.DataTypes,
    );
    db[model.name] = model;
  });

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;
// ./constants/security.constant.js

import 'dotenv/config';

// 환경변수의 값을 불러올 때에는 기본적으로 String으로 불러오게 된다
// 따라서 강제적으로 항변환을 해줘야한다.
export const PASSWORD_HASH_SALT_ROUNDS = Number.parseInt(
  process.env.PASSWORD_HASH_SALT_ROUNDS,
  10,
);

export const JWT_ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_TOKEN_SECRET;
export const JWT_ACCESS_TOKEN_EXPIRES_IN = '12h';

 

Additional webpage information

https://velog.io/@from_numpy/NestJS-How-to-implement-Refresh-Token-with-JWT

 

[NestJS] How to implement Refresh-Token using JWT?

오랜만에 작성하는 포스팅인 것 같다. 최근에 개인적인 일도 있고, 뭔가 쉬어가고 싶어서 천천히 공부를 하며 어떤 주제를 다뤄보며 좋을까 고민을 했었다. 그러던 와중, 예전부터 들어만보았지

velog.io

https://velog.io/@wlduq0150/GIT-github-branch%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%98%91%EC%97%85%ED%95%98%EA%B8%B0

 

[GIT] github branch를 활용해서 협업하기

과제에 대한 피드백에서 repo에 push할때 기능별로 파트를 분담해서 브랜치를 생성하고 코드를 관리하라는 내용을 받았다.사실 팀프로젝트 경험이 거의 없고, 브랜치가 무엇인지 잘 몰랐기 때문

velog.io

https://sequelize.org/docs/v6/core-concepts/model-querying-finders/

 

Model Querying - Finders | Sequelize

Finder methods are the ones that generate SELECT queries.

sequelize.org

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

https://developer.mozilla.org/ko/docs/Web/HTTP/Status

 

HTTP 상태 코드 - HTTP | MDN

HTTP 응답 상태 코드는 특정 HTTP 요청이 성공적으로 완료되었는지 알려줍니다. 응답은 5개의 그룹으로 나누어집니다: 정보를 제공하는 응답, 성공적인 응답, 리다이렉트, 클라이언트 에러, 그리고

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Optional_chaining

 

Optional chaining - JavaScript | MDN

optional chaining 연산자 (?.) 는 체인의 각 참조가 유효한지 명시적으로 검증하지 않고, 연결된 객체 체인 내에 깊숙이 위치한 속성 값을 읽을 수 있다.

developer.mozilla.org

https://www.avast.com/random-password-generator#pc

 

Random Password Generator | Create Strong Passwords | Avast

You are putting yourself at risk if you are entrusting an unknown online random sequence generator tool for your passwords. If you use a free password generator online, the site might be decrypted or presenting compromised information, meaning hackers coul

www.avast.com

https://www.npmjs.com/package/jsonwebtoken

 

jsonwebtoken

JSON Web Token implementation (symmetric and asymmetric). Latest version: 9.0.2, last published: 3 months ago. Start using jsonwebtoken in your project by running `npm i jsonwebtoken`. There are 25694 other projects in the npm registry using jsonwebtoken.

www.npmjs.com