vercel + vercel-postgres + auth0
半年以上前に技術調査しつつメモとして残していた記事を供養する。
特に内容精査もしていないし、何か結論があるわけではないので今もそのまま適用できる保証はありません。
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
vercel-postgres
add postgres
goto project dashboard
select region
choose connection
開発中のローカルのDBと切り替える手段あるのか?
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
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
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
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
import { handleAuth } from "@auth0/nextjs-auth0";
export const GET = handleAuth();
add UserProvider
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
"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
"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を許可する。
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
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.
use auth0 client library
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#'
// },,,
// }
ルートの保護
page単位で保護してもいいが、特定のディレクトリ以下まとめて保護したい場合はmiddlewareで一括指定するのが楽。
import { withMiddlewareAuthRequired } from "@auth0/nextjs-auth0/edge"
export default withMiddlewareAuthRequired()
export const config = {
matcher: ["/foo/:path*", "/bar/:path*"],
}
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