diff --git a/backend/package-lock.json b/backend/package-lock.json index a5544933432c9c1a3b91bc8b7ee9b9a667dd41df..cbf9625a07d8ad1263d0ad83f979b4c3bd9f8f82 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,8 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-session": "^1.17.3", + "memorystore": "^1.6.7", "moment": "^2.29.4", "mongoose": "^8.0.1", "nodemon": "^3.0.1" @@ -504,6 +506,32 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -803,6 +831,53 @@ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, + "node_modules/memorystore": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.7.tgz", + "integrity": "sha512-OZnmNY/NDrKohPQ+hxp0muBcBKrzKNtHr55DbqSx9hLsYVNnomSAMRAtI7R64t3gf3ID7tHQA7mG4oL3Hu9hdw==", + "dependencies": { + "debug": "^4.3.0", + "lru-cache": "^4.0.3" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/memorystore/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/memorystore/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/memorystore/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/memorystore/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -1087,6 +1162,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1128,6 +1211,11 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -1155,6 +1243,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1396,6 +1492,17 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/backend/package.json b/backend/package.json index 7f53a52bb2382775e9a25bd85794befd841acb82..5ce671e0e7fbd0a00ef23f19c659001fe4a308fa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,6 +17,8 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-session": "^1.17.3", + "memorystore": "^1.6.7", "moment": "^2.29.4", "mongoose": "^8.0.1", "nodemon": "^3.0.1" diff --git a/backend/src/index.js b/backend/src/index.js index 0f262aeaa6b6655a5573aac914d8d192a8ef44cc..222a9a94f0e164a595efa7e361ee6833a82c237e 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -4,7 +4,7 @@ import path from 'path' import process from 'process' import moment from 'moment'; import cookieParser from 'cookie-parser'; - +import session from 'express-session'; import db from './db.js' const app = express(); @@ -14,6 +14,18 @@ db.connectDB(); app.use(express.static(path.join(process.cwd(), '../frontend/build'))); +const maxAge = 1000 * 60 * 1; // 5분(임시) + +app.use(session({ + secret: '12345', + resave: true, + saveUninitialized: true, + cookie: { + secure: false, + maxAge: maxAge + } +})) + app.use(cookieParser()); app.use(express.json()); app.use(express.urlencoded({ extended: false })); @@ -37,19 +49,63 @@ function encode_utf8(s) { return unescape(encodeURIComponent(s)); } -app.post('/login', (req, res) => { +app.post('/login', async (req, res) => { /* TODO: 토큰의 무결성 체크 토큰이 이상이 없다면, 로그인/회원가입 로직을 수행 후 jwt 쿠키를 보낸다. */ - const expires = moment().add('2','m').toDate() - res.cookie('jwt', JSON.stringify(req.body), {expires}); + const expires = moment().add('1','m').toDate() + + // 정보가 없다면 회원 가입 (강제?) + const user = await db.UserModel.find({ user_id: req.body.email }); + if (!user.length) { // 유저가 없다면 회원 가입 후 세션 생성 + let userProfilePicture = req.body.picture || null + const userModel = new db.UserModel({ + user_id: req.body.email, + nickname: req.body.name, + email: req.body.email, + google: { + id: req.body.sub, + profileUrl: userProfilePicture, + }, + }); + await userModel.save(); + console.log('saved') + } + + + req.session.sessionid = req.body.name; //세션 생성 + res.cookie('name', req.body.name, {expires}); //사용자 이름 쿠키 + if(req.session.sessionid){ + console.log(req.session.sessionid); + //res.send('세션 o'); + } + //통과 못하면 에러를 뱉는다 res.send(req.body.name); }); +app.get("/logout", (req, res) => { + console.log("로그아웃"); + if (req.session.sessionid) { + + console.log("로그아웃중입니다!"); + req.session.destroy((err) => { + if (err) { + console.log("세션 삭제시에 에러가 발생했습니다."); + return; + } + console.log("세션이 삭제됐습니다."); + }); + res.clearCookie('name'); + res.send(req.body.name); + } else { + console.log("로그인이 안돼있으시네요?"); + res.send(req.body.name); + } +}); diff --git a/frontend/src/App.js b/frontend/src/App.js index 6315fab6001191c58ea66a8031a50b06734369fb..1182d9d190bad87a8a9caa8ceb06111e012bcf7e 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -2,20 +2,28 @@ import "./App.css"; import {Routes, Route } from 'react-router-dom'; import Header from "./Header.js"; import Main from "./Main.js"; +import Login from "./Login.js"; import Search from "./Search.js"; import GoogleLoginButton from "./GoogleLoginButton.js"; +import React, { useEffect, useState, useContext} from 'react'; +import { UserContext } from './Usercontext.js'; function App() { + + const [ myName, setMyName] = useState(null); + return( <div className="App"> - <Header/> - <Routes> - <Route path="/" element={<Main/>}></Route> - <Route path="/search" element={<Search/>}></Route> - {/* <Route path="/postwrite" element={<PostWrite/>}></Route> */} - </Routes> - <GoogleLoginButton/> - {/* <Footer/> */} + <UserContext.Provider value={setMyName}> + <Header user={myName}></Header> + <Routes> + <Route path="/" element={<Main/>}></Route> + <Route path="/login" element={<Login/>}></Route> + <Route path="/search" element={<Search/>}></Route> + {/* <Route path="/postwrite" element={<PostWrite/>}></Route> */} + </Routes> + </UserContext.Provider> + {/* <Footer/> */} </div> ); } diff --git a/frontend/src/GoogleLoginButton.js b/frontend/src/GoogleLoginButton.js index c0ae0354553b4db0b9e2cdbc60a2e40f21b34eab..572894ab9709f36a5023454fe457bcaa3d4a14c1 100644 --- a/frontend/src/GoogleLoginButton.js +++ b/frontend/src/GoogleLoginButton.js @@ -2,6 +2,8 @@ import { GoogleLogin } from "@react-oauth/google"; import { GoogleOAuthProvider } from "@react-oauth/google"; import { useNavigate, Navigate } from "react-router-dom"; import { useCookies } from 'react-cookie' +import React, { useEffect, useState, useContext} from 'react'; +import { UserContext } from './Usercontext.js'; // 안써도 자동으로 한국 시간을 불러온다. 명확하게 하기 위해 import import moment from 'moment'; @@ -11,37 +13,24 @@ import base64 from 'base-64'; import axios from 'axios'; axios.defaults.withCredentials = true; -/* - -*/ - const GoogleLoginButton = () => { + + const setUserName = useContext(UserContext); + const [cookies, setCookie, removeCookie] = useCookies(); const clientId = '716858812522-rb0pfisq317unkh4so5hvbu16p19kqp8.apps.googleusercontent.com' const navigate = useNavigate(); + const goMain = () => { - navigate("/main"); + navigate("/"); } return ( <> <GoogleOAuthProvider clientId={clientId}> <GoogleLogin onSuccess={(res) => { - /* 발급받은 토큰은 . 을 기준으로 3 개로 나뉜다. - aaaa.bbbb.cccc - - [base64]aaaa: 헤더 - [base64]bbbb: 페이로드 (실질적인 데이터) - [RS256]cccc: 서명 - - RS256 : 암호화 알고리즘, JWT 서명할 때 사용한다고 함 - */ - - // 쿠키 테스트 1분 뒤 만료 - const expires = moment().add('1','m').toDate() - setCookie('cookieTest','hello',{expires}) let datas = res.credential.split('.') const obj = JSON.parse(b64DecodeUnicode(datas[1])); @@ -49,11 +38,13 @@ const GoogleLoginButton = () => { // 토큰을 보내 로그인 로직 처리 // 로그인이 정상적으로 되었다면 쿠키를 등록 - let response = requestLogin(obj); - console.log(cookies.jwt) - if (response) { - goMain(); - } + let response = requestLogin(obj).then( + (val) => { + setUserName(val) + goMain(); + } + ); + }} onFailure={(err) => { console.log("Login Failed"); @@ -79,7 +70,6 @@ async function requestLogin(payloadObj) { method: 'post', // 통신할 방식 data: payloadObj }); - console.log(response) if (response.status === 200) { return response.data; } diff --git a/frontend/src/Header.js b/frontend/src/Header.js index f3d61773084d78b614049907fc9cc691d53b8120..e95d56439335032cfff0c1102f2c0299a2a6493c 100644 --- a/frontend/src/Header.js +++ b/frontend/src/Header.js @@ -1,23 +1,118 @@ import {Link} from "react-router-dom"; import logo from './logo.png'; import './Header.css'; +import cookie from 'react-cookies'; +import React, { useEffect, useState, useContext} from 'react'; +import { useNavigate, Navigate } from "react-router-dom"; +import { UserContext } from './Usercontext.js'; +import axios from 'axios'; +axios.defaults.withCredentials = true; -function Header(){ + +function Welcome(props){ + + return ( + <p> + {props.name?`${props.name}님, 환영합니다`:'로그인하세요.'}<br/> + </p> + ) +} + +function ButtonLink({link, onclick, children}){ + + return ( + <botton onClick = {()=>{ + onclick(link)}}> + {children} + </botton> + ) +} + +function Header({user}){ + //console.log(cookie.load('name')) + const [currentSession, setCurrentSession] = useState(false) + + const setUserName = useContext(UserContext); + const navigate = useNavigate(); + function checkSession_And_Navigate(link){ + if (cookie.load('name')) { + navigate(link); + setCurrentSession(true) + } + else { + setUserName(null) + if (currentSession){ + alert('세션이 만료되었습니다. 로그인 후 이용해 주세요.') + } + else { + alert('로그인 후 이용해 주세요.') + } + + navigate("/login"); + setCurrentSession(false) + } + } + + function dont_CheckSession_And_Navigate(link){ + if (cookie.load('name')) { + setCurrentSession(true) + } + else { + setUserName(null) + if (currentSession){ + alert('세션이 만료되었습니다. 자동으로 로그아웃됩니다.') + } + setCurrentSession(false) + } + navigate(link); + } + + function logOut(){ + let response = requestLogout(); + if (response){ + setUserName(null) + alert('로그아웃되었습니다. 메인 화면으로 돌아갑니다..') + setCurrentSession(false) + navigate('/'); + } + } + return( <div className="header"> - <Link to='/'><img className="logo_image" alt="logo" src={logo}/></Link> - <ul className="menu_list"> - <li><Link to="/">Home</Link></li> - <li><Link to="/search">검색</Link></li> - <li><Link to="/postwrite">포스트 작성</Link></li> - <li><Link to="/login">로그인</Link></li> {/*로그인 여부 로직 구현 필요*/} - {/* { Object.keys(user).length != 0 ? - <li><Link to={`/profile/${getUserId()}`}>profile</Link>/<span onClick={logout}>logout</span></li> : - <li><Link to="/login">login</Link></li> - } */} - </ul> + <ButtonLink link='/' onclick={dont_CheckSession_And_Navigate}> + <img className="logo_image" alt="logo" src={logo}/> + </ButtonLink> + + <ul> + + <Welcome name={user}></Welcome> + + <ul className="menu_list"> + <li><ButtonLink link='/' onclick={dont_CheckSession_And_Navigate}>Home</ButtonLink></li> + <li><ButtonLink link='/search' onclick={checkSession_And_Navigate}>검색</ButtonLink></li> + <li><ButtonLink link='/postwrite' onclick={checkSession_And_Navigate}>포스트 작성</ButtonLink></li> + <li><ButtonLink link='/login' onclick={user?logOut:dont_CheckSession_And_Navigate}>{user?'로그아웃':'로그인'}</ButtonLink></li> {/*로그인 여부 로직 구현 필요*/} + {/* { Object.keys(user).length != 0 ? + <li><Link to={`/profile/${getUserId()}`}>profile</Link>/<span onClick={logout}>logout</span></li> : + <li><Link to="/login">login</Link></li> + } */} + </ul> + </ul> </div> ); } +async function requestLogout() { + const response = await axios({ + url: 'http://localhost:8080/logout', // 통신할 웹문서 + method: 'get', // 통신할 방식 + }); + if (response.status === 200) { + return response.data; + } + else { + return null; + } +} + export default Header; diff --git a/frontend/src/Login.js b/frontend/src/Login.js new file mode 100644 index 0000000000000000000000000000000000000000..8acde8ebf37ebb4cfcf0d8f2717b42f3bfd267ef --- /dev/null +++ b/frontend/src/Login.js @@ -0,0 +1,22 @@ +import { useNavigate } from 'react-router-dom'; +import GoogleLoginButton from "./GoogleLoginButton.js"; + +function Button({history, children}){ + const navigate = useNavigate(); + return( + <button onClick={() => {navigate('/search');}}> + {children} + </button> + ); +} + +function Login() { + return( + <div className="App"> + <h1>로그인 '해줘'</h1> + <GoogleLoginButton></GoogleLoginButton> + </div>) + ; +} + +export default Login; \ No newline at end of file diff --git a/frontend/src/Main.js b/frontend/src/Main.js index 37d91294ed771898ba06cc3e1c9a10f0b4017778..86bc01a319f531b2c073b9e0f7ccd66a51cbb9ca 100644 --- a/frontend/src/Main.js +++ b/frontend/src/Main.js @@ -1,4 +1,6 @@ import { useNavigate } from 'react-router-dom'; +import cookie from 'react-cookies'; +import React, { useState, useEffect, useContext } from 'react'; function Button({history, children}){ const navigate = useNavigate(); @@ -10,6 +12,10 @@ function Button({history, children}){ } function Main() { + + useEffect(() => { + // 컴포넌트가 불러와지면 실행이 되는군. + }); return( <div className="App"> <h1>메인 페이지 입니다.</h1> diff --git a/frontend/src/Usercontext.js b/frontend/src/Usercontext.js new file mode 100644 index 0000000000000000000000000000000000000000..6278fb7652de86e6d770f86871656bd6fa5699fc --- /dev/null +++ b/frontend/src/Usercontext.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const UserContext = createContext(null); \ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/index.js index 532f33b6b06e3fb01b749b217e7da9f09c8fb5f1..3a2e456c1996ed173a7673527e6e790c6dbfa465 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -8,7 +8,7 @@ const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <BrowserRouter> - <App/> + <App/> </BrowserRouter> </React.StrictMode> );