Files
kan/packages/auth/src/auth.ts
Nick Meinhold 5f190b92de fix: allow invited users to sign up when registration is disabled (#418)
* fix: allow invited users to sign up when registration is disabled

Move sign-up restriction logic from better-auth's disableSignUp config
to the existing user.create.before database hook, which already checks
for pending invitations. The frontend signup and login pages now detect
invite flows (?next=/invite/...) and bypass the disabled UI accordingly.

Closes #411

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add regression tests for sign-up hook invite bypass

Verify that the user.create.before database hook correctly:
- allows sign-up when registration is not disabled
- blocks sign-up when disabled and no invitation exists
- allows sign-up when disabled but a pending invitation exists
- respects BETTER_AUTH_ALLOWED_DOMAINS in combination with invites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add OIDC/social sign-up path coverage for invite bypass

Address review suggestion: add explicit tests verifying the
user.create.before hook handles OIDC/social sign-ups the same way as
email/password — invited users are allowed, uninvited users are blocked.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:24:58 +00:00

72 lines
2.2 KiB
TypeScript

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { env } from "next-runtime-env";
import type { dbClient } from "@kan/db/client";
import * as schema from "@kan/db/schema";
import { sendEmail } from "@kan/email";
import { createDatabaseHooks, createMiddlewareHooks } from "./hooks";
import { createPlugins } from "./plugins";
import { configuredProviders } from "./providers";
export const initAuth = (db: dbClient) => {
const baseURL = env("NEXT_PUBLIC_BASE_URL") || env("BETTER_AUTH_URL");
const trustedOrigins =
env("BETTER_AUTH_TRUSTED_ORIGINS")?.split(",").filter(Boolean) ?? [];
return betterAuth({
secret: env("BETTER_AUTH_SECRET"),
baseURL,
trustedOrigins: [...(baseURL ? [baseURL] : []), ...trustedOrigins],
database: drizzleAdapter(db, {
provider: "pg",
schema: {
...schema,
user: schema.users,
},
}),
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 days
updateAge: 60 * 60 * 24 * 2, // Update session expiry every 48 hours if user is active
freshAge: 0,
},
emailAndPassword: {
enabled: env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true",
// Sign-up restriction is handled by the user.create.before database
// hook which checks for pending invitations, allowing invited users
// to register even when public sign-up is disabled.
disableSignUp: false,
sendResetPassword: async (data) => {
await sendEmail(data.user.email, "Reset Password", "RESET_PASSWORD", {
resetPasswordUrl: data.url,
resetPasswordToken: data.token,
});
},
},
socialProviders: configuredProviders,
user: {
deleteUser: {
enabled: true,
},
additionalFields: {
stripeCustomerId: {
type: "string",
required: false,
defaultValue: null,
input: false,
},
},
},
plugins: createPlugins(db),
databaseHooks: createDatabaseHooks(db),
hooks: createMiddlewareHooks(db),
advanced: {
cookiePrefix: "kan",
database: {
generateId: false,
},
},
});
};