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:
@@ -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 && (
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user