Commit fda21b20 authored by Yoon, Daeki's avatar Yoon, Daeki 😅
Browse files

Merge branch 'login-api' into main-login-merge

parents 84859bc2 ed3d6ae9
# 1. 첫 번째 로그인
1. [카카오 로그인] 버튼 클릭 - 'https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code' 으로 접속
2. kakao 쿠키 존재 여부 확인 (첫 로그인에는 존재X)
3. https://accounts.kakao.com/login 접속 및 로그인 인증
4. kakao.com에 종속적으로 쿠키 생성
5. 'https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code'로 돌아와서 동의항목 체크
6. Redirect URI의 주소에 /kakao?code={\*\*\*} code 파라미터에 인가코드 전달됨
7. 인가코드 및 기타 파라미터들을 kauth.kakao.com/oauth/token에 보내고 토큰 받아오기
# 2. 두 번째 로그인부터
1. [카카오 로그인] 버튼 클릭 - 'https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code' 으로 접속
2. kakao 쿠키 존재 여부 확인 (첫 번째 로그인 때 쿠키 생성O)
3. 위 5번부터 동일한 과정 진행
......@@ -16,10 +16,12 @@ import {
SurveyLayout,
SurveysLayout,
ResultLayout,
AdminLayout,
} from "./layouts";
import { Home } from "./home";
import { OAuthRedirectHandler } from "./auth/OAuthRedirectHandler";
import { LoginSuccess } from "./commons/LoginSuccess";
import { Admin, SocialLogin } from "./admin";
export const MainRouter = () => {
return (
......@@ -35,6 +37,11 @@ export const MainRouter = () => {
<Route path="/results/:surveyId" element={<ResultSurvey />} />
</Route>
<Route element={<AdminLayout />}>
<Route path="/admin" element={<Admin />}></Route>
<Route path="/admin/social-login" element={<SocialLogin />}></Route>
</Route>
<Route element={<AnswerLayout />}>
<Route path="/answers/:surveyId" element={<AnswerSurvey />} />
</Route>
......
import React from "react";
import { Link, Outlet } from "react-router-dom";
import { SideBar } from "./SideBar";
export const Admin = () => {
return <div className="flex"></div>;
};
import React from "react";
export const SideBar = () => {
return (
<aside className="w-48 h-screen" aria-label="Sidebar">
<div className="py-4 h-full px-3 bg-gray-50 rounded dark:bg-gray-800">
<ul className="flex flex-col items-center">
<li className="text-center font-bold p-2 border-b-2">ADMIN</li>
<div className="pt-2">
<li>
<a
href="#"
className="flex items-center p-2 text-base text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
>
<svg
aria-hidden="true"
className="w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path>
</svg>
<span className="ml-1">Dashboard</span>
</a>
</li>
<li>
<a
href="#"
className="flex items-center p-2 text-base text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
>
<svg
aria-hidden="true"
className="flex-shrink-0 w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>
</svg>
<span className="ml-1">Social</span>
</a>
</li>
<li>
<a
href="#"
className="flex items-center p-2 text-base text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
>
<svg
aria-hidden="true"
className="flex-shrink-0 w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"
></path>
</svg>
<span className="ml-1">Users</span>
</a>
</li>
<li>
<a
href="#"
className="flex items-center p-2 text-base text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
>
<svg
aria-hidden="true"
className="flex-shrink-0 w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M5 4a3 3 0 00-3 3v6a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H5zm-1 9v-1h5v2H5a1 1 0 01-1-1zm7 1h4a1 1 0 001-1v-1h-5v2zm0-4h5V8h-5v2zM9 8H4v2h5V8z"
clip-rule="evenodd"
></path>
</svg>
<span className="ml-1">Surveys</span>
</a>
</li>
</div>
</ul>
</div>
</aside>
);
};
import React, { useState, ChangeEvent, FormEvent } from "react";
import { authApi } from "../apis";
import { catchErrors } from "../helpers";
export const SocialLogin = () => {
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [keyInfo, setKeyInfo] = useState({
socialType: "kakao",
REST_API_KEY: "",
REDIRECT_URI: "",
CLIENT_SECRET_KEY: "",
});
const handleSubmit = async (e: FormEvent) => {
const { socialType, REST_API_KEY, REDIRECT_URI, CLIENT_SECRET_KEY } =
keyInfo;
try {
setLoading(true);
await authApi.saveOauthKeys(
socialType,
REST_API_KEY,
REDIRECT_URI,
CLIENT_SECRET_KEY
);
} catch (error) {
setLoading(false);
catchErrors(error, setError);
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget;
setKeyInfo({ ...keyInfo, [name]: value });
};
return (
<div className="flex flex-col w-full items-center">
<div className="text-slate-400 my-3">일단 카카오 로그인만 구현</div>
<form onSubmit={handleSubmit} className="flex flex-col mt-3 w-80">
<label className="block text-gray-700 text-sm font-bold mb-2 mt-3">
REST_API_KEY
</label>
<input
onChange={handleChange}
className="shadow appearance-none border rounded py-2 px-3 text-gray-70"
name="REST_API_KEY"
type="text"
placeholder="REST_API_KEY"
value={keyInfo.REST_API_KEY}
/>
<label className="block text-gray-700 text-sm font-bold mb-2 mt-3">
REDIRECT_URI
</label>
<input
onChange={handleChange}
className="shadow appearance-none border rounded py-2 px-3 text-gray-70"
name="REDIRECT_URI"
type="text"
placeholder="REDIRECT_URI"
value={keyInfo.REDIRECT_URI}
/>
<label className="block text-gray-700 text-sm font-bold mb-2 mt-3">
CLIENT_SECRET_KEY
</label>
<input
onChange={handleChange}
className="shadow appearance-none border rounded py-2 px-3 text-gray-70"
name="CLIENT_SECRET_KEY"
type="text"
placeholder="CLIENT_SECRET_KEY"
value={keyInfo.CLIENT_SECRET_KEY}
/>
<div className="flex justify-center items-center mt-3">
<button
type="submit"
className="bg-themeColor text-white border rounded w-full py-2 px-3"
>
저장하기
</button>
</div>
</form>
</div>
);
};
export { Admin } from "./Admin";
export { SideBar } from "./SideBar";
export { SocialLogin } from "./SocialLogin";
......@@ -21,9 +21,29 @@ export const signup = async (user: SignupUser) => {
};
export const getKakaoUserData = async (code: string) => {
const { data } = await axios.post(`${baseUrl}/auth/oauth/kakao`, {
const { data } = await axios.post(`${baseUrl}/auth/oauth/kakao/token`, {
code: code,
});
console.log("data=", data);
return data.kakaoUserData;
return data;
};
export const saveOauthKeys = async (
socialType: string,
REST_API_KEY: string,
REDIRECT_URI: string,
CLIENT_SECRET_KEY: string
) => {
const { data } = await axios.post(`${baseUrl}/auth/oauth`, {
socialType,
REST_API_KEY,
REDIRECT_URI,
CLIENT_SECRET_KEY,
});
return data;
};
export const getOauthKeys = async (socialType: string) => {
const { data } = await axios.get(`${baseUrl}/auth/oauth/${socialType}`);
return data;
};
......@@ -3,8 +3,8 @@ import { Link, useLocation, useNavigate } from "react-router-dom";
import { catchErrors } from "../helpers";
import { SpinnerIcon } from "../icons";
import { useAuth } from "./auth.context";
import { REST_API_KEY, REDIRECT_URI } from "../auth";
// import { authApi } from "../apis";
// import { REST_API_KEY, REDIRECT_URI } from "../auth";
import { authApi } from "../apis";
import KakaoLoginImg from "../icons/kakao_login_medium_wide.png";
interface LocationState {
......@@ -38,18 +38,17 @@ export const Login = () => {
}
}
// async function kakaoLogin() {
// try {
// // await authApi.kakaoLogin();
// console.log("성공?");
// } catch (error) {
// setLoading(false);
// catchErrors(error, setError);
// }
// }
const kakaoLogin = () => {
window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`;
const kakaoLogin = async () => {
// const data = {REST_API_KEY:"", REDIRECT_URI:""}
try {
// DB에서 카카오 API키 받아온 후 전달
const data = await authApi.getOauthKeys("kakao");
console.log(data);
window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${data.REST_API_KEY}&redirect_uri=${data.REDIRECT_URI}&response_type=code`;
} catch (error) {
setLoading(false);
catchErrors(error, setError);
}
};
return (
......
import React, { useEffect, useState } from "react";
import { authApi } from "../apis";
import { catchErrors } from "../helpers";
const LOCAL_USER_INFO = "survey-user-info";
export const OAuthRedirectHandler = () => {
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState(
"잠시만 기다려 주세요! 로그인 중입니다."
);
......@@ -12,13 +17,26 @@ export const OAuthRedirectHandler = () => {
}, []);
useEffect(() => {
const getKakaoUserData = async (code: string) => {
const userData = await authApi.getKakaoUserData(code);
console.log(userData);
const kakaoLogin = async (code: string) => {
try {
const user = await authApi.getKakaoUserData(code);
console.log(user);
if (user) {
localStorage.setItem(
LOCAL_USER_INFO,
JSON.stringify({
isLoggedIn: user.isLoggedIn,
})
);
}
} catch (error) {
setLoading(false);
catchErrors(error, setError);
}
};
if (code) {
console.log("code=", code);
getKakaoUserData(code);
kakaoLogin(code);
}
}, [code]);
......
......@@ -2,5 +2,3 @@ export { Login } from "./Login";
export { AuthProvider, useAuth } from "./auth.context";
export { SignUp } from "./SignUp";
export { RequireAuth } from "./RequireAuth";
export { REST_API_KEY, REDIRECT_URI} from "./OauthKey"
import React from "react";
import { Outlet } from "react-router-dom";
import { SideBar } from "../admin/SideBar";
export const AdminLayout = () => {
return (
<div className="flex">
<SideBar />
<Outlet />
</div>
);
};
......@@ -3,3 +3,4 @@ export { BaseLayout } from "./BaseLayout";
export { SurveyLayout, useSurvey } from "./SurveyLayout";
export { SurveysLayout, useSurveys } from "./SurveysLayout";
export { ResultLayout } from "./ResultLayout";
export { AdminLayout } from "./AdminLayout";
......@@ -104,34 +104,6 @@ export const createAnswerWithFile = asyncWrap(async (reqExp, res) => {
res.json(newAnswer);
});
// export const getAnswers = asyncWrap(async (reqExp, res) => {
// const req = reqExp as TypedRequest;
// const { surveyId } = req.params;
// try {
// const survey = await surveyDb.getSurveyById(surveyId);
// const answers = await answerDb.getAnswers(surveyId);
// console.log(answers);
// const jsonSurvey = survey?.toJSON();
// if (jsonSurvey && answers) {
// const a = answers.map(async (a) => {
// const targetObj = jsonSurvey.questions.find(
// (q: any) => String(q._id) === String(a._id)
// ) as any;
// if (targetObj) {
// if (a.file.length) {
// targetObj.answers = a.file;
// } else {
// targetObj.answers = a.answers;
// }
// }
// });
// await Promise.all(a);
// }
// return res.json(jsonSurvey);
// } catch (error: any) {
// res.status(422).send(error.message || "설문조사 결과 불러오기 오류");
// }
// });
export const getAnswers = asyncWrap(async (reqExp, res) => {
const req = reqExp as TypedRequest;
const { surveyId } = req.params;
......
......@@ -4,7 +4,7 @@ import jwt, { JwtPayload } from "jsonwebtoken";
import isLength from "validator/lib/isLength";
import isEmail from "validator/lib/isEmail";
import { asyncWrap } from "../helpers";
import { roleDb, userDb } from "../db";
import { roleDb, userDb, oauthDb } from "../db";
import { jwtCofig, envConfig, cookieConfig } from "../config";
import axios from "axios";
......@@ -158,28 +158,100 @@ export const signup = asyncWrap(async (req, res) => {
});
export const kakaoAuthenticate = asyncWrap(async (req, res) => {
// console.log(req.query);
// const code = req.query.code as string;
console.log(req.body);
// console.log(req.body);
const code = req.body.code;
try {
const params = new URLSearchParams({
grant_type: "authorization_code",
client_id: REST_API_KEY,
redirect_uri: REDIRECT_URI,
code: code,
client_secret: CLIENT_SECRET_KEY,
});
const kakaoResponse = await axios.post(
"https://kauth.kakao.com/oauth/token",
params
);
console.log(kakaoResponse.data);
console.log("jwt decode:", jwt.decode(kakaoResponse.data.id_token));
// return res.redirect("http://localhost:8080/login/success");
res.json({ kakaoUserData: jwt.decode(kakaoResponse.data.id_token) });
const socialKeys = await oauthDb.getSocialKey("kakao");
if (socialKeys) {
const params = new URLSearchParams({
grant_type: "authorization_code",
client_id: socialKeys.REST_API_KEY,
redirect_uri: socialKeys.REDIRECT_URI,
code: code,
client_secret: socialKeys.CLIENT_SECRET_KEY,
});
const kakaoResponse = await axios.post(
"https://kauth.kakao.com/oauth/token",
params
);
const kakaoUserData = jwt.decode(kakaoResponse.data.id_token) as any;
//카카오에서 받아온 user data를 db에 저장
if (kakaoUserData) {
const userExist = await userDb.isUser(kakaoUserData.email);
if (userExist) {
const kakaoUser = await userDb.isSocialType(
kakaoUserData.email,
"kakao"
);
if (kakaoUser) {
// 3) 비밀번호가 맞으면 토큰 생성
const token = jwt.sign({ userId: kakaoUser.id }, jwtCofig.secret, {
expiresIn: jwtCofig.expires,
});
// 4) 토큰을 쿠키에 저장
res.cookie(cookieConfig.name, token, {
maxAge: cookieConfig.maxAge,
path: "/",
httpOnly: envConfig.mode === "production",
secure: envConfig.mode === "production",
});
// 5) 사용자 반환
res.json({
isLoggedIn: true,
email: kakaoUser.email,
});
} else {
return res
.status(422)
.send(
`다른 로그인 방식의 ${kakaoUserData.email} 사용자가 이미 존재합니다`
);
}
} else {
const newUser = await userDb.createUser({
email: kakaoUserData.email,
password: "",
socialType: "kakao",
});
// 3) 비밀번호가 맞으면 토큰 생성
const token = jwt.sign({ userId: newUser.id }, jwtCofig.secret, {
expiresIn: jwtCofig.expires,
});
// 4) 토큰을 쿠키에 저장
res.cookie(cookieConfig.name, token, {
maxAge: cookieConfig.maxAge,
path: "/",
httpOnly: envConfig.mode === "production",
secure: envConfig.mode === "production",
});
// 5) 사용자 반환
res.json({
isLoggedIn: true,
email: newUser.email,
});
}
}
}
} catch (error) {
console.log(error);
res.send("에러");
}
});
export const saveOauthKeys = asyncWrap(async (req, res, next) => {
console.log(req.body);
try {
const oauth = await oauthDb.createSocialKey(req.body);
console.log(oauth);
return res.json(oauth);
} catch (error) {}
});
export const getOauthKeys = asyncWrap(async (req, res, next) => {
console.log(req.params);
try {
const socialKeys = await oauthDb.getSocialKey(req.params.socialType);
console.log(socialKeys);
return res.json(socialKeys);
} catch (error) {}
});
......@@ -4,3 +4,4 @@ export * as roleDb from "./role.db";
export * as surveyDb from "./survey.db";
export * as userDb from "./user.db";
export * as answerDb from "./answer.db";
export * as oauthDb from "./oauth.db";
import { Oauth, IOauth } from "../models";
export const createSocialKey = async (socialKeys: IOauth) => {
const newOauth = new Oauth({
socialType: socialKeys.socialType,
REST_API_KEY: socialKeys.REST_API_KEY,
REDIRECT_URI: socialKeys.REDIRECT_URI,
CLIENT_SECRET_KEY: socialKeys.CLIENT_SECRET_KEY,
});
const oauth = await newOauth.save();
return oauth;
};
export const getSocialKey = async (socialType: string) => {
const socialKeys = await Oauth.findOne({ socialType: socialType });
return socialKeys;
};
......@@ -16,6 +16,7 @@ export const createUser = async (user: IUser) => {
password: hash,
role: userRole,
avatar: user.avatar,
socialType: user.socialType,
isNew: true,
});
const retUser = await newUser.save();
......@@ -70,3 +71,8 @@ export const isValidUserId = async (userId: string) => {
return false;
}
};
export const isSocialType = async (socialType: string, email: string) => {
const user = await User.findOne({ email, socialType });
return user;
};
......@@ -4,3 +4,4 @@ export { default as Role } from "./role.model";
export { default as Survey, ISurvey } from "./survey.model";
export { default as User, IUser } from "./user.model";
export { default as Answer, IAnswer } from "./answer.model";
export { default as Oauth, IOauth } from "./oauth.model";
import { model, Schema } from "mongoose";
export interface IOauth {
socialType: string;
REST_API_KEY: string;
REDIRECT_URI: string;
CLIENT_SECRET_KEY: string;
}
const schema = new Schema<IOauth>(
{
socialType: { type: String, unique: true },
REST_API_KEY: { type: String },
REDIRECT_URI: { type: String },
CLIENT_SECRET_KEY: { type: String },
},
{ toJSON: { versionKey: false } }
);
export default model<IOauth>("Oauth", schema);
......@@ -6,6 +6,7 @@ export interface IUser {
password: string;
role?: Types.ObjectId;
avatar?: Types.ObjectId;
socialType?: string;
}
const validateEmail = (email: string) => {
......@@ -22,9 +23,10 @@ const schema = new Schema<IUser>(
validate: [validateEmail, "이메일을 입력해주세요"],
},
name: { type: String },
password: { type: String, required: true, select: false },
password: { type: String, select: false },
role: { type: Schema.Types.ObjectId, ref: "Role" },
avatar: { type: Schema.Types.ObjectId, ref: "FileInfo" },
socialType: { type: String },
},
{
toJSON: {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment