깃허브를 통해서 웹사이트 로그인을 하는 방법을 알아보려고 한다!
먼저, https://github.com/settings/applications/new 사이트에서
기본 사항을 입력하면 된다. 위에서 Autorization callback URL은 인증 과정이 이루어지고, 후에 callback할 URL 주소를 입력해주는 것인데, http://localhost:3000/github/complete로 입력해 주었다.
간단한 기본 사항을 입력하고 Register application 버튼을 누르게 되면, Client ID 와 Client secrets(API)를 우리에게 주는데,
.env 파일에 입력해 두었다.
이제, github 로그인 하기 버튼을 눌렀을 때 -> /github/start로 이동하기 위해서 폴더를 생성해보려고 한다. 
github/start/route.ts 폴더를 생성해 주었는데 route.ts를 생성해준 이유는,
route.ts 파일은 Next.js의 API Routes 기능을 활용하는 파일로, 특정 URL 경로에 대한 HTTP 요청을 처리하는 핸들러를 정의할 수 있는 곳이다.
이 파일을 사용하여 React.js나 HTML을 리턴하지 않고, 백엔드에서 데이터 처리, API 응답 생성 등의 작업을 수행할 수 있다.
Next.js에서 github/start/route.ts 파일을 생성하면, /github/start 경로에 대한 HTTP 요청(GET, POST 등)을 처리하는 로직을 이 파일 안에서 작성할 수 있다.
따라서, 이 파일을 이용해 백엔드 작업을 처리하고 클라이언트로 JSON 응답을 보내는 등의 작업을 수행할 수 있다.
export function GET(){}
이 GET함수는 /github/start URL로 GET request를 보낼 때 동작하게 된다.
https://docs.github.com/ko/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
이 수조에 들어가보면, OAuth 앱 권한 부여에 대해서 알아볼 수 있는데,
params에 이 정보들을 불러 올 수 있는 것을 확인할 수 있다.


우리는 client_id, scope, allow_signup 정보를 params로 넘겨줄 것이다.
여기서 중요한 scope는 Github에게 우리가 사용자로부터 원하는 데이터가 무엇인지 알려주는 것이라고 생각하면 된다. 사용자에게 보여주는 부분임
여기서 repo => 우리가 repo scope를 요청하고 사용자가 수락한다면, 그 사용자는 다시 말해 본인 계정의 모든 레파지토리에 대한 접근 권한을 넘겨준 것임.
--> public, private이든 전부 우리가 읽고 쓸 수 있다
read:user -> 사용자 프로필 읽기 권한을 주게 되고,
user:email -> 사용자 메일 주소 읽기 권한을 준다.
이러한 정보들을 가지고 GET()요청을 보내는 함수를 만들어보자

export function GET() { ... }
GET 함수는 HTTP GET 요청이 들어왔을 때 호출된다. Next.js의 API Routes에서 GET 요청에 대한 핸들러로 사용된다.
const baseURL = 'https://github.com/login/oauth/authorize'
baseURL은 GitHub의 OAuth 인증 URL이다. 이 URL을 사용하여 사용자를 GitHub의 로그인 페이지로 리다이렉트하게 된다.
const params = { ... }
params 객체는 GitHub OAuth 인증 URL에 추가될 쿼리 파라미터들을 정의한다.
client_id: GitHub OAuth 애플리케이션의 클라이언트 ID. process.env.GITHUB_CLIENT_ID! 를 통해 환경 변수에서 이 값을 가져온다.여기서 !는 TypeScript에서 값이 반드시 존재할 것이라고 명시적으로 알려주는 것을 의미한다
scope: GitHub API에 접근할 수 있는 권한 범위.
--> 여기서는 read:user (사용자 정보 읽기)와 user:email (사용자의 이메일 정보 접근)을 요청한다.
allow_signup: 'true' 로 설정하면 GitHub에 가입하지 않은 사용자가 인증 과정에서 새로운 계정을 만들 수 있다.
const formattedParams = new URLSearchParams(params).toString()
URLSearchParams 객체를 사용하여 params 객체를 URL의 쿼리 문자열로 변환한다. 예를 들어, client_id=your_client_id&scope=read:user,user:email&allow_signup=true와 같은 형식의 문자열을 생성한다.
const finalUrl = ${baseURL}?${formattedParams}
finalUrl은 GitHub OAuth 인증 URL에 쿼리 파라미터를 추가한 최종 URL이다.
return Response.redirect(finalUrl)
이 줄은 사용자를 finalUrl로 리다이렉트를 시킨다.
--> 즉, 이 API 엔드포인트에 GET 요청을 보내면, 사용자는 GitHub의 OAuth 인증 페이지로 이동하게 된다.

깃허브로 로그인 버튼을 누르게 되면, formattedParams를 해당한 URL로 이동하면서 깃허브 로그인 페이지로 이동하는 것을 확인할 수 있었다!
flow to authorize
1 . 유저들은 그들의 Github 신원을 요청하기 위해 redirect -> 우리는 'github/start'라는 URL로 유저들을 보내고, 여기서 여러 가지 parameter로 새로운 URL을 만든 다음에, 만든 github url로 유저를 redirect시키게 된다.
2 . 유저들은 Github에 의해 원래 접속했던 사이트로 다시 redirect하게 된다.
github/complete 로 redirect하게 된다. -> 사용자가 요청을 수락하게 되면, Github는 우리 사이트로 다시 redirect를 하게 되는데, 임시 code parameter를 보내주게 된다.
--> 이 임시 code parameter는 10분 후에 만료된다고 써있음.
이제 이 임시 code parameter를 access token으로 교환하는 작업을 하면 된다.
--> Github API로부터 유저 프로필 정보와 유저의 이메일을 가져올 수 있게 된다.

이제, github/complete로 접속했을 경우, 우리는 code=dsjlfsdfu와 같은 parameter를 얻게 된다.
이 parameter를 access token으로 변경하는 작업을 할 것이다.
import { notFound } from 'next/navigation';
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const code = request.nextUrl.searchParams.get('code');
if (!code) {
return notFound();
}
const accessTokenParams = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
client_secret: process.env.GITHUB_CLIENT_SECRET!,
code,
}).toString();
const accessTokenURL = `https://github.com/login/oauth/access_token?${accessTokenParams}`;
const accessTokenResponse = await fetch(accessTokenURL, {
method: 'POST',
headers: {
Accept: 'application/json',
});
const accessTokenData = await accessTokenResponse.json();
if ('error' in accessTokenData) {
return new Response(null, {
status: 400,
});
}
return Response.json({ accessTokenData });
}
export async function GET(request: NextRequest) { ... }const code = request.nextUrl.searchParams.get('code') :요청 URL에서 code라는 쿼리 파라미터를 추출한다.if (!code) { return notFound(); }const accessTokenParams = new URLSearchParams({ ... }).toString()const accessTokenURL = https://github.com/login/oauth/access_token?${accessTokenParams}const accessTokenResponse = await fetch(accessTokenURL, { ... })const accessTokenData = await accessTokenResponse.json()if ('error' in accessTokenData) { return new Response(null, { status: 400 }); }return Response.json({ accessTokenData });
github/complete?code= 형태로 URL에 code가 포함되어있는 것을 확인할 수 있으며, return Response.json({ accessTokenData}) 로 json() 형식으로 화면에 출력되는 것을 확인할 수 있다.
위에서 github가 보내준 code를 우리 백엔드에서 access token으로 교환을 했다.
--> 이제 이 access token을 이용해서 실제로 GitHub API에 요청할 수 있게 되었다.
이제 이 정보들을 가지고 GitHub OAuth 인증 후 사용자의 프로필 정보를 가져와, 해당 사용자가 이미 데이터베이스에 있는지 확인한 다음,
사용자를 세션에 저장하고, 프로필 페이지로 리디렉션하는 로직을 구현하면 된다.
--> 새로운 사용자라면 데이터베이스에 사용자를 생성한 후 세션에 저장하고, 마찬가지로 프로필 페이지로 리디렉션한다.
const { error, access_token } = await accessTokenResponse.json();
if (error) {
return new Response(null, {
status: 400,
});
}
const userProfileResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`,
},
cache: 'no-cache',
});
const { id, avatar_url, login } = await userProfileResponse.json();
const user = await db.user.findUnique({
where: {
github_id: id + '',
},
select: {
id: true,
},
});
if (user) {
const session = await getSession();
session.id = user.id;
await session.save();
return redirect('/profile');
}
const newUser = await db.user.create({
data: {
github_id: id + '',
avatar: avatar_url,
username: login,
},
select: {
id: true,
},
});
const session = await getSession();
session.id = newUser.id;
await session.save();
return redirect('/profile');
}
위의 코드에서 조금 수정하고 덧붙여주었다!
GitHub 사용자 프로필 정보 가져오기
/user 엔드포인트에서 사용자의 프로필 정보를 가져온다.사용자 정보 데이터베이스 조회
기존 사용자 처리
새 사용자 생성 및 처리

1 . 로그인이라는 function을 만들어보기
2 . username이 여기있는 GitHub username과 같도록 생성중임.
해당 username이 이미 사용중이면 username을 누군가 사용중인지를 먼저 확인하고, 이미 사용중이라면, 랜덤 username을 준다던지, Github id에다가 username을 붙여서 두개를 합친 것을 username으로 해줄 수도 있도록
3 . https://api.github.com/user/emails 로 요청 보내기 -> 우리에게 user email을 줌 -> user의 email을 받아오기
4 . request, response 코드 분리 -> request를 보내는 곳과 response를 JSON으로 바꾸는 것을 따로 function으로 바꾸기
1 . 로그인 function 만들기

( lib 디렉토리는 프로젝트의 유틸리티 함수, 도구, API 호출 등과 같은 공통된 기능을 저장하는 데 적합 )
lib/auth.ts 에 정의한 Auth 함수는 세션에 사용자 ID를 저장하고, 사용자를 /profile 페이지로 리다이렉트하는 기능을 제공한다.
이렇게 정의해 놓으면, 회원가입 후 혹은 로그인 후에 세션에 사용자를 저장하고 리다이렉트할 때 Auth 함수를 호출하기만 하면 된다.
GitHub으로 로그인할 때 -> username이 이미 저장되어있는지 확인해보기
깃허브로 로그인을 할 때, 
만약, email로 가입한 사용자가 이미 seoyeon1123 을 사용하고 있는데, 깃허브 id가 seoyeon1123 일 경우 -> 중복이 발생하게 된다.
이럴 경우 어떻게 하면 좋을 까?
먼저, 깃허브 사용자 이름이 username에 저장이 되어있는지 먼저 확인해야 한다.
만약, 저장이 안되어있다면, 사용자 이름 그대로 저장해도 되지만, 이미 사용자 이름이 username에 있다면, 깃허브 사용자 이름에 특수 기호를 붙여서 저장할 수 있도록 한번 해보도록 하겠다!
const { id, avatar_url, login } = await userProfileResponse.json();
// GitHub ID로 사용자 조회
const user = await db.user.findUnique({
where: {
github_id: id + '',
},
select: {
id: true,
username: true,
},
});
if (user) {
return Auth(user.id);
}
const findUser = await db.user.findUnique({
where: {
username: login,
},
select: {
id: true,
},
});
const timeStamp = Date.now();
const newUser = await db.user.create({
data: {
username: findUser ? login + timeStamp : login,
github_id: id + '',
avatar: avatar_url,
},
select: {
id: true,
},
});
// 새로 생성된 사용자로 Auth 처리
return Auth(newUser.id);
}
먼저, github_id로 user가 DB에 존재하는지 확인한다. 만약, DB에 user가 있다면, Auth(user.id)로 로그인을 시켜준다.
user가 없다면,
DB에 username이 login으로 존재하는 user가 있는지 확인한다(findUser)
만약, findUser가 있다면? login에 timeStamp를 추가해주고, findUser가 없다면, login 그대로 username에 저장되도록 해주었다.
그 후, Auth(newUser.id)로 세션에 저장하면서 로그인 처리를 해주었다.
기존에 seoyeon1123을 이메일로 회원가입을 시켜주었고,
같은 seoyeon1123을 가지고 있는 깃허브 주소로 로그인 시켜보았더니

https://api.github.com/user/emails 로 요청 보내서 user의 email 받아오기
const userEmailResponse = await fetch('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${access_token}`,
},
cache: 'no-cache',
});
헤더에 Authorization을 추가하여 GitHub API의 /user/emails 엔드포인트에서 이메일 정보를 받아오고 있다.
Bearer ${access_token} 형식의 Authorization 헤더를 사용하여, GitHub에서 발급한 access_token으로 인증을 처리하고 있다.
const emailData = await userEmailResponse.json();
const email = emailData[0].email;
userEmailResponse.json() 을 호출해서, API 응답이 JSON 형식으로 변환되어 emailData 변수에 저장한다.
그 후, emailData 배열의 첫 번째 요소에서 email 속성을 가져왔다.
그리고, DB에 저장할 때, email 속성을 추가해주었더니 깃허브로 로그인할 때, 깃허브 회원가입 했던 email 속성이 
그렇다면 username과 마찬가지로 email 중복도 확인해 주어야 한다.
const findEmail = await db.user.findUnique({
where: {
email: email,
},
select: {
id: true,
},
});
if (findEmail) {
return new NextResponse('Email is already in use.', {
status: 400,
});
}
만약 DB에 email이 저장되어 있다면, NextResponse() 메서드를 활용해서 400 상태로 'Email is already in use' 메시지를 출력할 수 있도록 작성해 두었다.
request, response 코드 분리
github/complete 하나의 코드 안에서 Response, Request의 코드가 많이 이루어지고 있어서 분리를 시켜보려고 한다.

export async function getGitHubAccessToken(code: string) {
const accessTokenParams = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
client_secret: process.env.GITHUB_CLIENT_SECRET!,
code,
}).toString();
const accessTokenURL = `https://github.com/login/oauth/access_token?${accessTokenParams}`;
const accessTokenResponse = await fetch(accessTokenURL, {
method: 'POST',
headers: {
Accept: 'application/json',
},
});
const { error, access_token } = await accessTokenResponse.json();
if (error) {
throw new Error('Error retrieving access token');
}
return access_token;
}
export async function getGitHubUserProfile(accessToken: string) {
const userProfileResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
cache: 'no-cache',
});
return userProfileResponse.json();
}
export async function getGitHubUserEmail(accessToken: string) {
const userEmailResponse = await fetch('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
cache: 'no-cache',
});
return userEmailResponse.json();
}
getGitHubAccessToken , getGitHubUserProfile , getGitHubUserEmail 를 export로 정의해 둔 뒤,
이렇게 불러다가 작성해두었다.
코드를 나눠놓으면서 가독성이 더욱 좋아지고, 재사용성이 좋아진 것을 알 수 있다!
token생성하는 로직 구현 -> token을 검증하는 것을 해볼 것임.
twilio를 이용해서 sms을 통해 token을 user에게 보내는 것
sms login action에는 두가지 상태가 있다. --> 1 . user에게 token을 보내기 전의 상태, 이 때는 user가 우리에게 전화번호를 보내주는 때가 될 것임.
1 . 먼저 전화번호를 입력한다
-> zod를 통해 전화번호 검증을 시작한다.
2 . 전화번호 검증에 성공하면, 이전에 저장되어있던 token이 있다면 삭제한다.
(db에 저장되어있는 phone number과, 사용자가 입력한 phone number이 같을 경우에)
db.sMSToken.deleteMany({})를 통해서 phone에 저장되어있는 token을 모두 삭제한다.
3 . 이후 token을 생성한다 (cryto를 이용)
crypto의 ramdonInt를 활용해서 6자리 랜덤 숫자를 생성한 후, token에 저장한다.
이후, token이 db에 존재하는지 확인한다. 만약, 존재한다면, getToken()함수를 반환해 새로운 token을 생성하고, 존재하지 않는다면, token을 반환해준다.
4 . 생성된 token을 db.sMSToken.create({})이용해 DB에 저장한다.
이때, user와 연결해야 한다.
DB에 phone number이 저장된 user가 있다면, 찾아서 sMSToken을 연결시켜주고, 없다면, 새로운 user를 생성한다.

db.sMSToken.create({}) 를 통해 sMSToken 테이블에 token과 사용자 정보를 저장한다. user 필드에서는 connectOrCreate 메서드를 사용하여 두 가지 작업을 수행한다:
정리
사용자가 token을 받고, token을 입력하고 verify token 버튼을 눌렀을 때, 어떤 일이 일어나는지 생각해보면
사용자가 입력한 토큰이 유효한지 검증하고, 토큰이 문제가 없다면 token의 userId를 얻고(token에 연결된 user가 누구인지 알 수 있도록),
그 후 얻은 userId를 이용해서 로그인 시켜주어야 한다.
(sMSToken -> userId를 가지고 있어서 어떤 userId가 SMS Token에 연결되어 있는지 알 수 있음.)
먼저 token이 DB에 저장되어있는 token인지 확인해보아야 한다.

tokenExists() 함수를 정의한다 -> db.sMSToken,findUnique({}) 를 통해서 token이 user가 입력한 token과 일치한지를 확인하고, 만약 일치하다면 true, 일치하지 않다면 false를 return 해서 "This token not exits" 오류 메시지를 출력하도록 한다.

토큰 검증:
tokenSchema.safeParseAsync(token) 을 통해 입력된 token이 유효한지 검증한다.토큰 조회 및 로그인:
토큰 삭제 및 리다이렉트:
https://console.twilio.com/auth0 링크에 들어가서 회원가입 절차를 끝나면,
이러한 정보들을 얻을 수 있다. 이 정보들을 .env 폴더에 넣어준 뒤,
npm install twilio를 통해 설치하면 된다.
twilio에 .env 에 저장해둔 값을 넣어준 뒤, client.message.create({}) 에 정보들을 넣어준다.
이렇게 넣어줄때, to에는 원래 이론적으로는 result.data -> 사용자가 입력한 Phone number가 입력되어야하지만, twilio는 기본적으로 회원가입한 번호에게만 무료로 문자 제공을 해주기 때문에 이렇게 넣어두었다.