Files
network-app-api/src/routes/invite.ts
Hammer 11ee9b946f fix: invite accept bypasses disableSignUp by falling back to internal adapter
signUpEmail throws when disableSignUp is true. Now catches that error
and creates the user directly via Better Auth's internal adapter:
createUser + linkAccount with hashed password.
2026-01-28 22:17:08 +00:00

174 lines
5.1 KiB
TypeScript

import { Elysia, t } from 'elysia';
import { db } from '../db';
import { invites, users } from '../db/schema';
import { eq, and } from 'drizzle-orm';
import { auth } from '../lib/auth';
export const inviteRoutes = new Elysia({ prefix: '/auth/invite' })
// Validate invite token (public - no auth required)
.get('/:token', async ({ params, set }: { params: { token: string }; set: any }) => {
const [invite] = await db.select()
.from(invites)
.where(and(eq(invites.token, params.token), eq(invites.status, 'pending')))
.limit(1);
if (!invite) {
set.status = 404;
throw new Error('Invite not found or already used');
}
if (new Date() > invite.expiresAt) {
await db.update(invites)
.set({ status: 'expired' })
.where(eq(invites.id, invite.id));
set.status = 410;
throw new Error('Invite has expired');
}
return {
id: invite.id,
email: invite.email,
name: invite.name,
role: invite.role,
expiresAt: invite.expiresAt,
};
}, {
params: t.Object({ token: t.String() }),
})
// Accept invite (public - no auth required)
// Uses Better Auth's internal API to create the user properly
.post('/:token/accept', async ({ params, body, set }: {
params: { token: string };
body: { password: string; name?: string };
set: any;
}) => {
const [invite] = await db.select()
.from(invites)
.where(and(eq(invites.token, params.token), eq(invites.status, 'pending')))
.limit(1);
if (!invite) {
set.status = 404;
throw new Error('Invite not found or already used');
}
if (new Date() > invite.expiresAt) {
await db.update(invites)
.set({ status: 'expired' })
.where(eq(invites.id, invite.id));
set.status = 410;
throw new Error('Invite has expired');
}
// Check if user with this email already exists
const [existing] = await db.select({ id: users.id })
.from(users)
.where(eq(users.email, invite.email))
.limit(1);
if (existing) {
set.status = 409;
throw new Error('An account with this email already exists');
}
try {
// Create user via internal adapter (bypasses disableSignUp restriction)
const userName = body.name || invite.name;
const createdUser = await auth.api.signUpEmail({
body: {
email: invite.email,
password: body.password,
name: userName,
},
headers: new Headers(),
// Use asResponse: false to get direct result
});
// Set the role from the invite
if (invite.role && invite.role !== 'user') {
await db.update(users)
.set({ role: invite.role })
.where(eq(users.email, invite.email));
}
// Mark invite as accepted
await db.update(invites)
.set({ status: 'accepted' })
.where(eq(invites.id, invite.id));
return {
success: true,
user: {
id: createdUser.user?.id,
email: invite.email,
name: userName,
role: invite.role,
},
};
} catch (error: any) {
console.error('Invite accept error:', error);
// If signUpEmail fails due to disableSignUp, use direct DB approach
if (error.message?.includes('not enabled') || error.status === 400) {
try {
const userName = body.name || invite.name;
// Hash password using Better Auth's context
const ctx = await (auth as any).$context;
const hash = await ctx.password.hash(body.password);
// Create user directly
const newUser = await ctx.internalAdapter.createUser({
email: invite.email.toLowerCase(),
name: userName,
emailVerified: false,
});
// Link credential account with hashed password
await ctx.internalAdapter.linkAccount({
userId: newUser.id,
providerId: 'credential',
accountId: newUser.id,
password: hash,
});
// Set the role from the invite
if (invite.role) {
await db.update(users)
.set({ role: invite.role })
.where(eq(users.id, newUser.id));
}
// Mark invite as accepted
await db.update(invites)
.set({ status: 'accepted' })
.where(eq(invites.id, invite.id));
return {
success: true,
user: {
id: newUser.id,
email: invite.email,
name: userName,
role: invite.role,
},
};
} catch (innerError: any) {
console.error('Direct user creation also failed:', innerError);
set.status = 400;
throw new Error(innerError.message || 'Failed to create account');
}
}
set.status = 400;
throw new Error(error.message || 'Failed to create account');
}
}, {
params: t.Object({ token: t.String() }),
body: t.Object({
password: t.String({ minLength: 8 }),
name: t.Optional(t.String()),
}),
});