/app/api/user/[id]
경로에 route.ts
파일을 만들어 준다.
API 에서도 페이지와 같이 Dynamic Route
를 사용할 수 있다.
여기서는 [id]
값을 slug로 사용한다.
//app/api/user/[id]/route.ts
import prisma from '@/app/lib/prisma'
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
console.log(params)
// 테이블에서 id 값은 Int 로 정의되어 있기 때문에 형변환
const id = Number(params.id)
const userPosts = await prisma.post.findMany({
where: {
authorId: id,
},
include: {
author: {
select: {
email: true,
name: true,
},
},
},
})
return new Response(JSON.stringify(userPosts))
}
findMany
는 조건과 일치하는 데이터들을 전부 가져오는 함수이다.
prisma studio
를 통해 데이터를 수기로 입력해준다.
authorId
를 필수로 입력해 주어야하며, User
테이블에 있는 id
값을 입력해준다.
Path Valiable
로 API를 호출하면 다음과 같은 Response를 얻을 수 있다.
✅ 성공!
현재는 Request url 만 알고 있으면 로그인을 하지 않아도 해당 데이터를 조회해 볼 수 있다.
Next-Auth에서는 인가되지 않은 사용자의 접근을 막기 위해 JWT Token
을 다루는 기능들을 제공한다.
☝🏻 먼저, 로그인 시 리턴되는 User
객체에 JWT Token
을 추가할 것이고
✌🏻 이 토큰을 NextAuth의 Session에 저장해 로그인 된 상태에서 사용 가능하도록 할 것이다.
💡 토큰을 세션에 저장하면 외부에 노출하지 않고 안전하게 사용할 수 있다.
🤟🏻 마지막으로 게시물 조회 API에서도 이 accessToken
이 있어야 호출될 수 있도록 변경할 것이다.
$ npm install jsonwebtoken
// type도 설치해준다.
$ npm install -D @types/jsonwebtoken
설치가 완료되었으면 .env
파일에 SECRET_KEY
환경변수를 만들어 준다.
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=topsecret
SECRET_KEY=nextauthsecretkey // << 추가
DATABASE_URL="file:./dev.db"
그 다음 app/lib
경로에 토큰을 생성하는 jwt.ts
를 만들어준다.
// app/lib/jwt.ts
import jwt, { JwtPayload } from "jsonwebtoken";
interface SignOption {
expiresIn?: string | number;
}
const DEFAULT_SIGN_OPTION: SignOption = {
expiresIn: "1h",
};
export function signJwtAccessToken(payload: JwtPayload, options: SignOption = DEFAULT_SIGN_OPTION) {
const secret_key = process.env.SECRET_KEY;
const token = jwt.sign(payload, secret_key!, options);
return token;
}
signJwtAccessToken()
함수는 jwt.sign()
를 통해 토큰을 리턴해준다.
토큰을 만들었으니 API 콜이 왔을 때 토큰의 유효성 체크를 해주는 verifyJwt()
함수도 만들어 준다.
// app/lib/jwt.ts
~~
~~
export function verifyJwt(token: string) {
try {
const secret_key = process.env.SECRET_KEY;
const decoded = jwt.verify(token, secret_key!);
return decoded as JwtPayload;
} catch (error) {
console.log(error);
return null;
}
}
기존 api/signin/route.ts
의 로직을 일부 변경해야한다.
// api/signin/route.ts
import prisma from '@/app/lib/prisma'
import * as bcrypt from 'bcrypt'
import {signJwtAccessToken} from "@/app/lib/jwt";
interface RequestBody {
username: string;
password: string;
}
export async function POST(request: Request) {
const body: RequestBody = await request.json()
const user = await prisma.user.findFirst({
where: {
email: body.username,
},
})
if (user && (await bcrypt.compare(body.password, user.password))) {
const { password, ...userWithoutPass } = user
// 토큰 생성 (추가된 부분)
const accessToken = signJwtAccessToken(userWithoutPass);
const result = {
...userWithoutPass,
accessToken,
};
return new Response(JSON.stringify(result))
} else return new Response(JSON.stringify(null))
}
Postman을 통해 로그인 api 를 호출하면 accessToken
이 추가된걸 볼 수 있다.
실제 로그인을 하게되면 콘솔로 확인할 수 있다.
💡 api 라우트는 서버사이드이기 때문에 서버 콘솔에서 확인 가능
/app/apit/auth/[...nextahtu]/route.ts
에 있는authorize()
함수의 console.log 내용이다.
✅ authorize()
를 수정해 토큰을 Session
에 저장해보자!
// /app/apit/auth/[...nextahtu]/route.ts
import NextAuth from 'next-auth/next'
import CredentialsProvider from 'next-auth/providers/credentials'
const handler = NextAuth({
providers: [
CredentialsProvider({
...
async authorize(credentials, req) {
// 로그인 로직
...
console.log('$$user: ', user)
},
}),
],
// 추가된 부분
callbacks:{
// token 정보와 user 정보를 하나의 object로 return
async jwt({ token, user }) {
return { ...token, ...user };
},
async session({ session, token }) {
console.log('$$$ token: ', token)
session.user = token as any;
console.log('$$$ session: ', session)
return session;
},
}
})
export { handler as GET, handler as POST }
callbacks
옵션은 로그인 로직( authorize()
)을 수행한 이후 실행되는 부분이다.
jwt()
콜백 함수에서 token
과 user
정보를 object
로 만들어 리턴하면 session()
콜백 함수에 token
으로 전달된다.
session()
콜백에서는 전달받은 값을 session.user
에 넣어 리턴해주어야 Client
에서 사용이 가능하다.
클라이언트에서는 const {data: session} = useSeesion()
를 호출해 사용할 수 있다.
위의 console 을 보면 session.user
에 accessToken
값이 들어가 있지만
Next-Auth 의 type을 보면 accessToken
이 정의되어있지 않다.
그렇기 때문에 type 커스터마이징이 필요하며, /app/type
경로에 next-auth.d.ts
파일을 만들어 구현한다.
// app/types/next-auth.d.ts
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: number;
name: string;
email: string;
accessToken: string;
};
}
}
✅ 생겼다!
게시물 조회 api인 /api/user/[id]/route.ts
를 호출 할 때 토큰을 검사하는 로직을 추가해 볼 것이다.
// /api/user/[id]/route.ts
import prisma from '@/app/lib/prisma'
import { verifyJwt } from '@/app/lib/jwt'
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
// accessToken 유효성 검사
const accessToken = request.headers.get('authorization')
if (!accessToken || !verifyJwt(accessToken)) {
return new Response(JSON.stringify({ error: 'No Authorization' }), {
status: 401,
})
}
console.log(params)
const id = Number(params.id)
const userPosts = await prisma.post.findMany({
where: {
authorId: id,
},
include: {
author: {
select: {
email: true,
name: true,
},
},
},
})
return new Response(JSON.stringify(userPosts))
}
request.headers
의 authorization
에 accessToken
이 없거나
verifyJwt()
유효성 검사를 통과하지 못하면 401(Unauthorized)
에러를 리턴한다.
⭕️ 통과
❌ 차단
App Router 에서는 Middleware
기능을 제공한다.
Middleware
는 페이지가 렌더링 되기 전에 로그인 여부
, 권한 체크
등의 작업을 수행할 수 있다.
활용 방법은 무궁무진하지만 이번에는 페이지 진입 전, 로그인 여부를 판단해 접근을 차단하는 작업만 하겠다.
/app
과 동일한 레벨에 middleware.ts
파일을 만들어 설정한다.
export { default } from 'next-auth/middleware'
export const config = {
matcher: ['/userposts/:path*'],
}
config
안에 있는 matcher에 페이지 경로를 등록하면 해당 페이지는 로그인한 사용자만 접근할 수 있다.
✅ 로그인 하지않은 사용자가 접근하게되면 로그인 화면으로 리다이렉트 시킨다.