BugFixes galore

This commit is contained in:
2026-03-08 17:01:36 +01:00
parent 781f0f14fa
commit c8895c8e80
39 changed files with 2255 additions and 237 deletions

View File

@@ -10,7 +10,11 @@ import type { Auth } from "./index";
* code. Mirrors the plugins registered on the server-side `auth` instance.
*/
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL ?? "http://localhost:3000",
// No baseURL — uses window.location.origin automatically, which always
// produces same-origin requests and avoids CSP connect-src issues.
...(process.env.NEXT_PUBLIC_BETTER_AUTH_URL
? { baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL }
: {}),
plugins: [
// Enables organization.* methods (createOrganization, getActiveMember, etc.)

View File

@@ -2,6 +2,7 @@ import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization } from "better-auth/plugins";
import { magicLink } from "better-auth/plugins/magic-link";
import { count, eq } from "drizzle-orm";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
@@ -19,13 +20,14 @@ export const auth = betterAuth({
// -------------------------------------------------------------------------
database: drizzleAdapter(db, {
provider: "sqlite",
// Keys must match Better Auth's internal model names (singular).
// usePlural: false (default) → "user", "session", "account", "verification"
schema: {
users: schema.users,
sessions: schema.sessions,
accounts: schema.accounts,
verifications: schema.verifications,
user: schema.users,
session: schema.sessions,
account: schema.accounts,
verification: schema.verifications,
},
usePlural: false,
}),
// -------------------------------------------------------------------------
@@ -52,6 +54,30 @@ export const auth = betterAuth({
maxPasswordLength: 128,
},
// -------------------------------------------------------------------------
// Database hooks — first registered user becomes admin automatically
// -------------------------------------------------------------------------
databaseHooks: {
user: {
create: {
after: async (user) => {
// Count all users; if this is the very first, promote to admin
const [{ total }] = await db
.select({ total: count() })
.from(schema.users);
if (total === 1) {
await db
.update(schema.users)
.set({ role: "admin" } as Record<string, unknown>)
.where(eq(schema.users.id, user.id));
console.log(`[Auth] First user ${user.id} (${user.email}) promoted to admin`);
}
},
},
},
},
// -------------------------------------------------------------------------
// Plugins
// -------------------------------------------------------------------------
@@ -104,5 +130,18 @@ export type Auth = typeof auth;
/** The server-side session type returned by auth.api.getSession */
export type Session = typeof auth.$Infer.Session.session;
/** The user type embedded in every session */
export type User = typeof auth.$Infer.Session.user;
/** The user type embedded in every session, with custom additionalFields */
export type User = typeof auth.$Infer.Session.user & {
role?: "superadmin" | "admin" | "moderator" | null;
};
type RawSession = NonNullable<Awaited<ReturnType<typeof auth.api.getSession>>>;
/** Typed wrapper around auth.api.getSession that includes the role field */
export async function getAuthSession(
headers: Headers,
): Promise<(Omit<RawSession, "user"> & { user: User }) | null> {
return auth.api.getSession({ headers }) as Promise<
(Omit<RawSession, "user"> & { user: User }) | null
>;
}

View File

@@ -24,8 +24,8 @@ export const users = sqliteTable("users", {
})
.notNull()
.default("moderator"),
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
});
export const sessions = sqliteTable("sessions", {
@@ -34,11 +34,11 @@ export const sessions = sqliteTable("sessions", {
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
token: text("token").notNull().unique(),
expiresAt: integer("expires_at").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
});
export const accounts = sqliteTable("accounts", {
@@ -50,18 +50,20 @@ export const accounts = sqliteTable("accounts", {
providerId: text("provider_id").notNull(),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
expiresAt: integer("expires_at"),
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
idToken: text("id_token"),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
password: text("password"), // hashed password for email/password auth
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
});
export const verifications = sqliteTable("verifications", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at").notNull(),
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
});
// ---------------------------------------------------------------------------