feat(auth): add staging email password flows

This commit is contained in:
Jesse_Chen
2026-07-27 17:02:16 +08:00
parent f402e6f79c
commit ab2944f7fa
11 changed files with 982 additions and 122 deletions
@@ -100,6 +100,17 @@ export function buildAuthOptions({
path: "/",
},
},
...(surface === "user"
? {
emailAndPassword: {
enabled: true,
disableSignUp: true,
minPasswordLength: 8,
maxPasswordLength: 128,
revokeSessionsOnPasswordReset: true,
},
}
: {}),
plugins: [
emailOTP(createEmailOtpOptions(emailSender, secret, surface === "admin")),
admin({
+96 -13
View File
@@ -1,40 +1,62 @@
import { createAuthClient } from "better-auth/react";
import { emailOTPClient } from "better-auth/client/plugins";
interface OtpClientResult {
interface AuthClientResult {
data: unknown;
error: unknown;
}
export interface SelfHostedOtpClient {
export interface SelfHostedAuthClient {
emailOtp: {
sendVerificationOtp(input: {
email: string;
type: "sign-in";
}): Promise<OtpClientResult>;
}): Promise<AuthClientResult>;
requestPasswordReset?(input: { email: string }): Promise<AuthClientResult>;
resetPassword?(input: {
email: string;
otp: string;
password: string;
}): Promise<AuthClientResult>;
};
signIn: {
emailOtp(input: {
email: string;
otp: string;
}): Promise<OtpClientResult>;
}): Promise<AuthClientResult>;
email?(input: {
email: string;
password: string;
}): Promise<AuthClientResult>;
};
signOut?(): Promise<OtpClientResult>;
signOut?(): Promise<AuthClientResult>;
}
export interface SelfHostedOtpActions {
export interface SelfHostedAuthActions {
send(email: string): Promise<void>;
verify(email: string, otp: string): Promise<void>;
signInWithPassword(email: string, password: string): Promise<void>;
requestPasswordReset(email: string): Promise<void>;
resetPassword(email: string, otp: string, password: string): Promise<void>;
hasPassword(): Promise<boolean>;
setPassword(password: string): Promise<void>;
signOut(): Promise<void>;
}
export function createSelfHostedOtpActions(
client: SelfHostedOtpClient,
): SelfHostedOtpActions {
type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
function normalizeEmail(email: string): string {
return email.trim().toLowerCase();
}
export function createSelfHostedAuthActions(
client: SelfHostedAuthClient,
fetcher: Fetcher = fetch,
): SelfHostedAuthActions {
return {
async send(email) {
const result = await client.emailOtp.sendVerificationOtp({
email: email.trim().toLowerCase(),
email: normalizeEmail(email),
type: "sign-in",
});
if (result.error) {
@@ -43,13 +65,68 @@ export function createSelfHostedOtpActions(
},
async verify(email, otp) {
const result = await client.signIn.emailOtp({
email: email.trim().toLowerCase(),
email: normalizeEmail(email),
otp,
});
if (result.error) {
throw new Error("验证码错误或已过期,请重新获取");
}
},
async signInWithPassword(email, password) {
if (!client.signIn.email) throw new Error("邮箱或密码错误");
const result = await client.signIn.email({
email: normalizeEmail(email),
password,
});
if (result.error) throw new Error("邮箱或密码错误");
},
async requestPasswordReset(email) {
if (!client.emailOtp.requestPasswordReset) {
throw new Error("暂时无法发送验证码,请稍后再试");
}
const result = await client.emailOtp.requestPasswordReset({
email: normalizeEmail(email),
});
if (result.error) {
throw new Error("暂时无法发送验证码,请稍后再试");
}
},
async resetPassword(email, otp, password) {
if (!client.emailOtp.resetPassword) {
throw new Error("验证码错误或已过期,请重新获取");
}
const result = await client.emailOtp.resetPassword({
email: normalizeEmail(email),
otp,
password,
});
if (result.error) {
throw new Error("验证码错误或已过期,请重新获取");
}
},
async hasPassword() {
const response = await fetcher("/api/account/password", {
credentials: "same-origin",
});
if (!response.ok) throw new Error("暂时无法确认密码状态,请稍后再试");
const body = (await response.json()) as { hasPassword?: unknown };
if (typeof body.hasPassword !== "boolean") {
throw new Error("暂时无法确认密码状态,请稍后再试");
}
return body.hasPassword;
},
async setPassword(password) {
const response = await fetcher("/api/account/password", {
method: "POST",
credentials: "same-origin",
headers: { "content-type": "application/json" },
body: JSON.stringify({ newPassword: password }),
});
if (response.status === 409) {
throw new Error("此账户已设置密码,原密码未被更改");
}
if (!response.ok) throw new Error("暂时无法设置密码,请稍后再试");
},
async signOut() {
if (!client.signOut) throw new Error("退出失败,请稍后再试");
const result = await client.signOut();
@@ -58,8 +135,14 @@ export function createSelfHostedOtpActions(
};
}
export type SelfHostedOtpClient = SelfHostedAuthClient;
export type SelfHostedOtpActions = SelfHostedAuthActions;
export const createSelfHostedOtpActions = createSelfHostedAuthActions;
const authClient = createAuthClient({ plugins: [emailOTPClient()] });
export const selfHostedOtpActions = createSelfHostedOtpActions(
authClient as SelfHostedOtpClient,
export const selfHostedAuthActions = createSelfHostedAuthActions(
authClient as unknown as SelfHostedAuthClient,
);
export const selfHostedOtpActions = selfHostedAuthActions;