feat(auth) : add admin account management
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user