- Kakao SDK v2.7에서 Auth.login() 제거 → authorize() 리다이렉트 플로우 사용 - services/kakao.ts: kakaoWebLoginStart() redirect 시작 - RootNavigator: 웹 URL의 code + state=kakao_login 감지 → kakaoCodeLogin 호출 - useAuthStore.kakaoCodeLogin 추가 (authApi.kakaoCode 호출) - 백엔드 /auth/kakao/code: REST API 키로 code→access_token 교환 후 기존 processKakaoLogin 재사용 - api-secrets에 kakaoRestApiKey/kakaoClientSecret 슬롯 추가 - 환경변수: KAKAO_REST_API_KEY 서버에 저장 완료 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+72
-48
@@ -1,4 +1,4 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -17,8 +17,10 @@ const LoginBody = z.object({
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
const KakaoBody = z.object({
|
||||
accessToken: z.string().min(10),
|
||||
const KakaoBody = z.object({ accessToken: z.string().min(10) });
|
||||
const KakaoCodeBody = z.object({
|
||||
code: z.string().min(5),
|
||||
redirectUri: z.string().url(),
|
||||
});
|
||||
|
||||
type KakaoMe = {
|
||||
@@ -40,6 +42,62 @@ async function fetchKakaoProfile(accessToken: string): Promise<KakaoMe> {
|
||||
return (await res.json()) as KakaoMe;
|
||||
}
|
||||
|
||||
async function exchangeKakaoCode(code: string, redirectUri: string): Promise<string> {
|
||||
const restKey = process.env.KAKAO_REST_API_KEY;
|
||||
if (!restKey) throw new Error('KAKAO_REST_API_KEY not configured');
|
||||
const form = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: restKey,
|
||||
redirect_uri: redirectUri,
|
||||
code,
|
||||
});
|
||||
if (process.env.KAKAO_CLIENT_SECRET) {
|
||||
form.set('client_secret', process.env.KAKAO_CLIENT_SECRET);
|
||||
}
|
||||
const res = await fetch('https://kauth.kakao.com/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' },
|
||||
body: form.toString(),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Kakao token exchange ${res.status}: ${await res.text()}`);
|
||||
const data = (await res.json()) as any;
|
||||
return data.access_token as string;
|
||||
}
|
||||
|
||||
async function processKakaoLogin(app: FastifyInstance, reply: FastifyReply, accessToken: string) {
|
||||
let me: KakaoMe;
|
||||
try {
|
||||
me = await fetchKakaoProfile(accessToken);
|
||||
} catch {
|
||||
return reply.code(401).send({ message: '카카오 인증 실패' });
|
||||
}
|
||||
const kakaoId = String(me.id);
|
||||
const email = me.kakao_account?.email;
|
||||
const name = me.kakao_account?.profile?.nickname ?? me.properties?.nickname ?? '카카오사용자';
|
||||
const profileImage = me.kakao_account?.profile?.profile_image_url ?? me.properties?.profile_image;
|
||||
|
||||
let user = await app.prisma.user.findUnique({
|
||||
where: { kakaoId },
|
||||
include: { profile: true },
|
||||
});
|
||||
if (!user) {
|
||||
user = await app.prisma.user.create({
|
||||
data: {
|
||||
kakaoId,
|
||||
email: email ?? null,
|
||||
name,
|
||||
phone: me.kakao_account?.phone_number,
|
||||
provider: 'KAKAO',
|
||||
profileImage,
|
||||
profile: { create: { age: 30, gender: 'MALE', job: '기타' } },
|
||||
},
|
||||
include: { profile: true },
|
||||
});
|
||||
}
|
||||
const token = await reply.jwtSign({ sub: user.id, email: user.email ?? undefined });
|
||||
return { token, user: publicUser(user) };
|
||||
}
|
||||
|
||||
export async function authRoutes(app: FastifyInstance) {
|
||||
app.post('/register', async (req, reply) => {
|
||||
const body = RegisterBody.parse(req.body);
|
||||
@@ -54,13 +112,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
name: body.name,
|
||||
phone: body.phone,
|
||||
provider: 'EMAIL',
|
||||
profile: {
|
||||
create: {
|
||||
age: body.age,
|
||||
gender: body.gender,
|
||||
job: body.job ?? '기타',
|
||||
},
|
||||
},
|
||||
profile: { create: { age: body.age, gender: body.gender, job: body.job ?? '기타' } },
|
||||
},
|
||||
include: { profile: true },
|
||||
});
|
||||
@@ -86,46 +138,18 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
|
||||
app.post('/kakao', async (req, reply) => {
|
||||
const body = KakaoBody.parse(req.body);
|
||||
let me: KakaoMe;
|
||||
return processKakaoLogin(app, reply, body.accessToken);
|
||||
});
|
||||
|
||||
app.post('/kakao/code', async (req, reply) => {
|
||||
const body = KakaoCodeBody.parse(req.body);
|
||||
let accessToken: string;
|
||||
try {
|
||||
me = await fetchKakaoProfile(body.accessToken);
|
||||
} catch (e) {
|
||||
return reply.code(401).send({ message: '카카오 인증 실패' });
|
||||
accessToken = await exchangeKakaoCode(body.code, body.redirectUri);
|
||||
} catch (e: any) {
|
||||
return reply.code(401).send({ message: `카카오 토큰 교환 실패: ${e?.message ?? ''}` });
|
||||
}
|
||||
|
||||
const kakaoId = String(me.id);
|
||||
const email = me.kakao_account?.email;
|
||||
const name = me.kakao_account?.profile?.nickname ?? me.properties?.nickname ?? '카카오사용자';
|
||||
const profileImage = me.kakao_account?.profile?.profile_image_url ?? me.properties?.profile_image;
|
||||
|
||||
let user = await app.prisma.user.findUnique({
|
||||
where: { kakaoId },
|
||||
include: { profile: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await app.prisma.user.create({
|
||||
data: {
|
||||
kakaoId,
|
||||
email: email ?? null,
|
||||
name,
|
||||
phone: me.kakao_account?.phone_number,
|
||||
provider: 'KAKAO',
|
||||
profileImage,
|
||||
profile: {
|
||||
create: {
|
||||
age: 30,
|
||||
gender: 'MALE',
|
||||
job: '기타',
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { profile: true },
|
||||
});
|
||||
}
|
||||
|
||||
const token = await reply.jwtSign({ sub: user.id, email: user.email ?? undefined });
|
||||
return { token, user: publicUser(user) };
|
||||
return processKakaoLogin(app, reply, accessToken);
|
||||
});
|
||||
|
||||
app.get('/me', { onRequest: [app.authenticate] }, async (req) => {
|
||||
|
||||
Reference in New Issue
Block a user