Files
todo-app/apps/api/src/routes/auth.ts

211 lines
5.8 KiB
TypeScript

import { Elysia, t } from 'elysia';
import { db } from '../db';
import { accounts, invites, users, projects, verifications } from '../db/schema';
import { eq, and, gt, like } from 'drizzle-orm';
import { auth } from '../lib/auth';
import { hashPassword } from 'better-auth/crypto';
export const authRoutes = new Elysia({ prefix: '/auth' })
// Validate invite token (public)
.get('/invite/:token', async ({ params, set }) => {
const invite = await db.query.invites.findFirst({
where: and(
eq(invites.token, params.token),
eq(invites.status, 'pending'),
gt(invites.expiresAt, new Date())
),
});
if (!invite) {
set.status = 404;
throw new Error('Invalid or expired invite');
}
return {
email: invite.email,
name: invite.name,
};
}, {
params: t.Object({
token: t.String(),
}),
})
// Accept invite and create account (public)
.post('/invite/:token/accept', async ({ params, body, set }) => {
const invite = await db.query.invites.findFirst({
where: and(
eq(invites.token, params.token),
eq(invites.status, 'pending'),
gt(invites.expiresAt, new Date())
),
});
if (!invite) {
set.status = 404;
throw new Error('Invalid or expired invite');
}
// Check if user already exists
const existingUser = await db.query.users.findFirst({
where: eq(users.email, invite.email),
});
if (existingUser) {
set.status = 400;
throw new Error('Account already exists for this email');
}
try {
// Create user via BetterAuth
const signUpResult = await auth.api.signUpEmail({
body: {
email: invite.email,
password: body.password,
name: invite.name,
},
});
if (!signUpResult) {
throw new Error('Failed to create account');
}
// Get the created user
const newUser = await db.query.users.findFirst({
where: eq(users.email, invite.email),
});
if (newUser) {
// Set role from invite if specified
if (invite.role && invite.role !== 'user') {
await db.update(users).set({ role: invite.role }).where(eq(users.id, newUser.id));
}
// Create default inbox project
await db.insert(projects).values({
userId: newUser.id,
name: 'Inbox',
isInbox: true,
color: '#808080',
});
// Create some default projects
await db.insert(projects).values([
{
userId: newUser.id,
name: 'Personal',
color: '#3b82f6',
sortOrder: 1,
},
{
userId: newUser.id,
name: 'Work',
color: '#22c55e',
sortOrder: 2,
},
]);
}
// Mark invite as accepted
await db
.update(invites)
.set({
status: 'accepted',
acceptedAt: new Date(),
})
.where(eq(invites.id, invite.id));
return { success: true, message: 'Account created successfully' };
} catch (error) {
console.error('Error creating account:', error);
set.status = 500;
throw new Error('Failed to create account');
}
}, {
params: t.Object({
token: t.String(),
}),
body: t.Object({
password: t.String({ minLength: 8 }),
}),
})
// Validate password reset token (public)
.get('/reset-password/:token', async ({ params, set }) => {
const verification = await db.query.verifications.findFirst({
where: and(
eq(verifications.value, params.token),
gt(verifications.expiresAt, new Date())
),
});
if (!verification || !verification.identifier.startsWith('password-reset:')) {
set.status = 404;
throw new Error('Invalid or expired reset link');
}
const userId = verification.identifier.replace('password-reset:', '');
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
columns: { name: true },
});
return { valid: true, userName: user?.name || 'User' };
}, {
params: t.Object({
token: t.String(),
}),
})
// Submit password reset (public)
.post('/reset-password/:token', async ({ params, body, set }) => {
const verification = await db.query.verifications.findFirst({
where: and(
eq(verifications.value, params.token),
gt(verifications.expiresAt, new Date())
),
});
if (!verification || !verification.identifier.startsWith('password-reset:')) {
set.status = 404;
throw new Error('Invalid or expired reset link');
}
const userId = verification.identifier.replace('password-reset:', '');
try {
// Hash the new password using BetterAuth's scrypt hasher
const hashedPassword = await hashPassword(body.newPassword);
// Update the credential account's password directly
const [updated] = await db
.update(accounts)
.set({ password: hashedPassword })
.where(and(
eq(accounts.userId, userId),
eq(accounts.providerId, 'credential')
))
.returning();
if (!updated) {
throw new Error('No credential account found for user');
}
} catch (error) {
console.error('Failed to reset password:', error);
set.status = 500;
throw new Error('Failed to reset password');
}
// Delete the used verification token
await db.delete(verifications).where(eq(verifications.id, verification.id));
return { success: true, message: 'Password reset successfully' };
}, {
params: t.Object({
token: t.String(),
}),
body: t.Object({
newPassword: t.String({ minLength: 8 }),
}),
});