First Commit

This commit is contained in:
cesnimda
2026-03-21 11:55:27 +01:00
commit 2e8a29b4d0
1757 changed files with 166084 additions and 0 deletions
@@ -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>
);
}