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:
parent
c36ab7c146
commit
93f7d20382
2 changed files with 79 additions and 1 deletions
78
src/app/api/auth/password/route.ts
Normal file
78
src/app/api/auth/password/route.ts
Normal 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 });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue