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 [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 = () => {
|
||||
fetch('/api/users')
|
||||
.then((res) => res.json())
|
||||
@@ -74,6 +82,40 @@ export default function UsersPage() {
|
||||
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) => {
|
||||
if (!confirm('Are you sure you want to delete this user?')) return;
|
||||
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
|
||||
@@ -121,7 +163,13 @@ export default function UsersPage() {
|
||||
<td className="py-2 pr-4 text-gray-400">
|
||||
{new Date(user.createdAt).toLocaleDateString()}
|
||||
</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
|
||||
onClick={() => handleDelete(user.id)}
|
||||
className="text-xs bg-red-800 hover:bg-red-700 px-2 py-1 rounded"
|
||||
@@ -135,6 +183,32 @@ export default function UsersPage() {
|
||||
</table>
|
||||
</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">
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
|
||||
@@ -15,10 +15,12 @@ export async function PATCH(
|
||||
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;
|
||||
|
||||
if (!isAdmin && !isSelf) {
|
||||
if (!isAdmin && !isCommissioner && !isSelf) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||
}
|
||||
|
||||
@@ -38,7 +40,7 @@ export async function PATCH(
|
||||
}
|
||||
|
||||
// Only admins can change roles
|
||||
if (body.role && isAdmin) {
|
||||
if (body.role && (isAdmin || isCommissioner)) {
|
||||
updateData.role = body.role;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export function GridHeader({ settings }: GridHeaderProps) {
|
||||
Admin
|
||||
</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
|
||||
</button>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user