alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
208 lines (200 loc) • 7.08 kB
text/typescript
import { $atom, type Static, z } from "alepha";
/**
* Tri-state field requirement for realm auth settings.
*
* - `"none"`: Field is disabled and not shown.
* - `"optional"`: Field is shown but not required.
* - `"required"`: Field is shown and required.
*/
export type FieldRequirement = "none" | "optional" | "required";
const fieldRequirement = (description: string) =>
z
.union([z.const("none"), z.const("optional"), z.const("required")])
.describe(description);
/**
* Username-specific field requirement, extending {@link FieldRequirement}
* with an additional auto-derivation mode.
*
* - `"none"` / `"optional"` / `"required"`: same as {@link FieldRequirement}.
* - `"email"`: Field is hidden in the registration UI and the value is
* auto-derived from the user's email at signup. Same handling on
* credentials and OAuth flows. See `UsernameSlugger` for the rule and
* collision behavior.
*/
export type UsernameFieldRequirement = FieldRequirement | "email";
const usernameFieldRequirement = (description: string) =>
z
.union([
z.const("none"),
z.const("optional"),
z.const("required"),
z.const("email"),
])
.describe(description);
export const realmAuthSettingsAtom = $atom({
name: "alepha.api.users.realmAuthSettings",
schema: z.object({
// Branding and display settings
displayName: z
.string()
.describe("Display name shown on auth pages (e.g., 'Customer Portal')")
.optional(),
description: z
.string()
.describe("Description shown on auth pages")
.optional(),
logoUrl: z.string().describe("Logo URL for auth pages").optional(),
// Auth settings
registrationAllowed: z.boolean().describe("Enable user self-registration"),
email: fieldRequirement(
"Email address field requirement for user accounts",
),
username: usernameFieldRequirement(
"Username field requirement for user accounts",
),
usernameRegExp: z
.string()
.describe(
"Regular expression that usernames must match (if username is enabled)",
),
usernameBlocklist: z
.array(z.text())
.describe(
"Usernames that the slugger / manual registration must reject. " +
"Default empty so apps can register `admin`/`root`/`me`/etc. if " +
"they want; populate it explicitly for handles you want to keep " +
"off-limits.",
),
phoneNumber: fieldRequirement(
"Phone number field requirement for user accounts",
),
verifyEmailRequired: z
.boolean()
.describe("Require email verification for user accounts"),
verifyPhoneRequired: z
.boolean()
.describe("Require phone verification for user accounts"),
firstNameLastName: fieldRequirement(
"First and last name field requirement for user accounts",
),
resetPasswordAllowed: z
.boolean()
.describe("Enable forgot password functionality"),
captchaRequired: z
.boolean()
.describe(
"Require captcha verification on registration (needs a CaptchaProvider registered, e.g. TurnstileCaptchaProvider)",
),
adminEmails: z
.array(z.email())
.describe(
"List of email addresses that are automatically promoted to admin role on login",
),
adminUsernames: z
.array(z.text())
.describe(
"List of usernames that are automatically promoted to admin role on login",
),
defaultRoles: z
.array(z.string())
.describe("Default roles assigned to newly registered users"),
verifyEmailUrl: z
.string()
.describe(
"Base URL for email verification links (used when verification method is 'link'). Token and email are appended as query params.",
)
.optional(),
passwordPolicy: z.object({
minLength: z
.integer()
.min(1)
.describe("Minimum password length")
.default(8),
requireUppercase: z
.boolean()
.describe("Require at least one uppercase letter"),
requireLowercase: z
.boolean()
.describe("Require at least one lowercase letter"),
requireNumbers: z.boolean().describe("Require at least one number"),
requireSpecialCharacters: z
.boolean()
.describe("Require at least one special character"),
}),
loginRateLimit: z.object({
ipMaxAttempts: z
.integer()
.min(1)
.describe("Max failed login attempts per IP before temporary lockout")
.default(15),
accountMaxAttempts: z
.integer()
.min(1)
.describe(
"Max failed login attempts per account before temporary lockout",
)
.default(5),
windowMs: z
.integer()
.min(1000)
.describe("Rate limit window duration in milliseconds")
.default(15 * 60 * 1000),
}),
registrationIpMaxAttempts: z
.integer()
.min(1)
.describe(
"Max registration attempts per IP before temporary lockout. Default 10 protects against signup abuse; raise it in dev/e2e environments where a single localhost IP spawns many test users.",
)
.default(10),
refreshToken: z.object({
expirationIdle: z
.integer()
.min(1000)
.describe(
"Maximum time in milliseconds a refresh token may stay unused before being invalidated. " +
"When set, sessions whose last refresh is older than this window are rejected and deleted, " +
"even if the absolute `expiresAt` has not been reached. Recommended for SaaS auth posture " +
"(SOC2/ISO27001). Leave undefined to disable idle invalidation (default).",
)
.optional(),
}),
}),
default: {
// for a fresh hello world setup, we accept registration and email login
registrationAllowed: true,
email: "required" as FieldRequirement,
username: "none" as UsernameFieldRequirement,
// Allow hyphens by default so the UsernameSlugger output (`ni-foures-testkv`)
// matches without app overrides. Existing `[a-zA-Z0-9_]` usernames remain
// valid because the new pattern is a strict superset.
usernameRegExp: "^[a-zA-Z0-9_-]{3,30}$",
usernameBlocklist: [] as string[],
phoneNumber: "none" as FieldRequirement,
verifyEmailRequired: false,
verifyPhoneRequired: false,
resetPasswordAllowed: false,
captchaRequired: false,
firstNameLastName: "none" as FieldRequirement,
adminEmails: [],
adminUsernames: [],
defaultRoles: ["user"],
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialCharacters: false,
},
loginRateLimit: {
ipMaxAttempts: 15,
accountMaxAttempts: 5,
windowMs: 15 * 60 * 1000,
},
registrationIpMaxAttempts: 10,
refreshToken: {
// expirationIdle: undefined — opt-in
},
},
});
export type RealmAuthSettings = Static<typeof realmAuthSettingsAtom.schema>;