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()), }), });