diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 40cfa2c..0145430 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1789,7 +1789,6 @@ function UsersSection({ onToast }: { onToast: (msg: string, type: string) => voi
setEditing({ ...editing, first_name: v })} /> setEditing({ ...editing, last_name: v })} /> - setEditing({ ...editing, password: v })} placeholder="Enter new password to change" />
setEditing({ ...editing, comments_disabled: e.target.checked })} /> diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 47da683..3b2cc0c 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -37,21 +37,7 @@ export async function PUT(request: NextRequest) { try { await requireRole('admin'); const body = await request.json(); - const { id, first_name, last_name, comments_disabled, password } = body; - - // If password provided, hash and update it too - if (password) { - const { hashPassword } = await import('@/lib/auth'); - const hash = await hashPassword(password); - const { rows } = await db.query( - 'UPDATE users SET first_name = $1, last_name = $2, comments_disabled = $3, password_hash = $4 WHERE id = $5 RETURNING id, username, role, first_name, last_name, comments_disabled, created_at', - [first_name || null, last_name || null, comments_disabled === true, hash, id] - ); - if (rows.length === 0) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }); - } - return NextResponse.json({ data: rows[0] }); - } + const { id, first_name, last_name, comments_disabled } = body; const { rows } = await db.query( 'UPDATE users SET first_name = $1, last_name = $2, comments_disabled = $3 WHERE id = $4 RETURNING id, username, role, first_name, last_name, comments_disabled, created_at', diff --git a/src/app/api/user/password/route.ts b/src/app/api/user/password/route.ts new file mode 100644 index 0000000..defa2ef --- /dev/null +++ b/src/app/api/user/password/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAuth, hashPassword } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function POST(request: NextRequest) { + try { + const session = await requireAuth(); + const body = await request.json(); + const { current_password, new_password } = body; + + if (!current_password || !new_password) { + return NextResponse.json({ error: 'Current and new password are required' }, { status: 400 }); + } + + if (new_password.length < 4) { + return NextResponse.json({ error: 'Password must be at least 4 characters' }, { status: 400 }); + } + + // Verify current password + const { rows } = await db.query('SELECT password_hash FROM users WHERE id = $1', [session.id]); + if (rows.length === 0) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const { verifyPassword } = await import('@/lib/auth'); + const valid = await verifyPassword(current_password, rows[0].password_hash); + if (!valid) { + return NextResponse.json({ error: 'Current password is incorrect' }, { status: 401 }); + } + + // Update to new password + const hash = await hashPassword(new_password); + await db.query('UPDATE users SET password_hash = $1 WHERE id = $2', [hash, session.id]); + + return NextResponse.json({ ok: true }); + } catch (err: any) { + return NextResponse.json({ error: err.message || 'Failed to change password' }, { status: err.message === 'Unauthorized' ? 401 : 500 }); + } +} \ No newline at end of file diff --git a/src/app/user/page.tsx b/src/app/user/page.tsx index c0bd753..cab07c1 100644 --- a/src/app/user/page.tsx +++ b/src/app/user/page.tsx @@ -215,6 +215,11 @@ export default function UserPage() {
+ {/* Change Password */} +
+ +
+ {/* Chat with Admins */}
@@ -268,4 +273,100 @@ function Section({ title, children, highlight }: { title: string; children: Reac {children}
); +} + +function ChangePassword() { + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setMessage(null); + + if (newPassword.length < 4) { + setMessage({ text: 'Password must be at least 4 characters', type: 'error' }); + return; + } + + if (newPassword !== confirmPassword) { + setMessage({ text: 'Passwords do not match', type: 'error' }); + return; + } + + setSaving(true); + try { + const res = await fetch('/api/user/password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }), + }); + + if (res.ok) { + setMessage({ text: 'Password changed successfully!', type: 'success' }); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + const err = await res.json(); + setMessage({ text: err.error || 'Failed to change password', type: 'error' }); + } + } catch { + setMessage({ text: 'Error changing password', type: 'error' }); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+ + setCurrentPassword(e.target.value)} + required + style={{ width: '100%', padding: '0.75rem 1rem', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none' }} + /> +
+
+ + setNewPassword(e.target.value)} + required + style={{ width: '100%', padding: '0.75rem 1rem', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none' }} + /> +
+
+ + setConfirmPassword(e.target.value)} + required + style={{ width: '100%', padding: '0.75rem 1rem', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none' }} + /> +
+ {message && ( +
+ {message.text} +
+ )} + +
+
+ ); } \ No newline at end of file