211 lines
5.8 KiB
TypeScript
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 }),
|
|
}),
|
|
});
|