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/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/server/controllers/todoController.js b/changmin-todolist/server/controllers/todoController.js
index 15a4092..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,32 +35,38 @@ 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();
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 (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 = ${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 = ${action.id}`;
+ sql = "DELETE FROM todo WHERE username = ? AND id = ?";
+ values = [username, action.id];
break;
case "EDIT":
- sql = `UPDATE todo SET text = '${action.editText}' WHERE id = ${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}`);
}
- 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 9acef6a..ed06557 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,
@@ -25,25 +26,32 @@ exports.createToken = async (req, res) => {
const conn = await pool.getConnection();
try {
- const [rows] = await conn.query(
- `SELECT * FROM user WHERE username = '${req.body.username}' AND password = '${req.body.password}'`
- );
+ const [rows] = await conn.query("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");
}
@@ -62,14 +70,20 @@ 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) {
- 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");
@@ -89,9 +103,12 @@ 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,
+ ]);
+ await conn.query("DELETE FROM todo WHERE username = ?", [
+ req.body.username,
+ ]);
res.send("OK");
} catch (err) {
console.log(err);
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/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 2007f87..95268bc 100644
--- a/changmin-todolist/src/App.js
+++ b/changmin-todolist/src/App.js
@@ -1,13 +1,15 @@
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";
import { TodoProvider } from "./components/TodoContext";
-import { useEffect, useState } from "react";
-import axios from "axios";
+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();
@@ -34,18 +36,18 @@ const MenuStyle = {
};
const App = () => {
- const [currentUsername, setCurrentUsername] = useState();
+ 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) => {
- setCurrentUsername(res.data);
+ actions.setUsername(res.data);
})
.catch((err) => {
console.log("Invalid token, removing the cookie");
+ actions.setUsername(null);
cookies.remove("user");
});
}
@@ -53,7 +55,8 @@ const App = () => {
useEffect(() => {
if (cookies.get("user") && cookies.get("user") !== "undefined")
verifyToken();
- }, []);
+ else actions.setUsername(null);
+ }, [state]);
return (
@@ -66,20 +69,36 @@ 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;
diff --git a/changmin-todolist/src/components/LoginForm.js b/changmin-todolist/src/components/LoginForm.js
index 8bdda38..d127edb 100644
--- a/changmin-todolist/src/components/LoginForm.js
+++ b/changmin-todolist/src/components/LoginForm.js
@@ -1,8 +1,10 @@
-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";
+import { TodoAPI } from "../utils/axios";
+import { useTodoDispatch } from "./TodoContext";
const cookies = new Cookies();
@@ -64,15 +66,25 @@ 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: "",
+ 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 dispatch = useTodoDispatch();
+
+ const setPasswordCheckInput = (node) => {
+ if (node) passwordCheckInput.current = node;
+ passwordCheckInput.current.focus();
+ };
const onChange = (e) => {
const { value, name } = e.target;
@@ -96,14 +108,13 @@ function LoginForm({ currentUsername, setCurrentUsername }) {
}
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":
- setCurrentUsername(username);
+ actions.setUsername(username);
alert(`성공적으로 로그인되었습니다. 안녕하세요, ${username}님!`);
navigate("/");
break;
@@ -142,9 +153,34 @@ function LoginForm({ currentUsername, setCurrentUsername }) {
return;
}
+ if (username.length < 4 || username.length > 20) {
+ alert("Username은 4~20글자만 가능합니다.");
+ usernameInput.current.focus();
+ return;
+ }
+ if (password.length < 8 || password.length > 20) {
+ alert("Password는 8~20글자만 가능합니다.");
+ passwordInput.current.focus();
+ 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)
+ await TodoAPI.post("/user/register", inputs)
.then((res) => {
switch (res.data) {
case "OK":
@@ -152,20 +188,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("알 수 없는 오류가 발생했습니다.");
@@ -182,8 +215,9 @@ function LoginForm({ currentUsername, setCurrentUsername }) {
const onLogout = (e) => {
e.preventDefault();
- setCurrentUsername(undefined);
+ actions.setUsername(null);
cookies.remove("user");
+ dispatch({ type: "INIT", todo: [] });
alert("로그아웃되었습니다.");
navigate("/");
@@ -192,14 +226,14 @@ function LoginForm({ currentUsername, setCurrentUsername }) {
const onUnregister = (e) => {
e.preventDefault();
async function checkUser() {
- await axios
- .post("http://localhost:3001/user/unregister", {
- username: currentUsername,
- })
+ await TodoAPI.post("/user/unregister", {
+ username: state.username,
+ })
.then((res) => {
switch (res.data) {
case "OK":
- alert(`${username} 계정을 성공적으로 삭제하였습니다.`);
+ alert(`${state.username} 계정을 성공적으로 삭제하였습니다.`);
+ dispatch({ type: "INIT", todo: [] });
navigate("/");
break;
default:
@@ -211,17 +245,22 @@ function LoginForm({ currentUsername, setCurrentUsername }) {
console.log(err);
alert("알 수 없는 오류가 발생했습니다.");
});
- setCurrentUsername(undefined);
+ actions.setUsername(null);
cookies.remove("user");
}
- checkUser();
+ if (
+ window.confirm(
+ `정말 ${state.username} 계정을 삭제하시겠습니까? 이 계정의 정보 및 투두리스트가 영구적으로 삭제되며, 되돌릴 수 없습니다.`
+ )
+ )
+ checkUser();
};
- if (!!currentUsername) {
+ if (state.username) {
// 로그인 된 화면
return (
- 안녕하세요, {currentUsername}님!
+ 안녕하세요, {state.username}님!
@@ -253,6 +292,19 @@ function LoginForm({ currentUsername, setCurrentUsername }) {
ref={passwordInput}
/>
+ {passwordCheckDisplay && (
+
+ Password:{" "}
+
+
+ )}
diff --git a/changmin-todolist/src/components/TodoContext.js b/changmin-todolist/src/components/TodoContext.js
index f99dd20..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,
@@ -6,9 +5,13 @@ import React, {
useReducer,
useRef,
} from "react";
+import UserContext from "../contexts/UserContext";
+import { TodoAPI } from "../utils/axios";
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 TodoAPI.post("/todo/edit", {
+ username: username,
+ action,
+ });
}
if (action.type !== "INIT") sendTodo();
return newState;
@@ -51,15 +57,21 @@ export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, todoList);
const nextId = useRef(0);
+ const value = useContext(UserContext);
+
+ async function getInitialTodo() {
+ 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(() => {
- async function getInitialTodo() {
- await axios.get("http://localhost:3001/todo").then((res) => {
- dispatch({ type: "INIT", todo: res.data });
- nextId.current = res.data[res.data.length - 1].id + 1;
- });
- }
- getInitialTodo();
- }, []);
+ username = value.state.username;
+ if (username) getInitialTodo();
+ }, [value]);
return (
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")
);
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",
+});
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"