feat(auth): use password-only admin login
This commit is contained in:
@@ -114,7 +114,10 @@ test("user and admin auth surfaces have host-only isolated cookies", () => {
|
||||
maxPasswordLength: 128,
|
||||
revokeSessionsOnPasswordReset: true,
|
||||
});
|
||||
assert.equal(adminOptions.emailAndPassword, undefined);
|
||||
assert.deepEqual(
|
||||
adminOptions.emailAndPassword,
|
||||
userOptions.emailAndPassword,
|
||||
);
|
||||
for (const options of [userOptions, adminOptions]) {
|
||||
const attributes = options.advanced?.defaultCookieAttributes;
|
||||
assert.equal(attributes?.secure, true);
|
||||
@@ -126,7 +129,7 @@ test("user and admin auth surfaces have host-only isolated cookies", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("admin surface disables sign-up and rejects non-admin session creation", async () => {
|
||||
test("admin surface is password-only and rejects non-admin session creation", async () => {
|
||||
const checkedUserIds: string[] = [];
|
||||
const options = buildAuthOptions({
|
||||
surface: "admin",
|
||||
@@ -141,7 +144,9 @@ test("admin surface disables sign-up and rejects non-admin session creation", as
|
||||
const emailPlugin = options.plugins?.find(
|
||||
(plugin) => plugin.id === "email-otp",
|
||||
);
|
||||
assert.ok(emailPlugin);
|
||||
assert.equal(emailPlugin, undefined);
|
||||
assert.equal(options.emailAndPassword?.enabled, true);
|
||||
assert.equal(options.emailAndPassword?.disableSignUp, true);
|
||||
|
||||
const before = options.databaseHooks?.session?.create?.before;
|
||||
assert.ok(before);
|
||||
@@ -163,12 +168,6 @@ test("admin surface disables sign-up and rejects non-admin session creation", as
|
||||
);
|
||||
assert.deepEqual(checkedUserIds, ["ordinary-user-id", "admin-user-id"]);
|
||||
|
||||
const otpOptions = createEmailOtpOptions(
|
||||
new FakeEmailOtpSender(),
|
||||
config.adminSecret,
|
||||
true,
|
||||
);
|
||||
assert.equal(otpOptions.disableSignUp, true);
|
||||
});
|
||||
|
||||
test("admin surface requires a server-side persisted-role authorizer", () => {
|
||||
|
||||
@@ -61,7 +61,7 @@ const envKeys = [
|
||||
"RESEND_FROM_EMAIL",
|
||||
] as const;
|
||||
|
||||
test("Better Auth supports OTP registration/login, first password, password login, and OTP reset while admin stays OTP-only", async () => {
|
||||
test("Better Auth supports user OTP/password flows and password-only admin login", async () => {
|
||||
const fixture = startPostgresFixture();
|
||||
const migration = spawnSync(process.execPath, [runnerPath], {
|
||||
encoding: "utf8",
|
||||
@@ -300,6 +300,15 @@ test("Better Auth supports OTP registration/login, first password, password logi
|
||||
/^(?:__Secure-)?jyotisha-user\.session_token=/,
|
||||
);
|
||||
|
||||
const nonAdminPasswordLogin = await handlers.POST(
|
||||
request(adminHost, "/api/auth/sign-in/email", {
|
||||
email: newEmail,
|
||||
password: resetPassword,
|
||||
}),
|
||||
);
|
||||
assert.notEqual(nonAdminPasswordLogin.status, 200);
|
||||
assert.equal(nonAdminPasswordLogin.headers.has("set-cookie"), false);
|
||||
|
||||
fixture.psqlAs(
|
||||
"identity_runtime",
|
||||
"identity-runtime-test-password",
|
||||
@@ -311,33 +320,28 @@ test("Better Auth supports OTP registration/login, first password, password logi
|
||||
password: resetPassword,
|
||||
}),
|
||||
);
|
||||
assert.notEqual(adminPasswordLogin.status, 200);
|
||||
assert.equal(adminPasswordLogin.headers.has("set-cookie"), false);
|
||||
assert.equal(adminPasswordLogin.status, 200);
|
||||
assert.match(
|
||||
sessionCookie(adminPasswordLogin),
|
||||
/^(?:__Secure-)?jyotisha-admin\.session_token=/,
|
||||
);
|
||||
|
||||
const sentMessageCount = sender.messages.length;
|
||||
const adminSend = await handlers.POST(
|
||||
request(adminHost, "/api/auth/email-otp/send-verification-otp", {
|
||||
email: newEmail,
|
||||
type: "sign-in",
|
||||
}),
|
||||
);
|
||||
assert.equal(adminSend.status, 200);
|
||||
const adminMessage = sender.messages.at(-1);
|
||||
assert.equal(adminMessage?.type, "sign-in");
|
||||
const adminSignIn = await handlers.POST(
|
||||
request(adminHost, "/api/auth/sign-in/email-otp", {
|
||||
email: newEmail,
|
||||
otp: adminMessage?.otp,
|
||||
}),
|
||||
);
|
||||
assert.equal(adminSignIn.status, 200);
|
||||
assert.match(sessionCookie(adminSignIn), /^(?:__Secure-)?jyotisha-admin\.session_token=/);
|
||||
assert.notEqual(adminSend.status, 200);
|
||||
assert.equal(sender.messages.length, sentMessageCount);
|
||||
|
||||
const adminPasswordRoute = await setAccountPassword(
|
||||
request(
|
||||
adminHost,
|
||||
"/api/account/password",
|
||||
{ newPassword: "admin-must-not-set-password" },
|
||||
sessionCookie(adminSignIn),
|
||||
sessionCookie(adminPasswordLogin),
|
||||
),
|
||||
);
|
||||
assert.equal(adminPasswordRoute.status, 401);
|
||||
|
||||
@@ -4,7 +4,7 @@ import test from "node:test";
|
||||
|
||||
import { createSelfHostedAuthActions } from "../src/modules/identity/client.ts";
|
||||
|
||||
test("login page selects the auth provider and limits passwords to the user surface", () => {
|
||||
test("login page uses password-only mode on the self-hosted admin surface", () => {
|
||||
const page = readFileSync(
|
||||
new URL("../src/app/login/page.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -18,9 +18,11 @@ test("login page selects the auth provider and limits passwords to the user surf
|
||||
assert.match(page, /surface === "admin"/);
|
||||
assert.match(
|
||||
page,
|
||||
/passwordEnabled = provider === "self-hosted" && surface === "user"/,
|
||||
/passwordEnabled = provider === "self-hosted"/,
|
||||
);
|
||||
assert.match(page, /passwordOnly = surface === "admin"/);
|
||||
assert.match(page, /passwordEnabled=\{passwordEnabled\}/);
|
||||
assert.match(page, /passwordOnly=\{passwordOnly\}/);
|
||||
assert.doesNotMatch(page, /NEXT_PUBLIC_AUTH_PROVIDER/);
|
||||
});
|
||||
|
||||
@@ -176,12 +178,13 @@ test("login UI preserves accessible OTP, password, registration, and reset input
|
||||
}
|
||||
assert.match(
|
||||
component,
|
||||
/canUsePassword && \(mode === "otp" \|\| mode === "password"\)/,
|
||||
/canUsePassword && !passwordOnly && \(mode === "otp" \|\| mode === "password"\)/,
|
||||
);
|
||||
assert.match(
|
||||
component,
|
||||
/canUsePassword && \(mode === "register" \|\| mode === "forgot"\)/,
|
||||
/canUsePassword && !passwordOnly && \(mode === "register" \|\| mode === "forgot"\)/,
|
||||
);
|
||||
assert.match(component, /useState<AuthMode>\(passwordOnly \? "password" : "otp"\)/);
|
||||
assert.match(component, /\{showLoginNavigation && \(/);
|
||||
assert.match(component, />\s*返回登录\s*</);
|
||||
for (const autocomplete of [
|
||||
|
||||
Reference in New Issue
Block a user