feat: instance invites

This commit is contained in:
Zoe
2026-10-08 13:50:44 -05:00
parent f6e1bc1f47
commit fae05ebc4c
17 changed files with 3962 additions and 26 deletions
+33 -7
View File
@@ -1,6 +1,6 @@
import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2";
import { db } from "../server/lib/db";
import { APIError, createAuthMiddleware, getSessionFromCtx } from 'better-auth/api';
import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from 'better-auth/api';
import { betterAuth } from 'better-auth/minimal';
import { createAccessControl } from 'better-auth/plugins/access';
import { admin } from 'better-auth/plugins/admin';
@@ -8,6 +8,7 @@ import { defaultStatements } from 'better-auth/plugins/admin/access';
import { twoFactor } from 'better-auth/plugins/two-factor';
import * as schema from '../drizzle/schema';
import { getRegistrationMode } from '../server/utils/instance';
import { claimInvite, INVITE_TOKEN_HEADER, releaseInvite } from '../server/utils/invites';
const MIN_PASSWORD_LENGTH = 8;
@@ -51,12 +52,26 @@ export const auth = betterAuth({
hooks: {
before: createAuthMiddleware(async (ctx) => {
// Who may register is an instance setting admins change at runtime.
// Invite links do not exist yet, so only open instances take sign ups.
if (ctx.path === '/sign-up/email' && await getRegistrationMode() !== 'open') {
throw new APIError('FORBIDDEN', {
message: 'Registration is closed on this instance',
code: 'REGISTRATION_CLOSED',
});
if (ctx.path === '/sign-up/email') {
const mode = await getRegistrationMode();
if (mode === 'closed') {
throw new APIError('FORBIDDEN', {
message: 'Registration is closed on this instance',
code: 'REGISTRATION_CLOSED',
});
}
if (mode === 'invite') {
const token = ctx.headers?.get(INVITE_TOKEN_HEADER);
if (!token || !await claimInvite(token)) {
throw new APIError('FORBIDDEN', {
message: 'This invite link is not valid anymore',
code: 'INVITE_INVALID',
});
}
}
}
// creating a user as an admin skips the sign up password rules
@@ -89,6 +104,17 @@ export const auth = betterAuth({
});
}
}),
// The invite was claimed before the account was made. When making it
// then fails, say because the email is taken, the use is given back.
after: createAuthMiddleware(async (ctx) => {
if (ctx.path !== '/sign-up/email' || !isAPIError(ctx.context.returned)) return;
const token = ctx.headers?.get(INVITE_TOKEN_HEADER);
if (token && await getRegistrationMode() === 'invite') {
await releaseInvite(token);
}
}),
},
plugins: [