fix(auth): migrate from next-auth to better-auth (#1453)
This commit is contained in:
parent
453f5bb3e6
commit
b4f595a68c
40 changed files with 668 additions and 390 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import type { UserType } from "@/app/(auth)/auth";
|
||||
import type { UserType } from "@/lib/auth";
|
||||
|
||||
type Entitlements = {
|
||||
maxMessagesPerHour: number;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { tool, type UIMessageStreamWriter } from "ai";
|
||||
import type { Session } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
artifactKinds,
|
||||
documentHandlersByArtifactKind,
|
||||
} from "@/lib/artifacts/server";
|
||||
import type { AuthSession } from "@/lib/auth";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
|
||||
type CreateDocumentProps = {
|
||||
session: Session;
|
||||
session: NonNullable<AuthSession>;
|
||||
dataStream: UIMessageStreamWriter<ChatMessage>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Output, streamText, tool, type UIMessageStreamWriter } from "ai";
|
||||
import type { Session } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import type { AuthSession } from "@/lib/auth";
|
||||
import { getDocumentById, saveSuggestions } from "@/lib/db/queries";
|
||||
import type { Suggestion } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
|
|
@ -8,7 +8,7 @@ import { generateUUID } from "@/lib/utils";
|
|||
import { getArtifactModel } from "../providers";
|
||||
|
||||
type RequestSuggestionsProps = {
|
||||
session: Session;
|
||||
session: NonNullable<AuthSession>;
|
||||
dataStream: UIMessageStreamWriter<ChatMessage>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { tool, type UIMessageStreamWriter } from "ai";
|
||||
import type { Session } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { documentHandlersByArtifactKind } from "@/lib/artifacts/server";
|
||||
import type { AuthSession } from "@/lib/auth";
|
||||
import { getDocumentById } from "@/lib/db/queries";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
|
||||
type UpdateDocumentProps = {
|
||||
session: Session;
|
||||
session: NonNullable<AuthSession>;
|
||||
dataStream: UIMessageStreamWriter<ChatMessage>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { UIMessageStreamWriter } from "ai";
|
||||
import type { Session } from "next-auth";
|
||||
import { codeDocumentHandler } from "@/artifacts/code/server";
|
||||
import { sheetDocumentHandler } from "@/artifacts/sheet/server";
|
||||
import { textDocumentHandler } from "@/artifacts/text/server";
|
||||
import type { ArtifactKind } from "@/components/artifact";
|
||||
import type { AuthSession } from "@/lib/auth";
|
||||
import { saveDocument } from "../db/queries";
|
||||
import type { Document } from "../db/schema";
|
||||
import type { ChatMessage } from "../types";
|
||||
|
|
@ -20,14 +20,14 @@ export type CreateDocumentCallbackProps = {
|
|||
id: string;
|
||||
title: string;
|
||||
dataStream: UIMessageStreamWriter<ChatMessage>;
|
||||
session: Session;
|
||||
session: NonNullable<AuthSession>;
|
||||
};
|
||||
|
||||
export type UpdateDocumentCallbackProps = {
|
||||
document: Document;
|
||||
description: string;
|
||||
dataStream: UIMessageStreamWriter<ChatMessage>;
|
||||
session: Session;
|
||||
session: NonNullable<AuthSession>;
|
||||
};
|
||||
|
||||
export type DocumentHandler<T = ArtifactKind> = {
|
||||
|
|
|
|||
58
lib/auth.ts
Normal file
58
lib/auth.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { compare, genSaltSync, hashSync } from "bcrypt-ts";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { nextCookies } from "better-auth/next-js";
|
||||
import { anonymous } from "better-auth/plugins";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { headers } from "next/headers";
|
||||
import postgres from "postgres";
|
||||
import { account, session, user, verification } from "./db/schema";
|
||||
|
||||
// biome-ignore lint: Forbidden non-null assertion.
|
||||
const client = postgres(process.env.POSTGRES_URL!);
|
||||
const db = drizzle(client);
|
||||
|
||||
export const auth = betterAuth({
|
||||
basePath: "/api/auth",
|
||||
baseURL: process.env.BETTER_AUTH_URL,
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: { user, session, account, verification },
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
password: {
|
||||
hash: async (password) => hashSync(password, genSaltSync(10)),
|
||||
verify: async ({ hash, password }) => compare(password, hash),
|
||||
},
|
||||
},
|
||||
...(process.env.VERCEL_CLIENT_ID && process.env.VERCEL_CLIENT_SECRET
|
||||
? {
|
||||
socialProviders: {
|
||||
vercel: {
|
||||
clientId: process.env.VERCEL_CLIENT_ID,
|
||||
clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: () => crypto.randomUUID(),
|
||||
},
|
||||
},
|
||||
plugins: [anonymous(), nextCookies()],
|
||||
});
|
||||
|
||||
export type UserType = "guest" | "regular";
|
||||
|
||||
export function getUserType(user: Record<string, unknown>): UserType {
|
||||
return (user as { isAnonymous?: boolean }).isAnonymous ? "guest" : "regular";
|
||||
}
|
||||
|
||||
export async function getSession() {
|
||||
return auth.api.getSession({ headers: await headers() });
|
||||
}
|
||||
|
||||
export type AuthSession = Awaited<ReturnType<typeof getSession>>;
|
||||
export type AuthUser = NonNullable<AuthSession>["user"];
|
||||
9
lib/client.ts
Normal file
9
lib/client.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { anonymousClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
basePath: `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/auth`,
|
||||
plugins: [anonymousClient()],
|
||||
});
|
||||
|
||||
export const { useSession, signIn, signUp, signOut } = authClient;
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import { generateDummyPassword } from "./db/utils";
|
||||
|
||||
export const isProductionEnvironment = process.env.NODE_ENV === "production";
|
||||
export const isDevelopmentEnvironment = process.env.NODE_ENV === "development";
|
||||
export const isTestEnvironment = Boolean(
|
||||
|
|
@ -7,7 +5,3 @@ export const isTestEnvironment = Boolean(
|
|||
process.env.PLAYWRIGHT ||
|
||||
process.env.CI_PLAYWRIGHT
|
||||
);
|
||||
|
||||
export const guestRegex = /^guest-\d+$/;
|
||||
|
||||
export const DUMMY_PASSWORD = generateDummyPassword();
|
||||
|
|
|
|||
49
lib/db/migrations/0010_better_auth.sql
Normal file
49
lib/db/migrations/0010_better_auth.sql
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "name" text;
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "emailVerified" boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "image" text;
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "isAnonymous" boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "createdAt" timestamp DEFAULT now() NOT NULL;
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "updatedAt" timestamp DEFAULT now() NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "Session" (
|
||||
"id" text PRIMARY KEY,
|
||||
"expiresAt" timestamp NOT NULL,
|
||||
"token" text NOT NULL UNIQUE,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||
"ipAddress" text,
|
||||
"userAgent" text,
|
||||
"userId" uuid NOT NULL REFERENCES "User"("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "Account" (
|
||||
"id" text PRIMARY KEY,
|
||||
"accountId" text NOT NULL,
|
||||
"providerId" text NOT NULL,
|
||||
"userId" uuid NOT NULL REFERENCES "User"("id") ON DELETE CASCADE,
|
||||
"accessToken" text,
|
||||
"refreshToken" text,
|
||||
"idToken" text,
|
||||
"accessTokenExpiresAt" timestamp,
|
||||
"refreshTokenExpiresAt" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "Verification" (
|
||||
"id" text PRIMARY KEY,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expiresAt" timestamp NOT NULL,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "User" SET "isAnonymous" = true WHERE email ~ '^guest-\d+$';
|
||||
|
||||
INSERT INTO "Account" ("id", "accountId", "providerId", "userId", "password", "createdAt", "updatedAt")
|
||||
SELECT gen_random_uuid()::text, id::text, 'credential', id, password, now(), now()
|
||||
FROM "User" WHERE password IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
|
@ -17,7 +17,7 @@ import postgres from "postgres";
|
|||
import type { ArtifactKind } from "@/components/artifact";
|
||||
import type { VisibilityType } from "@/components/visibility-selector";
|
||||
import { ChatbotError } from "../errors";
|
||||
import { generateUUID } from "../utils";
|
||||
|
||||
import {
|
||||
type Chat,
|
||||
chat,
|
||||
|
|
@ -27,58 +27,13 @@ import {
|
|||
type Suggestion,
|
||||
stream,
|
||||
suggestion,
|
||||
type User,
|
||||
user,
|
||||
vote,
|
||||
} from "./schema";
|
||||
import { generateHashedPassword } from "./utils";
|
||||
|
||||
// Optionally, if not using email/pass login, you can
|
||||
// use the Drizzle adapter for Auth.js / NextAuth
|
||||
// https://authjs.dev/reference/adapter/drizzle
|
||||
|
||||
// biome-ignore lint: Forbidden non-null assertion.
|
||||
const client = postgres(process.env.POSTGRES_URL!);
|
||||
const db = drizzle(client);
|
||||
|
||||
export async function getUser(email: string): Promise<User[]> {
|
||||
try {
|
||||
return await db.select().from(user).where(eq(user.email, email));
|
||||
} catch (_error) {
|
||||
throw new ChatbotError(
|
||||
"bad_request:database",
|
||||
"Failed to get user by email"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUser(email: string, password: string) {
|
||||
const hashedPassword = generateHashedPassword(password);
|
||||
|
||||
try {
|
||||
return await db.insert(user).values({ email, password: hashedPassword });
|
||||
} catch (_error) {
|
||||
throw new ChatbotError("bad_request:database", "Failed to create user");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGuestUser() {
|
||||
const email = `guest-${Date.now()}`;
|
||||
const password = generateHashedPassword(generateUUID());
|
||||
|
||||
try {
|
||||
return await db.insert(user).values({ email, password }).returning({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
} catch (_error) {
|
||||
throw new ChatbotError(
|
||||
"bad_request:database",
|
||||
"Failed to create guest user"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveChat({
|
||||
id,
|
||||
userId,
|
||||
|
|
|
|||
|
|
@ -15,10 +15,62 @@ export const user = pgTable("User", {
|
|||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
email: varchar("email", { length: 64 }).notNull(),
|
||||
password: varchar("password", { length: 64 }),
|
||||
name: text("name"),
|
||||
emailVerified: boolean("emailVerified").notNull().default(false),
|
||||
image: text("image"),
|
||||
isAnonymous: boolean("isAnonymous").notNull().default(false),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type User = InferSelectModel<typeof user>;
|
||||
|
||||
export const session = pgTable("Session", {
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expiresAt").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
ipAddress: text("ipAddress"),
|
||||
userAgent: text("userAgent"),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export type Session = InferSelectModel<typeof session>;
|
||||
|
||||
export const account = pgTable("Account", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("accountId").notNull(),
|
||||
providerId: text("providerId").notNull(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("accessToken"),
|
||||
refreshToken: text("refreshToken"),
|
||||
idToken: text("idToken"),
|
||||
accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
|
||||
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
|
||||
export const verification = pgTable("Verification", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expiresAt").notNull(),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type Verification = InferSelectModel<typeof verification>;
|
||||
|
||||
export const chat = pgTable("Chat", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
createdAt: timestamp("createdAt").notNull(),
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import { generateId } from "ai";
|
||||
import { genSaltSync, hashSync } from "bcrypt-ts";
|
||||
|
||||
export function generateHashedPassword(password: string) {
|
||||
const salt = genSaltSync(10);
|
||||
const hash = hashSync(password, salt);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function generateDummyPassword() {
|
||||
const password = generateId();
|
||||
const hashedPassword = generateHashedPassword(password);
|
||||
|
||||
return hashedPassword;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue