Add PUT /api/auth/password endpoint for credential users

Implements task 6.2: verifies current password with bcryptjs, rejects
OAuth users (no password_hash), validates new password (8+ chars),
hashes and persists the new password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-20 10:21:27 +01:00
parent c36ab7c146
commit 93f7d20382
2 changed files with 79 additions and 1 deletions

View file

@ -32,7 +32,7 @@
## 6. User Settings API
- [x] 6.1 `[haiku]` Create `PUT /api/auth/profile` endpoint: update user display name
- [ ] 6.2 `[sonnet]` Create `PUT /api/auth/password` endpoint: verify current password, hash new password, update; reject for OAuth users
- [x] 6.2 `[sonnet]` Create `PUT /api/auth/password` endpoint: verify current password, hash new password, update; reject for OAuth users
- [ ] 6.3 `[sonnet]` Create `DELETE /api/auth/account` endpoint: delete all user data (cascade) and user record
## 7. Update Existing API Routes

View file

@ -0,0 +1,78 @@
import { NextRequest, NextResponse } from 'next/server';
import bcryptjs from 'bcryptjs';
import { eq } from 'drizzle-orm';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { getAuthUser } from '@/lib/auth';
export async function PUT(request: NextRequest) {
// Get authenticated user
const user = await getAuthUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Fetch full user record from DB
const [dbUser] = await db
.select({
id: users.id,
password_hash: users.password_hash,
provider: users.provider,
})
.from(users)
.where(eq(users.id, user.id))
.limit(1);
if (!dbUser) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Reject OAuth users (no password_hash)
if (!dbUser.password_hash) {
return NextResponse.json(
{ error: 'Password change is not available for OAuth accounts' },
{ status: 400 }
);
}
// Parse request body
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
const { currentPassword, newPassword } = body as Record<string, unknown>;
// Validate currentPassword
if (!currentPassword || typeof currentPassword !== 'string' || currentPassword === '') {
return NextResponse.json({ error: 'Current password is required' }, { status: 400 });
}
// Validate newPassword
if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {
return NextResponse.json(
{ error: 'New password must be at least 8 characters' },
{ status: 400 }
);
}
// Verify current password
const isValid = await bcryptjs.compare(currentPassword, dbUser.password_hash);
if (!isValid) {
return NextResponse.json({ error: 'Current password is incorrect' }, { status: 400 });
}
// Hash new password and update in DB
const newPasswordHash = await bcryptjs.hash(newPassword, 12);
await db
.update(users)
.set({
password_hash: newPasswordHash,
updated_at: new Date(),
})
.where(eq(users.id, dbUser.id));
return NextResponse.json({ message: 'Password updated successfully' }, { status: 200 });
}