Fix admin seed, open username/email changes, invite refresh & revocation
- Seed checks for any admin role instead of username='admin' - Username change open to all registered users (was admin-only) - New change-email endpoint requiring password confirmation - Settings page: inline editing for username and email - Invitations: await refresh after generate so list updates visibly - Invitations: revoke button to delete unused invites (admin only) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ export async function seedAdmin() {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, 'admin'))
|
||||
.where(eq(users.role, 'admin'))
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
|
||||
@@ -152,8 +152,8 @@ auth.post('/change-password', authMiddleware, async (c) => {
|
||||
|
||||
auth.post('/change-username', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
if (user.role !== 'admin') {
|
||||
return c.json({ error: 'Forbidden' }, 403);
|
||||
if (!user.email && user.role !== 'admin') {
|
||||
return c.json({ error: 'Must be registered to change username' }, 403);
|
||||
}
|
||||
|
||||
const { username } = await c.req.json<{ username: string }>();
|
||||
@@ -180,4 +180,58 @@ auth.post('/change-username', authMiddleware, async (c) => {
|
||||
return c.json({ success: true, token });
|
||||
});
|
||||
|
||||
auth.post('/change-email', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
|
||||
if (!user.email && user.role !== 'admin') {
|
||||
return c.json({ error: 'Must be registered to change email' }, 403);
|
||||
}
|
||||
|
||||
const { email, currentPassword } = await c.req.json<{
|
||||
email: string;
|
||||
currentPassword: string;
|
||||
}>();
|
||||
|
||||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return c.json({ error: 'Valid email required' }, 400);
|
||||
}
|
||||
|
||||
if (!currentPassword) {
|
||||
return c.json({ error: 'Current password required' }, 400);
|
||||
}
|
||||
|
||||
const [dbUser] = await db
|
||||
.select({ passwordHash: users.passwordHash })
|
||||
.from(users)
|
||||
.where(eq(users.id, user.id))
|
||||
.limit(1);
|
||||
|
||||
if (!dbUser?.passwordHash) {
|
||||
return c.json({ error: 'No password set' }, 400);
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(currentPassword, dbUser.passwordHash);
|
||||
if (!valid) {
|
||||
return c.json({ error: 'Current password is incorrect' }, 401);
|
||||
}
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0 && existing[0].id !== user.id) {
|
||||
return c.json({ error: 'Email already in use' }, 409);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({ email })
|
||||
.where(eq(users.id, user.id));
|
||||
|
||||
const token = await createToken(user.id, email, user.role, user.username, user.mustResetPassword);
|
||||
return c.json({ success: true, token });
|
||||
});
|
||||
|
||||
export { auth };
|
||||
|
||||
@@ -141,4 +141,26 @@ invitesRouter.get('/', authMiddleware, requireAdmin, async (c) => {
|
||||
return c.json({ invitations: enriched });
|
||||
});
|
||||
|
||||
invitesRouter.delete('/:id', authMiddleware, requireAdmin, async (c) => {
|
||||
const inviteId = c.req.param('id');
|
||||
|
||||
const [invite] = await db
|
||||
.select({ id: invitations.id, usedBy: invitations.usedBy })
|
||||
.from(invitations)
|
||||
.where(eq(invitations.id, inviteId))
|
||||
.limit(1);
|
||||
|
||||
if (!invite) {
|
||||
return c.json({ error: 'Invitation not found' }, 404);
|
||||
}
|
||||
|
||||
if (invite.usedBy) {
|
||||
return c.json({ error: 'Cannot revoke a used invitation' }, 400);
|
||||
}
|
||||
|
||||
await db.delete(invitations).where(eq(invitations.id, inviteId));
|
||||
|
||||
return c.json({ deleted: true });
|
||||
});
|
||||
|
||||
export { invitesRouter };
|
||||
|
||||
Reference in New Issue
Block a user