토큰 model 필요.
// api/users/enter.tsx
if (email) {
// 클라이언트를 사용해서 DB에서 email 에 해당하는 user를 검색
user = await client.user.findUnique({
where: {
email,
},
});
if (user) console.log("found it.");
if (!user) {
// 유저가 없으면 생성한다.
console.log("Did not find. Will create.");
user = await client.user.create({
data: {
name: "Anonymous",
email,
},
});
}
console.log(user);
}
if (phone) {
// 클라이언트를 사용해서 DB에서 phone 에 해당하는 user를 검색
user = await client.user.findUnique({
where: {
phone: +phone,
},
});
if (user) console.log("found it.");
if (!user) {
console.log("Did not find. Will create.");
user = await client.user.create({
data: {
name: "Anonymous",
phone: +phone,
},
});
}
console.log(user);
}
이렇게 DB를체크해서 있으면 가져오고 없으면 생성하는 로직은 빈번하기 때문에, 위 코드를 간단히 하는 기능이 이미 갖춰져 있다.
const user = await client.user.upsert({
where: {
...payload,
},
create: {
// 없으면 생성하기
name: "Anonymous",
...payload,
},
update: {}, // 해당 유저를 찾을 경우 어떤 업데이트를 할 것인지
});
console.log(user);
schema.prisma
에 Token 모델추가// schema.prisma
model User {
id Int @id @default(autoincrement())
phone Int? @unique
email String? @unique
name String
avatar String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tokens Token[]
}
model Token {
id Int @id @default(autoincrement())
payload String @unique // 확인해야 하는 값. 필수이자 유일한 값
user User @relation(fields: [userId], references: [id]) // userId 는 User모델의 id 임을 명시함
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
npx prisma db push
planetscale 의 해당 브랜치의 schema 에 업데이트 됨
// api/users/enter.tsx
const user = phone ? { phone: +phone } : { email };
const payload = Math.floor(100000 + Math.random() * 900000) + "";
const token = await client.token.create({
// 토큰을 생성해서 db에 올림
data: {
payload,
user: {
connectOrCreate: {
// 생성한 유저와 토큰(payload)를 연결한다.
where: {
...user,
},
create: {
name: "Anonymous",
...user,
},
},
},
},
});
(npx prisma studio)
connect : 새로운 토큰을 이미 존재하는 유저와 연결
create: 새로운 토큰과 유저를 만듬
createOrCreate : 유저를 찾고 찾으면 토큰과 connect 하고 찾지 못하면 생성해준다.
Twilio 는 사람들한테 문자메세지 보내기, robocall, 폰번호 숨기기, 이메일기능 등 기능이 있다.
ACCOUNT SID
코드를 복사해서, .env
파일에 TWILIO_SID=코드문자열
을 넣고 저장한다.AUTH TOKEN
코드도 복사해서, .env
파일에 TWILIO_SID=코드문자열
을 넣고 저장한다.[Messaging] - [services] - [create messaging service]
서비스 이름
을 적고 [Notify my users] 선택 - [create messaging service] 클릭
Sender Pool 화면이 나오는데, 우선 [Try it out]메뉴-[Get Set Up] 으로 가본다. 메세지 서비스를 시작할수 있다.
[Start setup] - [select messaging service] 하면 기본적으로 월 1달러에 폰번호를 하나 제공해준다. (trial 로 15달러가 있으니까 1년은 사용한다)
[Provision and add this number] 클릭
[Try SMS] 버튼 클릭하면 메세지를 보내기 위해 필요한 폰번호, api요청을 확인할 수 있는 페이지가 나온다.
필수: 대시보드에 있는
ACCOUNT SID
와AUTH TOKEN
코드를 .env 파일에 제대로 저장했는지 확인한다.
> [Messaging]-[Service] 에서 Sid 코드도 .env 에 복사해야 한다. (test로 나 자신에게 보내야 하므로)
폰번호도 .env 파일에 추가하면 편리함
npm i twilio
설치const twilioClient = twilio(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
:twilio 를 import 한 뒤 twilio 클라이언트를 생성한다.import twilio from "twilio";
import { NextApiRequest, NextApiResponse } from "next";
import withHandler, { ResponseType } from "@libs/server/withHandler";
import client from "@libs/server/client";
// twilio 클라이언트를 생성
const twilioClient = twilio(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { phone, email } = req.body;
const user = phone ? { phone: +phone } : email ? { email } : null;
if (!user) return res.status(400).json({ ok: false });
const payload = Math.floor(100000 + Math.random() * 900000) + "";
const token = await client.token.create({
data: {
payload,
user: {
connectOrCreate: {
where: {
...user,
},
create: {
name: "Anonymous",
...user,
},
},
},
},
});
if (phone) {
// 메세지를 생성하고 보낸다.
const message = await twilioClient.messages.create({
messagingServiceSid: process.env.TWILIO_MSID,
to: process.env.MY_PHONE!, // !는 확실히 존재하는 변수임을 Typescript에게 알려준다.
body: `Your login token is ${payload}.`,
});
console.log(message);
}
return res.json({
ok: true,
});
}
export default withHandler("POST", handler);
console.log(message) 로 출력된 값
4. 이메일, 닉네임 등 필수입력사항을 기입한 뒤 입력한 메일주소로 verify 하면 된다. 5. sendgrid에서 [Email API] - [Integration Guide] - [Web API]
npm install --save @sendgrid/mail
import mail from "@sendgrid/mail"
을 적고 이메일을 보내는 코드를 작성한다.// api/users/enter.tsx
import mail from "@sendgrid/mail";
const email = await mail.send({
from: "abcd@naver.com",
to: "abcd@naver.com",
subject: "Your Carrot Market Verification Email",
text: `Your token is ${payload}`,
html: `<strong>Your token is ${payload}</strong>`,
});
console.log(email);
console.log(email);
가 찍힌 모습
로그인할 경우에만 UI 를 보이도록 구현하자
// @relation 에 onDelete: Cascade 를 추가한다.
model Token {
id Int @id @default(autoincrement())
payload String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
저장후
npx prisma db push
backend
// api/users/confirm.tsx
import { NextApiRequest, NextApiResponse } from "next";
import withHandler, { ResponseType } from "@libs/server/withHandler";
import client from "@libs/server/client";
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { token } = req.body;
console.log(token);
res.status(200).end();
}
export default withHandler("POST", handler);
frontend
// pages/enter.tsx
const Enter: NextPage = () => {
const [confirmToken, { loading: tokenLoading, data: tokenData }] =
useMutation<MutationResult>("/api/users/confirm");
const { register: tokenRegister, handleSubmit: tokenHandleSubmit } =
useForm<TokenForm>();
const onTokenValid = (validForm: TokenForm) => {
if (tokenLoading) return;
confirmToken(validForm);
};
return <>{/*보여지는 코드*/}</>;
};
백엔드에서 iron session 을 이용하여, 유저에게 쿠키를 주고 유저가 요청할 때 누구인지 알 수 있게 하자
iron session : 서명/암호화된 쿠키를 사용하는 NodeJs 의 stateless 세션 툴이다.
npm i iron-session
로 iron-session 설치withIronSessionApiRoute
이 함수로 핸들러(또다른 함수를 리턴하는 함수)를 감싸주면 iron session 이 req 객체 안에 요청하는 세션 유저들을 담아 보내준다.적용 예시
// pages/api/login.ts
import { withIronSessionApiRoute } from "iron-session/next";
export default withIronSessionApiRoute(
async function loginRoute(req, res) {
// 핸들러
// get user from database then:
req.session.user = {
// req.session.user 생성
id: 230,
admin: true,
};
await req.session.save(); // 세션을 저장
res.send({ ok: true });
},
{
cookieName: "myapp_cookiename",
password: "complex_password_at_least_32_characters_long",
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
}
);
// api/users/confirm.tsx
// 주어지는 토큰에 해당하는 유저를 db에서 찾고 유저id를 세션에 저장한다.
import { withIronSessionApiRoute } from "iron-session/next";
import { NextApiRequest, NextApiResponse } from "next";
import withHandler, { ResponseType } from "@libs/server/withHandler";
import client from "@libs/server/client";
// TS에게 req.session 의 모습을 알려준다
declare module "iron-session" {
interface IronSessionData {
user?: {
id: number;
};
}
}
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { token } = req.body;
// Prisma로 토큰과 일치 하는 데이터를 찾는다.
const exists = await client.token.findUnique({
where: {
payload: token,
},
});
if (!exists) return res.status(404).end();
req.session.user = { // 세션객체에에 user 객체를 만들어서 넣는다.
id: exists.userId,
};
await req.session.save(); // 세션을 저장한다.
res.status(200).end();
}
export default withIronSessionApiRoute(withHandler("POST", handler), {
cookieName: "carrotsession",
password:// password 는 .env 파일에 넣어주자
});
세션이 쿠키에 저장된 모습
// pages/api/users/me.tsx
import { withIronSessionApiRoute } from "iron-session/next";
import { NextApiRequest, NextApiResponse } from "next";
import withHandler, { ResponseType } from "@libs/server/withHandler";
import client from "@libs/server/client";
declare module "iron-session" {
interface IronSessionData {
user?: {
id: number;
};
}
}
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
console.log(req.session.user);
const profile = await client.user.findUnique({
where: { id: req.session.user?.id },
});
res.json({
ok: true,
profile,
});
}
// "GET" 메소드를 쓴다
export default withIronSessionApiRoute(withHandler("GET", handler), {
cookieName: "carrotsession",
password: // ,
});
/api/users/me
url 로 접근시,console.log(req.session.user)
에 의해 다음 내용이 출력된다.
// pages.enter.tsx
// 토큰 데이터가 ok 되면 '/' 으로 라우팅
const router = useRouter();
useEffect(() => {
if (tokenData?.ok) {
router.push("/");
}
}, [tokenData, router]);
NextAuth 는 Next.js 에서 authentication 구현을 도와주는 패키지이다.
간단한 설정만으로 인증 기능을 처리해준다. (해당 프로젝트에서 사용하진 않을 것임)
인증되지 않은 유저로부터 페이지를 보호하는 훅을 만들어보자
// lib/server/withHandler.ts
export interface ResponseType {
ok: boolean;
[key: string]: any;
}
interface ConfigType {
method: "GET" | "POST" | "DELETE";
handler: (req: NextApiRequest, res: NextApiResponse) => void;
isPrivate?: boolean; // 이 값으로 로그인 여부 체크
}
export default function withHandler({
method,
isPrivate = true,
handler,
}: ConfigType) {
return async function (
req: NextApiRequest,
res: NextApiResponse
): Promise<any> {
if (req.method !== method) {
return res.status(405).end();
}
// private 이고 user 가 없으면, 에러를 발생시킨다.
if (isPrivate && !req.session.user) {
return res.status(401).json({ ok: false, error: "Plz log in." });
}
try {
await handler(req, res);
} catch (error) {
console.log(error);
return res.status(500).json({ error });
}
};
}
로그인하지 않은 채로 api 요청시
// libs/client/useUser.ts
// 유저 정보를 가져오는 훅
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
export default function useUser() {
const [user, setUser] = useState();
const router = useRouter();
useEffect(() => {
fetch("/api/users/me")
.then((response) => response.json())
.then((data) => {
if (!data.ok) {
return router.replace("/enter"); // 히스토리에 남지 않게 그냥 페이지를 교체할때 replace 쓴다
}
setUser(data.profile);
});
}, [router]);
return user;
}
개선할 점: api 요청으로 데이터를 가져올 때 , 캐싱해서 쓰는 것이 네트워크 자원을 절약하는 데 좋다.
SWR 을 사용해 보자!