From c8a1a62b7964ef5acfc89afafe5ce7c8c1184e57 Mon Sep 17 00:00:00 2001 From: Jakads Date: Tue, 11 Jan 2022 21:29:59 +0900 Subject: [PATCH 01/17] Install crypto --- changmin-todolist/package.json | 1 + changmin-todolist/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/changmin-todolist/package.json b/changmin-todolist/package.json index eddb60c..e96e673 100644 --- a/changmin-todolist/package.json +++ b/changmin-todolist/package.json @@ -10,6 +10,7 @@ "body-parser": "^1.19.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^10.0.0", "express": "^4.17.2", "jsonwebtoken": "^8.5.1", diff --git a/changmin-todolist/yarn.lock b/changmin-todolist/yarn.lock index 3bdd31e..d8d6911 100644 --- a/changmin-todolist/yarn.lock +++ b/changmin-todolist/yarn.lock @@ -3892,6 +3892,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + css-blank-pseudo@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" From 7a8d7675af4f1e3cd35e743652d04b8f99acd557 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 12 Jan 2022 01:19:17 +0900 Subject: [PATCH 02/17] User DB encryption --- .../server/controllers/userController.js | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/changmin-todolist/server/controllers/userController.js b/changmin-todolist/server/controllers/userController.js index 9acef6a..f2f7155 100644 --- a/changmin-todolist/server/controllers/userController.js +++ b/changmin-todolist/server/controllers/userController.js @@ -1,6 +1,7 @@ const jwt = require("jsonwebtoken"); const mysql = require("mysql2/promise"); const dotenv = require("dotenv").config(); +const { randomBytes, scrypt } = require("crypto"); const options = { host: process.env.MYSQL_HOST, user: process.env.MYSQL_USER, @@ -26,24 +27,31 @@ exports.createToken = async (req, res) => { try { const [rows] = await conn.query( - `SELECT * FROM user WHERE username = '${req.body.username}' AND password = '${req.body.password}'` + `SELECT * FROM user WHERE username = '${req.body.username}'` ); if (rows.length) { - const token = jwt.sign( - { - username: rows[0].username, - }, - SECRET_KEY, - { - expiresIn: "1h", - } - ); - res.cookie("user", token, { - path: "/", - maxAge: 60 * 60 * 1000, + const [password, salt] = rows[0].password.split("$"); + + scrypt(req.body.password, salt, 64, (err, derivedKey) => { + if (err) throw err; + if (derivedKey.toString("base64") == password) { + const token = jwt.sign( + { + username: rows[0].username, + }, + SECRET_KEY, + { + expiresIn: "1h", + } + ); + res.cookie("user", token, { + path: "/", + maxAge: 60 * 60 * 1000, + }); + res.send("OK"); + } else res.send("USER_NOT_FOUND"); }); - res.send("OK"); } else { res.send("USER_NOT_FOUND"); } @@ -67,9 +75,16 @@ exports.createNewUser = async (req, res) => { ); if (!rows.length) { - await conn.query( - `INSERT INTO user (username, password) VALUES ('${req.body.username}', '${req.body.password}')` - ); + const salt = randomBytes(32).toString("base64"); + + scrypt(req.body.password, salt, 64, async (err, derivedKey) => { + if (err) throw err; + await conn.query( + `INSERT INTO user (username, password) VALUES ('${ + req.body.username + }', '${derivedKey.toString("base64")}$${salt}')` + ); + }); res.send("OK"); } else { res.send("USER_EXISTS"); From c46f8b95560a3667dfc8025bdfb1d7a1ff8d135c Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 12 Jan 2022 01:32:48 +0900 Subject: [PATCH 03/17] Prevent SQL injection --- .../server/controllers/todoController.js | 18 +++++++++------ .../server/controllers/userController.js | 23 +++++++++---------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/changmin-todolist/server/controllers/todoController.js b/changmin-todolist/server/controllers/todoController.js index 15a4092..d59a20f 100644 --- a/changmin-todolist/server/controllers/todoController.js +++ b/changmin-todolist/server/controllers/todoController.js @@ -38,25 +38,29 @@ exports.postTodoList = async (req, res) => { const pool = connectDB(); const conn = await pool.getConnection(); try { - let sql; + let sql, values; switch (action.type) { case "CREATE": - sql = `INSERT INTO todo (id, text, done) VALUES (${action.todo.id}, '${action.todo.text}', ${action.todo.done})`; + sql = "INSERT INTO todo (id, text, done) VALUES (?, ?, ?)"; + values = [action.todo.id, action.todo.text, action.todo.done]; break; case "TOGGLE": - sql = `UPDATE todo SET done = 1 - done WHERE id = ${action.id}`; + sql = "UPDATE todo SET done = 1 - done WHERE id = ?"; + values = [action.id]; break; case "REMOVE": - sql = `DELETE FROM todo WHERE id = ${action.id}`; + sql = "DELETE FROM todo WHERE id = ?"; + values = [action.id]; break; case "EDIT": - sql = `UPDATE todo SET text = '${action.editText}' WHERE id = ${action.id}`; + sql = "UPDATE todo SET text = ? WHERE id = ?"; + values = [action.editText, action.id]; break; default: throw new Error(`Undefined Action: ${action.type}`); } - console.log(sql); - await conn.query(sql); + console.log(`${sql} / ${values}`); + await conn.query(sql, values); res.sendStatus(200); } catch (err) { console.log(err); diff --git a/changmin-todolist/server/controllers/userController.js b/changmin-todolist/server/controllers/userController.js index f2f7155..f6c8205 100644 --- a/changmin-todolist/server/controllers/userController.js +++ b/changmin-todolist/server/controllers/userController.js @@ -26,9 +26,9 @@ exports.createToken = async (req, res) => { const conn = await pool.getConnection(); try { - const [rows] = await conn.query( - `SELECT * FROM user WHERE username = '${req.body.username}'` - ); + const [rows] = await conn.query("SELECT * FROM user WHERE username = ?", [ + req.body.username, + ]); if (rows.length) { const [password, salt] = rows[0].password.split("$"); @@ -70,9 +70,9 @@ exports.createNewUser = async (req, res) => { const conn = await pool.getConnection(); try { - const [rows] = await conn.query( - `SELECT * FROM user WHERE username = '${req.body.username}'` - ); + const [rows] = await conn.query("SELECT * FROM user WHERE username = ?", [ + req.body.username, + ]); if (!rows.length) { const salt = randomBytes(32).toString("base64"); @@ -80,9 +80,8 @@ exports.createNewUser = async (req, res) => { scrypt(req.body.password, salt, 64, async (err, derivedKey) => { if (err) throw err; await conn.query( - `INSERT INTO user (username, password) VALUES ('${ - req.body.username - }', '${derivedKey.toString("base64")}$${salt}')` + "INSERT INTO user (username, password) VALUES (?, ?)", + [req.body.username, `${derivedKey.toString("base64")}$${salt}`] ); }); res.send("OK"); @@ -104,9 +103,9 @@ exports.removeUser = async (req, res) => { const conn = await pool.getConnection(); try { - await conn.query( - `DELETE FROM user WHERE username = '${req.body.username}'` - ); + await conn.query("DELETE FROM user WHERE username = ?", [ + req.body.username, + ]); res.send("OK"); } catch (err) { console.log(err); From b620de7cc20374a6077e57a8558c9d416f530bcd Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 12 Jan 2022 01:39:22 +0900 Subject: [PATCH 04/17] Limit username and password length --- changmin-todolist/src/components/LoginForm.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index 8bdda38..6f04469 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -141,6 +141,16 @@ function LoginForm({ currentUsername, setCurrentUsername }) { passwordInput.current.focus(); return; } + if (username.length < 8 || username.length > 20) { + alert("Username은 8~20글자만 가능합니다."); + usernameInput.current.focus(); + return; + } + if (password.length < 8 || password.length > 20) { + alert("Password는 8~20글자만 가능합니다."); + passwordInput.current.focus(); + return; + } async function checkUser() { await axios From 8f57bcaabca634de63ad76be6f9f1eef332e7eb9 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 12 Jan 2022 03:01:48 +0900 Subject: [PATCH 05/17] Implement password check --- changmin-todolist/src/components/LoginForm.js | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index 6f04469..03de50b 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -68,12 +68,20 @@ function LoginForm({ currentUsername, setCurrentUsername }) { const [inputs, setInputs] = useState({ username: "", password: "", + passwordCheck: "", }); - const { username, password } = inputs; // 입력받은 username / password + const { username, password, passwordCheck } = inputs; // 입력받은 username / password / passwordCheck const usernameInput = useRef(); // Username 입력 focus 위해 사용 const passwordInput = useRef(); // Password 입력 focus 위해 사용 + const passwordCheckInput = useRef(); // PasswordCheck 입력 focus 위해 사용 + const [passwordCheckDisplay, setPasswordCheckDisplay] = useState(false); // PasswordCheck 입력 표시 여부 const navigate = useNavigate(); // 로그인 성공 후 navigate 위해 사용 + const setPasswordCheckInput = (node) => { + if (node) passwordCheckInput.current = node; + passwordCheckInput.current.focus(); + }; + const onChange = (e) => { const { value, name } = e.target; setInputs({ @@ -141,6 +149,7 @@ function LoginForm({ currentUsername, setCurrentUsername }) { passwordInput.current.focus(); return; } + if (username.length < 8 || username.length > 20) { alert("Username은 8~20글자만 가능합니다."); usernameInput.current.focus(); @@ -152,6 +161,21 @@ function LoginForm({ currentUsername, setCurrentUsername }) { return; } + if (!passwordCheckDisplay) { + setPasswordCheckDisplay(true); + return; + } + if (!passwordCheck) { + alert("Password Check를 입력해주세요."); + passwordCheckInput.current.focus(); + return; + } + if (password !== passwordCheck) { + alert("Password와 Password Check이 다릅니다."); + passwordCheckInput.current.focus(); + return; + } + async function checkUser() { await axios .post("http://localhost:3001/user/register", inputs) @@ -162,20 +186,17 @@ function LoginForm({ currentUsername, setCurrentUsername }) { `${username} 계정을 성공적으로 생성하였습니다. 다시 로그인해주세요.` ); - // username과 password input field 초기화 - setInputs({ username: "", password: "" }); + setInputs({ username: "", password: "", passwordCheck: "" }); - // username field에 focus + setPasswordCheckDisplay(false); usernameInput.current.focus(); break; case "USER_EXISTS": alert(`${username} 계정이 이미 존재합니다.`); - // password input field 초기화 - setInputs({ ...inputs, password: "" }); + setInputs({ ...inputs, password: "", passwordCheck: "" }); - // password field에 focus - passwordInput.current.focus(); + usernameInput.current.focus(); break; default: alert("알 수 없는 오류가 발생했습니다."); @@ -263,6 +284,19 @@ function LoginForm({ currentUsername, setCurrentUsername }) { ref={passwordInput} /> + {passwordCheckDisplay && ( +
+ Password:{" "} + +
+ )} From 43190c7db013a3009f82806ca4bd93f9b04f1c03 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 12 Jan 2022 03:18:50 +0900 Subject: [PATCH 06/17] Redirect to login if not logged in --- changmin-todolist/src/App.js | 12 +++++++++--- changmin-todolist/src/components/LoginForm.js | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/changmin-todolist/src/App.js b/changmin-todolist/src/App.js index 2007f87..9e7b865 100644 --- a/changmin-todolist/src/App.js +++ b/changmin-todolist/src/App.js @@ -1,6 +1,6 @@ import Main from "./Main"; import styled, { createGlobalStyle } from "styled-components"; -import { Routes, Route, Link } from "react-router-dom"; +import { Routes, Route, Link, Navigate } from "react-router-dom"; import TodoTemplate from "./components/TodoTemplate"; import LoginForm from "./components/LoginForm"; import MenuTemplate from "./components/MenuTemplate"; @@ -34,7 +34,7 @@ const MenuStyle = { }; const App = () => { - const [currentUsername, setCurrentUsername] = useState(); + const [currentUsername, setCurrentUsername] = useState(undefined); async function verifyToken() { await axios @@ -46,6 +46,7 @@ const App = () => { }) .catch((err) => { console.log("Invalid token, removing the cookie"); + setCurrentUsername(null); cookies.remove("user"); }); } @@ -72,7 +73,12 @@ const App = () => { - } /> + : + } + /> { e.preventDefault(); - setCurrentUsername(undefined); + setCurrentUsername(null); cookies.remove("user"); alert("로그아웃되었습니다."); @@ -242,7 +242,7 @@ function LoginForm({ currentUsername, setCurrentUsername }) { console.log(err); alert("알 수 없는 오류가 발생했습니다."); }); - setCurrentUsername(undefined); + setCurrentUsername(null); cookies.remove("user"); } checkUser(); From aba3cf9dd19cb48569a1385451b27a5c5ddbc647 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 12 Jan 2022 05:14:55 +0900 Subject: [PATCH 07/17] Use Context API for current username --- changmin-todolist/src/App.js | 25 +++++++------------ changmin-todolist/src/components/LoginForm.js | 20 ++++++++------- changmin-todolist/src/contexts/UserContext.js | 23 +++++++++++++++++ changmin-todolist/src/index.js | 5 +++- 4 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 changmin-todolist/src/contexts/UserContext.js diff --git a/changmin-todolist/src/App.js b/changmin-todolist/src/App.js index 9e7b865..b042ce4 100644 --- a/changmin-todolist/src/App.js +++ b/changmin-todolist/src/App.js @@ -5,9 +5,10 @@ import TodoTemplate from "./components/TodoTemplate"; import LoginForm from "./components/LoginForm"; import MenuTemplate from "./components/MenuTemplate"; import { TodoProvider } from "./components/TodoContext"; -import { useEffect, useState } from "react"; +import { useContext, useEffect } from "react"; import axios from "axios"; import Cookies from "universal-cookie"; +import UserContext from "./contexts/UserContext"; const cookies = new Cookies(); @@ -34,7 +35,7 @@ const MenuStyle = { }; const App = () => { - const [currentUsername, setCurrentUsername] = useState(undefined); + const { state, actions } = useContext(UserContext); async function verifyToken() { await axios @@ -42,11 +43,11 @@ const App = () => { withCredentials: true, }) .then((res) => { - setCurrentUsername(res.data); + actions.setUsername(res.data); }) .catch((err) => { console.log("Invalid token, removing the cookie"); - setCurrentUsername(null); + actions.setUsername(null); cookies.remove("user"); }); } @@ -54,7 +55,7 @@ const App = () => { useEffect(() => { if (cookies.get("user") && cookies.get("user") !== "undefined") verifyToken(); - }, []); + }, [state]); return ( @@ -67,7 +68,7 @@ const App = () => { - {!currentUsername ? "Login" : currentUsername} + {!state.username ? "Login" : state.username} @@ -76,18 +77,10 @@ const App = () => { : - } - /> - + state.username !== null ?
: } /> + } /> diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index c4a75e8..00f17d1 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -1,8 +1,9 @@ import axios from "axios"; -import React, { useRef, useState } from "react"; +import React, { useContext, useRef, useState } from "react"; import { useNavigate } from "react-router"; import styled from "styled-components"; import Cookies from "universal-cookie"; +import UserContext from "../contexts/UserContext"; const cookies = new Cookies(); @@ -64,7 +65,8 @@ const Input = styled.input` box-sizing: border-box; `; -function LoginForm({ currentUsername, setCurrentUsername }) { +function LoginForm() { + const { state, actions } = useContext(UserContext); const [inputs, setInputs] = useState({ username: "", password: "", @@ -111,7 +113,7 @@ function LoginForm({ currentUsername, setCurrentUsername }) { .then((res) => { switch (res.data) { case "OK": - setCurrentUsername(username); + actions.setUsername(username); alert(`성공적으로 로그인되었습니다. 안녕하세요, ${username}님!`); navigate("/"); break; @@ -213,7 +215,7 @@ function LoginForm({ currentUsername, setCurrentUsername }) { const onLogout = (e) => { e.preventDefault(); - setCurrentUsername(null); + actions.setUsername(null); cookies.remove("user"); alert("로그아웃되었습니다."); @@ -225,12 +227,12 @@ function LoginForm({ currentUsername, setCurrentUsername }) { async function checkUser() { await axios .post("http://localhost:3001/user/unregister", { - username: currentUsername, + username: state.username, }) .then((res) => { switch (res.data) { case "OK": - alert(`${username} 계정을 성공적으로 삭제하였습니다.`); + alert(`${state.username} 계정을 성공적으로 삭제하였습니다.`); navigate("/"); break; default: @@ -242,17 +244,17 @@ function LoginForm({ currentUsername, setCurrentUsername }) { console.log(err); alert("알 수 없는 오류가 발생했습니다."); }); - setCurrentUsername(null); + actions.setUsername(null); cookies.remove("user"); } checkUser(); }; - if (!!currentUsername) { + if (state.username) { // 로그인 된 화면 return ( -

안녕하세요, {currentUsername}님!

+

안녕하세요, {state.username}님!

diff --git a/changmin-todolist/src/contexts/UserContext.js b/changmin-todolist/src/contexts/UserContext.js new file mode 100644 index 0000000..4edde4d --- /dev/null +++ b/changmin-todolist/src/contexts/UserContext.js @@ -0,0 +1,23 @@ +import { createContext, useState } from "react"; + +const UserContext = createContext({ + state: { username: undefined }, + actions: { setUsername: () => {} }, +}); + +const UserProvider = ({ children }) => { + const [username, setUsername] = useState(undefined); + + const value = { + state: { username }, + actions: { setUsername }, + }; + + return {children}; +}; + +const { Consumer: UserConsumer } = UserContext; + +export { UserProvider, UserConsumer }; + +export default UserContext; diff --git a/changmin-todolist/src/index.js b/changmin-todolist/src/index.js index 4b591da..3d7c122 100644 --- a/changmin-todolist/src/index.js +++ b/changmin-todolist/src/index.js @@ -4,10 +4,13 @@ import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; import { BrowserRouter } from "react-router-dom"; +import { UserProvider } from "./contexts/UserContext"; ReactDOM.render( - + + + , document.getElementById("root") ); From 24dac30c2041b043858120d92ca7a1526acff0cf Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 12 Jan 2022 05:44:06 +0900 Subject: [PATCH 08/17] Save/Show separate TodoList per user --- .../server/controllers/todoController.js | 26 +++++++++------- changmin-todolist/server/routes/todo.js | 6 ++-- .../src/components/TodoContext.js | 30 ++++++++++++++----- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/changmin-todolist/server/controllers/todoController.js b/changmin-todolist/server/controllers/todoController.js index d59a20f..847e9f7 100644 --- a/changmin-todolist/server/controllers/todoController.js +++ b/changmin-todolist/server/controllers/todoController.js @@ -17,10 +17,14 @@ function connectDB() { } exports.getTodoList = async (req, res) => { + const username = req.body.username; + const pool = connectDB(); const conn = await pool.getConnection(); try { - const [rows] = await conn.query("SELECT * FROM todo"); + const [rows] = await conn.query("SELECT * FROM todo WHERE username = ?", [ + req.body.username, + ]); console.log(rows); res.send(rows); } catch (err) { @@ -31,8 +35,10 @@ exports.getTodoList = async (req, res) => { } }; -exports.postTodoList = async (req, res) => { +exports.editTodoList = async (req, res) => { + const username = req.body.username; const action = req.body.action; + console.log(action); const pool = connectDB(); @@ -41,20 +47,20 @@ exports.postTodoList = async (req, res) => { let sql, values; switch (action.type) { case "CREATE": - sql = "INSERT INTO todo (id, text, done) VALUES (?, ?, ?)"; - values = [action.todo.id, action.todo.text, action.todo.done]; + sql = "INSERT INTO todo (username, id, text, done) VALUES (?, ?, ?, ?)"; + values = [username, action.todo.id, action.todo.text, action.todo.done]; break; case "TOGGLE": - sql = "UPDATE todo SET done = 1 - done WHERE id = ?"; - values = [action.id]; + sql = "UPDATE todo SET done = 1 - done WHERE username = ? AND id = ?"; + values = [username, action.id]; break; case "REMOVE": - sql = "DELETE FROM todo WHERE id = ?"; - values = [action.id]; + sql = "DELETE FROM todo WHERE username = ? AND id = ?"; + values = [username, action.id]; break; case "EDIT": - sql = "UPDATE todo SET text = ? WHERE id = ?"; - values = [action.editText, action.id]; + sql = "UPDATE todo SET text = ? WHERE username = ? AND id = ?"; + values = [action.editText, username, action.id]; break; default: throw new Error(`Undefined Action: ${action.type}`); diff --git a/changmin-todolist/server/routes/todo.js b/changmin-todolist/server/routes/todo.js index 8b71cab..1093e79 100644 --- a/changmin-todolist/server/routes/todo.js +++ b/changmin-todolist/server/routes/todo.js @@ -2,9 +2,7 @@ const express = require("express"); const router = express.Router(); const todoController = require("../controllers/todoController"); -router - .route("/") - .get(todoController.getTodoList) - .post(todoController.postTodoList); +router.post("/get", todoController.getTodoList); +router.post("/edit", todoController.editTodoList); module.exports = router; diff --git a/changmin-todolist/src/components/TodoContext.js b/changmin-todolist/src/components/TodoContext.js index f99dd20..9acb558 100644 --- a/changmin-todolist/src/components/TodoContext.js +++ b/changmin-todolist/src/components/TodoContext.js @@ -6,9 +6,12 @@ import React, { useReducer, useRef, } from "react"; +import UserContext from "../contexts/UserContext"; const todoList = []; +let username; + function todoReducer(state, action) { let newState = []; switch (action.type) { @@ -37,7 +40,10 @@ function todoReducer(state, action) { throw new Error(`Undefined Action: ${action.type}`); } async function sendTodo() { - await axios.post("http://localhost:3001/todo", { action }); + await axios.post("http://localhost:3001/todo/edit", { + username: username, + action, + }); } if (action.type !== "INIT") sendTodo(); return newState; @@ -51,15 +57,23 @@ export function TodoProvider({ children }) { const [state, dispatch] = useReducer(todoReducer, todoList); const nextId = useRef(0); - useEffect(() => { - async function getInitialTodo() { - await axios.get("http://localhost:3001/todo").then((res) => { + const value = useContext(UserContext); + + async function getInitialTodo() { + await axios + .post("http://localhost:3001/todo/get", { username }) + .then((res) => { dispatch({ type: "INIT", todo: res.data }); - nextId.current = res.data[res.data.length - 1].id + 1; + nextId.current = res.data.length + ? res.data[res.data.length - 1].id + 1 + : 1; }); - } - getInitialTodo(); - }, []); + } + + useEffect(() => { + username = value.state.username; + if (username) getInitialTodo(); + }, [value]); return ( From fcd816246c23883ece9946faee54908e9cc3666b Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 03:04:19 +0900 Subject: [PATCH 09/17] Edit server requests for production mode --- changmin-todolist/server/server.js | 4 ++-- changmin-todolist/src/App.js | 6 +++--- changmin-todolist/src/components/LoginForm.js | 14 +++++++------- changmin-todolist/src/components/TodoContext.js | 8 ++++---- changmin-todolist/src/utils/axios.js | 5 +++++ 5 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 changmin-todolist/src/utils/axios.js diff --git a/changmin-todolist/server/server.js b/changmin-todolist/server/server.js index 4a45651..044689d 100644 --- a/changmin-todolist/server/server.js +++ b/changmin-todolist/server/server.js @@ -13,8 +13,8 @@ app.use(cors({ origin: true, credentials: true })); app.use(bodyParser.json()); app.use(cookieParser()); -app.use("/todo", todoRouter); -app.use("/user", userRouter); +app.use("/api/todo", todoRouter); +app.use("/api/user", userRouter); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`); diff --git a/changmin-todolist/src/App.js b/changmin-todolist/src/App.js index b042ce4..8cf21cf 100644 --- a/changmin-todolist/src/App.js +++ b/changmin-todolist/src/App.js @@ -6,9 +6,9 @@ import LoginForm from "./components/LoginForm"; import MenuTemplate from "./components/MenuTemplate"; import { TodoProvider } from "./components/TodoContext"; import { useContext, useEffect } from "react"; -import axios from "axios"; import Cookies from "universal-cookie"; import UserContext from "./contexts/UserContext"; +import { TodoAPI } from "./utils/axios"; const cookies = new Cookies(); @@ -38,8 +38,8 @@ const App = () => { const { state, actions } = useContext(UserContext); async function verifyToken() { - await axios - .post("http://localhost:3001/user/auth", null, { + await TodoAPI + .post("/user/auth", null, { withCredentials: true, }) .then((res) => { diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index 00f17d1..4f9082b 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -1,9 +1,9 @@ -import axios from "axios"; import React, { useContext, useRef, useState } from "react"; import { useNavigate } from "react-router"; import styled from "styled-components"; import Cookies from "universal-cookie"; import UserContext from "../contexts/UserContext"; +import { TodoAPI } from "../utils/axios"; const cookies = new Cookies(); @@ -106,8 +106,8 @@ function LoginForm() { } async function checkUser() { - await axios - .post("http://localhost:3001/user/login", inputs, { + await TodoAPI + .post("/user/login", inputs, { withCredentials: true, }) .then((res) => { @@ -179,8 +179,8 @@ function LoginForm() { } async function checkUser() { - await axios - .post("http://localhost:3001/user/register", inputs) + await TodoAPI + .post("/user/register", inputs) .then((res) => { switch (res.data) { case "OK": @@ -225,8 +225,8 @@ function LoginForm() { const onUnregister = (e) => { e.preventDefault(); async function checkUser() { - await axios - .post("http://localhost:3001/user/unregister", { + await TodoAPI + .post("/user/unregister", { username: state.username, }) .then((res) => { diff --git a/changmin-todolist/src/components/TodoContext.js b/changmin-todolist/src/components/TodoContext.js index 9acb558..d2fd551 100644 --- a/changmin-todolist/src/components/TodoContext.js +++ b/changmin-todolist/src/components/TodoContext.js @@ -1,4 +1,3 @@ -import axios from "axios"; import React, { createContext, useContext, @@ -7,6 +6,7 @@ import React, { useRef, } from "react"; import UserContext from "../contexts/UserContext"; +import { TodoAPI } from "../utils/axios"; const todoList = []; @@ -40,7 +40,7 @@ function todoReducer(state, action) { throw new Error(`Undefined Action: ${action.type}`); } async function sendTodo() { - await axios.post("http://localhost:3001/todo/edit", { + await TodoAPI.post("/todo/edit", { username: username, action, }); @@ -60,8 +60,8 @@ export function TodoProvider({ children }) { const value = useContext(UserContext); async function getInitialTodo() { - await axios - .post("http://localhost:3001/todo/get", { username }) + await TodoAPI + .post("/todo/get", { username }) .then((res) => { dispatch({ type: "INIT", todo: res.data }); nextId.current = res.data.length diff --git a/changmin-todolist/src/utils/axios.js b/changmin-todolist/src/utils/axios.js new file mode 100644 index 0000000..4e0d62c --- /dev/null +++ b/changmin-todolist/src/utils/axios.js @@ -0,0 +1,5 @@ +import axios from "axios"; + +export const TodoAPI = axios.create({ + baseURL: process.env.NODE_ENV === "development" ? "http://localhost:3001/api" : "/api" +}) \ No newline at end of file From 17ea1fb088a2829cb0417f65a0867e6b72b038ae Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 04:45:49 +0900 Subject: [PATCH 10/17] Fix: Force redirect to Login page not working on first render --- changmin-todolist/src/App.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changmin-todolist/src/App.js b/changmin-todolist/src/App.js index 8cf21cf..6ea4115 100644 --- a/changmin-todolist/src/App.js +++ b/changmin-todolist/src/App.js @@ -55,6 +55,8 @@ const App = () => { useEffect(() => { if (cookies.get("user") && cookies.get("user") !== "undefined") verifyToken(); + else + actions.setUsername(null); }, [state]); return ( From 1c2b4693b42ea4bb58564744d038295b9dceddd3 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 04:45:49 +0900 Subject: [PATCH 11/17] Fix: Force redirect to Login page not working on first render --- changmin-todolist/src/App.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changmin-todolist/src/App.js b/changmin-todolist/src/App.js index b042ce4..0166d96 100644 --- a/changmin-todolist/src/App.js +++ b/changmin-todolist/src/App.js @@ -55,6 +55,8 @@ const App = () => { useEffect(() => { if (cookies.get("user") && cookies.get("user") !== "undefined") verifyToken(); + else + actions.setUsername(null); }, [state]); return ( From 057896799f24730a61f6b1d59eaf1bae922a947f Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 03:04:19 +0900 Subject: [PATCH 12/17] Edit server requests for production mode --- changmin-todolist/server/server.js | 4 ++-- changmin-todolist/src/App.js | 12 +++++------ changmin-todolist/src/components/LoginForm.js | 20 +++++++++---------- .../src/components/TodoContext.js | 18 ++++++++--------- changmin-todolist/src/utils/axios.js | 8 ++++++++ 5 files changed, 32 insertions(+), 30 deletions(-) create mode 100644 changmin-todolist/src/utils/axios.js diff --git a/changmin-todolist/server/server.js b/changmin-todolist/server/server.js index 4a45651..044689d 100644 --- a/changmin-todolist/server/server.js +++ b/changmin-todolist/server/server.js @@ -13,8 +13,8 @@ app.use(cors({ origin: true, credentials: true })); app.use(bodyParser.json()); app.use(cookieParser()); -app.use("/todo", todoRouter); -app.use("/user", userRouter); +app.use("/api/todo", todoRouter); +app.use("/api/user", userRouter); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`); diff --git a/changmin-todolist/src/App.js b/changmin-todolist/src/App.js index 0166d96..d85fbda 100644 --- a/changmin-todolist/src/App.js +++ b/changmin-todolist/src/App.js @@ -6,9 +6,9 @@ import LoginForm from "./components/LoginForm"; import MenuTemplate from "./components/MenuTemplate"; import { TodoProvider } from "./components/TodoContext"; import { useContext, useEffect } from "react"; -import axios from "axios"; import Cookies from "universal-cookie"; import UserContext from "./contexts/UserContext"; +import { TodoAPI } from "./utils/axios"; const cookies = new Cookies(); @@ -38,10 +38,9 @@ const App = () => { const { state, actions } = useContext(UserContext); async function verifyToken() { - await axios - .post("http://localhost:3001/user/auth", null, { - withCredentials: true, - }) + await TodoAPI.post("/user/auth", null, { + withCredentials: true, + }) .then((res) => { actions.setUsername(res.data); }) @@ -55,8 +54,7 @@ const App = () => { useEffect(() => { if (cookies.get("user") && cookies.get("user") !== "undefined") verifyToken(); - else - actions.setUsername(null); + else actions.setUsername(null); }, [state]); return ( diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index 00f17d1..c81b5af 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -1,9 +1,10 @@ -import axios from "axios"; import React, { useContext, useRef, useState } from "react"; import { useNavigate } from "react-router"; import styled from "styled-components"; import Cookies from "universal-cookie"; import UserContext from "../contexts/UserContext"; +import { TodoAPI } from "../utils/axios"; +import { useTodoDispatch } from "./TodoContext"; const cookies = new Cookies(); @@ -106,10 +107,9 @@ function LoginForm() { } async function checkUser() { - await axios - .post("http://localhost:3001/user/login", inputs, { - withCredentials: true, - }) + await TodoAPI.post("/user/login", inputs, { + withCredentials: true, + }) .then((res) => { switch (res.data) { case "OK": @@ -179,8 +179,7 @@ function LoginForm() { } async function checkUser() { - await axios - .post("http://localhost:3001/user/register", inputs) + await TodoAPI.post("/user/register", inputs) .then((res) => { switch (res.data) { case "OK": @@ -225,10 +224,9 @@ function LoginForm() { const onUnregister = (e) => { e.preventDefault(); async function checkUser() { - await axios - .post("http://localhost:3001/user/unregister", { - username: state.username, - }) + await TodoAPI.post("/user/unregister", { + username: state.username, + }) .then((res) => { switch (res.data) { case "OK": diff --git a/changmin-todolist/src/components/TodoContext.js b/changmin-todolist/src/components/TodoContext.js index 9acb558..00a3f6f 100644 --- a/changmin-todolist/src/components/TodoContext.js +++ b/changmin-todolist/src/components/TodoContext.js @@ -1,4 +1,3 @@ -import axios from "axios"; import React, { createContext, useContext, @@ -7,6 +6,7 @@ import React, { useRef, } from "react"; import UserContext from "../contexts/UserContext"; +import { TodoAPI } from "../utils/axios"; const todoList = []; @@ -40,7 +40,7 @@ function todoReducer(state, action) { throw new Error(`Undefined Action: ${action.type}`); } async function sendTodo() { - await axios.post("http://localhost:3001/todo/edit", { + await TodoAPI.post("/todo/edit", { username: username, action, }); @@ -60,14 +60,12 @@ export function TodoProvider({ children }) { const value = useContext(UserContext); async function getInitialTodo() { - await axios - .post("http://localhost:3001/todo/get", { username }) - .then((res) => { - dispatch({ type: "INIT", todo: res.data }); - nextId.current = res.data.length - ? res.data[res.data.length - 1].id + 1 - : 1; - }); + await TodoAPI.post("/todo/get", { username }).then((res) => { + dispatch({ type: "INIT", todo: res.data }); + nextId.current = res.data.length + ? res.data[res.data.length - 1].id + 1 + : 1; + }); } useEffect(() => { diff --git a/changmin-todolist/src/utils/axios.js b/changmin-todolist/src/utils/axios.js new file mode 100644 index 0000000..4700d25 --- /dev/null +++ b/changmin-todolist/src/utils/axios.js @@ -0,0 +1,8 @@ +import axios from "axios"; + +export const TodoAPI = axios.create({ + baseURL: + process.env.NODE_ENV === "development" + ? "http://localhost:3001/api" + : "/api", +}); From 3c9db91be82b8ed0ad5cacb1a5697a5516b261d4 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 06:17:14 +0900 Subject: [PATCH 13/17] Fix: Clear Todolist immediately after logout/unregister --- changmin-todolist/src/components/LoginForm.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index c81b5af..72ce250 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -79,6 +79,7 @@ function LoginForm() { const passwordCheckInput = useRef(); // PasswordCheck 입력 focus 위해 사용 const [passwordCheckDisplay, setPasswordCheckDisplay] = useState(false); // PasswordCheck 입력 표시 여부 const navigate = useNavigate(); // 로그인 성공 후 navigate 위해 사용 + const dispatch = useTodoDispatch(); const setPasswordCheckInput = (node) => { if (node) passwordCheckInput.current = node; @@ -216,6 +217,7 @@ function LoginForm() { e.preventDefault(); actions.setUsername(null); cookies.remove("user"); + dispatch({ type: "INIT", todo: [] }); alert("로그아웃되었습니다."); navigate("/"); @@ -231,6 +233,7 @@ function LoginForm() { switch (res.data) { case "OK": alert(`${state.username} 계정을 성공적으로 삭제하였습니다.`); + dispatch({ type: "INIT", todo: [] }); navigate("/"); break; default: From 47aa8dc5c94dc5e9d1d08b8bc003206b0efa24e9 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 06:20:38 +0900 Subject: [PATCH 14/17] Remove the user's todos upon unregister --- changmin-todolist/server/controllers/userController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changmin-todolist/server/controllers/userController.js b/changmin-todolist/server/controllers/userController.js index f6c8205..ed06557 100644 --- a/changmin-todolist/server/controllers/userController.js +++ b/changmin-todolist/server/controllers/userController.js @@ -106,6 +106,9 @@ exports.removeUser = async (req, res) => { await conn.query("DELETE FROM user WHERE username = ?", [ req.body.username, ]); + await conn.query("DELETE FROM todo WHERE username = ?", [ + req.body.username, + ]); res.send("OK"); } catch (err) { console.log(err); From 4dbc338f027407056ed7169976eb3f598a54006c Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 19:34:36 +0900 Subject: [PATCH 15/17] Fix: Widen acceptable range of username length from 8~20 to 4~20 --- changmin-todolist/src/components/LoginForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index 72ce250..d4e90a8 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -153,8 +153,8 @@ function LoginForm() { return; } - if (username.length < 8 || username.length > 20) { - alert("Username은 8~20글자만 가능합니다."); + if (username.length < 4 || username.length > 20) { + alert("Username은 4~20글자만 가능합니다."); usernameInput.current.focus(); return; } From 20318d4e5e45e19e7cfa6c79388aeca9964be110 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 19:49:00 +0900 Subject: [PATCH 16/17] Change page title with react-router --- changmin-todolist/public/index.html | 2 +- changmin-todolist/src/App.js | 24 ++++++++++++++++++++++-- changmin-todolist/src/Page.js | 12 ++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 changmin-todolist/src/Page.js diff --git a/changmin-todolist/public/index.html b/changmin-todolist/public/index.html index aa069f2..b3b8152 100644 --- a/changmin-todolist/public/index.html +++ b/changmin-todolist/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Changmin TodoList diff --git a/changmin-todolist/src/App.js b/changmin-todolist/src/App.js index d85fbda..95268bc 100644 --- a/changmin-todolist/src/App.js +++ b/changmin-todolist/src/App.js @@ -9,6 +9,7 @@ import { useContext, useEffect } from "react"; import Cookies from "universal-cookie"; import UserContext from "./contexts/UserContext"; import { TodoAPI } from "./utils/axios"; +import Page from "./Page"; const cookies = new Cookies(); @@ -77,10 +78,29 @@ const App = () => { : + state.username !== null ? ( + +
+ + ) : ( + + ) + } + /> + + + + ) : ( + + + + ) } /> - } /> diff --git a/changmin-todolist/src/Page.js b/changmin-todolist/src/Page.js new file mode 100644 index 0000000..49ca562 --- /dev/null +++ b/changmin-todolist/src/Page.js @@ -0,0 +1,12 @@ +import { useEffect } from "react"; + +const Page = (props) => { + useEffect(() => { + document.title = `${ + props.title ? `${props.title} - ` : "" + }Changmin TodoList`; + }, [props.title]); + return props.children; +}; + +export default Page; From 5fbbc859895f79bfee9f747cad269ee00375ac66 Mon Sep 17 00:00:00 2001 From: Jakads Date: Wed, 19 Jan 2022 19:56:41 +0900 Subject: [PATCH 17/17] Display confirm window for unregisteration --- changmin-todolist/src/components/LoginForm.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js index d4e90a8..d127edb 100644 --- a/changmin-todolist/src/components/LoginForm.js +++ b/changmin-todolist/src/components/LoginForm.js @@ -248,7 +248,12 @@ function LoginForm() { actions.setUsername(null); cookies.remove("user"); } - checkUser(); + if ( + window.confirm( + `정말 ${state.username} 계정을 삭제하시겠습니까? 이 계정의 정보 및 투두리스트가 영구적으로 삭제되며, 되돌릴 수 없습니다.` + ) + ) + checkUser(); }; if (state.username) {