diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bde63e38330a4b32dda1d7eaaf8adaa837e87173..b5fcfa395b537bdb862331759c1f85a6db4d7c83 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,12 +5,22 @@ import { Login, Signup } from "./auth"; import { Board } from "./board"; import { Header, Body } from "./home"; import Posting from "./post/posting"; +import { AuthProvider } from "./auth/auth.context"; +import Layout from "./commons/layout"; export const App = () => { return ( - } /> + {/* }> */} + + + + } + /> } /> }> @@ -18,6 +28,7 @@ export const App = () => { } /> } /> + {/* */} ); diff --git a/frontend/src/auth/RequireAuth.tsx b/frontend/src/auth/RequireAuth.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3556a624f9942e70ce74d0f447ce2f9ec2fceb58 --- /dev/null +++ b/frontend/src/auth/RequireAuth.tsx @@ -0,0 +1,15 @@ +import React, { FC } from "react"; +import { Navigate, useLocation } from "react-router-dom"; +import { useAuth } from "./auth.context"; + +export const RequireAuth: FC<{ children: JSX.Element }> = ({ children }) => { + const { user } = useAuth(); + const location = useLocation(); + + if (!user.isLoggedIn) { + return ( + + ); + } + return children; +}; diff --git a/frontend/src/auth/auth.context.tsx b/frontend/src/auth/auth.context.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0d8bd6605d0e54225d9dcabe5e9cedabd0a13db2 --- /dev/null +++ b/frontend/src/auth/auth.context.tsx @@ -0,0 +1,49 @@ +import React, { + createContext, + FC, + ReactNode, + useContext, + useState, +} from "react"; +import { IUser } from "../types"; +import { getLocalUser, handleLogin, handleLogout } from "./auth.helper"; + +interface IAuthContext { + login: (email: string, password: string, cb?: VoidFunction) => Promise; + logout: (cb?: VoidFunction) => Promise; + user: IUser; +} + +const AuthContext = createContext({ + login: async () => {}, + logout: async () => {}, + user: { isLoggedIn: false }, +}); + +export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [user, setUser] = useState(getLocalUser()); + + const login = async ( + email: string, + password: string, + cb: VoidFunction = () => {} + ) => { + const user = await handleLogin(email, password); + setUser(user); + cb(); + }; + + const logout = async (cb: VoidFunction = () => {}) => { + await handleLogout(); + setUser({ ...user, isLoggedIn: false }); + cb(); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); diff --git a/frontend/src/auth/auth.helper.tsx b/frontend/src/auth/auth.helper.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3bcdcc73f4dfc2a587b9a5147a34fdfef64c5d95 --- /dev/null +++ b/frontend/src/auth/auth.helper.tsx @@ -0,0 +1,60 @@ +import { authApi } from "../apis"; +import { IUser } from "../types"; + +const LOCAL_USER_INFO = "survey-user-info"; + +/** + * 1. 백엔드 로그인을 호출하여 로그인 정보를 얻습니다. + * 2. 로컬 저장소에 저장합니다. + * 3. 사용자 정보를 반환합니다. + * @param email 이메일 + * @param password 비밀번호 + * @returns 사용자 정보 + */ +export const handleLogin = async (email: string, password: string) => { + const user: IUser = await authApi.login(email, password); + // 로컬 저장소에는 로그인 여부만 저장 + localStorage.setItem( + LOCAL_USER_INFO, + JSON.stringify({ + isLoggedIn: user.isLoggedIn, + }) + ); + return user; +}; + +/** + * 로컬 저장소의 정보를 삭제합니다. + * 백엔드 로그아웃을 호출하여 쿠키를 제거합니다. + */ +export const handleLogout = async () => { + console.log("handle logout called"); + localStorage.removeItem(LOCAL_USER_INFO); + try { + await authApi.logout(); + } catch (error) { + console.log("logout 중에 에러 발생:", error); + } +}; + +/** + * 1. 로컬 저장소에 저장된 사용자 로그인 정보를 반환합니다. + * 2. 로컬 저장소에 정보가 없으면 { isLoggedIn: false }를 반환합니다. + * @returns 로컬 저장소에 저장된 사용자 정보 + */ +export const getLocalUser = () => { + console.log("get local user called"); + const userInfo = localStorage.getItem(LOCAL_USER_INFO); + const user: IUser = { isLoggedIn: false }; + if (!userInfo) { + return user; + } + + const userData = JSON.parse(userInfo); + if (userData.isLoggedIn) { + user.isLoggedIn = true; + } else { + user.isLoggedIn = false; + } + return user; +}; diff --git a/frontend/src/auth/login.tsx b/frontend/src/auth/login.tsx index 1d09952da9d97d891adfe1a80e3efebafdd7be42..ff5371f2183e50801053e1001608bd9107f93bad 100644 --- a/frontend/src/auth/login.tsx +++ b/frontend/src/auth/login.tsx @@ -1,90 +1,109 @@ -import { Link } from "react-router-dom"; -import React, { useState, FormEventHandler } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import React, { useState, useEffect, FormEvent } from "react"; +import { LoginUser } from "../types"; +import { catchErrors } from "../helpers"; +import { useAuth } from "./auth.context"; -interface login { - id: string; - password: string; -} +export default function Login() { + const [user, setUser] = useState({ + email: "", + password: "", + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [disabled, setDisabled] = useState(false); + const [success, setSuccess] = useState(false); + const navigate = useNavigate(); + const { login } = useAuth(); -// const fake = { id: "asdf", password: "qwer" }; + useEffect(() => { + setDisabled(!(user.email && user.password)); + }, [user]); -function Logindata() { - const [id, setId] = useState(""); - const [password, setPassword] = useState(""); + function handleChange(event: React.ChangeEvent) { + const { name, value } = event.currentTarget; + setUser({ ...user, [name]: value }); + } - function login() { - fetch(`http://localhost:3000/api/auth/login`, { - method: "POST", + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + try { + setError(""); + console.log("user data", user); - body: JSON.stringify({ - email: `${id}`, - password: `${password}`, - }), - }).then((response) => { - console.log(response.json()); - }); + // setLoading(true); + await login(user.email, user.password, () => { + navigate("/", { replace: true }); + }); + // console.log("서버연결됬나요", res); + // console.log("로그인"); + // setSuccess(true); + // setError(""); + } catch (error) { + console.log("에러발생"); + // setError("이메일 혹은 비밀번호를 다시 입력해주세요."); + catchErrors(error, setError); + } finally { + setLoading(false); + } } + return ( -
- +
+ Travel Report +
+
+
+
+
+ setId(e.target.value)} - /> - + + setPassword(e.target.value)} - /> - - -
- ); -} - -export default function Login() { - return ( -
- {/* */} -
-
- Travel Report -
- -
-
- -
-
-
+ +
+ +
+ +
+
-
+
- {/* */} -
// Login Page +
); } diff --git a/frontend/src/auth/signup.tsx b/frontend/src/auth/signup.tsx index 3919dc2c328a759bea93607a040bf37d05f59516..4bca70e72267bf277579c6c5a690d0a90e2b89ad 100644 --- a/frontend/src/auth/signup.tsx +++ b/frontend/src/auth/signup.tsx @@ -1,5 +1,5 @@ import React, { FormEvent, useEffect, useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, Navigate } from "react-router-dom"; import { authApi } from "../apis"; import { catchErrors } from "../helpers"; import { SpinnerIcon } from "../icons"; @@ -17,7 +17,6 @@ export default function Signup() { const [error, setError] = useState(""); const [disabled, setDisabled] = useState(false); const [success, setSuccess] = useState(false); - const navigate = useNavigate(); useEffect(() => { setDisabled(!(user.name && user.email && user.password && user.password2)); @@ -65,7 +64,7 @@ export default function Signup() { if (success) { alert("회원가입 되었습니다"); - navigate("/login", { replace: true }); + return ; } return ( diff --git a/frontend/src/commons/layout.tsx b/frontend/src/commons/layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..28615c1968fe09b2b1b45d2eba98ca9f71e1e5f8 --- /dev/null +++ b/frontend/src/commons/layout.tsx @@ -0,0 +1,6 @@ +import React, { ReactNode } from "react"; +import { AuthProvider } from "../auth/auth.context"; + +export default function Layout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/frontend/src/home/header.tsx b/frontend/src/home/header.tsx index 2d78c8d8fcbd1cd4cfad3921ab244a350d481dc4..092bb7f2d91dc0b998a9f8a9cd6737fbd105ecb9 100644 --- a/frontend/src/home/header.tsx +++ b/frontend/src/home/header.tsx @@ -1,36 +1,63 @@ -import React from "react"; -import {Link, Outlet} from "react-router-dom"; +import React, { useState } from "react"; +import { Link, Outlet } from "react-router-dom"; +import { useAuth } from "../auth/auth.context"; import "tailwindcss/tailwind.css"; - export default function Header() { - return ( -
-
- -
- - +
+ -
- -
+ + +
+
-
- ); - } \ No newline at end of file + +
+ ); +} diff --git a/frontend/src/post/posting.tsx b/frontend/src/post/posting.tsx index 888c514df22e4689d29825b0ed825ea3a9eff788..28cd7266492d64f1a28200c1e1c5456129a0c883 100644 --- a/frontend/src/post/posting.tsx +++ b/frontend/src/post/posting.tsx @@ -70,7 +70,7 @@ function SelectTheme() { - + {/* */} ); } diff --git a/frontend/src/types/index.tsx b/frontend/src/types/index.tsx index 49f5d3af94043f5486e2195a465dbb0150a69f97..fc6d7803a11974001d481e8836d6eba24a7f5947 100644 --- a/frontend/src/types/index.tsx +++ b/frontend/src/types/index.tsx @@ -13,3 +13,14 @@ export interface SignupUser { name: string; password: string; } + +export interface LoginUser { + email: string; + password: string; +} + +export interface IUser { + email?: string; + isLoggedIn: boolean; + _id?: string; +} diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index b1b2ea5c0cd6c488f1d39ee488055673256052e2..481ae5da5c459ad1c9ce8f80250c76e9ad6b4af1 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -25,15 +25,18 @@ export const login = asyncWrap(async (req, res) => { return res.status(401).send("잘못된 비밀번호를 입력하셨습니다"); } // 3) 비밀번호가 맞으면 토큰 생성 - const token = jwt.sign({ userId: user.id }, jwtCofig.secret, { //userId를 토큰에다 넣는 중. + const token = jwt.sign({ userId: user.id }, jwtCofig.secret, { + //userId를 토큰에다 넣는 중. + expiresIn: jwtCofig.expires, }); - // 4) 토큰을 쿠키에 저장 - res.cookie(cookieConfig.name, token, {//token은 쿠키에 무엇을 실렸는가 이다. 항상 갖고 있다가 홈페이지 들어가면 서버로 접속 - maxAge: cookieConfig.maxAge,// 이 기간 한에서만 유효 - path: "/",//어떠한 경로에 관해서만 쓴다. 지금은 전부에 쓴다. - httpOnly: envConfig.mode === "production", //false 면 브라우저에서 쿠키를 조작, true면 조작할 수 있다. - secure: envConfig.mode === "production", //true 면 https 를 통해서만 쿠키 전달, false면 + res.cookie(cookieConfig.name, token, { + //token은 쿠키에 무엇을 실렸는가 이다. 항상 갖고 있다가 홈페이지 들어가면 서버로 접속 + maxAge: cookieConfig.maxAge, // 이 기간 내에서만 유효 + path: "/", //어떠한 경로에 관해서만 쓴다. 지금은 전부에 쓴다. + httpOnly: envConfig.mode === "production", //false면 브라우저에서 쿠키를 조작, true면 조작할 수 없다. + secure: envConfig.mode === "production", //true 면 https를 통해서만 쿠키 전달, false면 + }); // 5) 사용자 반환 res.json({ @@ -59,7 +62,7 @@ export const requireLogin = asyncWrap(async (reqExp, res, next) => { const decodedUser = jwt.verify(token, jwtCofig.secret); // 아까보낸 토근을 디코딩중. // 3) 요청 객체에 토큰 사용자 객체 추가 req.auth = decodedUser; - next();// 에러가 안나오면 next 사용, 나오면 catch쪽으로. + next(); // 에러가 안나오면 next 사용, 나오면 catch쪽으로. } catch (error) { res.clearCookie(cookieConfig.name); console.log("error in requreLogin===\n", error); @@ -84,13 +87,13 @@ export const signup = asyncWrap(async (req, res) => { if (userExist) { return res.status(422).send(`${email} 사용자가 이미 존재합니다`); } - // 3) 비밀번호 암호화 + // 3) 비밀번호 암호화는 useDb.createUser에서 처리 const hash = await bcrypt.hash(password, 10); // 4) 새로운 사용자 만들기 const newUser = await userDb.createUser({ email, - password: hash, + password, }); - // 5) 사용자 반환 + // 5) 사용자 반환(내부적으로 몽구스가 toJSON() 호출) res.json(newUser); }); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index e8b0db82d11788769e9dad164e3305d733f8ace2..8c4108433792ad2ed0074291c12dd0814a4dc6f9 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -10,5 +10,5 @@ export const createUser = asyncWrap(async (req, res) => { const user = req.body; console.log("user body", user); const newUser = await userDb.createUser(user); - return res.json(user); + return res.json(newUser); }); diff --git a/src/db/user.db.ts b/src/db/user.db.ts index a7c794ca22f8d6bad9225d444faf55715c9573cf..e32309a18d57d7b39522d9a4bc4b43a0289f6e49 100644 --- a/src/db/user.db.ts +++ b/src/db/user.db.ts @@ -1,7 +1,10 @@ +import bcrypt from "bcryptjs"; import { IUser, User } from "../models"; export const createUser = async (user: IUser) => { - const newUser = await User.create(user); + // 비밀번호 암호화 + const hash = await bcrypt.hash(user.password, 10); + const newUser = await User.create({ email: user.email, password: hash }); return newUser; }; diff --git a/src/models/user.model.ts b/src/models/user.model.ts index cd084624ed678fb169b42c4d6a85e56095d0bb3f..84cde5ba7085311ea14be3543d0b5c893aabef3d 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -1,4 +1,4 @@ -import { model, Schema, Types } from "mongoose"; +import { model, Schema, Types, version } from "mongoose"; export interface IUser { email: string; @@ -12,16 +12,27 @@ const validateEmail = (email: string) => { return re.test(email); }; -const schema = new Schema({ - email: { - type: String,//mongoose type 인 String 으로 일반적인 string 과는 겉으로는 대문자 차이 - rquired: true, - unique: true, - validate: [validateEmail, "이메일을 입력해주세요"], +const schema = new Schema( + { + email: { + type: String, //mongoose type인 String으로 일반적인 string과는 겉으로는 대문자 차이 + rquired: true, + unique: true, + validate: [validateEmail, "이메일을 입력해주세요"], + }, + name: { type: String }, + password: { type: String, required: true, select: false }, + role: { type: Schema.Types.ObjectId, ref: "Role" }, }, - name: { type: String }, - password: { type: String, required: true, select: false }, - role: { type: Schema.Types.ObjectId, ref: "Role" }, -}); + { + toJSON: { + versionKey: false, + transform(doc, ret, options) { + delete ret.password; + }, + }, + } + ); + export default model("User", schema);