송용우

Merge commit '02ae3279' into feature/database

1 -SERVER_PORT=4000
2 -MONGO_URL=mongodb://localhost:27017/jaksimsamil
...\ No newline at end of file ...\ No newline at end of file
1 { 1 {
2 + "parser": "babel-eslint",
2 "env": { 3 "env": {
3 "commonjs": true, 4 "commonjs": true,
4 "es6": true, 5 "es6": true,
...@@ -12,6 +13,5 @@ ...@@ -12,6 +13,5 @@
12 "parserOptions": { 13 "parserOptions": {
13 "ecmaVersion": 11 14 "ecmaVersion": 11
14 }, 15 },
15 - "rules": { 16 + "rules": {}
16 - }
17 } 17 }
......
...@@ -21,14 +21,17 @@ ...@@ -21,14 +21,17 @@
21 | group | description | method | URL | Detail | Auth | 21 | group | description | method | URL | Detail | Auth |
22 | ------- | ------------------------ | ------ | -------------------------- | -------- | --------- | 22 | ------- | ------------------------ | ------ | -------------------------- | -------- | --------- |
23 | user | 유저 등록 | POST | api/user | 바로가기 | JWT Token | 23 | user | 유저 등록 | POST | api/user | 바로가기 | JWT Token |
24 -| user | 유저 삭제 | DEELTE | api/user:id | 바로가기 | JWT Token | 24 +| user | 유저 삭제 | DELETE | api/user:id | 바로가기 | JWT Token |
25 | user | 특정 유저 조회 | GET | api/user:id | 바로가기 | None | 25 | user | 특정 유저 조회 | GET | api/user:id | 바로가기 | None |
26 | user | 전체 유저 조회 | GET | api/user | 바로가기 | JWT Token | 26 | user | 전체 유저 조회 | GET | api/user | 바로가기 | JWT Token |
27 | friend | 유저 친구 등록 | POST | api/friend | 바로가기 | JWT Token | 27 | friend | 유저 친구 등록 | POST | api/friend | 바로가기 | JWT Token |
28 | friend | 유저의 친구 조회 | GET | api/friend:id | 바로가기 | None | 28 | friend | 유저의 친구 조회 | GET | api/friend:id | 바로가기 | None |
29 | profile | 유저가 푼 문제 조회 | GET | api/profile/solved:id | 바로가기 | None | 29 | profile | 유저가 푼 문제 조회 | GET | api/profile/solved:id | 바로가기 | None |
30 +| profile | 유저가 푼 문제 동기화 | Update | api/profile/solved:id | 바로가기 | None |
30 | profile | 유저가 푼 문제 개수 조회 | GET | api/profile/solvednum:id | 바로가기 | None | 31 | profile | 유저가 푼 문제 개수 조회 | GET | api/profile/solvednum:id | 바로가기 | None |
31 | profile | 추천 문제 조회 | GET | api/profile/recommendps:id | 바로가기 | None | 32 | profile | 추천 문제 조회 | GET | api/profile/recommendps:id | 바로가기 | None |
32 | notify | 슬랙 메시지 전송 요청 | POST | api/notify/slack | 바로가기 | Jwt Token | 33 | notify | 슬랙 메시지 전송 요청 | POST | api/notify/slack | 바로가기 | Jwt Token |
33 | auth | 로그인 | POST | api/auth/login | 바로가기 | None | 34 | auth | 로그인 | POST | api/auth/login | 바로가기 | None |
34 | auth | 로그아웃 | GET | api/auth/logout | 바로가기 | JWT Token | 35 | auth | 로그아웃 | GET | api/auth/logout | 바로가기 | JWT Token |
36 +| auth | 회원가입 | POST | api/auth/register | 바로가기 | None |
37 +| auth | 로그인 확인 | GET | api/auth/check | 바로가기 | None |
......
1 -const express = require("express"); 1 +const Koa = require("koa");
2 -const morgan = require("morgan"); 2 +const Router = require("koa-router");
3 +const bodyParser = require("koa-bodyparser");
3 const mongoose = require("mongoose"); 4 const mongoose = require("mongoose");
4 -const app = express(); 5 +const jwtMiddleware = require("./src/lib/jwtMiddleware");
6 +const api = require("./src/api");
7 +
5 require("dotenv").config(); 8 require("dotenv").config();
9 +
10 +const app = new Koa();
11 +const router = new Router();
12 +
13 +app.use(bodyParser());
14 +app.use(jwtMiddleware);
15 +
6 const { SERVER_PORT, MONGO_URL } = process.env; 16 const { SERVER_PORT, MONGO_URL } = process.env;
7 -app.use( 17 +
8 - morgan("[:date[iso]] :method :status :url :response-time(ms) :user-agent") 18 +router.use("/api", api.routes());
9 -); 19 +app.use(router.routes()).use(router.allowedMethods());
10 -app.use(express.json());
11 -app.use(express.urlencoded({ extended: false }));
12 -app.use("/api", require("./api"));
13 20
14 mongoose 21 mongoose
15 - .connect(MONGO_URL, { useNewUrlParser: true, useFindAndModify: false }) 22 + .connect(MONGO_URL, {
23 + useNewUrlParser: true,
24 + useFindAndModify: false,
25 + useUnifiedTopology: true,
26 + })
16 .then(() => { 27 .then(() => {
17 console.log("Connected to MongoDB"); 28 console.log("Connected to MongoDB");
18 }) 29 })
19 .catch((e) => { 30 .catch((e) => {
20 console.log(e); 31 console.log(e);
21 }); 32 });
33 +
22 app.listen(SERVER_PORT, () => { 34 app.listen(SERVER_PORT, () => {
23 console.log("Server is running on port", process.env.SERVER_PORT); 35 console.log("Server is running on port", process.env.SERVER_PORT);
24 }); 36 });
......
This diff could not be displayed because it is too large.
...@@ -4,15 +4,27 @@ ...@@ -4,15 +4,27 @@
4 "main": "index.js", 4 "main": "index.js",
5 "license": "MIT", 5 "license": "MIT",
6 "dependencies": { 6 "dependencies": {
7 + "axios": "^0.19.2",
8 + "bcrypt": "^4.0.1",
9 + "body-parser": "^1.19.0",
10 + "cheerio": "^1.0.0-rc.3",
11 + "cookie-parser": "^1.4.5",
7 "dotenv": "^8.2.0", 12 "dotenv": "^8.2.0",
8 "eslint-config-prettier": "^6.11.0", 13 "eslint-config-prettier": "^6.11.0",
9 - "express": "^4.17.1",
10 "fs": "^0.0.1-security", 14 "fs": "^0.0.1-security",
15 + "iconv": "^3.0.0",
16 + "joi": "^14.3.1",
17 + "jsonwebtoken": "^8.5.1",
18 + "koa": "^2.12.0",
19 + "koa-bodyparser": "^4.3.0",
20 + "koa-router": "^9.0.1",
11 "mongoose": "^5.9.17", 21 "mongoose": "^5.9.17",
12 "morgan": "^1.10.0", 22 "morgan": "^1.10.0",
13 - "path": "^0.12.7" 23 + "path": "^0.12.7",
24 + "voca": "^1.4.0"
14 }, 25 },
15 "devDependencies": { 26 "devDependencies": {
27 + "babel-eslint": "^10.1.0",
16 "eslint": "^7.1.0", 28 "eslint": "^7.1.0",
17 "nodemon": "^2.0.4" 29 "nodemon": "^2.0.4"
18 }, 30 },
......
1 +const Joi = require("joi");
2 +const User = require("../../models/user");
3 +/*
4 +POST /api/auth/register
5 +{
6 + username: 'userid'
7 + password: 'userpassword'
8 +}
9 +*/
10 +exports.register = async (ctx) => {
11 + const schema = Joi.object().keys({
12 + username: Joi.string().alphanum().min(3).max(20).required(),
13 + password: Joi.string().required(),
14 + });
15 +
16 + const result = Joi.validate(ctx.request.body, schema);
17 + if (result.error) {
18 + ctx.status = 400;
19 + ctx.body = result.error;
20 + return;
21 + }
22 +
23 + const { username, password } = ctx.request.body;
24 + try {
25 + const isNameExist = await User.findByUsername(username);
26 + if (isNameExist) {
27 + ctx.status = 409;
28 + return;
29 + }
30 + const user = new User({
31 + username,
32 + });
33 + await user.setPassword(password);
34 + await user.save();
35 + ctx.body = user.serialize();
36 +
37 + const token = user.generateToken();
38 + ctx.cookies.set("acces_token", token, {
39 + //3일동안 유효
40 + maxAge: 1000 * 60 * 60 * 24 * 3,
41 + httpOnly: true,
42 + });
43 + } catch (e) {
44 + ctx.throw(500, e);
45 + }
46 +};
47 +/*
48 +POST /api/auth/login
49 +{
50 + username: 'userid'
51 + password: 'userpassword'
52 +}
53 + */
54 +exports.login = async (ctx) => {
55 + const { username, password } = ctx.request.body;
56 + if (!username || !password) {
57 + ctx.status = 401;
58 + return;
59 + }
60 + try {
61 + const user = await User.findByUsername(username);
62 + if (!user) {
63 + ctx.status = 401;
64 + return;
65 + }
66 + const isPasswordValid = await user.checkPassword(password);
67 + if (!isPasswordValid) {
68 + ctx.status = 401;
69 + return;
70 + }
71 + ctx.body = user.serialize();
72 + const token = user.generateToken();
73 + ctx.cookies.set("acces_token", token, {
74 + //7일동안 유효
75 + maxAge: 1000 * 60 * 60 * 24 * 7,
76 + httpOnly: true,
77 + });
78 + } catch (e) {
79 + ctx.throw(500, e);
80 + }
81 +};
82 +/*
83 +GET api/auth/check
84 +*/
85 +exports.check = async (ctx) => {
86 + const { user } = ctx.state;
87 + if (!user) {
88 + ctx.status = 401;
89 + return;
90 + }
91 + ctx.body = user;
92 +};
93 +/*
94 +POST /api/auth/logout
95 +*/
96 +exports.logout = async (ctx) => {
97 + ctx.cookies.set("access_token");
98 + ctx.status = 204;
99 +};
1 +const Router = require("koa-router");
2 +const auth = new Router();
3 +const authCtrl = require("./auth.ctrl");
4 +auth.post("/login", authCtrl.login);
5 +auth.get("/logout", authCtrl.logout);
6 +auth.post("/register", authCtrl.register);
7 +
8 +module.exports = auth;
1 +const Router = require("koa-router");
2 +const friend = new Router();
3 +
4 +friend.post("/");
5 +friend.delete("/:id");
6 +friend.get("/:id");
7 +friend.get("");
8 +
9 +module.exports = friend;
1 +const Router = require("koa-router");
2 +const api = new Router();
3 +
4 +const auth = require("./auth");
5 +const friend = require("./friend");
6 +const notify = require("./profile");
7 +const user = require("./user");
8 +const profile = require("./profile");
9 +
10 +api.use("/auth", auth.routes());
11 +api.use("/friend", friend.routes());
12 +api.use("/notify", notify.routes());
13 +api.use("/user", user.routes());
14 +api.use("/profile", profile.routes());
15 +
16 +module.exports = api;
......
1 +const Router = require("koa-router");
2 +const notify = new Router();
3 +
4 +notify.post("/slack");
5 +
6 +module.exports = notify;
1 +const Router = require("koa-router");
2 +const profile = new Router();
3 +
4 +profile.post("/solved:id");
5 +profile.get("/solvednum:id");
6 +profile.get("recommendps:id");
7 +
8 +module.exports = profile;
1 +const Router = require("koa-router");
2 +const user = new Router();
3 +user.post("/");
4 +user.delete("/:id");
5 +user.get("/:id");
6 +user.get("");
7 +
8 +module.exports = user;
1 +const jwt = require("jsonwebtoken");
2 +const User = require("../models/user");
3 +const jwtMiddleware = async (ctx, next) => {
4 + const token = ctx.cookies.get("access_token");
5 + if (!token) {
6 + //토큰이 없을 때
7 + return next();
8 + }
9 + try {
10 + const decoded = jwt.verify(token, process.env.JWT_TOKEN);
11 + ctx.state.user = {
12 + _id: decoded._id,
13 + username: decoded.username,
14 + };
15 + //토큰의 남은 유효 기간이 2일 이하라면 재발급
16 + if (decoded.exp - Date.now() / 1000 < 60 * 60 * 24 * 2) {
17 + const user = await User.findById(decoded._id);
18 + const token = user.generateToken();
19 + ctx.cookies.set("access_token", token, {
20 + maxAge: 1000 * 60 * 60 * 24 * 7,
21 + httpOnly: true,
22 + });
23 + }
24 + return next();
25 + } catch (e) {
26 + return next();
27 + }
28 +};
29 +
30 +module.exports = jwtMiddleware;
1 +const mongoose = require("mongoose");
2 +const bcrypt = require("bcrypt");
3 +const jwt = require("jsonwebtoken");
4 +
5 +const Schema = mongoose.Schema;
6 +
7 +const UserSchema = new Schema({
8 + username: String,
9 + hashedPassword: String,
10 +});
11 +
12 +UserSchema.methods.setPassword = async function (password) {
13 + const hash = await bcrypt.hash(password, 10);
14 + this.hashedPassword = hash;
15 +};
16 +UserSchema.methods.checkPassword = async function (password) {
17 + const result = await bcrypt.compare(password, this.hashedPassword);
18 + return result;
19 +};
20 +UserSchema.statics.findByUsername = function (username) {
21 + return this.findOne({ username });
22 +};
23 +UserSchema.methods.serialize = function () {
24 + const data = this.toJSON();
25 + delete data.hashedPassword;
26 + return data;
27 +};
28 +UserSchema.methods.generateToken = function () {
29 + const token = jwt.sign(
30 + {
31 + _id: this.id,
32 + username: this.username,
33 + },
34 + process.env.JWT_SECRET,
35 + {
36 + expiresIn: "7d",
37 + }
38 + );
39 + return token;
40 +};
41 +const User = mongoose.model("User", UserSchema);
42 +module.exports = User;
1 +exports.StringToDate_BJ = function (date_str) {
2 + let arr_date = date_str.split(" "); //yyyy m dd tt MM SS Fomat LIST
3 + let arr_date_r = arr_date.map(function (str) {
4 + let str_r = str.slice(0, -1);
5 +
6 + return str_r.length == 1 ? "0" + str_r : str_r;
7 + });
8 +
9 + return arr_date_r[0] + arr_date_r[1] + arr_date_r[2]; //YYYYMMDD 형식으로 반환
10 +};
1 +const axios = require("axios");
2 +const cheerio = require("cheerio");
3 +const StringToDate = require("./StringToDate");
4 +/*
5 +ToDO
6 +- 유저 네임 검증
7 +- 예외 처리
8 +*/
9 +exports.getBJ = async function (userid) {
10 + let data_list = [];
11 + let next_page_link = "";
12 +
13 + await getStartPage(userid).then((html) => {
14 + //시작 페이지를 가져온다.
15 + //같은 객체를 두번 선언한다. 퍼포먼스에 문제 생길수도
16 + //함수에 객체를 넘기는 방법도 있다.
17 + //첫 페이지 가져온다.
18 + data_list.push(getData(html));
19 + next_page_link = getNextPageLink(html);
20 + });
21 + while (next_page_link != -1) {
22 + //다음 페이지를 가져온다.
23 + await getNextPage(next_page_link).then((html) => {
24 + data_list.push(getData(html));
25 + next_page_link = getNextPageLink(html);
26 + });
27 + }
28 + return data_list.flat(1);
29 +};
30 +
31 +const getStartPage = async (userid) => {
32 + //유저 아이디 입력
33 + try {
34 + return await axios.get(
35 + "https://www.acmicpc.net/status?user_id=" + userid + "&result_id=4"
36 + );
37 + } catch (error) {
38 + console.log(error);
39 + }
40 +};
41 +
42 +const getNextPage = async (link) => {
43 + //링크 입력
44 + try {
45 + return await axios.get(link);
46 + } catch (error) {
47 + console.log(error);
48 + }
49 +};
50 +
51 +const getData = (html) => {
52 + //페이지 데이터 파싱
53 + let psArr = [];
54 + const $ = cheerio.load(html.data);
55 + const $bodyList = $("#status-table > tbody");
56 + $bodyList.children().each((index, element) => {
57 + psArr.push({
58 + problem_number: $(element).find("a.problem_title").text(),
59 + problem_title: $(element).find("a.problem_title").attr("title"),
60 + solved_date: StringToDate.StringToDate_BJ(
61 + $(element).find("a.real-time-update").attr("title")
62 + ),
63 + });
64 + });
65 + return psArr;
66 +};
67 +const getNextPageLink = (html) => {
68 + //다음 페이지가 있으면 다음 페이지 주소 return, 없으면 -1 return
69 + const $ = cheerio.load(html.data);
70 + return $("#next_page").attr("href")
71 + ? "https://www.acmicpc.net/" + $("#next_page").attr("href")
72 + : -1;
73 +};
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.