Undergrad Research

[Backend]유저 모델 관리하기

Jay_J 2024. 6. 29. 00:47

정말 아무것도 없는 백지 상태에서 꽤 멀리 왔다,,,(내가 생각하기에) 정말 0에서 혼자 시작하여 웹을 구축하고, 프론트, 서버, 데이터베이스까지 모두 기초적인 단계는 혼자 완료했다. 이제 SSO CAS 로그인 프로토콜을 이용해 유저 로그인도 구현이 되어있고, 여러가지 보안 문제 등 HTTPS에서 발생하는 문제들도 모두 해결하였다. 

 

이제, 해야 할 것이 하나 더 생겼다. 위의 자잘자잘한 일들을 끝냄과 동시에 교수님이 곧바로 하신 말은,,, "If it is first time a user logging in, check if that user exists in our database, and if not, ask their full name and save it to the database and don't ask it again."

 

우선,,,해야 할 일들은

  • 유저 모델(스키마) 정의하기

- 유저 모델(스키마)을 정의해 보자. 먼저, /backend 디렉토리에 models라는 디렉토리를 만들어 준 후, user.js 라는 유저 모델을 관리할 클래스를 하나 만들어 준다. 보통 Node.js 프로젝트에서는 코드 구조를 체계적으로 관리하기 위해 models라는 디렉토리를 사용하여 데이터베이스 모델을 정의한다. 따라서 models 디렉토리에 user.js 파일을 정의하는 것이 일반적인 관례이다.

const mongoose = require('mongoose');

// user model or schema 
const userSchema = new mongoose.Schema({
  pid: { type: String, required: true, unique: true },
  name: { type: String }, 
  email: { type: String, required: false, unique: true },
  isFirstLogin: { type: Boolean, default: true }, 
  createdAt: { type: Date, default: Date.now }
});

// create a user model
const User = mongoose.model('User', userSchema);

module.exports = User;

 



const mongoose = require('mongoose');

- 이 줄은 `mongoose` 모듈을 가져온다. Mongoose는 Node.js에서 MongoDB와 상호 작용하기 위한 객체 데이터 모델링(ODM) 라이브러리이다.


const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  studentPid: { type: String },
  createdAt: { type: Date, default: Date.now }
});

- 이 부분은 `userSchema`라는 이름의 새로운 스키마를 정의한다. 
  
  - `username: { type: String, required: true, unique: true }`
    - `type: String`: `username` 필드는 문자열 타입이어야 한다.
    - `required: true`: `username` 필드는 필수 항목이다. 즉, 스키마를 생성할 때 반드시 이 필드에 값이 있어야 한다.
    - `unique: true`: `username` 필드는 유일해야 한다. 즉, 데이터베이스 내에서 중복될 수 없다.

  - `email: { type: String, required: true, unique: true }`
    - `type: String`: `email` 필드는 문자열 타입이어야 한다.
    - `required: true`: `email` 필드는 필수 항목이다.
    - `unique: true`: `email` 필드는 유일해야 한다.

  - `studentPid: { type: String }`
    - `type: String`: `studentPid` 필드는 문자열 타입이어야 한다.
    - 이 필드는 필수 항목이 아니다(`required` 속성이 없기 때문).

  - `createdAt: { type: Date, default: Date.now }`
    - `type: Date`: `createdAt` 필드는 날짜 타입이어야 한다.
    - `default: Date.now`: 이 필드는 문서가 생성될 때 자동으로 현재 날짜와 시간으로 설정된다.


const User = mongoose.model('User', userSchema);

- 이 줄은 `userSchema`를 기반으로 `User`라는 모델을 생성한다. 이 모델은 MongoDB의 `users` 컬렉션과 연결된다(모델 이름의 소문자 복수형으로 컬렉션 이름이 자동 설정된다).

module.exports = User;

- 이 줄은 `User` 모델을 모듈로 내보낸다. 이렇게 하면 다른 파일에서 이 모델을 `require`하여 사용할 수 있다.

이 코드의 전체 목적은 MongoDB 데이터베이스에 저장될 사용자 정보를 정의하고, 이 정보를 처리하기 위한 모델을 생성하는 것이다. `required` 속성은 특정 필드가 반드시 존재해야 함을 의미하고, `unique` 속성은 필드 값이 중복되지 않도록 보장한다.

 

  • 백엔드 서버 파일에서 방금 만든 유저 모델 가져오기
    우선, 백엔드 파일에서 방금 만든 유저 모델을 가져오기 위해 소스코드 상단에 
require('dotenv').config();
const fs = require('fs');
const https = require('https');
const express = require('express');
const session = require('express-session');
const axios = require('axios');
const cors = require('cors');
const mongoose = require('mongoose');
const xml2js = require('xml2js');
const User = require('./models/user');

 

의 맨 아랫줄과 같이 User = require(' ./models/user');을 통해 유저를 가져온다. 그 후, /Dashboard 엔드포인트에 정의된 함수 안에서 Dashboard로 get요청을 받으면 데이터베이스에서 유저를 확인하는 코드를 작성한다.

 

app.get('/Dashboard', async (req, res) => {
  console.log('Request received at /Dashboard');
  console.log('Session data:', req.session);
  console.log('User data:' , req.session.user);
  if (!req.session.user) {
    const { ticket } = req.query;
    if (!ticket) {
      return res.redirect(`https://login.vt.edu/cas/login?service=${encodeURIComponent(CAS_SERVICE_URL)}`);
    }
    try {
      const response = await axios.get(CAS_VALIDATE_URL, {
        params: {
          ticket,
          service: CAS_SERVICE_URL
        }
      });
      const parser = new xml2js.Parser();
      parser.parseString(response.data, async(err, result) => {
        console.log("Result : " , JSON.stringify(result)) 
        if (err) {
          console.error('XML parsing error:', err);
          return res.status(500).send('CAS ticket validation failed');
        }
        const user = result['cas:serviceResponse']['cas:authenticationSuccess'][0];
        const pid = user['cas:user'][0];
        const attributes = user['cas:attributes'][0];
        const email = attributes['cas:eduPersonPrincipalName'][0];
        req.session.user = {pid, email};
        let dbUser = await User.findOne({ email: email });
        console.log("DBUSER:" , dbUser);
        if (!dbUser) {
          // if it's first time a user logging in, save it to db and ask name
          console.log("1ST TIME USER LOGIN");
          dbUser = new User({ pid, email, isFirstLogin : true });
          await dbUser.save();
          req.session.user.isFirstLogin = true;
        }
        else{
          req.session.user.name = dbUser.name;
          req.session.user.isFirstLogin = false;
          console.log("NOT 1ST TIME LOGIN");
        }
        res.redirect('https://crescendo.cs.vt.edu/Dashboard');
      });
    } catch (error) {
      console.error('CAS ticket validation failed123:', error);
      res.status(500).send('ticket validation failed');
    }
  } else {
    res.redirect('https://crescendo.cs.vt.edu/Dashboard');
  }
});

 

대략적인 방식은 이러하다

 

- 같은 backend 디렉토리 안에 user.js라는 유저 모델을 정의하는 클래스를 만들어주고, export된 User를 여기서 받아 사용한다.

- let duUser = await User.findOne({email : email})로 데이터베이스에서 email이라는 필드를 가진 유저를 검색 한 후

- dbUser가 없다면(처음 로그인 하는 유저라면), 유저 추가 로직을 실행한다

 

하지만, 단순히 이름을 입력받는데에 새로운 페이지로 리디렉션 하는 것 보다는 모달을 띄워 서버에 따로 요청을 보내는것이 더 직관적이기 때문에 모달을 띄우기로 했다.

 

백엔드 디렉토리의 서버 파일에서 새로운 API 엔드포인트를 준비한다.

app.post('/saveName', async (req, res) => {
  console.log("Save name enpoint reached");
  const { firstName, lastName } = req.body;
  const email = req.session.user.email;

  // update user info in db
  await User.updateOne({ email: email }, { name: `${firstName} ${lastName}` });

  // update name in session
  req.session.user.name = `${firstName} ${lastName}`;
  req.session.user.isFirstLogin = false;

  res.status(200).json({ message: 'Name updated successfully' });
});

 

그후, 클라이언트 디렉토리로 이동하여 모달을 띄울 코드를 작성한다.

<Modal.Body>
                    <Form>
                        <Form.Group controlId="formFirstName">
                            <Form.Label>First Name</Form.Label>
                            <Form.Control type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
                        </Form.Group>
                        <Form.Group controlId="formLastName">
                            <Form.Label>Last Name</Form.Label>
                            <Form.Control type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} />
                        </Form.Group>
                    </Form>
                </Modal.Body>
                <Modal.Footer>

 

이렇게 하면 성공적으로 처음 접근하는 유저에게 이름을 물어보고, 유저가 이미 이름을 입력한 적이 있다면 더이상 묻지 않는다.