First Commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Box, Button, Paper, TextField, Typography } from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { getAuthToken } from "../auth";
|
||||
import { useToast } from "../toast";
|
||||
|
||||
type UserDto = {
|
||||
id: string;
|
||||
email?: string | null;
|
||||
userName?: string | null;
|
||||
emailConfirmed: boolean;
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export default function UserManagementCard() {
|
||||
const { toast } = useToast();
|
||||
const token = getAuthToken();
|
||||
|
||||
const [supported, setSupported] = useState<boolean | null>(null);
|
||||
const [users, setUsers] = useState<UserDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newIsAdmin, setNewIsAdmin] = useState(false);
|
||||
|
||||
const canRender = useMemo(() => Boolean(token), [token]);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.get<UserDto[]>("/users");
|
||||
setUsers(res.data ?? []);
|
||||
setSupported(true);
|
||||
} catch (e: any) {
|
||||
const status = e?.response?.status;
|
||||
if (status === 401 || status === 403) {
|
||||
setSupported(false);
|
||||
} else {
|
||||
toast("Failed to load users.", "error");
|
||||
setSupported(false);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!canRender) {
|
||||
setSupported(null);
|
||||
setUsers([]);
|
||||
return;
|
||||
}
|
||||
void load();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [canRender]);
|
||||
|
||||
if (!canRender) return null;
|
||||
if (supported === false) return null;
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
User management
|
||||
</Typography>
|
||||
<Typography sx={{ color: "text.secondary", mb: 2 }}>
|
||||
Admin-only.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.5 }}>
|
||||
<TextField
|
||||
label="Email"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
type="password"
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1, mt: 1.5 }}>
|
||||
<Button
|
||||
variant={newIsAdmin ? "contained" : "outlined"}
|
||||
onClick={() => setNewIsAdmin((v) => !v)}
|
||||
>
|
||||
{newIsAdmin ? "Admin: Yes" : "Admin: No"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await api.post("/users", {
|
||||
email: newEmail,
|
||||
password: newPassword,
|
||||
roles: newIsAdmin ? ["Admin"] : [],
|
||||
});
|
||||
setNewEmail("");
|
||||
setNewPassword("");
|
||||
setNewIsAdmin(false);
|
||||
toast("User created.", "success");
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data || e?.message || "Failed to create user.";
|
||||
toast(String(msg), "error");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create user
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 2, display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{users.length === 0 ? (
|
||||
<Typography sx={{ color: "text.secondary" }}>No users.</Typography>
|
||||
) : (
|
||||
users.map((u) => (
|
||||
<Box
|
||||
key={u.id}
|
||||
sx={{
|
||||
border: "1px solid rgba(15,23,42,0.08)",
|
||||
borderRadius: 2,
|
||||
p: 1.25,
|
||||
background: "rgba(255,255,255,0.6)",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 900, lineHeight: 1.2 }}>
|
||||
{u.email || u.userName || u.id}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
||||
Roles: {u.roles?.length ? u.roles.join(", ") : "—"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={async () => {
|
||||
if (!window.confirm("Delete this user?")) return;
|
||||
try {
|
||||
await api.delete(`/users/${u.id}`);
|
||||
toast("User deleted.", "info");
|
||||
await load();
|
||||
} catch {
|
||||
toast("Failed to delete user.", "error");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await api.post(`/users/${u.id}/send-password-reset`);
|
||||
toast("Password reset email sent.", "success");
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data || e?.message || "Failed to send reset email.";
|
||||
toast(String(msg), "error");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send reset
|
||||
</Button>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user