Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

미래학자

우버 클론 코딩 (nomad coders) #7 본문

nomad corders

우버 클론 코딩 (nomad coders) #7

미래학자 2019. 4. 17. 07:48

노마드 코더 - 우버 클론 코딩

#1.34 Introduction to Twilio

이번에는 휴대폰으로 문자 인증 서비스를 사용하기 위해 Twilio 서비스를 사용할 것이다. 이 서비스는 유료지만 그러게 부담스럽지 않은 가격으로 필요한 인증 서비스를 제공할 것이다.

https://twilio.com 에 접속해서 회원가입을 한 후 번호를 하나 발급 받는데, 꼭 미국 번호로 발급 받아야 한다. 발급 받는데 비용은 $1 다. 이게 어떻게 내는 것인지 잘 모르겠지만,,

https://www.twilio.com/console/sms/settings/geo-permissions 여기서 모든 체크를 해줘야한다. 체크를 안한 국가로는 SMS가 안갈 수 있다.

대쉬보드에서 토큰을 확인할 수 있다.

#1.35 StartPhoneVerification Resolver part One

twilio를 통해 폰인증을 구현할 것인데, 앞서 twilio 인증 정보를 .env에 추가하자.

  • src/.env 파일에 TWILIO_SID, TWILIO_PHONE, TWILIO_TOKEN을 추가하자.

      ...
      TWILIO_SID=AC1fe68ff538d041200e74b814eb015e41
      TWILIO_PHONE=+13347588283
      TWILIO_TOKEN=

이번에는 Verification entity에서 User, used를 제거할 것인데, 그 이유는 익명 유저일 때 무언가 문제가 되는 것 같다. used는 사용이 끝난 Verification은 삭제할 것이므로 이 필드가 필요가 없다.

  • src/api/Verification/shared/Verification.graphql

      type Verification {
        id: Int!
        target: String!
        payload: String!
        key: String!
        createAt: String!
        updateAt: String
      }
  • src/api/User/shared/User.graphql

      type User {
        id: Int!
        email: String
        verifiedEmail: Boolean!
        firstName: String!
        lastName: String!
        age: Int
        password: String
        phoneNumber: String
        verifiedPhoneNumber: Boolean!
        profilePhoto: String
        createAt: String!
        updateAt: String
        fullName: String
        chat: Chat
        messages: [Message]
        ridesAsPassenger: [Ride]
        ridesAsDriver: [Ride]
        isDriving: Boolean!
        isRiding: Boolean!
        isTaken: Boolean!
        lastLng: Float
        lastLat: Float
        lastOrientation: Float
        fbId: String
      }
    
      type Query {
        user: User
      }
  • src/entities/Verification.ts

      import {
        BaseEntity,
        BeforeInsert,
        Column,
        CreateDateColumn,
        Entity,
        PrimaryGeneratedColumn,
        UpdateDateColumn,
       } from 'typeorm'
      import { verificationTarget } from 'src/types/types';
    
      const PHONE = "PHONE";
      const EMAIL = "EMAIL";
    
      @Entity()
      class Verification extends BaseEntity {
        @PrimaryGeneratedColumn() id: number;
    
        @Column({ type: "text", enum: [EMAIL, PHONE]})
        target: verificationTarget;
    
        @Column({ type: "text"})
        payload: string;
    
        @Column({ type: "text"})
        key: string;
    
        @CreateDateColumn() createAt: string;
        @UpdateDateColumn() updateAt: string;
    
        @BeforeInsert()
        createKey(): void {
          if(this.target === PHONE) {
            this.key = Math.floor(Math.random() * 10000).toString();
          } else if(this.target === EMAIL) {
            this.key = Math.random().toString(36).substr(2);
          }
        }
      }
    
       export default Verification;
  • src/entities/User.ts

      import bcrypt from 'bcrypt';
      import { IsEmail } from 'class-validator';
      import { 
        BaseEntity, 
        BeforeInsert,
        BeforeUpdate,
        Column, 
        CreateDateColumn,
        Entity,
        ManyToOne,
        OneToMany, 
        PrimaryGeneratedColumn,
        UpdateDateColumn,
      } from 'typeorm';
      import Chat from './Chat';
      import Message from './Message';
      import Ride from './Ride';
    
      const BCRYPT_ROUNDS = 10;
    
      @Entity()
      class User extends BaseEntity {
        @PrimaryGeneratedColumn() id: number;
    
        @Column({ type: "text", nullable: true})
        @IsEmail()
        email: string | null;
    
        @Column({ type: "boolean", default: false})
        verifiedEmail: boolean;
    
        @Column({ type: "text"})
        firstName: string;
    
        @Column({ type: "text"})
        lastName: string;
    
        @Column({ type: "int", nullable: true})
        age: number;
    
        @Column({ type: "text", nullable: true})
        password: string;
    
        @Column({ type: "text", nullable: true})
        phoneNumber: string;
    
        @Column({ type: "boolean", default: false})
        verifiedPhoneNumber: boolean;
    
        @Column({ type: "text"})
        profilePhoto: string;
    
        @ManyToOne(type => Chat, chat => chat.participants)
        chat: Chat;
    
        @OneToMany(type => Message, message => message.user)
        messages: Message[];
    
        @OneToMany(type => Ride, ride => ride.passenger)
        ridesAsPassenger: Ride[];
    
        @OneToMany(type => Ride, ride => ride.driver)
        ridesAsDriver: Ride[];
    
        @Column({ type: "boolean", default: false})
        isDriving: boolean;
    
        @Column({ type: "boolean", default: false})
        isRiding: boolean;
    
        @Column({ type: "boolean", default: false})
        isTaken: boolean;
    
        @Column({ type: "double precision", default:0})
        lastLng: number;
    
        @Column({ type: "double precision", default:0})
        lastLat: number;
    
        @Column({ type: "double precision", default:0})
        lastOrientation: number;
    
        @Column({ type: "text", nullable: true})
        fbId: string;
    
        public comparePassword(password: string): Promise<boolean> {
          return bcrypt.compare(password, this.password);
        }
    
        get fullName() : string {
          return `${this.firstName} ${this.lastName}`
        }
    
        @CreateDateColumn() createAt: string;
    
        @UpdateDateColumn() updateAt: string;
    
        private hashPassword(password: string): Promise<string> {
          return bcrypt.hash(password, BCRYPT_ROUNDS);
        }
    
        @BeforeInsert()
        @BeforeUpdate()
        async savePassword() : Promise<void> {
          if(this.password) {
              const hashedPassword = await this.hashPassword(this.password);
              this.password = hashedPassword;
          }
        }
      }
    
      export default User;

그리고 twilio 모듈을 설치하여 손쉽게 사용하자.

$ yarn add twilio
$ yarn add @types/twilio --dev
  • src/api/User/StartPhoneVerification/StartPhoneVerification.graphql

      type StartPhoneVerificationResponse {
        ok: Boolean!
        error: String
      }
    
      type Mutation {
        StartPhoneVerification(phoneNumber: String!): StartPhoneVerificationResponse!
      }

    graphql type을 생성한 후 yarn types 로 src/types/graph에 추가되도록 하자.

  • src/api/User/StartPhoneVerification/StartPhoneVerification.resolvers.ts

      import { Resolvers } from 'src/types/resolvers';
      import { 
        StartPhoneVerificationMutationArgs, 
        StartPhoneVerificationResponse 
      } from 'src/types/graph';
      import Verification from '../../../entities/Verification';
    
      const resolvers: Resolvers = {
        Mutation: {
          StartPhoneVerification: async (
            _, 
            args: StartPhoneVerificationMutationArgs
          ) : Promise<StartPhoneVerificationResponse> => {
            const { phoneNumber } = args;
            try {
              const existingVerification = await Verification.findOne({ 
                payload: phoneNumber 
              });
              if(existingVerification) {
                existingVerification.remove();
              }
    
            } catch(error) {
              return {
                ok: false,
                error: error.message
              }
            }
          }
        }
      }
    
      export default resolvers;

    아직 리턴값을 정의하지않아서 타입 오류가 발생한다. 이건 아래에서 더 채운다.

#1.36 StartPhoneVerification Resolver part Two

  • src/utils/sendSMS 위 강의에 이어서 Twilio로 SMS을 보내는 유틸 파일을 추가할 것이다.

      import Twilio from 'twilio';
    
      const { TWILIO_SID, TWILIO_TOKEN, TWILIO_PHONE } = process.env;
    
      const twilioClient = Twilio(TWILIO_SID, TWILIO_TOKEN);
    
      const snedSMS = (to: string, body: string) => {
        return twilioClient.messages.create({
          body,
          to,
          from: TWILIO_PHONE
        });
      };
    
      export const sendVerificationSMS = (to: string, key: string) => 
        snedSMS(to, `Your verification key is : ${key}`);

    snedSMS: 메시지를 보냄

    sendVerificationSMS: snedSMS을 이용하여 인증 메시지를 보냄

  • src/api/User/StartPhoneVerification/StartPhoneVerification.resolvers.ts

      import { Resolvers } from 'src/types/resolvers';
      import { 
        StartPhoneVerificationMutationArgs, 
        StartPhoneVerificationResponse 
      } from 'src/types/graph';
      import Verification from '../../../entities/Verification';
      import { sendVerificationSMS } from '../../../utils/sendSMS';
    
      const resolvers: Resolvers = {
        Mutation: {
          StartPhoneVerification: async (
            _, 
            args: StartPhoneVerificationMutationArgs
          ) : Promise<StartPhoneVerificationResponse> => {
            const { phoneNumber } = args;
            try {
              const existingVerification = await Verification.findOne({ 
                payload: phoneNumber 
              });
              if(existingVerification) {
                existingVerification.remove();
              }
              const newVerification = await Verification.create({
                payload: phoneNumber,
                target: "PHONE"
              }).save();
              await sendVerificationSMS(newVerification.payload, newVerification.key);
              console.log(newVerification);  // 아래에서 확인 후 지울 내용
                      return {
                ok: true,
                error: null
              }
            } catch(error) {
              return {
                ok: false,
                error: error.message
              }
            }
          }
        }
      }
    
      export default resolvers;

자 이제 올바로 메시지를 보내는지 확인해보자.

http://localhost:4000/playground 에서 쿼리를 실행시키자.

mutation {
  StartPhoneVerification(phoneNumber: "+82-1033337777") {
    ok
    error
  }
}

33337777은 휴대폰 010을 제외한 번호이니 각자 자신의 번호를 대입하면 된다.

올바로 메시지를 보내는 것을 확인하자.

README.md 에서 Start Phone Number Verification 항목을 완료했다.

#1.38 CompletePhoneVerification part One

인증 완료 기능을 진행할 텐데, 먼저 자세하게 어떤 순서로 동작하는지 flow chart가 없어서 설명하는 것이 어렵다. 강의에도 그런 내용이 먼저 나오지는 않는다. 하지만 강의를 좀 더 진행해서 내가 로직을 이해하면 flow chart를 포스팅에 추가하도록 하겠다.

  • src/api/Verification/shared/Verification.graphql 에 verified 필드를 추가하자.

      ...
      key: String!
      verified: Boolean!
      createAt: String!
      ...
  • src/entities/Verification.ts

      ...
      @Column({ type: "text"})
      key: string;
    
      @Column({ type: "boolean", default: false })
      verified: boolean;
    
      @CreateDateColumn() createAt: string;
      ...
  • src/api/User/CompletePhoneVerification/CompletePhoneVerification.graphql ComplatePhoneVerification 이라는 mutation과 응답 타입을 정의하자.

      type CompletePhoneVerificationResponse {
        ok: Boolean!
        error: String
        token: String
      }
    
      type Mutation {
        CompletePhoneVerification(phoneNumber: String!, key: String!) : CompletePhoneVerificationResponse!
      }

#1.38 CompletePhoneVerification part Two

이어서 resolver를 작성하자.

  • src/api/Verification/shared/Verification.resolvers.ts

      import Verification from "../../../entities/Verification";
      import User from "../../../entities/User";
      import { 
        CompletePhoneVerificationResponse, 
        CompletePhoneVerificationMutationArgs 
      } from "src/types/graph";
      import { Resolvers } from 'src/types/resolvers';
    
      const resolvers: Resolvers = {
        Mutation: {
          CompletePhoneVerification: async (
            _,
            args: CompletePhoneVerificationMutationArgs
          ) : Promise<CompletePhoneVerificationResponse> => {
            const { phoneNumber, key } = args;
            try {
              const verification = await Verification.findOne({ 
                payload: phoneNumber,
                key
              })
              if(!verification) {
                return {
                  ok: false,
                  error: 'Verification token key not valid',
                  token: null
                }
              } else {
                verification.verified = true;
                verification.save();
              }
            } catch(error) {
              return {
                ok: false,
                error: error.message,
                token: null
              }
            }
    
            try {
              const user = await User.findOne({ phoneNumber });
              if(user) {
                user.verifiedPhoneNumber = true;
                user.save();
                return {
                  ok: true,
                  error: null,
                  token: 'Coming soon'
                }
              } else {
                return {
                  ok: true,
                  error: null,
                  token: null
                }
              }
            } catch(error) {
              return {
                ok: false,
                error: error.message,
                token: null
              }
            }
          }
        }
      }
    
      export default resolvers;

이번에는 StartPhoneVerification에 내 폰번를 입력해서 문자를 받은 후에 아래처럼 내 번호와 받은 숫자 4자리를 입력해보자. 잘 입력했다면 ok가 true를 떨어질 것이다. 또 키를 임의의 다른 값으로 바꿔보자. 그러면 false를 리턴하는 것을 확인할 수 있다.

    mutation { 
        CompletePhoneVerification(phoneNumber: "+82-1022228888", key: "0000") { 
          ok 
          error 
          } 
    }

README.md에서 Sign Up with Email 항목이 완료됐다.

Comments