mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
Add auto-close registration and admin settings page
First user to register automatically becomes admin and registration closes. Admins can re-open registration from a new settings page. Uses Better Auth admin plugin for role management and a new appSettings table for the registration flag. Mobile tab bar now links to settings instead of inline logout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -1,7 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { adminClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient();
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [adminClient()],
|
||||
});
|
||||
|
||||
export const { signIn, signUp, signOut, useSession } = authClient;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { admin } from "better-auth/plugins";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { db } from "@/lib/db/client";
|
||||
import {
|
||||
getUserCount,
|
||||
isRegistrationOpen,
|
||||
setSetting,
|
||||
} from "@/lib/services/settings";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
@@ -10,9 +17,40 @@ export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
plugins: [admin()],
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: () => uuid(),
|
||||
},
|
||||
},
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
before: async (userData) => {
|
||||
const userCount = await getUserCount();
|
||||
if (userCount === 0) {
|
||||
return {
|
||||
data: {
|
||||
...userData,
|
||||
role: "admin",
|
||||
},
|
||||
};
|
||||
}
|
||||
const open = await isRegistrationOpen();
|
||||
if (!open) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "Registration is currently closed",
|
||||
});
|
||||
}
|
||||
return { data: userData };
|
||||
},
|
||||
after: async () => {
|
||||
const userCount = await getUserCount();
|
||||
if (userCount === 1) {
|
||||
await setSetting("registrationOpen", "false");
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,6 +24,10 @@ export const user = sqliteTable("user", {
|
||||
.notNull()
|
||||
.default(false),
|
||||
image: text("image"),
|
||||
role: text("role").default("user"),
|
||||
banned: int("banned", { mode: "boolean" }).default(false),
|
||||
banReason: text("banReason"),
|
||||
banExpires: int("banExpires", { mode: "timestamp" }),
|
||||
createdAt: int("createdAt", { mode: "timestamp" }).notNull(),
|
||||
updatedAt: int("updatedAt", { mode: "timestamp" }).notNull(),
|
||||
});
|
||||
@@ -37,6 +41,7 @@ export const session = sqliteTable("session", {
|
||||
expiresAt: int("expiresAt", { mode: "timestamp" }).notNull(),
|
||||
ipAddress: text("ipAddress"),
|
||||
userAgent: text("userAgent"),
|
||||
impersonatedBy: text("impersonatedBy"),
|
||||
createdAt: int("createdAt", { mode: "timestamp" }).notNull(),
|
||||
updatedAt: int("updatedAt", { mode: "timestamp" }).notNull(),
|
||||
});
|
||||
@@ -277,3 +282,10 @@ export const titleRecommendations = sqliteTable(
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// ─── App Settings ───────────────────────────────────────────────────
|
||||
|
||||
export const appSettings = sqliteTable("appSettings", {
|
||||
key: text("key").primaryKey(),
|
||||
value: text("value"),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { count, eq } from "drizzle-orm";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { appSettings, user } from "@/lib/db/schema";
|
||||
|
||||
export async function getSetting(key: string): Promise<string | null> {
|
||||
const row = await db
|
||||
.select()
|
||||
.from(appSettings)
|
||||
.where(eq(appSettings.key, key))
|
||||
.get();
|
||||
return row?.value ?? null;
|
||||
}
|
||||
|
||||
export async function setSetting(key: string, value: string): Promise<void> {
|
||||
await db
|
||||
.insert(appSettings)
|
||||
.values({ key, value })
|
||||
.onConflictDoUpdate({ target: appSettings.key, set: { value } });
|
||||
}
|
||||
|
||||
export async function getUserCount(): Promise<number> {
|
||||
const result = await db.select({ count: count() }).from(user).get();
|
||||
return result?.count ?? 0;
|
||||
}
|
||||
|
||||
export async function isRegistrationOpen(): Promise<boolean> {
|
||||
const userCount = await getUserCount();
|
||||
if (userCount === 0) return true;
|
||||
|
||||
const setting = await getSetting("registrationOpen");
|
||||
return setting === "true";
|
||||
}
|
||||
Reference in New Issue
Block a user