feat: users can change their own password via user portal
This commit is contained in:
@@ -1789,7 +1789,6 @@ function UsersSection({ onToast }: { onToast: (msg: string, type: string) => voi
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<I label="First Name" value={editing.first_name || ''} onChange={(v: string) => setEditing({ ...editing, first_name: v })} />
|
||||
<I label="Last Name" value={editing.last_name || ''} onChange={(v: string) => setEditing({ ...editing, last_name: v })} />
|
||||
<I label="New Password (leave blank to keep current)" type="password" value={editing.password || ''} onChange={(v: string) => setEditing({ ...editing, password: v })} placeholder="Enter new password to change" />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input type="checkbox" checked={editing.comments_disabled || false} onChange={(e) => setEditing({ ...editing, comments_disabled: e.target.checked })} />
|
||||
<label style={{ fontSize: 13, color: C.text }}>Disable comments</label>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -215,6 +215,11 @@ export default function UserPage() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Change Password */}
|
||||
<Section title="Change Password">
|
||||
<ChangePassword />
|
||||
</Section>
|
||||
|
||||
{/* Chat with Admins */}
|
||||
<Section title="Message Staff">
|
||||
<div style={{ background: cream, borderRadius: '16px', overflow: 'hidden', boxShadow: '0 2px 8px rgba(1,13,30,0.08)' }}>
|
||||
@@ -268,4 +273,100 @@ function Section({ title, children, highlight }: { title: string; children: Reac
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} style={{ background: cream, padding: '1.5rem', borderRadius: '16px', boxShadow: '0 2px 8px rgba(1,13,30,0.08)', maxWidth: '400px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '13px', fontWeight: 600, color: navy, display: 'block', marginBottom: '0.5rem' }}>Current Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
required
|
||||
style={{ width: '100%', padding: '0.75rem 1rem', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '13px', fontWeight: 600, color: navy, display: 'block', marginBottom: '0.5rem' }}>New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
style={{ width: '100%', padding: '0.75rem 1rem', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '13px', fontWeight: 600, color: navy, display: 'block', marginBottom: '0.5rem' }}>Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
style={{ width: '100%', padding: '0.75rem 1rem', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
{message && (
|
||||
<div style={{ padding: '0.75rem', borderRadius: '8px', background: message.type === 'success' ? '#d4edda' : '#f8d7da', color: message.type === 'success' ? '#155724' : '#721c24', fontSize: '14px' }}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving || !currentPassword || !newPassword || !confirmPassword}
|
||||
style={{ padding: '0.75rem 1.5rem', background: gold, color: '#fff', border: 'none', borderRadius: '8px', fontWeight: 600, cursor: saving ? 'wait' : 'pointer', opacity: saving ? 0.7 : 1 }}
|
||||
>
|
||||
{saving ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user