Showing
20 changed files
with
364 additions
and
30 deletions
jaksimsamil-server/.env
deleted
100644 → 0
1 | { | 1 | { |
2 | - "env": { | 2 | + "parser": "babel-eslint", |
3 | - "commonjs": true, | 3 | + "env": { |
4 | - "es6": true, | 4 | + "commonjs": true, |
5 | - "node": true | 5 | + "es6": true, |
6 | - }, | 6 | + "node": true |
7 | - "extends": "eslint:recommended", | 7 | + }, |
8 | - "globals": { | 8 | + "extends": "eslint:recommended", |
9 | - "Atomics": "readonly", | 9 | + "globals": { |
10 | - "SharedArrayBuffer": "readonly" | 10 | + "Atomics": "readonly", |
11 | - }, | 11 | + "SharedArrayBuffer": "readonly" |
12 | - "parserOptions": { | 12 | + }, |
13 | - "ecmaVersion": 11 | 13 | + "parserOptions": { |
14 | - }, | 14 | + "ecmaVersion": 11 |
15 | - "rules": { | 15 | + }, |
16 | - } | 16 | + "rules": {} |
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 | }); | ... | ... |
jaksimsamil-server/package-lock.json
0 → 100644
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 | }, | ... | ... |
jaksimsamil-server/src/api/auth/auth.ctrl.js
0 → 100644
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 | +}; |
jaksimsamil-server/src/api/auth/index.js
0 → 100644
jaksimsamil-server/src/api/friend/index.js
0 → 100644
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; | ... | ... |
jaksimsamil-server/src/api/notify/index.js
0 → 100644
jaksimsamil-server/src/api/profile/index.js
0 → 100644
jaksimsamil-server/src/api/user/index.js
0 → 100644
jaksimsamil-server/src/api/user/user.ctrl.js
0 → 100644
File mode changed
jaksimsamil-server/src/lib/jwtMiddleware.js
0 → 100644
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; |
jaksimsamil-server/src/models/user.js
0 → 100644
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; |
jaksimsamil-server/src/util/StringToDate.js
0 → 100644
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 | +}; |
jaksimsamil-server/src/util/getBJ.js
0 → 100644
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 | +}; |
jaksimsamil-server/yarn-error.log
0 → 100644
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment