feat(auth) : add admin account management
CI / Verify (push) Successful in 57s
CI / Build immutable images and deploy (push) Successful in 2m7s

This commit is contained in:
2026-09-04 10:54:08 +07:00 Unverified
parent 53b79cc7e8
commit a865913a08
15 changed files with 3703 additions and 245 deletions
+5 -54
View File
@@ -1,9 +1,7 @@
import { describe, expect, it } from "vitest";
import {
isAllowedAdminRegistration,
isAuthorizedAdmin,
isConfiguredAdminEmail,
} from "./authorization";
const verified = {
@@ -13,61 +11,14 @@ const verified = {
};
describe("admin authorization", () => {
it("recognizes only the exact configured administrator email", () => {
expect(isConfiguredAdminEmail(verified.email, "[email protected]")).toBe(
true,
);
expect(isConfiguredAdminEmail(verified.email, "[email protected]")).toBe(
false,
);
expect(isConfiguredAdminEmail(verified.email, undefined)).toBe(false);
});
it("supports a comma-separated administrator email list", () => {
it("allows any verified credential user", () => {
expect(isAuthorizedAdmin(verified)).toBe(true);
expect(
isConfiguredAdminEmail(
"[email protected]",
"[email protected], [email protected]",
),
).toBe(true);
expect(
isConfiguredAdminEmail(
"[email protected]",
"[email protected], [email protected]",
),
isAuthorizedAdmin({ ...verified, emailVerified: false }),
).toBe(false);
});
it("allows only credential registration for the configured administrator", () => {
expect(
isAllowedAdminRegistration(
verified.email,
"email-password",
verified.email,
),
).toBe(true);
expect(
isAllowedAdminRegistration(
"[email protected]",
"email-password",
verified.email,
),
).toBe(false);
expect(
isAllowedAdminRegistration(verified.email, "oauth", verified.email),
).toBe(false);
});
it("requires an exact verified email match", () => {
expect(isAuthorizedAdmin(verified, "[email protected]")).toBe(true);
expect(isAuthorizedAdmin(verified, "[email protected]")).toBe(false);
expect(
isAuthorizedAdmin({ ...verified, emailVerified: false }, verified.email),
).toBe(false);
});
it("denies missing users and missing configuration", () => {
expect(isAuthorizedAdmin(null, verified.email)).toBe(false);
expect(isAuthorizedAdmin(verified, undefined)).toBe(false);
it("denies missing users", () => {
expect(isAuthorizedAdmin(null)).toBe(false);
});
});
+1 -30
View File
@@ -6,37 +6,8 @@ export interface SessionUserLike {
image?: string | null;
}
export function isConfiguredAdminEmail(
email: string | null | undefined,
adminEmailConfig: string | null | undefined,
): boolean {
if (!email || !adminEmailConfig) return false;
return adminEmailConfig
.split(",")
.map((configuredEmail) => configuredEmail.trim())
.filter(Boolean)
.includes(email);
}
export function isAllowedAdminRegistration(
email: string | null | undefined,
authMethod: string,
adminEmail: string | null | undefined,
): boolean {
return (
authMethod === "email-password" &&
isConfiguredAdminEmail(email, adminEmail)
);
}
export function isAuthorizedAdmin(
user: SessionUserLike | null | undefined,
adminEmail: string | null | undefined,
): user is SessionUserLike {
return Boolean(
user &&
user.emailVerified === true &&
isConfiguredAdminEmail(user.email, adminEmail),
);
return Boolean(user?.email && user.emailVerified === true);
}
+7 -32
View File
@@ -3,6 +3,7 @@ import "server-only";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { admin } from "better-auth/plugins";
import { headers } from "next/headers";
import { getDb } from "@/db";
@@ -13,12 +14,7 @@ import {
verifications,
} from "@/db/schema";
import {
isAllowedAdminRegistration,
isAuthorizedAdmin,
isConfiguredAdminEmail,
type SessionUserLike,
} from "./authorization";
import { isAuthorizedAdmin, type SessionUserLike } from "./authorization";
function required(name: string): string {
const value = process.env[name];
@@ -27,14 +23,12 @@ function required(name: string): string {
}
function hasAuthConfiguration(): boolean {
return ["DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_SECRET", "ADMIN_EMAIL"].every(
return ["DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_SECRET"].every(
(name) => Boolean(process.env[name]),
);
}
function createAuth() {
const adminEmail = required("ADMIN_EMAIL");
return betterAuth({
appName: "Buzz Sheet",
database: drizzleAdapter(getDb(), {
@@ -55,37 +49,18 @@ function createAuth() {
secret: required("BETTER_AUTH_SECRET"),
emailAndPassword: {
enabled: true,
disableSignUp: true,
minPasswordLength: 8,
maxPasswordLength: 128,
},
user: {
validateUserInfo: async ({ user, source }) => {
if (
!isAllowedAdminRegistration(
user.email,
source.method,
adminEmail,
)
) {
return {
error: "ADMIN_REGISTRATION_FORBIDDEN",
errorDescription:
"Registration is limited to the configured administrator.",
};
}
},
},
databaseHooks: {
user: {
create: {
before: async (user) => {
if (!isConfiguredAdminEmail(user.email, adminEmail)) return false;
return { data: { emailVerified: true } };
},
before: async () => ({ data: { emailVerified: true } }),
},
},
},
plugins: [nextCookies()],
plugins: [admin({ defaultRole: "admin" }), nextCookies()],
});
}
@@ -123,7 +98,7 @@ export async function getAdminSession(): Promise<AdminSession | null> {
const session = await getAuth().api.getSession({ headers: await headers() });
if (
!session?.session ||
!isAuthorizedAdmin(session.user, process.env.ADMIN_EMAIL)
!isAuthorizedAdmin(session.user)
) {
return null;
}