diff --git a/.env.sample b/.env.sample
index 25113b4d..4998fcef 100644
--- a/.env.sample
+++ b/.env.sample
@@ -19,13 +19,11 @@ DATABASE_URL=postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}
GOOGLE_CLIENT_ID=xxxx
GOOGLE_CLIENT_SECRET=xxxx
-# NextAuth.js
-NEXTAUTH_URL=${NEXT_PUBLIC_SITE_URL}
-# https://next-auth.js.org/configuration/options#secret
+# Better Auth
+BETTER_AUTH_URL=${NEXT_PUBLIC_SITE_URL}
# you must generate a new secret
-# error: "ikm" must be at least one byte in length'
# $ openssl rand -base64 32
-NEXTAUTH_SECRET=TKDdLVjf7cTyTs5oWVpv04senu6fia4RGQbYHRQIR5Q=
+BETTER_AUTH_SECRET=TKDdLVjf7cTyTs5oWVpv04senu6fia4RGQbYHRQIR5Q=
# start: otel #
# OpenTelemetry
diff --git a/.env.test b/.env.test
index bf83991d..1567c444 100644
--- a/.env.test
+++ b/.env.test
@@ -2,8 +2,9 @@
GOOGLE_CLIENT_ID=dummy
GOOGLE_CLIENT_SECRET=dummy
-# NextAuth.js
-NEXTAUTH_TEST_MODE=true
+# Better Auth
+BETTER_AUTH_URL=http://localhost:3000
+BETTER_AUTH_SECRET=test_secret_key_for_testing_only
# start: stripe #
# Stripe
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2dc46c25..c90a5d8d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -70,7 +70,8 @@ jobs:
--build-arg GOOGLE_CLIENT_ID=${{env.GOOGLE_CLIENT_ID}} \
--build-arg GOOGLE_CLIENT_SECRET=${{env.GOOGLE_CLIENT_SECRET}} \
--build-arg NEXT_PUBLIC_SITE_URL=${{env.NEXT_PUBLIC_SITE_URL}} \
- --build-arg NEXTAUTH_SECRET=${{env.NEXTAUTH_SECRET}} \
+ --build-arg BETTER_AUTH_SECRET=${{env.BETTER_AUTH_SECRET}} \
+ --build-arg BETTER_AUTH_URL=${{env.BETTER_AUTH_URL}} \
--build-arg TRACE_EXPORTER_URL=${{env.TRACE_EXPORTER_URL}} \
--build-arg STRIPE_PRICE_ID=${{env.STRIPE_PRICE_ID}} \
--build-arg STRIPE_SECRET_KEY=${{env.STRIPE_SECRET_KEY}} \
@@ -84,7 +85,8 @@ jobs:
DATABASE_HOST: host.docker.internal
DATABASE_PORT: 5432
DATABASE_SCHEMA: public
- NEXTAUTH_SECRET: TKDdLVjf7cTyTs5oWVpv04senu6fia4RGQbYHRQIR5Q=
+ BETTER_AUTH_SECRET: TKDdLVjf7cTyTs5oWVpv04senu6fia4RGQbYHRQIR5Q=
+ BETTER_AUTH_URL: http://localhost:3000
GOOGLE_CLIENT_ID: dummy
GOOGLE_CLIENT_SECRET: dummy
# start: otel #
diff --git a/Dockerfile b/Dockerfile
index 61959920..f087f609 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,8 @@ ARG DATABASE_SCHEMA=''
ARG GOOGLE_CLIENT_ID=''
ARG GOOGLE_CLIENT_SECRET=''
ARG NEXT_PUBLIC_SITE_URL=''
-ARG NEXTAUTH_SECRET=''
+ARG BETTER_AUTH_SECRET=''
+ARG BETTER_AUTH_URL=''
# start: otel #
ARG TRACE_EXPORTER_URL=''
# end: otel #
@@ -30,8 +31,8 @@ ENV DATABASE_SCHEMA=$DATABASE_SCHEMA
ENV GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
ENV GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
-ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
-ENV NEXTAUTH_URL=$NEXT_PUBLIC_SITE_URL
+ENV BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET
+ENV BETTER_AUTH_URL=$BETTER_AUTH_URL
# start: otel #
ENV TRACE_EXPORTER_URL=$TRACE_EXPORTER_URL
# end: otel #
diff --git a/README.md b/README.md
index 816307b4..dfcb4034 100644
--- a/README.md
+++ b/README.md
@@ -12,18 +12,18 @@
Installing this template automatically sets up the following libraries/tools. By saving you significant effort, it allows you to focus entirely on writing your product code.🤗
-| | | | | |
-| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **App** |
Next.js
| Tailwind CSS
| NextAuth.js
| React Hook Form
|
-| | Zod
| OpenTelemetry
| Prisma
| PostgreSQL
|
-| | Stripe
| | |
-| | | | |
-| **Tools** | TypeScript
| pnpm
| Biome
| Prettier
|
-| | Knip
| EditorConfig
| lefthook
| Docker
|
-| | | | |
-| **Testing** | Vitest
| Testing Library
| Playwright
| Testcontainers
|
+| | | | | |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **App** | Next.js
| Tailwind CSS
| Better Auth
| React Hook Form
|
+| | Zod
| OpenTelemetry
| Prisma
| PostgreSQL
|
+| | Stripe
| | |
+| | | | |
+| **Tools** | TypeScript
| pnpm
| Biome
| Prettier
|
+| | Knip
| EditorConfig
| lefthook
| Docker
|
+| | | | |
+| **Testing** | Vitest
| Testing Library
| Playwright
| Testcontainers
|
| | | |
-| **Others** | GitHub Actions
| Renovate
| VSCode
|
+| **Others** | GitHub Actions
| Renovate
| VSCode
|
Please read the features provided by this template first! 👉
[Challenges Solved](https://hiroppy.github.io/web-app-template/introduction/challenges-solved.html)
diff --git a/e2e/dummyUsers.ts b/e2e/dummyUsers.ts
index 5d69e7ab..f077d665 100644
--- a/e2e/dummyUsers.ts
+++ b/e2e/dummyUsers.ts
@@ -1,10 +1,8 @@
-import type { User } from "next-auth";
+import type { User } from "../src/app/_clients/betterAuth";
-type RemoveNullish = {
- [K in keyof T]-?: NonNullable;
-};
-
-type NonNullableUser = RemoveNullish;
+type NonNullableUser = Required<
+ Pick
+>;
export const user1: NonNullableUser = {
id: "id1",
diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts
index ea210774..9d2511f6 100644
--- a/e2e/fixtures.ts
+++ b/e2e/fixtures.ts
@@ -1,6 +1,6 @@
import AxeBuilder from "@axe-core/playwright";
import { test as base } from "@playwright/test";
-import type { User } from "next-auth";
+import type { User } from "../src/app/_clients/betterAuth";
import { setupDB } from "../tests/db.setup";
import { setupApp } from "./helpers/app";
import { registerUserToDB } from "./helpers/users";
@@ -21,7 +21,9 @@ export type TestFixtures = {
signInPage: SignInPage;
notFoundPage: NotFoundPage;
storageState: string;
- registerToDB: (user: User) => Promise;
+ registerToDB: (
+ user: Pick,
+ ) => Promise;
reset: () => Promise;
a11y: () => AxeBuilder;
};
@@ -82,9 +84,11 @@ export const test = base.extend({
},
],
registerToDB: async ({ reset, setup }, use) => {
- await use(async (user: User) => {
- await registerUserToDB(user, setup.dbURL);
- });
+ await use(
+ async (user: Pick) => {
+ await registerUserToDB(user, setup.dbURL);
+ },
+ );
await reset();
},
reset: ({ context, setup }, use) => {
diff --git a/e2e/helpers/app.ts b/e2e/helpers/app.ts
index 15dbce00..6c5ea828 100644
--- a/e2e/helpers/app.ts
+++ b/e2e/helpers/app.ts
@@ -6,7 +6,7 @@ export async function setupApp(dbPort: number) {
const appPort = await getRandomPort();
const baseURL = `http://localhost:${appPort}`;
const cp = exec(
- `NEXTAUTH_URL=${baseURL} DATABASE_PORT=${dbPort} pnpm start --port ${appPort}`,
+ `BETTER_AUTH_URL=${baseURL} DATABASE_PORT=${dbPort} pnpm start --port ${appPort}`,
);
await waitForHealth(baseURL);
diff --git a/e2e/helpers/users.ts b/e2e/helpers/users.ts
index e2ad6a12..c9a61e98 100644
--- a/e2e/helpers/users.ts
+++ b/e2e/helpers/users.ts
@@ -1,22 +1,28 @@
+import crypto from "node:crypto";
import type { BrowserContext, TestType } from "@playwright/test";
-import type { User } from "next-auth";
-import type { JWT } from "next-auth/jwt";
+import type { User } from "../../src/app/_clients/betterAuth";
import type { TestFixtures, WorkerFixtures } from "../fixtures";
import { generatePrismaClient } from "./prisma";
-export async function registerUserToDB(user: User, dbUrl: string) {
+export async function registerUserToDB(
+ user: Pick,
+ dbUrl: string,
+) {
await using db = await generatePrismaClient(dbUrl);
await db.prisma.user.create({
data: {
- ...user,
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ emailVerified: false,
+ image: user.image ?? null,
+ role: user.role as "USER" | "ADMIN",
accounts: {
create: {
- type: "oauth",
- provider: "google",
- providerAccountId: `${Math.random()}`,
- id_token: "id_token",
- access_token: "access_token",
- token_type: "Bearer",
+ accountId: `${Math.random()}`,
+ providerId: "google",
+ accessToken: "access_token",
+ idToken: "id_token",
scope: "scope",
},
},
@@ -24,24 +30,95 @@ export async function registerUserToDB(user: User, dbUrl: string) {
});
}
-export async function createUserAuthState(context: BrowserContext, jwt: JWT) {
+export async function createUserAuthState(
+ context: BrowserContext,
+ jwt: { user: Pick },
+ dbUrl: string,
+) {
+ // Create session directly in database
+ await using db = await generatePrismaClient(dbUrl);
+
+ const sessionToken = crypto.randomUUID();
+ const expiresAt = new Date(Date.now() + 60 * 60 * 24 * 1000 * 7); // 7 days
+
+ const session = await db.prisma.session.create({
+ data: {
+ token: sessionToken,
+ userId: jwt.user.id,
+ expiresAt,
+ ipAddress: "127.0.0.1",
+ userAgent: "test",
+ },
+ });
+
+ // Create session data with signature (required by better-auth)
+ // Note: The key order matters for signature verification
+ // better-auth uses {session: {...}, user: {...}} order in setCookieCache
+ const sessionData = {
+ session: {
+ userId: jwt.user.id,
+ expiresAt: session.expiresAt.toISOString(),
+ },
+ user: {
+ id: jwt.user.id,
+ name: jwt.user.name,
+ email: jwt.user.email,
+ image: jwt.user.image,
+ role: jwt.user.role,
+ },
+ };
+
+ const expiresAtTimestamp = Date.now() + 60 * 60 * 1000; // 1 hour
+
+ // Create HMAC signature using the same method as better-auth
+ const secret = process.env.BETTER_AUTH_SECRET;
+ if (!secret) {
+ throw new Error("BETTER_AUTH_SECRET is not set");
+ }
+
+ // The signature is verified against sessionData spread with expiresAt
+ // See better-auth getCookieCache: JSON.stringify({...sessionDataPayload.session, expiresAt: sessionDataPayload.expiresAt})
+ const dataToSign = JSON.stringify({
+ session: sessionData.session,
+ user: sessionData.user,
+ expiresAt: expiresAtTimestamp,
+ });
+
+ const signature = crypto
+ .createHmac("sha256", secret)
+ .update(dataToSign)
+ .digest("base64url");
+
+ const cookieValue = Buffer.from(
+ JSON.stringify({
+ session: sessionData,
+ expiresAt: expiresAtTimestamp,
+ signature,
+ }),
+ ).toString("base64url");
+
+ // Set cookies with the session token and session data
await context.addCookies([
{
- name: "authjs.session-token",
- value: btoa(
- JSON.stringify({
- ...jwt,
- // google provider attaches `sub` to the token
- sub: jwt.user.id,
- }),
- ),
+ name: "better-auth.session_token",
+ value: sessionToken,
+ domain: "localhost",
+ path: "/",
+ httpOnly: true,
+ sameSite: "Lax",
+ expires: Math.round(expiresAt.getTime() / 1000),
+ },
+ {
+ name: "better-auth.session_data",
+ value: cookieValue,
domain: "localhost",
path: "/",
httpOnly: true,
sameSite: "Lax",
- expires: Math.round((Date.now() + 60 * 60 * 24 * 1000 * 7) / 1000),
+ expires: Math.round(expiresAtTimestamp / 1000),
},
]);
+
await context.storageState({
path: getStorageStatePath(jwt.user.id ?? ""),
});
@@ -49,7 +126,7 @@ export async function createUserAuthState(context: BrowserContext, jwt: JWT) {
export async function useUser>(
test: T,
- user: User,
+ user: Pick,
) {
test.use({ storageState: getStorageStatePath(user.id) });
test.beforeEach(async ({ registerToDB }) => {
diff --git a/e2e/models/Base.ts b/e2e/models/Base.ts
index f0d1a605..7c9222e3 100644
--- a/e2e/models/Base.ts
+++ b/e2e/models/Base.ts
@@ -1,5 +1,5 @@
import { expect, type Locator, type Page } from "@playwright/test";
-import type { User } from "next-auth";
+import type { User } from "../../src/app/_clients/betterAuth";
export class Base {
page: Page;
diff --git a/e2e/models/TopPage.ts b/e2e/models/TopPage.ts
index 980669cb..685afaf1 100644
--- a/e2e/models/TopPage.ts
+++ b/e2e/models/TopPage.ts
@@ -1,5 +1,5 @@
import { expect, type Locator, type Page } from "@playwright/test";
-import type { User } from "next-auth";
+import type { User } from "../../src/app/_clients/betterAuth";
import { Base } from "./Base";
export class TopPage extends Base {
diff --git a/e2e/setup/auth.ts b/e2e/setup/auth.ts
index e467a9f2..634d4cc8 100644
--- a/e2e/setup/auth.ts
+++ b/e2e/setup/auth.ts
@@ -1,15 +1,25 @@
-import { test as setup } from "@playwright/test";
import { admin1, user1 } from "../dummyUsers";
-import { createUserAuthState } from "../helpers/users";
+import { test as setup } from "../fixtures";
+import { createUserAuthState, registerUserToDB } from "../helpers/users";
-setup("Create user1 auth", async ({ context }) => {
- await createUserAuthState(context, {
- user: user1,
- });
+setup("Create user1 auth", async ({ context, setup: setupFixture }) => {
+ await registerUserToDB(user1, setupFixture.dbURL);
+ await createUserAuthState(
+ context,
+ {
+ user: user1,
+ },
+ setupFixture.dbURL,
+ );
});
-setup("Create admin1 auth", async ({ context }) => {
- await createUserAuthState(context, {
- user: admin1,
- });
+setup("Create admin1 auth", async ({ context, setup: setupFixture }) => {
+ await registerUserToDB(admin1, setupFixture.dbURL);
+ await createUserAuthState(
+ context,
+ {
+ user: admin1,
+ },
+ setupFixture.dbURL,
+ );
});
diff --git a/env.ts b/env.ts
index e6ed235c..9d25741f 100644
--- a/env.ts
+++ b/env.ts
@@ -24,8 +24,8 @@ const staticEnv = z.object({
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
- NEXTAUTH_URL: z.string().min(1),
- NEXTAUTH_SECRET: z.string().min(1),
+ BETTER_AUTH_SECRET: z.string().min(1),
+ BETTER_AUTH_URL: z.string().min(1),
/* start: otel */
TRACE_EXPORTER_URL: z.url().optional().or(z.literal("")),
diff --git a/package.json b/package.json
index cba1f763..22ed7e01 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,6 @@
"fmt": "prettier -w './**/*.{md,yml}' && biome format --write . && prisma format"
},
"dependencies": {
- "@auth/prisma-adapter": "2.10.0",
"@hookform/resolvers": "5.2.2",
"@next/env": "15.5.4",
"@opentelemetry/exporter-metrics-otlp-grpc": "0.205.0",
@@ -36,9 +35,9 @@
"@opentelemetry/semantic-conventions": "1.37.0",
"@prisma/client": "6.16.2",
"@prisma/instrumentation": "6.16.2",
+ "better-auth": "1.3.27",
"clsx": "2.1.1",
"next": "15.5.4",
- "next-auth": "5.0.0-beta.29",
"prisma": "6.16.2",
"react": "19.1.1",
"react-dom": "19.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1149e7e1..0440ac3d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,9 +8,6 @@ importers:
.:
dependencies:
- '@auth/prisma-adapter':
- specifier: 2.10.0
- version: 2.10.0(@prisma/client@6.16.2(prisma@6.16.2(typescript@5.9.3))(typescript@5.9.3))
'@hookform/resolvers':
specifier: 5.2.2
version: 5.2.2(react-hook-form@7.63.0(react@19.1.1))
@@ -47,15 +44,15 @@ importers:
'@prisma/instrumentation':
specifier: 6.16.2
version: 6.16.2(@opentelemetry/api@1.9.0)
+ better-auth:
+ specifier: 1.3.27
+ version: 1.3.27(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
clsx:
specifier: 2.1.1
version: 2.1.1
next:
specifier: 15.5.4
version: 15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- next-auth:
- specifier: 5.0.0-beta.29
- version: 5.0.0-beta.29(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)
prisma:
specifier: 6.16.2
version: 6.16.2(typescript@5.9.3)
@@ -142,25 +139,6 @@ packages:
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
- '@auth/core@0.40.0':
- resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==}
- peerDependencies:
- '@simplewebauthn/browser': ^9.0.1
- '@simplewebauthn/server': ^9.0.2
- nodemailer: ^6.8.0
- peerDependenciesMeta:
- '@simplewebauthn/browser':
- optional: true
- '@simplewebauthn/server':
- optional: true
- nodemailer:
- optional: true
-
- '@auth/prisma-adapter@2.10.0':
- resolution: {integrity: sha512-EliOQoTjGK87jWWqnJvlQjbR4PjQZQqtwRwPAe108WwT9ubuuJJIrL68aNnQr4hFESz6P7SEX2bZy+y2yL37Gw==}
- peerDependencies:
- '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6'
-
'@axe-core/playwright@4.10.2':
resolution: {integrity: sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==}
peerDependencies:
@@ -256,6 +234,15 @@ packages:
'@balena/dockerignore@1.0.2':
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
+ '@better-auth/core@1.3.27':
+ resolution: {integrity: sha512-3Sfdax6MQyronY+znx7bOsfQHI6m1SThvJWb0RDscFEAhfqLy95k1sl+/PgGyg0cwc2cUXoEiAOSqYdFYrg3vA==}
+
+ '@better-auth/utils@0.3.0':
+ resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==}
+
+ '@better-fetch/fetch@1.1.18':
+ resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
+
'@biomejs/biome@2.2.4':
resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==}
engines: {node: '>=14.21.3'}
@@ -517,6 +504,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
+ '@hexagon/base64@1.1.28':
+ resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
+
'@hookform/resolvers@5.2.2':
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
peerDependencies:
@@ -671,6 +661,9 @@ packages:
'@js-sdsl/ordered-map@4.4.2':
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
+ '@levischuck/tiny-cbor@0.2.11':
+ resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
+
'@napi-rs/wasm-runtime@1.0.5':
resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==}
@@ -725,6 +718,14 @@ packages:
cpu: [x64]
os: [win32]
+ '@noble/ciphers@2.0.1':
+ resolution: {integrity: sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==}
+ engines: {node: '>= 20.19.0'}
+
+ '@noble/hashes@2.0.1':
+ resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
+ engines: {node: '>= 20.19.0'}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1010,8 +1011,41 @@ packages:
cpu: [x64]
os: [win32]
- '@panva/hkdf@1.2.1':
- resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
+ '@peculiar/asn1-android@2.5.0':
+ resolution: {integrity: sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==}
+
+ '@peculiar/asn1-cms@2.5.0':
+ resolution: {integrity: sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==}
+
+ '@peculiar/asn1-csr@2.5.0':
+ resolution: {integrity: sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==}
+
+ '@peculiar/asn1-ecc@2.5.0':
+ resolution: {integrity: sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==}
+
+ '@peculiar/asn1-pfx@2.5.0':
+ resolution: {integrity: sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==}
+
+ '@peculiar/asn1-pkcs8@2.5.0':
+ resolution: {integrity: sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==}
+
+ '@peculiar/asn1-pkcs9@2.5.0':
+ resolution: {integrity: sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==}
+
+ '@peculiar/asn1-rsa@2.5.0':
+ resolution: {integrity: sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==}
+
+ '@peculiar/asn1-schema@2.5.0':
+ resolution: {integrity: sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==}
+
+ '@peculiar/asn1-x509-attr@2.5.0':
+ resolution: {integrity: sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==}
+
+ '@peculiar/asn1-x509@2.5.0':
+ resolution: {integrity: sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==}
+
+ '@peculiar/x509@1.14.0':
+ resolution: {integrity: sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
@@ -1195,6 +1229,13 @@ packages:
cpu: [x64]
os: [win32]
+ '@simplewebauthn/browser@13.2.2':
+ resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==}
+
+ '@simplewebauthn/server@13.2.2':
+ resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==}
+ engines: {node: '>=20.0.0'}
+
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@@ -1460,6 +1501,10 @@ packages:
asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
+ asn1js@3.0.6:
+ resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==}
+ engines: {node: '>=12.0.0'}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -1516,6 +1561,38 @@ packages:
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
+ better-auth@1.3.27:
+ resolution: {integrity: sha512-SwiGAJ7yU6dBhNg0NdV1h5M8T5sa7/AszZVc4vBfMDrLLmvUfbt9JoJ0uRUJUEdKRAAxTyl9yA+F3+GhtAD80w==}
+ peerDependencies:
+ '@lynx-js/react': '*'
+ '@sveltejs/kit': '*'
+ next: '*'
+ react: '*'
+ react-dom: '*'
+ solid-js: '*'
+ svelte: '*'
+ vue: '*'
+ peerDependenciesMeta:
+ '@lynx-js/react':
+ optional: true
+ '@sveltejs/kit':
+ optional: true
+ next:
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ solid-js:
+ optional: true
+ svelte:
+ optional: true
+ vue:
+ optional: true
+
+ better-call@1.0.19:
+ resolution: {integrity: sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==}
+
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
@@ -2033,6 +2110,10 @@ packages:
'@types/node': '>=18'
typescript: '>=5.0.4 <7'
+ kysely@0.28.8:
+ resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==}
+ engines: {node: '>=20.0.0'}
+
lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
@@ -2243,27 +2324,15 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanostores@1.0.1:
+ resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+
napi-postinstall@0.3.3:
resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
hasBin: true
- next-auth@5.0.0-beta.29:
- resolution: {integrity: sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==}
- peerDependencies:
- '@simplewebauthn/browser': ^9.0.1
- '@simplewebauthn/server': ^9.0.2
- next: ^14.0.0-0 || ^15.0.0-0
- nodemailer: ^6.6.5
- react: ^18.2.0 || ^19.0.0-0
- peerDependenciesMeta:
- '@simplewebauthn/browser':
- optional: true
- '@simplewebauthn/server':
- optional: true
- nodemailer:
- optional: true
-
next@15.5.4:
resolution: {integrity: sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@@ -2300,9 +2369,6 @@ packages:
engines: {node: ^14.16.0 || >=16.10.0}
hasBin: true
- oauth4webapi@3.8.1:
- resolution: {integrity: sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==}
-
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -2375,14 +2441,6 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
- preact-render-to-string@6.5.11:
- resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
- peerDependencies:
- preact: '>=10'
-
- preact@10.24.3:
- resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
-
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
@@ -2430,6 +2488,13 @@ packages:
pure-rand@6.1.0:
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
+ pvtsutils@1.3.6:
+ resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
+
+ pvutils@1.1.3:
+ resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
+ engines: {node: '>=6.0.0'}
+
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
@@ -2480,6 +2545,9 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
+ reflect-metadata@0.2.2:
+ resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
+
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -2510,6 +2578,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ rou3@0.5.1:
+ resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
+
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
@@ -2541,6 +2612,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
sharp@0.34.3:
resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -2752,9 +2826,16 @@ packages:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
+ tslib@1.14.1:
+ resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tsyringe@4.10.0:
+ resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
+ engines: {node: '>= 6.0.0'}
+
tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
@@ -2763,6 +2844,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ uncrypto@0.1.3:
+ resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
@@ -2975,23 +3059,6 @@ snapshots:
'@asamuzakjp/nwsapi@2.3.9': {}
- '@auth/core@0.40.0':
- dependencies:
- '@panva/hkdf': 1.2.1
- jose: 6.1.0
- oauth4webapi: 3.8.1
- preact: 10.24.3
- preact-render-to-string: 6.5.11(preact@10.24.3)
-
- '@auth/prisma-adapter@2.10.0(@prisma/client@6.16.2(prisma@6.16.2(typescript@5.9.3))(typescript@5.9.3))':
- dependencies:
- '@auth/core': 0.40.0
- '@prisma/client': 6.16.2(prisma@6.16.2(typescript@5.9.3))(typescript@5.9.3)
- transitivePeerDependencies:
- - '@simplewebauthn/browser'
- - '@simplewebauthn/server'
- - nodemailer
-
'@axe-core/playwright@4.10.2(playwright-core@1.55.1)':
dependencies:
axe-core: 4.10.3
@@ -3113,6 +3180,15 @@ snapshots:
'@balena/dockerignore@1.0.2': {}
+ '@better-auth/core@1.3.27':
+ dependencies:
+ better-call: 1.0.19
+ zod: 4.1.12
+
+ '@better-auth/utils@0.3.0': {}
+
+ '@better-fetch/fetch@1.1.18': {}
+
'@biomejs/biome@2.2.4':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.2.4
@@ -3278,6 +3354,8 @@ snapshots:
protobufjs: 7.5.4
yargs: 17.7.2
+ '@hexagon/base64@1.1.28': {}
+
'@hookform/resolvers@5.2.2(react-hook-form@7.63.0(react@19.1.1))':
dependencies:
'@standard-schema/utils': 0.3.0
@@ -3403,6 +3481,8 @@ snapshots:
'@js-sdsl/ordered-map@4.4.2': {}
+ '@levischuck/tiny-cbor@0.2.11': {}
+
'@napi-rs/wasm-runtime@1.0.5':
dependencies:
'@emnapi/core': 1.5.0
@@ -3436,6 +3516,10 @@ snapshots:
'@next/swc-win32-x64-msvc@15.5.4':
optional: true
+ '@noble/ciphers@2.0.1': {}
+
+ '@noble/hashes@2.0.1': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -3760,7 +3844,101 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.8.3':
optional: true
- '@panva/hkdf@1.2.1': {}
+ '@peculiar/asn1-android@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-cms@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ '@peculiar/asn1-x509-attr': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-csr@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-ecc@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-pfx@2.5.0':
+ dependencies:
+ '@peculiar/asn1-cms': 2.5.0
+ '@peculiar/asn1-pkcs8': 2.5.0
+ '@peculiar/asn1-rsa': 2.5.0
+ '@peculiar/asn1-schema': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-pkcs8@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-pkcs9@2.5.0':
+ dependencies:
+ '@peculiar/asn1-cms': 2.5.0
+ '@peculiar/asn1-pfx': 2.5.0
+ '@peculiar/asn1-pkcs8': 2.5.0
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ '@peculiar/asn1-x509-attr': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-rsa@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-schema@2.5.0':
+ dependencies:
+ asn1js: 3.0.6
+ pvtsutils: 1.3.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-x509-attr@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-x509@2.5.0':
+ dependencies:
+ '@peculiar/asn1-schema': 2.5.0
+ asn1js: 3.0.6
+ pvtsutils: 1.3.6
+ tslib: 2.8.1
+
+ '@peculiar/x509@1.14.0':
+ dependencies:
+ '@peculiar/asn1-cms': 2.5.0
+ '@peculiar/asn1-csr': 2.5.0
+ '@peculiar/asn1-ecc': 2.5.0
+ '@peculiar/asn1-pkcs9': 2.5.0
+ '@peculiar/asn1-rsa': 2.5.0
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ pvtsutils: 1.3.6
+ reflect-metadata: 0.2.2
+ tslib: 2.8.1
+ tsyringe: 4.10.0
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -3899,6 +4077,19 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.50.0':
optional: true
+ '@simplewebauthn/browser@13.2.2': {}
+
+ '@simplewebauthn/server@13.2.2':
+ dependencies:
+ '@hexagon/base64': 1.1.28
+ '@levischuck/tiny-cbor': 0.2.11
+ '@peculiar/asn1-android': 2.5.0
+ '@peculiar/asn1-ecc': 2.5.0
+ '@peculiar/asn1-rsa': 2.5.0
+ '@peculiar/asn1-schema': 2.5.0
+ '@peculiar/asn1-x509': 2.5.0
+ '@peculiar/x509': 1.14.0
+
'@standard-schema/spec@1.0.0': {}
'@standard-schema/utils@0.3.0': {}
@@ -4186,6 +4377,12 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
+ asn1js@3.0.6:
+ dependencies:
+ pvtsutils: 1.3.6
+ pvutils: 1.1.3
+ tslib: 2.8.1
+
assertion-error@2.0.1: {}
async-lock@1.4.1: {}
@@ -4229,6 +4426,34 @@ snapshots:
dependencies:
tweetnacl: 0.14.5
+ better-auth@1.3.27(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ dependencies:
+ '@better-auth/core': 1.3.27
+ '@better-auth/utils': 0.3.0
+ '@better-fetch/fetch': 1.1.18
+ '@noble/ciphers': 2.0.1
+ '@noble/hashes': 2.0.1
+ '@simplewebauthn/browser': 13.2.2
+ '@simplewebauthn/server': 13.2.2
+ better-call: 1.0.19
+ defu: 6.1.4
+ jose: 6.1.0
+ kysely: 0.28.8
+ nanostores: 1.0.1
+ zod: 4.1.12
+ optionalDependencies:
+ next: 15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ better-call@1.0.19:
+ dependencies:
+ '@better-auth/utils': 0.3.0
+ '@better-fetch/fetch': 1.1.18
+ rou3: 0.5.1
+ set-cookie-parser: 2.7.1
+ uncrypto: 0.1.3
+
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
@@ -4782,6 +5007,8 @@ snapshots:
typescript: 5.9.3
zod: 4.1.12
+ kysely@0.28.8: {}
+
lazystream@1.0.1:
dependencies:
readable-stream: 2.3.8
@@ -4940,13 +5167,9 @@ snapshots:
nanoid@3.3.11: {}
- napi-postinstall@0.3.3: {}
+ nanostores@1.0.1: {}
- next-auth@5.0.0-beta.29(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1):
- dependencies:
- '@auth/core': 0.40.0
- next: 15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- react: 19.1.1
+ napi-postinstall@0.3.3: {}
next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
@@ -4987,8 +5210,6 @@ snapshots:
pkg-types: 2.3.0
tinyexec: 1.0.1
- oauth4webapi@3.8.1: {}
-
object-inspect@1.13.4: {}
ohash@2.0.11: {}
@@ -5074,12 +5295,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- preact-render-to-string@6.5.11(preact@10.24.3):
- dependencies:
- preact: 10.24.3
-
- preact@10.24.3: {}
-
prettier@3.6.2: {}
pretty-format@27.5.1:
@@ -5135,6 +5350,12 @@ snapshots:
pure-rand@6.1.0: {}
+ pvtsutils@1.3.6:
+ dependencies:
+ tslib: 2.8.1
+
+ pvutils@1.1.3: {}
+
qs@6.14.0:
dependencies:
side-channel: 1.1.0
@@ -5191,6 +5412,8 @@ snapshots:
readdirp@4.1.2: {}
+ reflect-metadata@0.2.2: {}
+
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
@@ -5240,6 +5463,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.50.0
fsevents: 2.3.3
+ rou3@0.5.1: {}
+
rrweb-cssom@0.8.0: {}
run-parallel@1.2.0:
@@ -5262,6 +5487,8 @@ snapshots:
semver@7.7.2: {}
+ set-cookie-parser@2.7.1: {}
+
sharp@0.34.3:
dependencies:
color: 4.2.3
@@ -5529,12 +5756,20 @@ snapshots:
dependencies:
punycode: 2.3.1
+ tslib@1.14.1: {}
+
tslib@2.8.1: {}
+ tsyringe@4.10.0:
+ dependencies:
+ tslib: 1.14.1
+
tweetnacl@0.14.5: {}
typescript@5.9.3: {}
+ uncrypto@0.1.3: {}
+
undici-types@5.26.5: {}
undici-types@6.21.0: {}
diff --git a/prisma/schema/user.prisma b/prisma/schema/user.prisma
index 9457af38..f88cb722 100644
--- a/prisma/schema/user.prisma
+++ b/prisma/schema/user.prisma
@@ -3,47 +3,69 @@ enum Role {
ADMIN
}
-// https://authjs.dev/getting-started/adapters/prisma#schema
-model Account {
- id String @id @default(cuid())
- userId String @map("user_id")
- type String
- provider String
- providerAccountId String @map("provider_account_id")
- refresh_token String? @db.Text
- access_token String? @db.Text
- expires_at Int?
- token_type String?
- scope String?
- id_token String? @db.Text
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@unique([provider, providerAccountId])
- @@map("accounts")
-}
-
+// https://www.better-auth.com/docs/concepts/database#core-schema
model User {
id String @id @default(cuid())
- name String?
- email String? @unique
- emailVerified DateTime? @map("email_verified")
+ name String
+ email String @unique
+ emailVerified Boolean @map("email_verified")
image String?
- accounts Account[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
- // https://authjs.dev/guides/basics/role-based-access-control
+ accounts Account[]
+ sessions Session[]
role Role @default(USER)
- // start: sample
items Item[]
- // end: sample
stripeId String? @unique @map("stripe_id") // cus_XXXX
subscriptions Subscription[]
@@map("users")
}
+model Session {
+ id String @id @default(cuid())
+ expiresAt DateTime @map("expires_at")
+ token String @unique
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ ipAddress String? @map("ip_address")
+ userAgent String? @map("user_agent")
+ userId String @map("user_id")
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@map("sessions")
+}
+
+model Account {
+ id String @id @default(cuid())
+ accountId String @map("account_id")
+ providerId String @map("provider_id")
+ userId String @map("user_id")
+ accessToken String? @map("access_token")
+ refreshToken String? @map("refresh_token")
+ idToken String? @map("id_token")
+ accessTokenExpiresAt DateTime? @map("access_token_expires_at")
+ refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at")
+ scope String?
+ password String?
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@map("accounts")
+}
+
+model Verification {
+ id String @id @default(cuid())
+ identifier String
+ value String
+ expiresAt DateTime @map("expires_at")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+
+ @@map("verifications")
+}
+
model Subscription {
id String @id @default(cuid())
subscriptionId String @unique @map("subscription_id") // sub_XXXX
diff --git a/src/app/(private)/me/page.tsx b/src/app/(private)/me/page.tsx
index 214af57e..6f6d233d 100644
--- a/src/app/(private)/me/page.tsx
+++ b/src/app/(private)/me/page.tsx
@@ -9,7 +9,5 @@ export default async function Page() {
notFound();
}
- const { user } = session.data;
-
- return ;
+ return ;
}
diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx
index 6f520c6a..044ed8ff 100644
--- a/src/app/(public)/page.tsx
+++ b/src/app/(public)/page.tsx
@@ -25,11 +25,11 @@ async function Status() {
return (
- {session?.data?.user
- ? `you are signed in as ${session.data.user.name} 😄`
+ {session.success
+ ? `you are signed in as ${session.data.name} 😄`
: "you are not signed in 🥲"}
- {session?.data?.user &&
}
+ {session.success &&
}
);
}
diff --git a/src/app/(public)/signin/page.tsx b/src/app/(public)/signin/page.tsx
index 728348c7..34258802 100644
--- a/src/app/(public)/signin/page.tsx
+++ b/src/app/(public)/signin/page.tsx
@@ -1,12 +1,21 @@
"use client";
-import { signIn } from "next-auth/react";
+import { signIn } from "../../_clients/betterAuthClient";
import { Button } from "../../_components/Button";
export default function SignIn() {
return (
- signIn("google")}>Sign in with Google
+ {
+ signIn.social({
+ provider: "google",
+ callbackURL: "/",
+ });
+ }}
+ >
+ Sign in with Google
+
);
}
diff --git a/src/app/_actions/items.test.ts b/src/app/_actions/items.test.ts
index 479c3d4f..dac7af04 100644
--- a/src/app/_actions/items.test.ts
+++ b/src/app/_actions/items.test.ts
@@ -36,7 +36,7 @@ describe("actions/items", () => {
});
test("should throw an error if there is no session token", async () => {
- mock.auth.mockReturnValueOnce(null);
+ mock.getSession.mockResolvedValueOnce(null);
expect(await create({ content: "hello" })).toMatchInlineSnapshot(`
{
@@ -92,7 +92,7 @@ describe("actions/items", () => {
});
test("should throw an error if there is no session token", async () => {
- mock.auth.mockReturnValueOnce(null);
+ mock.getSession.mockResolvedValueOnce(null);
await expect(deleteAll()).rejects.toThrow("no session token");
});
diff --git a/src/app/_actions/items.ts b/src/app/_actions/items.ts
index a76a0d2f..ad939891 100644
--- a/src/app/_actions/items.ts
+++ b/src/app/_actions/items.ts
@@ -19,7 +19,6 @@ export async function create(input: ItemSchema): Promise {
return session;
}
- const { user } = session.data;
const validatedFields = itemSchema.safeParse(input);
if (!validatedFields.success) {
@@ -35,7 +34,7 @@ export async function create(input: ItemSchema): Promise {
content: validatedFields.data.content,
user: {
connect: {
- id: user.id,
+ id: session.data.id,
},
},
},
@@ -61,12 +60,10 @@ export async function deleteAll(): Promise {
throw new Error("no session token");
}
- const { user } = session.data;
-
await prisma.$transaction(async (prisma) => {
await prisma.item.deleteMany({
where: {
- userId: user.id,
+ userId: session.data.id,
},
});
});
diff --git a/src/app/_actions/payment.test.ts b/src/app/_actions/payment.test.ts
index a43c3e42..a3fdf11b 100644
--- a/src/app/_actions/payment.test.ts
+++ b/src/app/_actions/payment.test.ts
@@ -51,7 +51,7 @@ describe("actions/payment", () => {
describe("checkout", () => {
test("should throw an error if there is no session token", async () => {
- mock.auth.mockReturnValueOnce(null);
+ mock.getSession.mockResolvedValueOnce(null);
expect(await checkout()).toMatchInlineSnapshot(`
{
@@ -97,7 +97,6 @@ describe("actions/payment", () => {
expect(await getUser()).toMatchInlineSnapshot(`
{
"email": "hello@a.com",
- "emailVerified": null,
"id": "id",
"image": "https://a.com",
"name": "name",
@@ -153,7 +152,6 @@ describe("actions/payment", () => {
expect(await getUser()).toMatchInlineSnapshot(`
{
"email": "hello@a.com",
- "emailVerified": null,
"id": "id",
"image": "https://a.com",
"name": "name",
@@ -180,7 +178,6 @@ describe("actions/payment", () => {
expect(await getUser()).toMatchInlineSnapshot(`
{
"email": "hello@a.com",
- "emailVerified": null,
"id": "id",
"image": "https://a.com",
"name": "name",
@@ -228,7 +225,6 @@ describe("actions/payment", () => {
expect(await getUser()).toMatchInlineSnapshot(`
{
"email": "hello@a.com",
- "emailVerified": null,
"id": "id",
"image": "https://a.com",
"name": "name",
@@ -250,7 +246,6 @@ describe("actions/payment", () => {
expect(await getUser()).toMatchInlineSnapshot(`
{
"email": "hello@a.com",
- "emailVerified": null,
"id": "id",
"image": "https://a.com",
"name": "name",
@@ -274,7 +269,7 @@ describe("actions/payment", () => {
});
test("should throw an error if there is no session token", async () => {
- mock.auth.mockReturnValueOnce(null);
+ mock.getSession.mockResolvedValueOnce(null);
expect(await update(true)).toMatchInlineSnapshot(`
{
@@ -375,7 +370,7 @@ describe("actions/payment", () => {
describe("redirectToBillingPortal", () => {
test("should throw an error if there is no session token", async () => {
- mock.auth.mockReturnValueOnce(null);
+ mock.getSession.mockResolvedValueOnce(null);
await expect(redirectToBillingPortal()).rejects.toThrow(
"no session token",
diff --git a/src/app/_actions/payment.ts b/src/app/_actions/payment.ts
index c0b9a6c6..b8aa4398 100644
--- a/src/app/_actions/payment.ts
+++ b/src/app/_actions/payment.ts
@@ -19,7 +19,7 @@ export async function checkout(): Promise {
const me = await prisma.user.findUnique({
where: {
- id: session.data.user.id,
+ id: session.data.id,
},
});
@@ -153,7 +153,7 @@ export async function redirectToBillingPortal(): Promise {
const me = await prisma.user.findUnique({
where: {
- id: session.data.user.id,
+ id: session.data.id,
},
});
diff --git a/src/app/_actions/users.test.ts b/src/app/_actions/users.test.ts
index 023f2a3a..ebe941db 100644
--- a/src/app/_actions/users.test.ts
+++ b/src/app/_actions/users.test.ts
@@ -39,7 +39,6 @@ describe("actions/users", () => {
expect(await getUser()).toMatchInlineSnapshot(`
{
"email": "b@c.com",
- "emailVerified": null,
"id": "id",
"image": "https://a.com",
"name": "foo",
@@ -75,7 +74,7 @@ describe("actions/users", () => {
});
test("should throw an error if there is no session token", async () => {
- mock.auth.mockReturnValueOnce(null);
+ mock.getSession.mockResolvedValueOnce(null);
const formData = new FormData();
diff --git a/src/app/_actions/users.ts b/src/app/_actions/users.ts
index 96a5c091..4083bf87 100644
--- a/src/app/_actions/users.ts
+++ b/src/app/_actions/users.ts
@@ -23,12 +23,10 @@ export async function updateMe(
return session;
}
- const { user } = session.data;
-
const input: PartialWithNullable = {
- name: user.name,
- email: user.email,
- image: user.image,
+ name: session.data.name,
+ email: session.data.email,
+ image: session.data.image,
...Object.fromEntries(formData.entries()),
};
@@ -45,7 +43,7 @@ export async function updateMe(
await prisma.user.update({
where: {
- id: user.id,
+ id: session.data.id,
},
data: validatedFields.data,
});
diff --git a/src/app/_clients/betterAuth.ts b/src/app/_clients/betterAuth.ts
new file mode 100644
index 00000000..b43fe820
--- /dev/null
+++ b/src/app/_clients/betterAuth.ts
@@ -0,0 +1,46 @@
+import { betterAuth } from "better-auth";
+import { prismaAdapter } from "better-auth/adapters/prisma";
+import { prisma } from "./prisma";
+
+export const auth = betterAuth({
+ database: prismaAdapter(prisma, {
+ provider: "postgresql",
+ }),
+ emailAndPassword: {
+ enabled: false,
+ },
+ socialProviders: {
+ google: {
+ clientId: process.env.GOOGLE_CLIENT_ID as string,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
+ },
+ },
+ session: {
+ // JWT session strategy for Edge Runtime compatibility
+ expiresIn: 60 * 60, // Session expires after 1 hour
+ updateAge: 60 * 15, // Session auto-refreshes every 15 minutes when used
+ cookieCache: {
+ enabled: true,
+ maxAge: 60 * 60, // Cache expires after 1 hour
+ },
+ },
+ user: {
+ additionalFields: {
+ role: {
+ type: "string",
+ required: true,
+ defaultValue: "USER",
+ },
+ },
+ },
+ advanced: {
+ database: {
+ generateId: () => {
+ return crypto.randomUUID();
+ },
+ },
+ },
+ trustedOrigins: [process.env.NEXT_PUBLIC_SITE_URL as string],
+});
+
+export type User = typeof auth.$Infer.Session.user;
diff --git a/src/app/_clients/betterAuthClient.ts b/src/app/_clients/betterAuthClient.ts
new file mode 100644
index 00000000..bf87fa1f
--- /dev/null
+++ b/src/app/_clients/betterAuthClient.ts
@@ -0,0 +1,8 @@
+import { createAuthClient } from "better-auth/react";
+
+const authClient = createAuthClient({
+ baseURL: process.env.NEXT_PUBLIC_SITE_URL,
+});
+
+// e.g. useSession
+export const { signIn, signOut } = authClient;
diff --git a/src/app/_clients/nextAuth.ts b/src/app/_clients/nextAuth.ts
deleted file mode 100644
index e9e8b12c..00000000
--- a/src/app/_clients/nextAuth.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { PrismaAdapter } from "@auth/prisma-adapter";
-import NextAuth from "next-auth";
-import type { Adapter } from "next-auth/adapters";
-import type { JWT } from "next-auth/jwt";
-import { config, configForTest } from "./nextAuthConfig";
-import { prisma } from "./prisma";
-
-export const { auth, handlers } = NextAuth({
- // https://authjs.dev/getting-started/migrating-to-v5#edge-compatibility
- ...config,
- adapter: PrismaAdapter(prisma) as Adapter,
- session: {
- strategy: "jwt",
- },
- callbacks: {
- ...config.callbacks,
- jwt: async ({
- token,
- /* user exists when only signing in */ user,
- trigger,
- }): Promise => {
- const userId = token.sub ?? user?.id;
-
- if (!userId) {
- throw new Error("Token is invalid");
- }
-
- // support for multiple devices
- const me = await prisma.user.findUnique({
- where: {
- id: userId,
- },
- });
-
- // to avoid accessing devices having invalid JWT if user is not found
- if (trigger !== "signUp" && !user && !me) {
- throw new Error("User not found");
- }
-
- // in favor of the user's latest data and update the token
- token.user = {
- id: userId,
- name: me?.name ?? token.name ?? "",
- email: me?.email ?? token.email ?? "",
- role: me?.role ?? user.role,
- image: me?.image ?? token.picture ?? "",
- };
-
- return token;
- },
- },
- ...(process.env.NEXTAUTH_TEST_MODE === "true" ? configForTest : {}),
-});
diff --git a/src/app/_clients/nextAuthConfig.ts b/src/app/_clients/nextAuthConfig.ts
deleted file mode 100644
index b7f015f2..00000000
--- a/src/app/_clients/nextAuthConfig.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-// don't import prisma or @auth/prisma-adapter here to avoid importing it at Edge
-// [auth][error] JWTSessionError: Read more at https://errors.authjs.dev#jwtsessionerror
-// [auth][cause]: Error: PrismaClient is not configured to run in Edge Runtime (Vercel Edge Functions, Vercel Edge Middleware, Next.js (Pages Router) Edge API Routes, Next.js (App Router) Edge Route Handlers or Next.js Middleware). In order to run Prisma Client on edge runtime, either:
-
-import type { NextAuthConfig } from "next-auth";
-import GoogleProvider from "next-auth/providers/google";
-
-export const configForTest = {
- jwt: {
- encode: async ({ token }) => {
- return btoa(JSON.stringify(token));
- },
- decode: async ({ token }) => {
- if (!token) {
- return {};
- }
-
- return JSON.parse(atob(token));
- },
- },
-} satisfies Omit;
-
-export const config = {
- pages: {
- signIn: "/signin",
- },
- providers: [
- GoogleProvider({
- clientId: process.env.GOOGLE_CLIENT_ID,
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
- profile(profile) {
- return {
- id: profile.sub,
- name: profile.name,
- email: profile.email,
- image: profile.picture,
- // https://authjs.dev/guides/role-based-access-control#persisting-the-role
- role: profile.role ?? "USER",
- };
- },
- }),
- ],
- callbacks: {
- redirect: ({ baseUrl }) => {
- return baseUrl;
- },
- session: ({ session, token }) => {
- if (session?.user && token.user.id) {
- session.user.id = token.user.id;
- session.user.name = token.user.name;
- session.user.email = token.user.email ?? "";
- session.user.image = token.user.image;
- session.user.role = token.user.role;
- }
-
- return session;
- },
- },
- // https://authjs.dev/getting-started/deployment#docker
- trustHost: true,
- ...(process.env.NEXTAUTH_TEST_MODE === "true" ? configForTest : {}),
-} satisfies NextAuthConfig;
diff --git a/src/app/_components/AuthButtons.tsx b/src/app/_components/AuthButtons.tsx
index c0597480..87af1bcc 100644
--- a/src/app/_components/AuthButtons.tsx
+++ b/src/app/_components/AuthButtons.tsx
@@ -1,12 +1,32 @@
"use client";
-import { signIn, signOut } from "next-auth/react";
+import { signIn, signOut } from "../_clients/betterAuthClient";
import { Button } from "./Button";
export function SignInButton() {
- return signIn()}>Sign in ;
+ return (
+ {
+ signIn.social({
+ provider: "google",
+ callbackURL: "/",
+ });
+ }}
+ >
+ Sign in
+
+ );
}
export function SignOutButton() {
- return signOut()}>Sign out ;
+ return (
+ {
+ await signOut();
+ window.location.href = "/";
+ }}
+ >
+ Sign out
+
+ );
}
diff --git a/src/app/_components/Header.tsx b/src/app/_components/Header.tsx
index eb228ab0..833fcc07 100644
--- a/src/app/_components/Header.tsx
+++ b/src/app/_components/Header.tsx
@@ -10,10 +10,10 @@ export async function Header() {
return (
- {session?.data?.user?.image ? (
+ {session.success && session.data.image ? (
{
expect(await getSessionOrReject()).toMatchInlineSnapshot(`
{
"data": {
- "user": {
- "email": "hello@a.com",
- "id": "id",
- "image": "https://a.com",
- "name": "name",
- "role": "USER",
- },
+ "email": "hello@a.com",
+ "id": "id",
+ "image": "https://a.com",
+ "name": "name",
+ "role": "USER",
},
"success": true,
}
diff --git a/src/app/_utils/auth.ts b/src/app/_utils/auth.ts
index 1d8b2587..09f4d8b9 100644
--- a/src/app/_utils/auth.ts
+++ b/src/app/_utils/auth.ts
@@ -1,10 +1,12 @@
-import type { Session } from "next-auth";
-import { auth } from "../_clients/nextAuth";
+import { headers } from "next/headers";
+import { auth, type User } from "../_clients/betterAuth";
import type { Result } from "../_types/result";
-export async function getSessionOrReject(): Promise> {
+export async function getSessionOrReject(): Promise> {
try {
- const session = await auth();
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
if (!session?.user?.id) {
return {
@@ -15,7 +17,7 @@ export async function getSessionOrReject(): Promise> {
return {
success: true,
- data: session,
+ data: session.user,
};
} catch {
return {
diff --git a/src/app/_utils/payment.test.ts b/src/app/_utils/payment.test.ts
index 18eebc8e..36990704 100644
--- a/src/app/_utils/payment.test.ts
+++ b/src/app/_utils/payment.test.ts
@@ -161,7 +161,7 @@ describe("utils/payment", () => {
describe("status", () => {
test("should throw an error if there is no session token", async () => {
- mock.auth.mockReturnValueOnce(null);
+ mock.getSession.mockResolvedValueOnce(null);
expect(await status()).toMatchInlineSnapshot(`
{
diff --git a/src/app/_utils/payment.ts b/src/app/_utils/payment.ts
index 4ae34815..0ec5fcf8 100644
--- a/src/app/_utils/payment.ts
+++ b/src/app/_utils/payment.ts
@@ -17,10 +17,9 @@ export async function status(): Promise {
return session;
}
- const { user } = session.data;
const subscription = await prisma.subscription.findFirst({
where: {
- userId: user.id,
+ userId: session.data.id,
status: {
in: ["active", "complete"],
},
diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts
new file mode 100644
index 00000000..d13d9786
--- /dev/null
+++ b/src/app/api/auth/[...all]/route.ts
@@ -0,0 +1,4 @@
+import { auth } from "../../../_clients/betterAuth";
+
+export const GET = auth.handler;
+export const POST = auth.handler;
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 04b7d342..00000000
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { handlers } from "../../../_clients/nextAuth";
-
-export const { GET, POST } = handlers;
diff --git a/src/app/globals.d.ts b/src/app/globals.d.ts
index 50e1819f..9db6b202 100644
--- a/src/app/globals.d.ts
+++ b/src/app/globals.d.ts
@@ -1,5 +1,4 @@
import type { Schema } from "../../env";
-import type { Role } from "./__generated__/prisma";
declare global {
namespace NodeJS {
@@ -10,23 +9,3 @@ declare global {
[P in keyof T]?: T[P] | null;
};
}
-
-declare module "next-auth" {
- interface User {
- id: string;
- name: string;
- email: string;
- image: string;
- role: Role;
- }
-
- interface Session {
- user: User;
- }
-}
-
-declare module "next-auth/jwt" {
- interface JWT {
- user: import("next-auth").Session["user"];
- }
-}
diff --git a/src/middleware.test.ts b/src/middleware.test.ts
index 8c705b98..7e195ada 100644
--- a/src/middleware.test.ts
+++ b/src/middleware.test.ts
@@ -1,30 +1,17 @@
-import { beforeEach } from "node:test";
-import type { AppRouteHandlerFn } from "next/dist/server/route-modules/app-route/module";
import {
getRewrittenUrl,
isRewrite,
unstable_doesMiddlewareMatch,
} from "next/experimental/testing/server.js";
import { NextRequest, type NextResponse } from "next/server";
-import type { NextAuthResult } from "next-auth";
-import { describe, expect, test, vi } from "vitest";
+import { beforeEach, describe, expect, test, vi } from "vitest";
import nextConfig from "../next.config";
import middleware, { config } from "./middleware";
-type NextAuthRequest = Parameters[0]>[0];
-
describe("middleware", () => {
beforeEach(() => {
- vi.mock("next-auth", async (actual) => ({
- ...(await actual()),
- default: () => ({
- auth: (
- fn: (
- req: NextAuthRequest,
- ctx: AppRouteHandlerFn,
- ) => Promise,
- ) => fn,
- }),
+ vi.mock("better-auth/cookies", async () => ({
+ getCookieCache: vi.fn(),
}));
});
@@ -46,28 +33,42 @@ describe("middleware", () => {
});
test("should route /signin to when fallback", async () => {
- const req = new NextRequest("http://localhost:3000");
- const res = (await middleware(req, {
- params: Promise.resolve({}),
- })) as NextResponse;
+ const { getCookieCache } = await import("better-auth/cookies");
+ vi.mocked(getCookieCache).mockResolvedValue(null);
+
+ const req = new NextRequest("http://localhost:3000/me");
+ const res = (await middleware(req)) as NextResponse;
- expect(isRewrite(res)).toEqual(true);
- expect(getRewrittenUrl(res)).toEqual("http://localhost:3000/signin");
+ expect(isRewrite(res)).toEqual(false);
+ expect(getRewrittenUrl(res)).toEqual(null);
+ expect(res.status).toEqual(307);
+ expect(res.headers.get("location")).toEqual("http://localhost:3000/signin");
});
test("should accept only users having role of user", async () => {
- const req = {
- auth: {
- user: {
- role: "USER",
- },
- expires: "expires",
+ const { getCookieCache } = await import("better-auth/cookies");
+ vi.mocked(getCookieCache).mockResolvedValue({
+ user: {
+ id: "id",
+ name: "name",
+ email: "email@example.com",
+ emailVerified: false,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ role: "USER",
+ },
+ session: {
+ id: "session-id",
+ userId: "id",
+ expiresAt: new Date(),
+ token: "token",
+ createdAt: new Date(),
+ updatedAt: new Date(),
},
- } as NextAuthRequest;
+ });
- const res = (await middleware(req, {
- params: Promise.resolve({}),
- })) as NextResponse;
+ const req = new NextRequest("http://localhost:3000/me");
+ const res = (await middleware(req)) as NextResponse;
expect(isRewrite(res)).toEqual(false);
expect(getRewrittenUrl(res)).toEqual(null);
diff --git a/src/middleware.ts b/src/middleware.ts
index 7af18483..8b68b854 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,18 +1,17 @@
+import { getCookieCache } from "better-auth/cookies";
import type { MiddlewareSourceConfig } from "next/experimental/testing/server";
-import { NextResponse } from "next/server";
-import NextAuth from "next-auth";
-import { config as authConfig } from "./app/_clients/nextAuthConfig";
-
-const { auth } = NextAuth(authConfig);
+import { type NextRequest, NextResponse } from "next/server";
export const config: MiddlewareSourceConfig = {
matcher: ["/me(.*)"],
};
-export default auth(async function middleware(req) {
- if (req.auth?.user.role === "USER") {
+export default async function middleware(request: NextRequest) {
+ const session = await getCookieCache(request);
+
+ if (session?.user && (session.user as { role?: string }).role === "USER") {
return NextResponse.next();
}
- return NextResponse.rewrite(new URL("/signin", req.url));
-});
+ return NextResponse.redirect(new URL("/signin", request.url));
+}
diff --git a/tests/vitest.helper.ts b/tests/vitest.helper.ts
index 5282a8bd..9abfb9a3 100644
--- a/tests/vitest.helper.ts
+++ b/tests/vitest.helper.ts
@@ -1,5 +1,5 @@
-import type { User } from "next-auth";
import { afterAll, afterEach, expect, vi } from "vitest";
+import type { User } from "../src/app/_clients/betterAuth";
export async function setup() {
const { container, prisma, truncate, down } = await vi.hoisted(async () => {
@@ -9,7 +9,7 @@ export async function setup() {
});
const mock = vi.hoisted(() => ({
- auth: vi.fn(),
+ getSession: vi.fn(),
revalidatePath: vi.fn(),
revalidateTag: vi.fn(),
redirect: vi.fn(),
@@ -20,11 +20,13 @@ export async function setup() {
prisma,
}));
- vi.mock("next-auth", async (actual) => ({
- ...(await actual()),
- default: () => ({
- auth: mock.auth,
- }),
+ vi.mock("../src/app/_clients/betterAuth", async (actual) => ({
+ ...(await actual()),
+ auth: {
+ api: {
+ getSession: mock.getSession,
+ },
+ },
}));
vi.mock("next/cache", async (actual) => ({
@@ -38,6 +40,10 @@ export async function setup() {
redirect: mock.redirect,
}));
+ vi.mock("next/headers", async () => ({
+ headers: vi.fn().mockResolvedValue(new Headers()),
+ }));
+
afterAll(async () => {
await down();
});
@@ -47,7 +53,7 @@ export async function setup() {
});
async function createUser() {
- const user: User = {
+ const user: Pick = {
id: "id",
name: "name",
email: "hello@a.com",
@@ -56,12 +62,26 @@ export async function setup() {
};
await prisma.user.create({
- data: user,
+ data: {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ emailVerified: false,
+ image: user.image,
+ role: user.role as "USER" | "ADMIN",
+ },
});
- mock.auth.mockReturnValue({
+ const sessionMock = {
+ session: {
+ id: "session-id",
+ userId: user.id,
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
+ },
user,
- });
+ };
+
+ mock.getSession.mockResolvedValue(sessionMock);
expect(await prisma.user.count()).toBe(1);
@@ -75,7 +95,7 @@ export async function setup() {
throw new Error("User not found");
}
- const { createdAt: _, updatedAt: __, ...rest } = me;
+ const { createdAt: _, updatedAt: __, emailVerified: ___, ...rest } = me;
return rest;
}
diff --git a/vitest.config.ts b/vitest.config.ts
index bd4a307a..3dab85f6 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -15,12 +15,6 @@ export default defineConfig(async () => {
include: ["./src/**/*.test.{ts,tsx}"],
globalSetup: "./tests/vitest.setup.ts",
environment: "jsdom",
- // https://github.com/nextauthjs/next-auth/discussions/9385
- server: {
- deps: {
- inline: ["next"],
- },
- },
},
};
});