30강. 블로그 앱 만들기 - 관리자 로그인

한시현·2024년 4월 13일

UDR 백엔드 야생형

목록 보기
14/15

Section 4.

30강. 블로그 앱 만들기 - 관리자 로그인

관리자 등록하기

관리자로 로그인하는 방법을 알아보겠다. 관리자 정보를 DB에 저장하고 그것을 처리하는 방법도 함께 공부해 보자.

관리자 정보를 등록하기 위해선 등록 폼이 필요하다. 등록을 위한 폼을 별도의 ejs 파일로 만들어도 되고, 임시로 만들었다가 삭제할 수도 있다. 후자는 사용자를 한 명만 등록할 것이기 때문에 가능한 것이고, 여러 사용자를 등록할 생각이라면 전자로 진행하는 것이 좋다.
우리는 후자의 방법을 사용해 볼 것이다.

index.ejs

<h3>로그인</h3>
<form action="/admin" method="POST">
    <label for="username"><b>사용자 이름</b></label>
    <input type="text" name="username" id="username">

    <label for="password"><b>비밀번호</b></label>
    <input type="password" name="password" id="password">

    <input type="submit" value="로그인" class="btn">
</form>

<h3>관리자 등록</h3>
<form action="/register" method="POST">
    <label for="username"><b>사용자 이름</b></label>
    <input type="text" name="username" id="username">

    <label for="password"><b>비밀번호</b></label>
    <input type="password" name="password" id="password">

    <input type="submit" value="등록" class="btn">
</form>

이렇게 index.ejs의 코드를 복사해서 아래에 붙여넣고 조금의 수정만 하자.

실행 결과

이렇게 관리자 등록이 나온다. 아래 부분을 잠깐 이용하고 삭제 할 것이다.

register 경로로 요청이 들어왔을때 이를 처리하는 라우트 코드를 작성하자.
등록 폼을 보여주는 get 요청과 id, pw 입력 후 db에 저장하는 post 요청을 처리해 줘야 한다.

admin.js

const asyncHandler = require("express-async-handler"); // 비동기 처리 위해서 모듈 불러옴

...

// View Register Form
// GET /register
router.get("/register", asyncHandler(async(req,res)=>{
    res.render("admin/index", {layout: adminLayout2});
}))

이렇게 index 파일을 화면에 표시해 줄 것이다.

register 경로로 post 요청이 들어오면 db에 저장을 할 것이다. 그 저장이 끝난다면 브라우저 창에 성공했다고 표시해주겠다.

// Register Administrator
// POST /register
router.post("/register", asyncHandler(async(req,res)=>{
    res.send("Register");
}))

이제 post 요청이 들어왔을 때 id와 pw값을 받아서 db에 저장해줘야 한다.
우리는 관리자 혹은 사용자를 등록하게 하려고 하고 있으므로 새로운 모델이 필요하다.
user.js 파일을 models 폴더 안에 만들자.

user.js

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema( {
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true
    }
});

module.exports = mongoose.model("User", userSchema);

db가 만들어졌으니 이제는 본격적으로 post 요청을 했을 때 id, pw를 받아서 db에 저장하는 코드를 작성해 보도록 하겠다.

admin.js

const User = require("../models/User");

우선 User 모델을 가져온다.
인터넷을 통해 id, pw를 작성해서 서버로 넘기게 되는데, pw를 암호화 시켜서 넘겨줘야 한다. 즉, bcrypt 모듈을 설치해야 한다.

모듈 설치 : bcrypt

npm i bcrypt

설치 되었다면 여기다가 가지고 오자.

const bcrypt = require("bcrypt");

우리는 브라우저 창을 통해 사용자의 이름, pw를 받게 되는데 이는 요청 본문(req.body)에 담기게 된다. 이 요청 본문에 담긴 값을 프로그램에 사용할 수 있도록 파싱해주는 미들웨어를 추가해 줘야 한다.

app.js

app.use(express.json());
app.use(express.urlencoded({extended: true}));

이제 다시 admin.js에 db에 가지고 오는 코드를 작성하자.

admin.js 수정

// Register Administrator
// POST /register
router.post("/register", asyncHandler(async(req,res)=>{
    const hashedPassword = await bcrypt.hash(req.body.password,10);
    const user = await User.create({
        username: req.body.username,
        password: hashedPassword
    });
    res.json(`user created: ${user}`); // 이 코드는 사용자 정보를 db에 저장한 후엔 필요가 없으므로 이후 주석 처리 혹은 삭제하자.
}))

User 라는 db에 create 함수를 이용해서 새 사용자를 추가할 것이다.
이 때 사용자 이름은 req.body의 이름을 사용하면 되고, pw는 hash된 pw를 사용할 것이다.
저장된 후에는 잘 되었는지 확인하기 위해 json 형태로 브라우저에 표시할 것이다.

실행 결과

이렇게 사용자 이름과 pw를 입력하고 등록을 누르면

제대로 db에 등록된 것을 확인할 수 있다.

몽고 db에서 우리가 입력한 사용자 정보가 저장된 것을 볼 수도 있다.
여기까지 한다면 관리자 등록이 끝난 것이다.

이제 index.js 부분에서 관리자 등록 부분이 더 이상 필요하지 않다. 지워버리자.

index.js 삭제

<h3>관리자 등록</h3>
<form action="/register" method="POST">
    <label for="username"><b>사용자 이름</b></label>
    <input type="text" name="username" id="username">

    <label for="password"><b>비밀번호</b></label>
    <input type="password" name="password" id="password">

    <input type="submit" value="등록" class="btn">
</form>

이 부분을 삭제하자.

로그인 처리하기

이제 관리자 id와 pw를 이용해 로그인을 처리해 보도록 하자.
이를 처리하기 위해 json web token을 사용할 것이다. cookie-parser 모듈과 jsonwebtoken 모듈이 필요하다.

모듈 설치 : cookie-parser, jsonwebtoken

npm i cookie-parser jsonwebtoken

이제 설치한 cookie-parser를 app.js에 미들웨어로 등록해 줘야 한다.

const cookieParser = require("cookie-parser"); // 가져오기

app.use(cookieParser); // 미들웨어로 등록

이렇게 하면 애플리케이션 안에서 cookie-parser를 이용할 수 있게 된다.

JWT에서는 클라이언트가 서버로 보내는 토큰을 보고 내가 발행한 토큰인지를 확인하기 위해 외부로 드러나면 안되는 비밀키를 사용한다. 비밀키를 env 파일에 넣어두고 그 값을 코드에서 불러 사용하도록 지정하자. 비밀키 값은 어떤것을 사용해도 상관없다.

env

JWT_SECRET = mycode

이를 라우트 코드에서 사용할 것이다.

admin.js

const jwt = require("jsonwebtoken")
const jwtSecret = process.env.JWT_SECRET;

이제 로그인을 했을 때 post 방식으로 서버로 넘어가는 폼을 위해 post 방식을 처리해주는 라우트 코드를 작성해 주면 된다.

// Check Login
// POST /admin
router.post("/admin", asyncHandler(async(req, res)=>{
    const {username, password} = req.body;
    const user = await User.findOne({username})
    if (!user){
        return res.status(401).json({message: "일치하는 사용자가 없습니다."});
    }

    const isValidPassword = await bcrypt.compare(password, user.password);

    if (!isValidPassword){
        return res.status(401).json({message: "비밀번호가 일치하기 않습니다."});
    }

    const token = jwt.sign({id: user._id}, jwtSecret);
    res.cookie("token", token, {httpOnly: true});
    res.redirect("/allPosts");
}))

allPost 는 아직 작성하지 않은 ejs 파일인데, 관리자로 로그인이 되었으니 모든 게시물을 보여줄 수 있게끔 한 것이다.

이렇게 로그인을 처리하는 코드를 완성했다. 이제 잘 동작하는지 확인해보자.

실행 결과

로그인 페이지에서 관리자의 id와 pw를 입력하고 로그인을 누르면

이렇게 나온다. 현재 allPosts가 없기 때문이다. 이렇게 나온다면 라우트 코드가 제대로 처리됐단 뜻이고, 토큰이 만들어졌다는 뜻이다. 확인해보자.

이렇게 우리가 만든 토큰이 들어가 있는 것을 확인할 수 있다. 로그인이 잘 처리된 것이다.

전체 게시물 표시하기

이제 db의 게시물들을 불러와서 수정하거나 삭제할 수 있도록 보여주는 allPosts를 만들자.
라우트 코드에서 allPosts 라는 경로를 처리하도록 하자.

admin.js

const Post = require("../models/Post");

Post 라는 모델을 가지고 오고,

// Get all Posts
// GET /allPosts
router.get("/allPosts", asyncHandler(async(req,res)=>{
    const locals = {
        title: "Posts"
    }
    const data = await Post.find();
    res.render("admin/allPosts", {locals, data, layout: adminLayout})
}))

이제 ejs 파일을 만들어야 한다. admin 폴더 안에 allPosts.ejs 파일을 만들자.
이 파일은 전체 게시물을 한 눈에 볼 수 있게 해주는 파일이고, 새 게시물이라는 버튼과 관리자가 게시물을 수정, 삭제할 수 있도록 하는 버튼을 넣어줄 것이다.
두 가지 레이아웃 링크 중 로그아웃 링크가 있는 레이아웃을 사용해 줄 것이다.

allPosts.ejs

<div class = "admin-title">
    <h2><%= locals.title %></h2>
    <a href="#" class = "button">+ 새 게시물</a>
</div>

<ul class = "admin-posts">
    <% data.forEach(post =>{ %>
        <li>
            <a herf="/post/<%= post._id %>">
                <%= post.title %>
            </a>
        </li>
    <% }) %>
</ul>

data 변수는 아까 라우트 코드에서 전체 게시물을 담아서 보내준 변수이다.
게시물이 여러 개 들어가 있기 때문에 for-each 문으로 순회를 해 줄 것이다.
그 안의 각각의 게시물들은 post라는 이름으로 받았다.

그런데 여기서 우리는 게시물이 오른쪽에 수정, 삭제할 수 있도록 하는 버튼을 넣어줄 것이다.
이는 각 게시물마다 계속 반복해야 하기 때문에 for-each문 안에 들어가야 한다.

<ul class = "admin-posts">
    <% data.forEach(post =>{ %>
        <li>
            <a herf="/post/<%= post._id %>">
                <%= post.title %>
            </a>
            <div class="admin-post-controls">
                <a href="#" class="btn">편집</a>
                <form action="#">
                    <input type="submit" value="삭제" class="btn-delete btn">
                </form>
            </div>
        </li>
    <% }) %>
</ul>

편집(a 태그)와 삭제(form)의 차이는, a 태그의 경우는 링크이기 때문에 이 부분을 클릭했을 때 get 요청 방식이 발생한다. 삭제의 경우, 버튼을 클릭하는 순간 삭제할 화면을 보여주지 않고 즉시 delete 요청을 보내고 싶기 때문에 form을 사용한 것이다.

이제 화면에 제대로 보여주는지 확인해보자.

실행 결과

예상대로 잘 되었다.

이제 남은 것은 관리자 레이아웃에서 [관리자 로그아웃]을 눌렀을 때 로그아웃을 시켜주는 것이다.
로그아웃을 시켜 준다는 것쿠키에 있는 토큰을 삭제해 주면 된다는 뜻이다.
로그아웃 후엔 일반 사용자가 보는 화면으로 리다이렉트 시켜주면 된다.
이러한 동작을 하도록 코드를 수정해보겠다.

admin.ejs 수정

      <!-- 관리자 로그아웃 -->
      <div class="header-button">
        <a href="/logout">관리자 로그아웃</a>
      </div>
    </header>

[관리자 로그아웃]을 눌렀을 때 logout 경로로 요청을 보내 주라는 뜻이다.

이제 logout 경로에 대해 라우트 코드를 추가해주자.

admin.js

// Admin logout
// GET /logout
router.get("/logout", (req,res)=>{
    res.clearCookie("token");
    res.redirect("/");
})

logout 경로로 요청이 들어왔을 때 쿠키에 있던 token이란 이름을 가진 정보를 삭제 할 것이고, /로 바로 리다이렉트 시킬 것이다.

실행 결과

관리자 로그아웃을 누르면

이렇게 로그아웃 되면서 첫 화면으로 이동한다.

[관리자 로그인] 버튼을 눌러서 로그인 화면으로 이동하도록 링크도 수정해 주자.

main.ejs 수정

      <!-- 관리자 로그인 -->
      <div class="header-button">
        <a href="/admin">관리자 로그인</a>
      </div>
    </header>

실행 결과

[관리자 로그인] 을 누르면

해당 화면으로 잘 이동한다.

로그인도 잘 된다.

문제 발생

localhost에 접속되지 않고 이렇게 무한 로딩되는 상황이 발생했다.

  • 방안 1) 포트를 리스닝하고 있을 수 있다. kill-port 포트번호 를 사용해보자. -> 실패
  • chatGPT 도움) app.use(cookieParser()); 부분에서 반드시 안에 ()를 넣어야 한다.

0개의 댓글