Carrot征服市場ノート[8]-「認証ページ」

16220 ワード

このページでは、ユーザー認証のすべての機能が実装され、ログイン時に一度にパスワードを入力してログインします.そのため,token活用,T柳,SendGridなどが含まれている.

1. Accounts logic


phoe numberまたはemail adressを使用してアカウントを作成するロジックを実装します.
pages/api/users/enter.tsx
import { NextApiRequest, NextApiResponse } from "next";
import withHandler from "@libs/server/withHandler";
import client from "@libs/server/client";

async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { phone, email } = req.body; // user 가 입렵후 submit 한 phone 또는 email 정보를 req.body 로 가져옴.
  const inputInfo = phone ? { phone: +phone } : { email };  //es6 삼항 연산 !!
  const user = await client.user.upsert({  
    where: {
      ...inputInfo,
    },
    create: {
      name: "Anonymous", 	   // name is required in user table so, init as Anonymous
      ...inputInfo,
    },
    update: {},
  });
  console.log(user);
  
  return res.status(200).end();
}

export default withHandler("POST", handler);

※ Point

  • は通常クエリーです.検索するデータにfindUniqueクエリーがない場合は、createクエリーを作成するためのコードを作成できます.これは長いコードですが、ここで使用するupsertクエリーはクエリーです.MySQLのUNIQE KEY値に重複値がない場合は、Updataを実行し、重複する場合はInsertを実行できます.コードを簡略化できます.使用する場合は、3つのオプション(create、where、update)を明記する必要があります.また、constペイロード=phone?{ phone: +phone } : { email }; これは3つの演算で、コードを簡略化するために必要な複数の値を条件付きで使用できます.
  • 2. Token Logic


    prisma/schema.prisma
    model User {
    	// ...
      tokens    Token[]      // 여러개[]의 Token 을 User 에 담음.
    }
    
    model Token {
      id        Int      @id @default(autoincrement())
      payload   String   @unique
      user      User     @relation(fields: [userId], references: [id])
      // User 테이블에 Token 테이블에 관계성을 Token의 userId를 필드로 와 User의 id를 참조로 지정.  
      userId    Int
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }
    pages/api/users/enter.tsx
    async function handler(req: NextApiRequest, res: NextApiResponse) {
      const { phone, email } = req.body;
      const user = phone ? { phone: +phone } : { email };
      const payload = Math.floor(100000 + Math.random() * 900000) + "";
      // payload 는 User 모델 내에서 unique한 값이어야 되어, 6자리의 random 숫자를 생성하고 + "" 의 의미는 string 으로 변환.
      const token = await client.token.create({
        data: {
          payload,
          user: {
            connectOrCreate: { // command 키를 누르고 data 를 클릭해보면, user 필드를 꼭 넣어주야 한다는 것을 알수 있는데 한층 더 가보게되면 3가지 옵션이 나오는데 그중, connectOrCreate 사용 하여 modle 들을 연결하고 생성 하겠다는 의미
              where: {
                ...user,
              },
              create: {
                name: "Anonymous",
                ...user,
              },
            },
          },
        },
    
      });
      
      console.log(token);

    ※ Point


  • ConnectOrCreateを使用すると、既存のユーザーに新しいtokenを接続できます.ユーザーがいない場合は、新しいユーザーアカウントIDとtokenを作成できます.whereとcreateを記入する必要があります.

  • connectOrCreateを使用してtoken接続だけでなく、ユーザーアカウントIDも検索し、ない場合は新しい(tokenとともに)を作成します.
  • 3.req、resブール化


    libs/server/withHandler.ts
    export interface ResponseType {
      ok: boolean;
      [key: string]: any;
    }
    
    pages/api/users/enter.tsx
    import withHandler, { ResponseType } from "@libs/server/withHandler";
    
    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 });
      // 만약 phone 이나 email 정보 없이 user 가 form 을 제출 하게되면 400 에러 코드를 보낸다.
    return res.json({
        ok: true,
      });
    }

    ※ Point


    pages/api/users/enter.tsxでres.status(200)を返します.end(); このように、httpステータスコードを返すのではなく、req、res変数をboolean typeとして指定することで、上記のコードのように使用できます.

    4. Twilio


    この項目では,ログインはユーザが入力したPhoneまたはemailを介してワンタイムパスワードを提供するとともに,ログインロジックを提供し,TwiioはSMSを送信する機能を提供することができる.その他、WebRTC、ビデオ電話なども利用できます.

    Setup

  • Twilioアカウントを作成し、Account SIDとAUTH TOKENを使用します.envファイルを入れます.
  • 「メッセージサービス」タブでsender poolフェーズでtryit out-get setupページに移動し、setupボタンを押すと、米国の電話番号が表示されます.
  • Trialアカウントなので、送った番号は変えられません.試用料は毎月1ドルです.
  • メッセージングサービスは、MSG SIDに再アクセスする.envファイルに保存します.
  • 4-1 Twiio SMSメッセージの送信方法


    pages/api/users/enter.tsx
    import twilio from "twilio";
    
    const twilioClient = twilio(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
    // .env 파일에있는 Twilio SID 와 TOKEN 을 받아옴.
     if (phone) {
        const message = await twilioClient.messages.create({
          messagingServiceSid: process.env.TWILIO_MSID,
          to: process.env.MY_PHONE!, 
          body: `Your login token is ${payload}.`,
        });
        console.log(message);
      }

    4-2 Twiioメールの送信方法


    これは、ワンタイムパスワードtokenをSMSではなく、SendGrid(bytwiio)を介してユーザに電子メールアドレスを送信する方法である.

    Setup

  • 会員加入後、API KEYを作成する.envファイル
  • に挿入
    pages/api/users/enter.tsx
    import mail from "@sendgrid/mail";
    
    mail.setApiKey(process.env.SENDGRID_KEY!);
    
    else if (email) {
       const email = await mail.send({
         from: "Email 주소 입력",
         to: "Email 주소 입력",
         subject: "Your Carrot Market Verification Email",
         text: `Your token is ${payload}`,
         html: `<strong>Your token is ${payload}</strong>`, //만약을 위해 html 도 같이 보내준다.
       });
       console.log(email);
     }
    

    ※ Point


    SMSもemailも、現在は試用目的で、to:一部を私の携帯電話番号、電子メールアドレスに設定しますが、後でユーザー情報を受信して処理します.注意しなければならないのは、この部分を処理して、必ず費用を最低限のmoneyに下げなければなりません...TESTプロセスでコメント処理を行います.

    5 Token UI


    5-1 Type Script Genericを使用します。


    まず、Type Scriptでは、Genericは再利用可能な構成部品を作成するために使用され、異なるタイプに役立つ構成部品を作成することができます.ここでTはType parameterと呼ばれ、JENARICを宣言する際に慣用される識別子である.
    libs/client/useMutation.tsx
    import { useState } from "react";
    
    interface UseMutationState<T> {
      loading: boolean;
      data?: T;  				// 이전엔 data의 type 은 object이였다.
      error?: object;
    }
    type UseMutationResult<T> = [(data: any) => void, UseMutationState<T>];
    // 이 코드 또한 <T>를 선언해준다
    
    export default function useMutation<T = any>(      //첫번째, <T> 선언 해주고 UseMutationState으로 보냄.
      url: string
    ): UseMutationResult<T> // 두번째, <T>를 다시 UseMutationResult 넣어주고 {
      const [state, setSate] = useState<UseMutationState<T>>({
        loading: false,
        data: undefined,
        error: undefined,
      });
    

    ※ Point 5-1


    このプロジェクトでは、UserMutionStateに宣言されたデータ型をobjectとして指定しましたが、pages/enterです.tsxファイルでは、Booleanタイプを使用してデータが存在するかどうかを表すためにGenericを使用しています.また、データを使用するすべてのオブジェクトもTを宣言する必要があります.

    5-2 Token入力ページを作成します。


    Tokenがemailまたはphone#に送信された場合、UIにはパスワードtokenを入力してログインする画面が必要です.
    pages/enter.tsx
    interface TokenForm {  // token을 위한 type 선언
     token: string;
    }
    
    interface MutationResult {
     ok: boolean;
    }
    
    const Enter: NextPage = () => {
    
    const [enter, { loading, data, error }] =
       useMutation<MutationResult>("/api/users/enter");
       
     const [confirmToken, { loading: tokenLoading, data: tokenData }] =
       useMutation<MutationResult>("/api/users/confirm");
       // token을 위한 새로운 mutation hook. 이름을 바꿀쑤 있는것은 useMutation 이 배열을 리턴하기 때문이다.
     const { register, handleSubmit, reset } = useForm<EnterForm>();
     
     const { register: tokenRegister, handleSubmit: tokenHandleSubmit } =
       useForm<TokenForm>();
       // register과 handleSubmit은 이미 사용 되어 지고 있는 이름이기 때문에 새로운 type을 적용해 다른 변수로 취급하게 한다.
       
     const onTokenValid = (validForm: TokenForm) => {
       if (tokenLoading) return;
       confirmToken(validForm);
     };
     
    return (
     {data?.ok ? (           // 이 부분에서 .ok? 부분을 boolean 으로 하고 싶기 때문에 boolean type 으로 선언되어 있는 MutationResult객체 타입을 useMutation 에 적용 한 것이다.
             <form
               onSubmit={tokenHandleSubmit(onTokenValid)}
               className="flex flex-col mt-8 space-y-4"
             >
              <Input
                 register={tokenRegister("token", {
                   required: true,
                 })}
                  name="token"
                 label="Confirmation Token"
                 type="number"
                 required
               />
                <Button text={tokenLoading ? "Loading" : "Confirm Token"} />
             </form>
           ) : (
           <>
           // 기존 enter 페이지의 UI
           </>
             
          )

    ※ Point 5-2


    Tokenに対してuseMutation hookを再使用し、同じ変数名を別の同じ変数名に書き込む場合は、タイプ名を異なる変数として指定できます.

    Tokenチェックの作成


    pages/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; //req.body 에서 token 을 받아 온다.
      console.log(token);
      res.status(200).end(); // 전송 ok
    }
    
    export default withHandler("POST", handler);

    6. Serverless Sessions


    今回は、ironセッションを使用してユーザーの認証を行います.サーバlessが成立するのは,ユーザがログインする際に使用するトークンが暗号化され,クッキー方式で格納・ロードされるためである.操作手順は、ペイロード(トークン番号)を暗号化する->暗号化されたペイロードをクッキーとしてユーザに送信する->受信したクッキーを暗号化する->現在のページにアクセスするユーザのidにアクセス性を付与する方式で実現される.
    ちょっと待って.
    JWT(Json Web Token)は、ユーザIDを持つオブジェクトに署名し、署名とともにユーザにトークンを送信するものであり、JWTはトークン中の情報を識別することができ、セキュリティ上少し脆弱である可能性がある.また、セッションのバックエンドを構築する必要があります.しかしながら、iron sessionを使用すると、これらの欠点を保護し、認証ロジックを実現することができる.
    インストーラのマニュアルは次のとおりです.npm i iron-session
  • Ironセッション使用方法
  • 使用したい関数をiron session helper関数で包みます.
  • ironセッションに暗号化パスワードを設定します(長い複雑なパスワードを使用します).
  • より前に受信したTokenをセッションに保存します.
  • クッキーで暗号化されたtokenがユーザ情報と同じである場合、ログインは完了する.
  • Tokenデータセッションの読み込み(POST)


    pages/api/users/confirm.tsx
    import { withIronSessionApiRoute } from "iron-session/next";
    
    declare module "iron-session" {       // 아래 res.session.user 에서 모듈의 정의를 찾을수 없다는 error가 생긴다. 
    이런 경우엔, declare를 선언 하여 해당 변수가 존재 한다는것을 알려주어야 한다.
      interface IronSessionData {
        user?: {
          id: number;
        };
      }
    }
    
    async function handler(
      req: NextApiRequest,
      res: NextApiResponse<ResponseType>
    ) {
      const { token } = req.body;
    
    const exists = await client.token.findUnique({
        where: {
          payload: token,       // token 모듈에 payload 필드 값을 저장 한다.
        },
      });         
      if (!exists) return res.status(404).end(); // Token 이 없다면 404 에러 코드
      req.session.user = {     // 토큰이 존재한다면, userId 를 세션에 id를 넣어 생성.
        id: exists?.userId,
      };
      await req.session.save(); // 암호화가 된 세션을 저장
      res.status(200).end();
    }
    
    export default withIronSessionApiRoute(withHandler("POST", handler), {
      cookieName: "carrotsession",
      password:
        "복잡한 구조의 아무런 번호 넣는곳",
    }); 

    Tokenデータ情報の読み込み(GET)


    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,
      });
    }
    
    export default withIronSessionApiRoute(withHandler("GET", handler), {
      cookieName: "carrotsession",
      password:
        "9845904809485098594385093840598df;slkgjfdl;gkfsdjg;ldfksjgdsflgjdfklgjdflgjflkgjdgd",
    });

    ※ Point

  • withHandler関数をironsession withIronSessionApiRouteで包むとres.sessionにアクセスできます.このプロジェクトでは、すべてのapiが同じサーバではなく単独で実行されます.したがって、apiごとにironセッションをカプセル化します.
  • 以下に示すように、
  • ironセッション設定を追加する必要があります.次に、promission voidまたはanyをwithHandlerに返さなければなりません.
  • 知っておくべきこと。


    Prismaについて知りたいこと

  • tokenのような関係のあるモデルを削除する必要がある場合は、次のコードのようにCascadeを追加できます.これはparent recordを削除するとchild recordも削除されることを意味します.Setnullを使用する場合はtokenを使用できますが、nullを使用して置き換えることができます.
  • prisma/schema.prisma
    model Token {
     user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
     }

  • phone#の場合、国番号も一緒に入力されるため、ユーザーモデルタイプはBigIntまたはStringに処理する必要があります.

  • npx prisma dbpush:アーキテクチャの更新時に提供