184 lines
5.9 KiB
TypeScript
184 lines
5.9 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
|
|
import { Box, Button, Paper, TextField, Typography } from "@mui/material";
|
|
|
|
import { api } from "../api";
|
|
import { useToast } from "../toast";
|
|
import { useDialogActions } from "../dialogs";
|
|
import { useI18n } from "../i18n/I18nProvider";
|
|
|
|
type UserDto = {
|
|
id: string;
|
|
email?: string | null;
|
|
userName?: string | null;
|
|
emailConfirmed: boolean;
|
|
roles: string[];
|
|
};
|
|
|
|
export default function UserManagementCard() {
|
|
const { toast } = useToast();
|
|
const { confirmAction } = useDialogActions();
|
|
const { t } = useI18n();
|
|
|
|
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);
|
|
|
|
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(t("adminUsersLoadFailed"), "error");
|
|
setSupported(false);
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void load();
|
|
const onAuthChanged = () => { void load(); };
|
|
window.addEventListener("auth-changed", onAuthChanged);
|
|
return () => window.removeEventListener("auth-changed", onAuthChanged);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
if (supported === false) return null;
|
|
if (supported === null) return null;
|
|
|
|
return (
|
|
<Paper sx={{ mt: 2, p: 2 }}>
|
|
<Typography variant="h6" sx={{ mb: 1 }}>
|
|
{t("adminUsersTitle")}
|
|
</Typography>
|
|
<Typography sx={{ color: "text.secondary", mb: 2 }}>
|
|
{t("adminUsersSubtitle")}
|
|
</Typography>
|
|
|
|
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.5 }}>
|
|
<TextField
|
|
label={t("profileEmail")}
|
|
value={newEmail}
|
|
onChange={(e) => setNewEmail(e.target.value)}
|
|
fullWidth
|
|
/>
|
|
<TextField
|
|
label={t("adminUsersPassword")}
|
|
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 ? t("adminUsersAdminYes") : t("adminUsersAdminNo")}
|
|
</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(t("adminUsersCreated"), "success");
|
|
await load();
|
|
} catch (e: any) {
|
|
const msg = e?.response?.data || e?.message || t("adminUsersCreateFailed");
|
|
toast(String(msg), "error");
|
|
}
|
|
}}
|
|
>
|
|
{t("adminUsersCreateUser")}
|
|
</Button>
|
|
</Box>
|
|
|
|
<Box sx={{ mt: 2, display: "flex", flexDirection: "column", gap: 1 }}>
|
|
{users.length === 0 ? (
|
|
<Typography sx={{ color: "text.secondary" }}>{t("adminUsersNoUsers")}</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.userName || u.email || u.id}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
|
{t("adminUsersRolesLabel")}: {u.roles?.length ? u.roles.join(", ") : "—"}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Button
|
|
variant="outlined"
|
|
color="error"
|
|
onClick={async () => {
|
|
if (!(await confirmAction(t("adminUsersDeleteConfirmBody"), { title: t("adminUsersDeleteConfirmTitle"), confirmLabel: t("adminUsersDelete"), destructive: true }))) return;
|
|
try {
|
|
await api.delete(`/users/${u.id}`);
|
|
toast(t("adminUsersDeleted"), "info");
|
|
await load();
|
|
} catch {
|
|
toast(t("adminUsersDeleteFailed"), "error");
|
|
}
|
|
}}
|
|
>
|
|
{t("adminUsersDelete")}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outlined"
|
|
onClick={async () => {
|
|
try {
|
|
await api.post(`/users/${u.id}/send-password-reset`);
|
|
toast(t("adminUsersResetSent"), "success");
|
|
} catch (e: any) {
|
|
const msg = e?.response?.data || e?.message || t("adminUsersResetFailed");
|
|
toast(String(msg), "error");
|
|
}
|
|
}}
|
|
>
|
|
{t("adminUsersSendReset")}
|
|
</Button>
|
|
</Box>
|
|
))
|
|
)}
|
|
</Box>
|
|
</Paper>
|
|
);
|
|
}
|