@better-auth-ui/core
Version:
Authentication components and data utilities for [Better Auth](https://better-auth.com), available for React and Solid.
273 lines (260 loc) • 9.95 kB
text/typescript
import { defaultAuthConfig } from "../../config"
import type { AdditionalFields } from "../../config/additional-fields-config"
import type { AvatarConfig } from "../../config/avatar-config"
import type {
AuthPluginBase,
AuthPluginLocalizationContext
} from "../../lib/auth-plugin"
import { createAuthPlugin } from "../../lib/create-auth-plugin"
import {
type OrganizationLocalization,
organizationLocalization
} from "./organization-localization"
import type { OrganizationViewPaths } from "./organization-view-paths"
declare module "../../lib/view-paths" {
/** Widens `AuthViewPaths` with the organization invitation path when this plugin is imported. */
interface AuthViewPaths {
/** @default "accept-invitation" */
acceptInvitation?: string
}
/** Widens `SettingsViewPaths` by adding the `"organizations"` path when this plugin is imported. */
interface SettingsViewPaths {
/** @default "organizations" */
organizations?: string
}
}
declare module "../../lib/auth-plugin" {
interface AuthPluginViewPaths {
organization?: Partial<OrganizationViewPaths>
}
}
export type OrganizationPluginOptions = {
/** Role Better Auth assigns to organization creators. @default "owner" */
creatorRole?: string
/**
* Whether to call `organization.checkSlug` when entering an organization slug.
* @default true
*/
checkSlug?: boolean
/**
* Override the plugin's default localization strings.
* @remarks `OrganizationLocalization`
*/
localization?: Partial<OrganizationLocalization>
/**
* Override URL segments contributed by this plugin.
*
* - `auth.acceptInvitation` — segment for the direct invitation acceptance view (default `"accept-invitation"`).
* - `settings.organizations` — segment for the organizations settings view (default `"organizations"`).
* - `organization.settings` — segment for the `/organization/...` profile and danger zone tab (default `"settings"`).
* - `organization.people` — segment for the `/organization/...` members and invitations tab (default `"people"`).
* - `organization.teams` — segment for the `/organization/...` teams tab (default `"teams"`).
* - `organization.roles` — segment for the dynamic roles tab (default `"roles"`).
*/
viewPaths?: {
auth?: {
/** @default "accept-invitation" */
acceptInvitation?: string
}
settings?: {
/** @default "organizations" */
organizations?: string
}
organization?: Partial<OrganizationViewPaths>
}
/**
* Organization logo upload, optimization, and deletion configuration.
* Same shape as {@link AvatarConfig} used for user avatars (`AuthConfig.avatar`).
* @remarks `AvatarConfig`
* @default { enabled: true, resize: resizeAvatar, size: 256, extension: "png" }
*/
logo?: Partial<AvatarConfig>
/**
* Map of role keys to display labels. When omitted, defaults to localized
* labels for `owner`, `admin`, and `member` (from `localization.owner`
* etc.) plus {@link OrganizationPluginOptions.additionalRoles}. When set,
* replaces that default map entirely; use {@link OrganizationPluginOptions.additionalRoles}
* to add more labels on top. Looked up at render time via `roles?.[role]`.
* @remarks `Record<string, string>`
*/
roles?: Record<string, string>
/**
* Extra role labels merged after the effective role map (either
* {@link OrganizationPluginOptions.roles} when provided, or the localized
* defaults). Use this for custom server roles without redefining built-in
* labels.
* @remarks `Record<string, string>`
*/
additionalRoles?: Record<string, string>
/**
* Enable runtime organization roles and configure the permission editor.
* The Better Auth client must also enable `dynamicAccessControl`.
*/
dynamicAccessControl?: {
/** @default true */
enabled?: boolean
/** Resources and actions available in the role permission matrix. */
permissions: OrganizationPermissionRegistry
}
slug?: string | null
/** Hide slugs in organization forms and views. Switchers hide slugs by default. */
hideSlug?: boolean
/**
* Prefix prepended to organization slugs.
* @default ""
*/
slugPrefix?: string
/** Additional organization fields rendered during creation and profile editing. */
additionalFields?: AdditionalFields
/**
* Additional fields grouped by the Better Auth organization model that
* owns them. `organization` overrides the legacy `additionalFields` array.
*/
modelFields?: OrganizationAdditionalFields
/** Maximum organizations the current user can create. */
organizationLimit?: number
/** Maximum members per organization. */
membershipLimit?: number
/** Maximum pending invitations per organization. */
invitationLimit?: number
/**
* Whether members and invitations can have more than one role.
* @default true
*/
allowMultipleRoles?: boolean
/** Whether organization creation controls are available. @default true */
allowOrganizationCreation?: boolean
/** Enable Better Auth team management controls and mirror static server policies. */
teams?: boolean | OrganizationTeamsOptions
}
export type OrganizationTeamsOptions = {
/** Enable team management controls. @default true */
enabled?: boolean
/** Maximum teams an organization can contain. */
maximumTeams?: number
/** Maximum members a team can contain. */
maximumMembersPerTeam?: number
/** Allow deletion of an organization's final team. @default false */
allowRemovingAllTeams?: boolean
}
export type OrganizationAdditionalFields = {
/** Fields rendered during organization creation and profile editing. */
organization?: AdditionalFields
/** Read-only member details returned by Better Auth member queries. */
member?: AdditionalFields
/** Fields rendered when an invitation is created. */
invitation?: AdditionalFields
/** Fields rendered when a team is created or edited. */
team?: AdditionalFields
/** Fields rendered when a dynamic organization role is created or edited. */
role?: AdditionalFields
}
export type OrganizationPermissionResource = {
/** Resource label shown above its actions. Defaults to the resource key. */
label?: string
/** Map of Better Auth action keys to display labels. */
actions: Record<string, string>
}
export type OrganizationPermissionRegistry = Record<
string,
OrganizationPermissionResource
>
const resolvePolicyLimit = (limit?: number) =>
limit !== undefined && Number.isSafeInteger(limit) && limit >= 0
? limit
: undefined
export const organizationPlugin = createAuthPlugin(
"organization",
(options: OrganizationPluginOptions = {}) => {
const localization = {
...organizationLocalization,
...options.localization
}
const teamOptions =
typeof options.teams === "object" ? options.teams : undefined
return {
creatorRole: options.creatorRole?.trim() || "owner",
slug: options.slug,
hideSlug: options.hideSlug,
slugPrefix: options.slugPrefix ?? "",
checkSlug: options.checkSlug ?? true,
localization,
logo: {
...defaultAuthConfig.avatar,
...options.logo
},
roles: {
...(options.roles ?? {
owner: localization.owner,
admin: localization.admin,
member: localization.member
}),
...options.additionalRoles
},
_localizationResolver: (
plugin: AuthPluginBase,
context: AuthPluginLocalizationContext
) => {
const messages = context.localization as OrganizationLocalization
return {
...plugin,
roles: {
...(options.roles ?? {
owner: messages.owner,
admin: messages.admin,
member: messages.member
}),
...options.additionalRoles
}
}
},
additionalFields:
options.modelFields?.organization ?? options.additionalFields ?? [],
modelFields: {
organization:
options.modelFields?.organization ?? options.additionalFields ?? [],
member: options.modelFields?.member ?? [],
invitation: options.modelFields?.invitation ?? [],
team: options.modelFields?.team ?? [],
role: options.modelFields?.role ?? []
},
dynamicAccessControl: options.dynamicAccessControl
? {
enabled: options.dynamicAccessControl.enabled ?? true,
permissions: options.dynamicAccessControl.permissions
}
: undefined,
organizationLimit: resolvePolicyLimit(options.organizationLimit),
membershipLimit: resolvePolicyLimit(options.membershipLimit),
invitationLimit: resolvePolicyLimit(options.invitationLimit),
allowMultipleRoles: options.allowMultipleRoles ?? true,
allowOrganizationCreation: options.allowOrganizationCreation ?? true,
teams:
options.teams === true ||
(teamOptions !== undefined && (teamOptions.enabled ?? true)),
teamPolicy: {
maximumTeams: resolvePolicyLimit(teamOptions?.maximumTeams),
maximumMembersPerTeam: resolvePolicyLimit(
teamOptions?.maximumMembersPerTeam
),
allowRemovingAllTeams: teamOptions?.allowRemovingAllTeams ?? false
},
viewPaths: {
settings: {
organizations:
options.viewPaths?.settings?.organizations ?? "organizations"
},
auth: {
acceptInvitation:
options.viewPaths?.auth?.acceptInvitation ?? "accept-invitation"
},
organization: {
settings: options.viewPaths?.organization?.settings ?? "settings",
people: options.viewPaths?.organization?.people ?? "people",
teams: options.viewPaths?.organization?.teams ?? "teams",
roles: options.viewPaths?.organization?.roles ?? "roles"
}
}
}
}
)