feat: admin password reset generates self-service link instead of direct password set
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { users, invites } from '../db/schema';
|
import { users, invites, verifications } from '../db/schema';
|
||||||
import { eq, desc } from 'drizzle-orm';
|
import { eq, desc } from 'drizzle-orm';
|
||||||
import { sendInviteEmail } from '../lib/email';
|
import { sendInviteEmail } from '../lib/email';
|
||||||
import { auth } from '../lib/auth';
|
import { auth } from '../lib/auth';
|
||||||
@@ -83,8 +83,8 @@ export const adminRoutes = new Elysia({ prefix: '/admin' })
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset user password
|
// Generate password reset link
|
||||||
.post('/users/:id/reset-password', async ({ params, body, set }) => {
|
.post('/users/:id/reset-password', async ({ params, set }) => {
|
||||||
const targetUser = await db.query.users.findFirst({
|
const targetUser = await db.query.users.findFirst({
|
||||||
where: eq(users.id, params.id),
|
where: eq(users.id, params.id),
|
||||||
});
|
});
|
||||||
@@ -94,27 +94,24 @@ export const adminRoutes = new Elysia({ prefix: '/admin' })
|
|||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
await auth.api.setPassword({
|
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
|
||||||
body: {
|
|
||||||
userId: params.id,
|
|
||||||
newPassword: body.newPassword,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to set password via auth.api:', error);
|
|
||||||
set.status = 500;
|
|
||||||
throw new Error('Failed to reset password');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: 'Password reset successfully' };
|
await db.insert(verifications).values({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
identifier: `password-reset:${params.id}`,
|
||||||
|
value: token,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appUrl = process.env.APP_URL || 'https://todo.donovankelly.xyz';
|
||||||
|
const resetUrl = `${appUrl}/reset-password?token=${token}`;
|
||||||
|
|
||||||
|
return { success: true, resetUrl };
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
id: t.String(),
|
id: t.String(),
|
||||||
}),
|
}),
|
||||||
body: t.Object({
|
|
||||||
newPassword: t.String({ minLength: 8 }),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Delete user
|
// Delete user
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { invites, users, projects } from '../db/schema';
|
import { invites, users, projects, verifications } from '../db/schema';
|
||||||
import { eq, and, gt } from 'drizzle-orm';
|
import { eq, and, gt, like } from 'drizzle-orm';
|
||||||
import { auth } from '../lib/auth';
|
import { auth } from '../lib/auth';
|
||||||
|
|
||||||
export const authRoutes = new Elysia({ prefix: '/auth' })
|
export const authRoutes = new Elysia({ prefix: '/auth' })
|
||||||
@@ -127,4 +127,73 @@ export const authRoutes = new Elysia({ prefix: '/auth' })
|
|||||||
body: t.Object({
|
body: t.Object({
|
||||||
password: t.String({ minLength: 8 }),
|
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 {
|
||||||
|
await auth.api.setPassword({
|
||||||
|
body: {
|
||||||
|
userId,
|
||||||
|
newPassword: body.newPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set 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 }),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user