Fix sign out redirect, add admin user password reset

- Sign out: skip NextAuth redirect (which resolves against NEXTAUTH_URL)
  and use window.location.href='/' instead — works from any hostname
- Admin users page: add 'Reset PW' button per user that opens a modal
  to set a new password (no current password required)
- Allow COMMISSIONER role to reset user passwords via PATCH /api/users/[id]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Philip
2026-03-12 11:33:02 -07:00
parent e7b7536e70
commit 52bb170d7a
3 changed files with 81 additions and 5 deletions
+75 -1
View File
@@ -24,6 +24,14 @@ export default function UsersPage() {
const [newRole, setNewRole] = useState('PLAYER'); const [newRole, setNewRole] = useState('PLAYER');
const [error, setError] = useState(''); const [error, setError] = useState('');
// Reset password modal
const [resetModal, setResetModal] = useState(false);
const [resetUserId, setResetUserId] = useState('');
const [resetUserName, setResetUserName] = useState('');
const [resetPassword, setResetPassword] = useState('');
const [resetError, setResetError] = useState('');
const [resetLoading, setResetLoading] = useState(false);
const fetchUsers = () => { const fetchUsers = () => {
fetch('/api/users') fetch('/api/users')
.then((res) => res.json()) .then((res) => res.json())
@@ -74,6 +82,40 @@ export default function UsersPage() {
fetchUsers(); fetchUsers();
}; };
const openResetModal = (user: User) => {
setResetUserId(user.id);
setResetUserName(user.name);
setResetPassword('');
setResetError('');
setResetModal(true);
};
const handleResetPassword = async () => {
setResetError('');
if (resetPassword.length < 8) {
setResetError('Password must be at least 8 characters');
return;
}
setResetLoading(true);
try {
const res = await fetch(`/api/users/${resetUserId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: resetPassword }),
});
const data = await res.json();
if (!res.ok) {
setResetError(data.error || 'Failed to reset password');
} else {
setResetModal(false);
}
} catch {
setResetError('Failed to reset password');
} finally {
setResetLoading(false);
}
};
const handleDelete = async (userId: string) => { const handleDelete = async (userId: string) => {
if (!confirm('Are you sure you want to delete this user?')) return; if (!confirm('Are you sure you want to delete this user?')) return;
await fetch(`/api/users/${userId}`, { method: 'DELETE' }); await fetch(`/api/users/${userId}`, { method: 'DELETE' });
@@ -121,7 +163,13 @@ export default function UsersPage() {
<td className="py-2 pr-4 text-gray-400"> <td className="py-2 pr-4 text-gray-400">
{new Date(user.createdAt).toLocaleDateString()} {new Date(user.createdAt).toLocaleDateString()}
</td> </td>
<td className="py-2"> <td className="py-2 flex gap-2">
<button
onClick={() => openResetModal(user)}
className="text-xs bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded"
>
Reset PW
</button>
<button <button
onClick={() => handleDelete(user.id)} onClick={() => handleDelete(user.id)}
className="text-xs bg-red-800 hover:bg-red-700 px-2 py-1 rounded" className="text-xs bg-red-800 hover:bg-red-700 px-2 py-1 rounded"
@@ -135,6 +183,32 @@ export default function UsersPage() {
</table> </table>
</div> </div>
<Modal open={resetModal} onClose={() => setResetModal(false)} title={`Reset Password — ${resetUserName}`}>
<div className="space-y-4">
{resetError && (
<div className="bg-red-900/50 border border-red-700 rounded-lg px-4 py-2 text-sm text-red-300">
{resetError}
</div>
)}
<Input
id="resetPassword"
label="New Password"
type="password"
value={resetPassword}
onChange={(e) => setResetPassword(e.target.value)}
placeholder="Min 8 characters"
/>
<div className="flex gap-3">
<Button variant="secondary" onClick={() => setResetModal(false)} className="flex-1">
Cancel
</Button>
<Button onClick={handleResetPassword} loading={resetLoading} className="flex-1">
Reset Password
</Button>
</div>
</div>
</Modal>
<Modal open={createModal} onClose={() => setCreateModal(false)} title="Create User"> <Modal open={createModal} onClose={() => setCreateModal(false)} title="Create User">
<div className="space-y-4"> <div className="space-y-4">
{error && ( {error && (
+5 -3
View File
@@ -15,10 +15,12 @@ export async function PATCH(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
const isAdmin = (session.user as any).role === 'ADMIN'; const role = (session.user as any).role;
const isAdmin = role === 'ADMIN';
const isCommissioner = role === 'COMMISSIONER';
const isSelf = (session.user as any).id === params.id; const isSelf = (session.user as any).id === params.id;
if (!isAdmin && !isSelf) { if (!isAdmin && !isCommissioner && !isSelf) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
} }
@@ -38,7 +40,7 @@ export async function PATCH(
} }
// Only admins can change roles // Only admins can change roles
if (body.role && isAdmin) { if (body.role && (isAdmin || isCommissioner)) {
updateData.role = body.role; updateData.role = body.role;
} }
+1 -1
View File
@@ -74,7 +74,7 @@ export function GridHeader({ settings }: GridHeaderProps) {
Admin Admin
</Link> </Link>
)} )}
<button onClick={() => signOut({ callbackUrl: window.location.origin + '/' })} className="text-sm text-gray-400 hover:text-white"> <button onClick={async () => { await signOut({ redirect: false }); window.location.href = '/'; }} className="text-sm text-gray-400 hover:text-white">
Sign Out Sign Out
</button> </button>
</> </>