iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

一些讓你看來很強的全端- trcp 伴讀系列 第 25

Day-025. 一些讓你看來很強的全端 TRPC 伴讀 -Token authorization ( Init )

  • 分享至 

  • xImage
  •  

今天我們要來實作 refetch_tokenaccess_token 實作~流程大概會是如下。

  • Add a new user to the database
  • Refresh the access token
  • Sign in the registered user
  • Log out the user

SetUp

step1 install nextjs

> npx create-next-app@latest authorization --ts

step2 修改 tsconfig.json

這邊使 trpc 團隊推薦的 tsconfig ,筆者可以根據自己偏好調整

//tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "strictNullChecks": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "noUncheckedIndexedAccess": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["src/*"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    "./*.js",
    "./src/**/*.js"
  ],
  "exclude": ["node_modules"]
}

step3 setUp dockerFile

這邊我們的 db 會用 docker 起一個 postgresQLredis

// docker-compose.yml
version: '3'
services:
  postgres:
    image: postgres
    container_name: postgres
    ports:
      - '6500:5432'
    restart: always
    env_file:
      - ./.env
    volumes:
      - postgres-db:/var/lib/postgresql/data
  redis:
    image: redis:latest
    container_name: redis
    ports:
      - '6379:6379'
    volumes:
      - redis:/data
volumes:
  postgres-db:
  redis:

step4 env

這邊是本次專案會用到的 env ,以下是範例讀者也可以自行調整。

// .env
DATABASE_PORT=6500
POSTGRES_PASSWORD=password123
POSTGRES_USER=postgres
POSTGRES_DB=trpc-nextjs
POSTGRES_HOST=postgres
POSTGRES_HOSTNAME=127.0.0.1

NEXT_PUBLIC_TRPC_ENDPOINT=""
ACCESS_TOKEN_PRIVATE_KEY=""
ACCESS_TOKEN_PUBLIC_KEY=""
REFRESH_TOKEN_PRIVATE_KEY=""
REFRESH_TOKEN_PUBLIC_KEY=""

接著我們 docker 啟動~

> docker-compose up -d

然後檢查 port 有沒有成功對接。

> docker ps -a                               
CONTAINER ID   IMAGE          COMMAND                  CREATED        STATUS                       PORTS                    NAMES
4c9ab62e6c53   postgres       "docker-entrypoint.s…"   24 hours ago   Up 2 minutes                 0.0.0.0:6500->5432/tcp   postgres
6544419c75b1   redis:latest   "docker-entrypoint.s…"   2 days ago     Exited (255) 2 minutes ago   0.0.0.0:6379->6379/tcp   redis

step5 prisma

> npm install @prisma/client && npm install -D prisma

初始化 prisma

> npx prisma init

然後記得修改一下 DATABASE_URL 的值

// .env
DATABASE_PORT=6500
POSTGRES_PASSWORD=password123
POSTGRES_USER=postgres
POSTGRES_DB=trpc-nextjs
POSTGRES_HOST=postgres
POSTGRES_HOSTNAME=127.0.0.1

DATABASE_URL="postgresql://postgres:password123@localhost:6500/trpc-nexjs?schema=public"

新增 schema ,這邊順便補充一下用到的 decorator 好了。

@@map : 指定 orm 映射到 dbtable 名稱叫什麼, @@map("users") 代表 User modal 會在 db 中,對應到 userstable
@db.VarChar : prisma 有提供一些 dbvalidate 功能例如 VarChar 設定。
enum : prisma 可以透過 enum type 列舉出有哪些 value 可以填入。


model User{
  @@map("users")

  id String @id @default(uuid())
  name String @db.VarChar(255)
  email String @unique
  photo String @default("default.png")
  verified Boolean @default(false)

  hashPassword String?
  role RoleEnumType @default(user)

  createAt DateTime @default(now())
  updateAt DateTime @updatedAt

  provider String?
}

enum RoleEnumType {
  user
  admin
}

之後我們寫 scriptmigrate

// package.json
{
"scripts": {
    "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
    "db:push": "npx prisma db push"
  },
}

--create-only : 先產生 migrate schema 但還沒應用到 db ,可用於檢視 SQL command 有無錯誤。

>npx prisma migrate dev --create-only

之後再跑一次 npx prisma migrate dev db 就會吃 migrate 的內容了~

>npx prisma migrate dev 

之後只要每次到 npm run 就會幫你 migrate schema 然後產生 prisma clienttype了。

> npm run db:migrate && npm run db:push

redis client

透過 dockerredislocal 環境後,就需要加 client instance 拉~

// src/server/utils/connectRedis.ts
import { createClient } from 'redis'
const redisUrl = `redis://localhost:6379`;
export const redisClient = createClient({
  url: redisUrl
})
const connectRedis = async () => {
  try {
    console.log('? Redis client connected...');

    await redisClient.connect()
    redisClient.set(
      'trpc',
      'welcome to trpc'
    )
  } catch (e: any) {
    await redisClient.disconnect()
    console.log(e.message)
    process.exit(1)
  }
}

connectRedis()
redisClient.on('error', (err) => console.error(err))

Prisma client

import { PrismaClient } from "@prisma/client";

declare global {
  var prisma: PrismaClient | undefined
}

export const prisma = global.prisma || new PrismaClient()

if (process.env.NODE_ENV == 'development') global.prisma = prisma

export const connectDB = async () => {
  try {
    await prisma.$connect()
    console.log('? Database connected successfully');
  } catch (e: any) {
    console.error(e.message)
    await prisma.$disconnect()
    process.exit(1)
  } finally {
    await prisma.$disconnect()
  }
}

step6 trpc-init

> npm install @trpc/server@next @trpc/next@next superjson redis

@trpc/next : 這是 trpc server 管理 proceduresrouter 的套件。
@trpc/next : 讓 trpc server 可以用在 nextjsapi handler
superjson : 序列化與反序列化的套件,方便解析 Date 或是 new Set 等等的資料。
redis : redis server

createContext

這邊我們簡單寫一個 createContext function 日後可以用到 createNextApiHandler 中, 並使用 inferAsyncReturnType 去自動推論 Context type 是什麼。

//src/server/createContext.ts

import { inferAsyncReturnType } from "@trpc/server";
import { NextApiRequest, NextApiResponse } from "next";

export function createContext({
  req,
  res,
}: {
  req: NextApiRequest;
  res: NextApiResponse;
}) {
  return { req, res };
}

export type Context = inferAsyncReturnType<typeof createContext>;

tRPC Server

這邊我們用 superjson 幫我們做 transformer ,好處就是 transformer 會自動序列化 Date 甚至是 new Set 的資料,這樣就不需要額外處理了。

// src/server/createRouter.ts
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { Context } from "./createContext";

export const t = initTRPC.context<Context>().create({
  transformer: superjson,
});

export const publicProcedure = t.procedure
export const router = t.router

routers

這邊簡單測試 redisClient 功能~

import { t } from "../createRouter";
import redisClient from "../utils/connectRedis";

export const appRouter = t.router({
  getHello: t.procedure.query(async ({ ctx }) => {
    const message = await redisClient.get("trpc");
    return { message };
  }),
});

export type AppRouter = typeof appRouter;

api handler

這邊別放忘記把 createContext 放進去喔~

import * as trpcNext from "@trpc/server/adapters/next";
import { appRouter } from "~/server/routers/app.routes";
import { createContext } from "~/server/createContext";

export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext,
});

測試一下 api

https://ithelp.ithome.com.tw/upload/images/20231009/20145677c9mZfcvXv8.png

tRPC Client

> npm install @trpc/react-query@next @trpc/client@next @tanstack/react-query @tanstack/react-query-devtools 

client utils

這邊特定設定 staleTime 5 秒,如此 5 秒內的 request 永遠都是 refresh的。

import { AppRouter } from '../server/routers/app.routes';

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import SuperJSON from 'superjson';

function getBaseUrl() {
  if (typeof window !== 'undefined')
    // browser should use relative path
    return '';
  if (process.env.VERCEL_URL)
    // reference for vercel.com
    return `https://${process.env.VERCEL_URL}`;
  if (process.env.RENDER_INTERNAL_HOSTNAME)
    // reference for render.com
    return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
  // assume localhost
  return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const api = createTRPCNext<AppRouter>({
  config(opts) {
    return {
      transformer: SuperJSON,
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
          // You can pass any HTTP headers you wish here
          async headers() {
            return {
            };
          },
        }),
      ],
      queryClientConfig: {
        defaultOptions: {
          queries: {
            staleTime: 5 * 1000
          }
        }
      }
    };
  },
  ssr: false,
});

step7 provider

之後記得加 ReactQueryDevtools_app.tsx

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppType } from 'next/app';

import { api } from '~/utils/trpc';

const MyApp: AppType = ({ Component, pageProps }) => {
  return (
    <>
      <Component {...pageProps} />
      <ReactQueryDevtools initialIsOpen={false} />
    </>
  );
};
export default api.withTRPC(MyApp);

相關連結

https://github.com/Danny101201/refetch-token/tree/main

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day-024. 一些讓你看來很強的全端 TRPC 伴讀 -TRPC with App router
下一篇
Day-026. 一些讓你看來很強的全端 TRPC 伴讀 -Token authorization (JWT )
系列文
一些讓你看來很強的全端- trcp 伴讀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言