Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next auth config 수정 #46

Merged
merged 10 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@
"tailwindcss/enforces-shorthand": "off"
}
}
]
],
"ignorePatterns": ["auth.ts"]
}
182 changes: 87 additions & 95 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -1,140 +1,126 @@
// ? Reference https://github.dev/nextauthjs/next-auth-example/blob/main/app/api/protected/route.ts
// ? Reference https://www.heropy.dev/p/MI1Khc
import type { User } from "next-auth";
import type { JWT } from "next-auth/jwt";
import NextAuth from "next-auth";
import KakaoProvider from "next-auth/providers/kakao";

import NextAuth, { Account, NextAuthConfig, Session, User } from "next-auth";
import { AdapterUser } from "next-auth/adapters";
import { JWT } from "next-auth/jwt";
import { ProviderType } from "next-auth/providers";
import Kakao from "next-auth/providers/kakao";

import {
getRefreshToken,
getServerSideSession,
postOauthLogin,
} from "@/apis/auth/auth";
import { getRefreshToken, postOauthLogin } from "@/apis/auth/auth";
import { SocialLoginRequest } from "@/apis/auth/authType";
import { getMe } from "@/apis/user/me/me";
import { env } from "@/env";
import { decodeToken } from "@/lib/jwt";
import { isMatchPath } from "@/utils";

const AuthRequiredPage = ["/mypage"];

const config = {
providers: [Kakao],
// ? 사용자 지정 로그인, 로그아웃 및 오류 페이지를 만들 때 사용할 URL을 지정합니다. 지정한 페이지는 해당 기본 제공 페이지를 재정의합니다.
debug: process.env.NODE_ENV === "development",
export const { handlers, auth, signIn, signOut, unstable_update } = NextAuth({
providers: [
KakaoProvider({
clientId: env.AUTH_KAKAO_ID,
clientSecret: env.AUTH_KAKAO_SECRET,
}),
],
secret: env.AUTH_SECRET,
pages: {
signIn: "/auth/sign-in",
newUser: "/auth/sign-in",
signOut: "/",
signOut: "/auth/sign-in",
},
callbacks: {
// * protected page 설정
authorized({ request, auth }) {
const { pathname } = request.nextUrl;
if (isMatchPath(pathname, AuthRequiredPage)) {
return !!auth;
}
async signIn() {
return true;
},
// * callbackUrl이 있다면 callbackUrl로 리다이렉트
redirect: async ({ url, baseUrl }: { url: string; baseUrl: string }) => {
redirect: async ({ url, baseUrl }) => {
if (url.startsWith("/")) return `${baseUrl}${url}`;
if (url) {
const { search, origin } = new URL(url);
const callbackUrl = new URLSearchParams(search).get("callbackUrl");
if (callbackUrl) {
const session = await getServerSideSession();

// 프로필 등록안했으면 onboarding으로
if (!session?.isProfileRegistered) {
return `${baseUrl}/onboarding`;
}

if (callbackUrl)
return callbackUrl.startsWith("/")
? `${baseUrl}${callbackUrl}`
: callbackUrl;
}
if (origin === baseUrl) return url;
}
return baseUrl;
},
// ? 이 콜백은 JSON 웹 토큰이 생성되거나(즉, 로그인할 때) 업데이트될 때마다(즉, 클라이언트에서 세션에 액세스할 때마다) 호출됩니다.
// ? 여기서 반환하는 모든 내용은 JWT에 저장되어 세션 콜백으로 전달됩니다. 여기에서 클라이언트에 반환할 항목을 제어할 수 있습니다.
// ? 그 외의 모든 내용은 프런트엔드에서 보관됩니다. JWT는 기본적으로 AUTH_SECRET 환경 변수를 통해 암호화됩니다.
jwt: async ({
token,
account,
user,
}: {
token: JWT;
user?: User | AdapterUser;
account?: Account | null;
}) => {
let response: JWT = token;
async jwt(params) {
let userResponse: UserMeResponse | null = null;

// * 토큰 재발급
if (
params.token.refreshToken &&
params.token.exp &&
params.token.exp * 1000 < Date.now()
) {
const { accessToken, refreshToken } = await getRefreshToken(
params.token.refreshToken,
);

const decodedJWT = decodeToken(accessToken);

// * jwt token decode
let jwtPayload;
if (token.accessToken) {
jwtPayload = decodeToken(token.accessToken);
return {
...params.token,
accessToken,
refreshToken,
exp: decodedJWT?.exp,
};
}

// * 로그인 시 토큰 발급
if (account && user) {
const body: SocialLoginRequest = {
id: user.id as string,
email: user.email as string,
accessToken: account.access_token as string,
if (params.trigger === "update") {
params.token = {
...params.token,
};
}

if (!Object.hasOwn(token, "isProfileRegistered")) {
response = await postOauthLogin(body);
}
if (params.user) {
if (params.account && params.account.access_token) {
const body: SocialLoginRequest = {
id: params.user.id as string,
email: params.user.email as string,
accessToken: params.account.access_token as string,
};

token.accessToken = response?.accessToken;
token.refreshToken = response.refreshToken;
token.isProfileRegistered = response.isProfileRegistered;
token.email = user?.email;
token.exp = response.exp;
token.iat = response.iat;
token.sub = response.sub;
params.token = await postOauthLogin(body);
userResponse = await getMe({
headers: {
Authorization: `Bearer ${params.token.accessToken}`,
},
});
}

return token;
}
const decodedJWT = decodeToken(params.token.accessToken ?? "");

// * 토큰 재발급
if (
token.refreshToken &&
jwtPayload?.exp &&
jwtPayload.exp * 1000 < Date.now()
) {
const { accessToken, refreshToken } = await getRefreshToken(
token.refreshToken,
);
token.accessToken = accessToken;
token.refreshToken = refreshToken;
return token;
return {
...params.user,
...userResponse,
...params.token,
exp: decodedJWT?.exp,
};
}

return token;
return { ...params.token, ...params.session };
},
// ? 로그인한 사용자의 활성 세션입니다.
session: async ({ session, token }: { session: Session; token: JWT }) => {
async session({ session, token }) {
if (token?.accessToken) {
const decodedJWT = decodeToken(token.accessToken ?? "");
session.user = {
...session.user,
userId: token.userId as number,
email: token.email as string,
nickname: token.nickname as string,
statusMessage: token.statusMessage as string,
profileImage: token.profileImage as string,
userTypeId: token.userTypeId as number,
isProfileRegistered: token.isProfileRegistered as boolean,
};
session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken;
session.isProfileRegistered = token.isProfileRegistered;
session.exp = token.exp;
session.exp = decodedJWT?.exp;
session.iat = token.iat;
session.sub = token.sub;
}
return session;
},
},

// ? 인증 및 데이터베이스 작업에 대한 디버그 메시지를 사용하려면 디버그를 true로 설정합니다.
// debug: process.env.NODE_ENV !== "production" ? true : false,
} satisfies NextAuthConfig;

export const { signIn, signOut, handlers, auth } = NextAuth(config);
});

declare module "next-auth" {
// * 카카오 Account 응답에 맞게 수정
Expand All @@ -146,11 +132,11 @@ declare module "next-auth" {
refresh_token_expires_in?: number;
expires_at?: number;
provider: string;
type: ProviderType;
providerAccountId: string;
}
// * 세션이 accessToken 필드값 추가 ( 필요에 따라 더 추가 )
interface Session {
user: User & UserMeResponse;
accessToken?: string;
refreshToken?: string;
isProfileRegistered?: boolean;
Expand All @@ -161,6 +147,12 @@ declare module "next-auth" {
}
declare module "next-auth/jwt" {
interface JWT {
userId?: number;
email?: string;
nickname?: string;
statusMessage?: string;
profileImage?: string;
userTypeId?: number;
accessToken?: string;
refreshToken?: string;
isProfileRegistered?: boolean;
Expand Down
2 changes: 0 additions & 2 deletions src/apis/festivals/recommendFestival/recommendFestival.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use server";

import { ClientError } from "@/apis/error";
import instance from "@/apis/instance";
import FIESTA_ENDPOINTS from "@/config/apiEndpoints";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const defaultParams: PaginationParamter = { page: 0, size: 6 };

export const recommendFestivalKeys = {
all: ["RecommendFestival"],
user: (userId: number | string) => [...recommendFestivalKeys.all, userId],
list: (params: PaginationParamter = defaultParams) => [
recommendFestivalKeys.all,
params,
Expand Down
2 changes: 1 addition & 1 deletion src/apis/festivals/searchFestival/searchFestivalKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const defaultParams: SearchFestivalParameter = { query: "" };
export const SearchFestivalKeys = {
all: ["searchFestival"] as const,
list: (params: SearchFestivalParameter = defaultParams) => [
SearchFestivalKeys.all,
...SearchFestivalKeys.all,
params,
],
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type TopKeywordParameter = {
festivalId: number;
festivalId: number | string;
size?: number;
};

Expand Down
5 changes: 1 addition & 4 deletions src/apis/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function getClientSideSession() {
return session;
}

type FiestaFetchOptions = Omit<RequestInit, "body">;
export type FiestaFetchOptions = Omit<RequestInit, "body">;

export class CreateFiestaFetch {
private baseUrl: string;
Expand Down Expand Up @@ -62,7 +62,6 @@ export class CreateFiestaFetch {
Authorization: `Bearer ${userSession.accessToken}`,
};
}

const response = await fetch(this.baseUrl + url, finalOptions);
if (!response.ok) {
const errorData = await response.json();
Expand All @@ -71,8 +70,6 @@ export class CreateFiestaFetch {
}
return await response.json();
} catch (error: unknown) {
console.error(error);

if (error instanceof ServerError) {
// TODO: Server Error
throw error;
Expand Down
2 changes: 1 addition & 1 deletion src/apis/review/reviews/reviewsType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type FestivalReviewsParameters = {
festivalId?: number;
festivalId?: number | string;
sort?: "createdAt" | "likeCount";
page?: number;
size?: number;
Expand Down
5 changes: 3 additions & 2 deletions src/apis/user/me/me.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import instance from "@/apis/instance";
import instance, { FiestaFetchOptions } from "@/apis/instance";
import { FIESTA_ENDPOINTS } from "@/config";

export const getMe = async () => {
export const getMe = async (options?: FiestaFetchOptions) => {
const endpoint = FIESTA_ENDPOINTS.users.me;
const { data } = await instance.get<UserMeResponse>(endpoint, {
...options,
cache: "no-store",
});

Expand Down
4 changes: 3 additions & 1 deletion src/apis/user/me/meType.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
type UserMeResponse = {
userId: number;
email: string;
nickname: string;
statusMessage: string;
profileImage: string;
isProfileCreated?: boolean;
isProfileRegistered: boolean;
userType: number;
userTypeId: number;
};
8 changes: 4 additions & 4 deletions src/apis/user/profile/profileType.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export type UserProfileRequest = {
category: number[];
mood: number[];
companion: number[];
priority: number[];
categoryIds: number[];
moodIds: number[];
companionIds: number[];
priorityIds: number[];
};

export type UserProfileResponse = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import Image from "next/image";
import { User } from "next-auth";
import { FC } from "react";

import { UserTypeImage, UserTypeText } from "@/utils";

interface Props {
user: UserMeResponse;
user: User & UserMeResponse;
}

const RecommendFestivalHeader: FC<Props> = ({ user }) => {
return (
<div className="relative flex w-full justify-between">
<div className="flex w-1/2 flex-wrap pb-[18px] text-title-bold">
<span className={UserTypeText[user.userType ?? 1]}>
{user.nickname}
<span className={UserTypeText[user.userTypeId ?? 1]}>
{user.nickname ?? "피에스타"}
</span>
<span>들을</span>
<span>위한 페스티벌이에요!</span>
</div>
<Image
className="absolute bottom-[-15px] right-[20px]"
src={UserTypeImage[user.userType ?? 1] ?? "/images/fallbackLogo.png"}
alt={user.nickname}
src={UserTypeImage[user.userTypeId ?? 1] ?? "/images/fallbackLogo.png"}
alt={user.nickname ?? "피에스타"}
width={109}
height={109}
/>
Expand Down
Loading
Loading