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.
174 lines
5.1 KiB
TypeScript
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()),
|
|
}),
|
|
});
|