🐕

vercel + vercel-postgres + auth0

2024/09/11に公開

半年以上前に技術調査しつつメモとして残していた記事を供養する。
特に内容精査もしていないし、何か結論があるわけではないので今もそのまま適用できる保証はありません。

create next app

npx create-next-app@latest

vercel

create vercel project

add new project

import repository

deploy

change region

default is washington US

Error

node/pnpm version error

ERR_PNPM_UNSUPPORTED_ENGINE  Unsupported environment (bad pnpm and/or Node.js version)

node version

settings > general > node.js version

corepack

settings > Environment Variables

ENABLE_EXPERIMENTAL_COREPACK=1

https://vercel.com/docs/deployments/configure-a-build#corepack

vercel-postgres

https://vercel.com/docs/storage/vercel-postgres/quickstart

add postgres

goto project dashboard

select region

choose connection

開発中のローカルのDBと切り替える手段あるのか?
https://gal.hagever.com/posts/running-vercel-postgres-locally

install @vercel/postgres

npm i @vercel/postgres

install vercel cli

add cli locally

npm i vercel@latest --save-dev

login to vercel cli

npx vercel login

link local project to project on vercel.

npx vercel link

Prisma

https://vercel.com/guides/nextjs-prisma-postgres

setup prisma

install

npm install prisma --save-dev
npm install @prisma/client

init

npx prisma init

create schema

model User {
  id            Int            @id @default(autoincrement())
  name          String
}

setup with vercel-postgres

create env file

dashboard > storage > {vercel-postgres}
に書いてある以下を転記

datasource db {
  provider = "postgresql"
  url = env("POSTGRES_PRISMA_URL") // uses connection pooling
  directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
}

.envをコピーして転記するか、以下のコマンドを打つと自動で.envファイルが生成される

vercel env pull .env.development.local
POSTGRES_URL="postgres://..."
POSTGRES_PRISMA_URL="postgres://..."
POSTGRES_URL_NO_SSL="postgres://..."
POSTGRES_URL_NON_POOLING="postgres://..."
POSTGRES_USER="..."
POSTGRES_HOST="..."
POSTGRES_PASSWORD="..."
POSTGRES_DATABASE="..."

apply

npx prisma db push
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "verceldb", schema "public" at "***"

🚀  Your database is now in sync with your Prisma schema. Done in 4.85s

✔ Generated Prisma Client (v5.10.2) to ./node_modules/@prisma/client in 67ms

動作確認

insert data

npx prisma studio

create component

export const BankList = async () => {
  const users = await prisma.user.findMany()

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map((x) => (
          <li key={x.id}>{x.name}</li>
        ))}
      </ul>
    </div>
  );
};

check next dev page

npm run dev

ERROR

PrismaClientInitializationError: Prisma has detected that this project was built on Vercel, which caches dependencies. This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered. To fix this, make sure to run the `prisma generate` command during the build process.

vercel deploy時にはprismaClientがキャッシュされているので、build中にprisma generateコマンドを足しなさいと指示されます。

    "build": "prisma generate && next build",

Auth0

setup & tutorial

create account

https://auth0.com/jp/pricing

get started


test login

execute sample app

envファイルを書き換え
(AUHT0_SECRETはそのままでOK)

AUTH0_SECRET=replace-with-your-own-secret-generated-with-openssl
AUTH0_BASE_URL=http://localhost:3000
AUTH0_ISSUER_BASE_URL='https://{yourDomain}'
AUTH0_CLIENT_ID='{yourClientId}'
AUTH0_CLIENT_SECRET='{yourClientSecret}'
AUTH0_AUDIENCE=
AUTH0_SCOPE='openid profile'

無事ログイン

auth0 + nextjs

install

https://auth0.com/docs/quickstart/webapp/nextjs/01-login

npm install @auth0/nextjs-auth0

add env variables

AUTH0_SECRET='use [openssl rand -hex 32] to generate a 32 bytes value'
AUTH0_BASE_URL='http://localhost:3000'
AUTH0_ISSUER_BASE_URL='https://{yourDomain}'
AUTH0_CLIENT_ID='{yourClientId}'
AUTH0_CLIENT_SECRET='{yourClientSecret}'

add route handler

app/api/auth/[auth0]/route.ts
import { handleAuth } from "@auth0/nextjs-auth0";

export const GET = handleAuth();

add UserProvider

app/layout.tsx
import { UserProvider } from "@auth0/nextjs-auth0/client";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <UserProvider>
        <body className={inter.className}>{children}</body>
      </UserProvider>
    </html>
  );
}

add login button component

components/LoginButton.tsx
"use client";
import { useUser } from "@auth0/nextjs-auth0/client";

export const LoginButton = () => {
  const { user, error, isLoading } = useUser();
  console.log("user", user);
  return user ? (
    <a href="/api/auth/logout">Logout</a>
  ) : (
    <a href="/api/auth/login">Login</a>
  );
};

add profile component

components/Profile.tsx
"use client";

import { useUser } from "@auth0/nextjs-auth0/client";
import Image from "next/image";

export function User() {
  const { user, error, isLoading } = useUser();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>{error.message}</div>;

  return (
    user && (
      <div>
        <Image
          src={user.picture ?? ""}
          alt={user.name ?? ""}
          width={50}
          height={50}
        />
        <h2>{user.name}</h2>
        <p>{user.email}</p>
      </div>
    )
  );
}

gravatar

Auth0アカウントの場合gravatarの画像パスを返してくるためgravatarのURLを許可する。

next.config.js
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "s.gravatar.com",
        port: "",
        pathname: "/avatar/**",
      },
    ],
  },
};

Auth0

disable sign up

authentication > Database > settings
からDisable Sign UpsをONにする。

sign upが消えるが、未登録アカウントからのソーシャルログインで事実上のサインアップができてしまう。

management api

regular web appで作ったapplicationはデフォルトだとmanagement api使えないため、設定からauthorizedに変更する。(今回の場合はNext Testがregular web app)

how to get access token
https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens

Token quotas
Tokens issued for Auth0 APIs (Management API, Authentication API, MFA API, etc.) do not count toward the M2M token quota listed in the Dashboard. Only tokens with external audiences count toward your quota. See Auth0 Management API Rate Limits for details.

https://auth0.com/docs/secure/tokens/access-tokens/get-management-api-access-tokens-for-production

use auth0 client library
https://www.npmjs.com/package/auth0

import { ManagementClient } from 'auth0';

const management = new ManagementClient({
  domain: '{YOUR_TENANT_AND REGION}.auth0.com',
  clientId: '{YOUR_CLIENT_ID}',
  clientSecret: '{YOUR_CLIENT_SECRET}',
});

管理者がユーザ登録してユーザがパスワードを自身で設定する

managementApiでverify_email: falseでユーザ作成(∵ verifyメールを送らないため)
authenticateApiでchange passwordのリクエストを送る。
=> ユーザにパスワードリセットのemailが届く
初回ユーザにとってはパスワードリセットではないためテンプレートを改変した方がユーザーフレンドリー

const res = await authenticateApi.database.changePassword({
  connection: "Username-Password-Authentication",
  email,
})

managementApiからchange passwordした場合はresponseにパスワード変更用のurlが戻る。

const res = await managementApi.tickets.changePassword({
  user_id: response.data.user_id,
})

// { 
//   data: {
//     ticket: 'https://{domain}.auth0.com/u/reset-verify?ticket=hogehoge#'
//   },,,
// }

https://auth0.com/docs/customize/email/send-email-invitations-for-application-signup
https://auth0.com/docs/authenticate/database-connections/password-change

ルートの保護

page単位で保護してもいいが、特定のディレクトリ以下まとめて保護したい場合はmiddlewareで一括指定するのが楽。

middleware.ts
import { withMiddlewareAuthRequired } from "@auth0/nextjs-auth0/edge"

export default withMiddlewareAuthRequired()

export const config = {
  matcher: ["/foo/:path*", "/bar/:path*"],
}

https://auth0.github.io/nextjs-auth0/types/helpers_with_middleware_auth_required.WithMiddlewareAuthRequired.html

user with role

ユーザを取得してから1ユーザごとにroleを取り直すか、roleごとにユーザ一覧をとるしか方法がない。

const response = await managementClient.users.getAll({
q: `app_metadata.groupId:"${groupId}"`,
fields: "user_id,email,name,app_metadata",
})

const userListWithRole = response.data.map(user=>{
  // get role
  ...
})
const usersPromiseList = [roles.admin, roles.user].map(async (role) => {
    const response = await managementClient.roles.getUsers({ id: role })
    
    return response.data.map((user) => ({ role, ...user }))
})

// getUsersの戻りの型
type User = {
  user_id: string
  picture: string
  name: string
  email: string
}

metadataに適当な値を入れてユーザを任意のグループで管理していた場合、
1つ目の方法ではrole取得するクエリが増えすぎる。
2つ目の方法ではユーザ情報にはmetadataがないためユーザを分類するためにmetadataを取得するクエリが必要。
となり、効率が悪い。

管理者ユーザがユーザを登録するようなシステムで、管理画面でrole込みで一覧表示するなら細かいデータ設計をauth0上でやるのは避けた方がいいかも。やるにしてもユーザ登録時やrole変更時にmetadataにroleを書き出すなどひと工夫が要る。

Discussion