Skip to content

Commit 5f61ac4

Browse files
committed
feat: Try kakao login
1 parent 0e1cdc7 commit 5f61ac4

File tree

9 files changed

+128
-109
lines changed

9 files changed

+128
-109
lines changed

backend/app/api/auth_router.py

+57-53
Original file line numberDiff line numberDiff line change
@@ -19,73 +19,77 @@ async def login() -> RedirectResponse:
1919
책임
2020
- kakao oauth server로 redirect
2121
"""
22-
kakao_login_url = "https://kauth.kakao.com/oauth/authorize"
23-
query_param = {
24-
"client_id": Config.Kakako.ACCESS_KEY,
25-
"redirect_uri": Config.Kakako.REDIRECT_URI,
26-
"response_type": "code",
27-
}
28-
return RedirectResponse(
29-
kakao_login_url + "?" + utils.build_query_param(query_param)
22+
kakao_auth_url = (
23+
f"https://kauth.kakao.com/oauth/authorize"
24+
f"?client_id={Config.Kakako.ACCESS_KEY}"
25+
f"&redirect_uri={Config.Kakako.REDIRECT_URI}"
26+
f"&response_type=code"
3027
)
28+
return RedirectResponse(url=kakao_auth_url)
3129

32-
30+
3331
@router.get("/authenticate")
34-
async def auhtenticate(
35-
response : Response,
32+
async def authenticate(
33+
response: Response,
3634
user_repo: Annotated[UserRepository, Depends(get_user_repository)],
3735
code: str | None = None,
3836
error: str | None = None,
3937
error_description: str | None = None,
40-
) -> JwtResponse:
38+
) -> dict:
4139
"""
42-
(참고) 인증 흐름
43-
1. /login 에서 kauth.kakako.com/oauth/authroize로 redirect
44-
2. 카카오톡 서버에서 이 api를 호출하며 query param으로 인증코드와 에러 코드들을 넘겨준다.
45-
46-
책임
47-
- 카카오톡 서버로부터 전달받은 인증코드로 access_token 요청 보내기
48-
- access_token으로 user info 요청 보내기
49-
:param
50-
code: 카카오톡 access_token 인증코드
40+
Callback function for handling OAuth with Kakao.
5141
"""
5242
if not code:
5343
raise HTTPException(
54-
status_code=400, detail={"error": error, "error_desc": error_description}
44+
status_code=400,
45+
detail={
46+
"error": error or "Authorization code missing",
47+
"error_description": error_description or "No further details provided",
48+
},
5549
)
5650

57-
access_token: str = request_access_token(
58-
redirect_uri=Config.Kakako.REDIRECT_URI,
59-
auth_code=code,
60-
client_id=Config.Kakako.ACCESS_KEY,
61-
)["access_token"]
62-
user_kakao_id: int = int(request_user_info(access_token)["id"])
63-
64-
# register new user
65-
# user: User = User(kakao_id=user_id, username="foo", nickname="foo")
66-
user: User | None = user_repo.find_by_kakao_id(user_kakao_id)
67-
if user is None:
68-
user = User(kakao_id=user_kakao_id, username="foo", nickname="foo")
69-
user_repo.insert(user)
70-
71-
if user.id is None:
72-
raise Exception("user id must not be None")
73-
74-
# TODO: expire and path
75-
jwt_token = JwtAuth.create_token(user.id)
76-
77-
# 5. JWT를 HttpOnly 쿠키에 저장
78-
response.set_cookie(
79-
key="access_token", # 쿠키 이름
80-
value=jwt_token, # JWT 토큰 값
81-
httponly=True, # HttpOnly 속성 (JS에서 접근 불가)
82-
secure=False, # HTTPS 환경에서 True로 설정
83-
samesite="Lax", # CSRF 방지
84-
max_age=3600, # 쿠키 만료 시간 (초)
85-
)
51+
try:
52+
# Step 1: Exchange the authorization code for an access token
53+
access_token: str = request_access_token(
54+
redirect_uri=Config.Kakao.REDIRECT_URI,
55+
auth_code=code,
56+
client_id=Config.Kakao.ACCESS_KEY,
57+
)["access_token"]
58+
59+
# Step 2: Fetch user info from Kakao API
60+
user_info = request_user_info(access_token)
61+
user_kakao_id: int = int(user_info["id"])
62+
username = user_info.get("properties", {}).get("nickname", "default_username")
63+
64+
# Step 3: Register user if not exists
65+
user = user_repo.find_by_kakao_id(user_kakao_id)
66+
if user is None:
67+
user = User(kakao_id=user_kakao_id, username=username, nickname=username)
68+
user_repo.insert(user)
69+
70+
if user.id is None:
71+
raise Exception("User ID must not be None after insertion")
72+
73+
# Step 4: Create JWT token
74+
jwt_token = JwtAuth.create_token(user.id)
75+
76+
# Step 5: Set the token in an HttpOnly cookie
77+
response.set_cookie(
78+
key="access_token",
79+
value=jwt_token,
80+
httponly=True,
81+
secure=False, # Set to True in production with HTTPS
82+
samesite="Lax",
83+
max_age=3600,
84+
)
8685

87-
# 6. 프론트엔드 대시보드로 리디렉션
88-
return RedirectResponse(url="http://localhost:8001")
86+
# Step 6: Redirect to frontend dashboard or return a response
87+
return {"message": "Authentication successful", "user": {"id": user.id, "nickname": user.nickname}}
88+
89+
except HTTPException as http_exc:
90+
raise http_exc
91+
except Exception as exc:
92+
raise HTTPException(status_code=500, detail=f"Unexpected error: {str(exc)}")
8993

9094

9195

backend/app/auth/kakao_oauth.py

+22-17
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,29 @@
22

33

44
def request_access_token(redirect_uri: str, auth_code: str, client_id: str) -> dict:
5-
# TODO: http exception handling
6-
resp = httpx.post(
7-
"https://kauth.kakao.com/oauth/token",
8-
data={
9-
"client_id": client_id,
10-
"redirect_uri": redirect_uri,
11-
"code": auth_code,
12-
"grant_type": "authorization_code",
13-
},
14-
)
15-
return resp.json()
5+
try:
6+
resp = httpx.post(
7+
"https://kauth.kakao.com/oauth/token",
8+
data={
9+
"client_id": client_id,
10+
"redirect_uri": redirect_uri,
11+
"code": auth_code,
12+
"grant_type": "authorization_code",
13+
},
14+
)
15+
return resp.json()
16+
except Exception as exc:
17+
print(f"An unexpected error occurred: {exc}")
18+
1619

1720

1821
def request_user_info(access_token: str) -> dict:
19-
# TODO: http exception handling
20-
resp = httpx.post(
21-
url="https://kapi.kakao.com/v2/user/me",
22-
headers={"Authorization": f"Bearer {access_token}"},
23-
)
22+
try:
23+
resp = httpx.post(
24+
url="https://kapi.kakao.com/v2/user/me",
25+
headers={"Authorization": f"Bearer {access_token}"},
26+
)
2427

25-
return resp.json()
28+
return resp.json()
29+
except Exception as exc:
30+
print(f"An unexpected error occurred: {exc}")

backend/app/core/config.py

+3
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ class AWS:
2828

2929
class ENV:
3030
SERVER_HOST = os.getenv("SERVER_HOST")
31+
32+
class FRONTEND:
33+
NEXT_PUBLIC_BASE_URL = os.getenv("NEXT_PUBLIC_BASE_URL")

backend/app/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
app.include_router(user_router.router)
2525

2626
if __name__ == "__main__":
27-
uvicorn.run(app, host="0.0.0.0", port=8000)
27+
uvicorn.run(app, host="localhost", port=8000)

backend/requirements.txt

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
pyjwt==2.8.0
2+
python-dotenv==1.0.1
3+
httpx
4+
fastapi[standard]==0.115.1
5+
uvicorn[standard]
6+
mysqlclient
7+
python-multipart
8+
boto3
9+
boto3-stubs[s3]
10+
pytest
11+
sqlalchemy

frontend/app/api/login.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import dotenv from 'dotenv';
2+
dotenv.config();
3+
4+
console.log('API_BASE_URL:', process.env.NEXT_PUBLIC_API_BASE_URL);
5+
6+
const fetchData = async () => {
7+
// try {
8+
// // Call the backend API to get the Kakao OAuth URL
9+
// const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/login`);
10+
// const data = await response.json();
11+
12+
// if (data.kakao_auth_url) {
13+
// // Redirect to the Kakao login page
14+
// window.location.href = data.kakao_auth_url;
15+
// } else {
16+
// console.error('Failed to fetch Kakao login URL:', data);
17+
// }
18+
// } catch (error) {
19+
// console.error('Error fetching Kakao login URL:', error);
20+
// }
21+
};
22+
23+
export default fetchData;

frontend/app/components/LoginButton.tsx

+6-33
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,18 @@
22
// components/LoginButton.js
33

44
import React from 'react';
5+
// import fetchData from '../api/login';
56

67
const LoginButton = () => {
7-
const fetchData = async () => {
8-
try {
9-
const response = await fetch('http://0.0.0.0:8000/login', {
10-
method: 'GET',
11-
headers: {
12-
'Content-Type': 'application/json',
13-
},
14-
});
15-
16-
console.log(response);
17-
18-
if (response.ok) {
19-
const result = await response.json();
20-
alert('Login successful: ' + JSON.stringify(result));
21-
22-
// Redirect to /
23-
window.location.replace('/');
24-
} else {
25-
const error = await response.json();
26-
alert('Login failed: ' + error.message);
27-
}
28-
} catch (err) {
29-
if (err instanceof Error) {
30-
// Handle error of type Error
31-
alert('An error occurred: ' + err.message);
32-
} else {
33-
// Handle unexpected error types
34-
console.error('Unexpected error:', err);
35-
alert('An unexpected error occurred.');
36-
}
37-
}
8+
const handleLogin = () => {
9+
// Redirect the user to the FastAPI login endpoint
10+
window.location.href = "http://localhost:8000/login";
3811
};
39-
12+
4013
return (
4114
<button
4215
id="fetchButton"
43-
onClick={fetchData}
16+
onClick={handleLogin}
4417
className="px-4 py-2 border-red-300 bg-red-600 border-2 rounded-full hover:bg-gray-200 transition-colors text-white font-extrabold"
4518
>
4619
로그인

frontend/package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12-
"dotenv": "^16.4.5",
12+
"dotenv": "^16.4.7",
1313
"next": "15.0.3",
1414
"react": "19.0.0-rc-66855b96-20241106",
1515
"react-dom": "19.0.0-rc-66855b96-20241106"

0 commit comments

Comments
 (0)