* 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>
72 lines
2.2 KiB
TypeScript
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,
|
|
},
|
|
},
|
|
});
|
|
};
|