feat(auth): password change (self + admin reset) with rate limiting; dedupe watchlist to user menu
Build & Push Docker Image / build (push) Successful in 2m19s
Build & Push Docker Image / build (push) Successful in 2m19s
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
useCreateUser,
|
||||
useUpdateUser,
|
||||
useDeleteUser,
|
||||
useSetUserPassword,
|
||||
useListAuditLogs,
|
||||
useGetVersion,
|
||||
getListUsersQueryKey,
|
||||
@@ -35,7 +36,8 @@ export default function Admin() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editUser, setEditUser] = useState<{ id: number; username: string; role: string; tier: string } | null>(null);
|
||||
const [editUser, setEditUser] = useState<{ id: number; username: string; role: string; tier: string; authProvider?: string } | null>(null);
|
||||
const [editPassword, setEditPassword] = useState("");
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{ id: number; username: string } | null>(null);
|
||||
|
||||
const [newUsername, setNewUsername] = useState("");
|
||||
@@ -58,6 +60,7 @@ export default function Admin() {
|
||||
const createUser = useCreateUser();
|
||||
const updateUser = useUpdateUser();
|
||||
const deleteUser = useDeleteUser();
|
||||
const setUserPassword = useSetUserPassword();
|
||||
|
||||
if (!authLoading && !isAdmin) {
|
||||
return (
|
||||
@@ -110,6 +113,26 @@ export default function Admin() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSetUserPassword = () => {
|
||||
if (!editUser || !editPassword) return;
|
||||
if (editPassword.length < 8) {
|
||||
toast({ title: "Password too short", description: "Minimum 8 characters.", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
setUserPassword.mutate(
|
||||
{ id: editUser.id, data: { password: editPassword } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({ title: "Password updated", description: `Password for ${editUser.username} has been set.` });
|
||||
setEditPassword("");
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({ title: "Failed to set password", description: (err.data as { error?: string } | null)?.error ?? err.message, variant: "destructive" });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteUser = () => {
|
||||
if (!deleteConfirm) return;
|
||||
deleteUser.mutate(
|
||||
@@ -206,7 +229,7 @@ export default function Admin() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setEditUser({ id: u.id, username: u.username, role: u.role, tier: u.tier ?? "free" })}
|
||||
onClick={() => setEditUser({ id: u.id, username: u.username, role: u.role, tier: u.tier ?? "free", authProvider: u.authProvider })}
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -385,7 +408,7 @@ export default function Admin() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={!!editUser} onOpenChange={(open) => !open && setEditUser(null)}>
|
||||
<Dialog open={!!editUser} onOpenChange={(open) => { if (!open) { setEditUser(null); setEditPassword(""); } }}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit User — {editUser?.username}</DialogTitle>
|
||||
@@ -422,15 +445,41 @@ export default function Admin() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{editUser?.authProvider !== "oidc" ? (
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<Label>Set Password</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={editPassword}
|
||||
onChange={(e) => setEditPassword(e.target.value)}
|
||||
placeholder="min. 8 characters"
|
||||
data-testid="input-set-password"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Resets the user's password immediately.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 border-t pt-4 text-sm text-muted-foreground">
|
||||
Password is managed by the identity provider (Keycloak). Reset it there.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditUser(null)}>Cancel</Button>
|
||||
<DialogFooter className="flex-col sm:flex-row sm:justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => { setEditUser(null); setEditPassword(""); }}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleUpdateUser}
|
||||
disabled={updateUser.isPending}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{editUser?.authProvider !== "oidc" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSetUserPassword}
|
||||
disabled={setUserPassword.isPending || !editPassword}
|
||||
>
|
||||
{setUserPassword.isPending ? "Setting…" : "Set Password"}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user