v2.4.2: frontend enhancements, user/get fix

This commit is contained in:
Roman 2024-05-13 20:32:31 +02:00
parent 150e4eb195
commit df4582c7e5
11 changed files with 47 additions and 31 deletions

@ -237,7 +237,7 @@ namespace Core\API\User {
public function __construct(Context $context, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($context, $externalCall, parent::__construct($context, $externalCall,
self::getPaginationParameters(['id', 'name', 'fullName', 'email', 'groups', 'registeredAt', 'active', 'confirmed'], self::getPaginationParameters(['id', 'name', 'fullName', 'email', 'groups', 'lastOnline', 'registeredAt', 'active', 'confirmed'],
'id', 'asc') 'id', 'asc')
); );
} }
@ -316,20 +316,17 @@ namespace Core\API\User {
} else if ($user === null) { } else if ($user === null) {
return $this->createError("User not found"); return $this->createError("User not found");
} else { } else {
// allow access to unconfirmed users only when we have administrative privileges, or we are querying ourselves
$queriedUser = $user->jsonSerialize();
$currentUser = $this->context->getUser(); $currentUser = $this->context->getUser();
// full info only when we have administrative privileges, or we are querying ourselves
$fullInfo = ($userId === $currentUser->getId() || $fullInfo = ($userId === $currentUser->getId() ||
$currentUser->hasGroup(Group::ADMIN) || $currentUser->hasGroup(Group::ADMIN) ||
$currentUser->hasGroup(Group::SUPPORT)); $currentUser->hasGroup(Group::SUPPORT));
if (!$fullInfo && !$queriedUser["confirmed"]) { if (!$fullInfo && !$user->isConfirmed()) {
return $this->createError("No permissions to access this user"); return $this->createError("No permissions to access this user");
} }
$this->result["user"] = $queriedUser; $this->result["user"] = $user->jsonSerialize();
} }
return $this->success; return $this->success;

@ -10,7 +10,7 @@ if (is_file($autoLoad)) {
require_once $autoLoad; require_once $autoLoad;
} }
const WEBBASE_VERSION = "2.4.1"; const WEBBASE_VERSION = "2.4.2";
spl_autoload_extensions(".php"); spl_autoload_extensions(".php");
spl_autoload_register(function ($class) { spl_autoload_register(function ($class) {

@ -88,7 +88,7 @@ export default function AdminDashboard(props) {
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/> <Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
<Route path={"/admin/permissions"} element={<AccessControlList {...controlObj} />}/> <Route path={"/admin/permissions"} element={<AccessControlList {...controlObj} />}/>
<Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/> <Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/>
<Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/> <Route path={"/admin/route/:routeId"} element={<RouteEditView {...controlObj} />}/>
<Route path={"/admin/settings"} element={<SettingsView {...controlObj} />}/> <Route path={"/admin/settings"} element={<SettingsView {...controlObj} />}/>
<Route path={"/admin/profile"} element={<ProfileView {...controlObj} />}/> <Route path={"/admin/profile"} element={<ProfileView {...controlObj} />}/>
<Route path={"*"} element={<View404 />} /> <Route path={"*"} element={<View404 />} />

@ -15,6 +15,6 @@ export default function ProfileLink(props) {
return <Box display={"grid"} sx={newSx} gridTemplateColumns={size + "px auto"} alignItems={"center"} {...other}> return <Box display={"grid"} sx={newSx} gridTemplateColumns={size + "px auto"} alignItems={"center"} {...other}>
<ProfilePicture user={user} size={size} /> <ProfilePicture user={user} size={size} />
{text ? text : (user.fullName || user.name)} {typeof text === "string" ? text : (user.fullName || user.name)}
</Box> </Box>
} }

@ -23,8 +23,7 @@ const DrawerHeader = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 1), padding: theme.spacing(0, 1),
...theme.mixins.toolbar, ...theme.mixins.toolbar,
"& > button": { "& > button": {
display: 'flex', display: "flex",
marginLeft: "auto",
}, },
"& > img": { "& > img": {
width: 30, width: 30,
@ -122,22 +121,25 @@ export default function Sidebar(props) {
onFetchLanguages(); onFetchLanguages();
}, []); }, []);
const menuItems = { const menuItems= {
"dashboard": { "dashboard": {
"name": "admin.dashboard", "name": "admin.dashboard",
"icon": <QueryStats /> "icon": <QueryStats />
}, },
"users": { "users": {
"name": "admin.users", "name": "admin.users",
"icon": <People /> "icon": <People />,
"match": /\/admin\/(users|user\/.*)/
}, },
"groups": { "groups": {
"name": "admin.groups", "name": "admin.groups",
"icon": <Groups /> "icon": <Groups />,
"match": /\/admin\/(groups|group\/.*)/
}, },
"routes": { "routes": {
"name": "admin.page_routes", "name": "admin.page_routes",
"icon": <Route /> "icon": <Route />,
"match": /\/admin\/(routes|route\/.*)/
}, },
"settings": { "settings": {
"name": "admin.settings", "name": "admin.settings",
@ -172,8 +174,15 @@ export default function Sidebar(props) {
let li = []; let li = [];
for (const [id, menuItem] of Object.entries(menuItems)) { for (const [id, menuItem] of Object.entries(menuItems)) {
const match= /^\/admin\/(.*)$/.exec(currentPath);
const active = match?.length >= 2 && match[1] === id; let active;
if (menuItem.hasOwnProperty("match")) {
active = !!menuItem.match.exec(currentPath);
} else {
const match= /^\/admin\/(.*)$/.exec(currentPath);
active = match?.length >= 2 && match[1] === id;
}
li.push(<NavbarItem key={id} {...menuItem} active={active} onClick={() => navigate(`/admin/${id}`)} />); li.push(<NavbarItem key={id} {...menuItem} active={active} onClick={() => navigate(`/admin/${id}`)} />);
} }
@ -188,14 +197,16 @@ export default function Sidebar(props) {
<img src={"/img/icons/logo.png"} alt={"Logo"} /> <img src={"/img/icons/logo.png"} alt={"Logo"} />
<span>WebBase</span> <span>WebBase</span>
</>} </>}
<IconButton onClick={() => setDrawerOpen(!drawerOpen)}> <IconButton sx={{marginLeft: drawerOpen ? "auto" : 0}} onClick={() => setDrawerOpen(!drawerOpen)}>
{drawerOpen ? <ChevronLeftIcon/> : <ChevronRightIcon/>} {drawerOpen ? <ChevronLeftIcon/> : <ChevronRightIcon/>}
</IconButton> </IconButton>
</DrawerHeader> </DrawerHeader>
<Divider/> <Divider/>
<ListItem sx={{display: 'block'}}> <ListItem sx={{display: 'block'}}>
<Box sx={{opacity: drawerOpen ? 1 : 0}}>{L("account.logged_in_as")}:</Box> <Box sx={{opacity: drawerOpen ? 1 : 0}}>{L("account.logged_in_as")}:</Box>
<ProfileLink user={api.user} size={30} sx={{marginTop: 1, gridGap: 16, fontWeight: "bold" }} <ProfileLink text={drawerOpen ? null : ""}
user={api.user} size={30}
sx={{marginTop: 1, gridGap: 16, fontWeight: "bold" }}
onClick={() => navigate("/admin/profile")} /> onClick={() => navigate("/admin/profile")} />
</ListItem> </ListItem>
<Divider/> <Divider/>
@ -216,7 +227,6 @@ export default function Sidebar(props) {
: <ListItemButton sx={{ : <ListItemButton sx={{
minHeight: 48, minHeight: 48,
justifyContent: 'center', justifyContent: 'center',
px: 2.5,
}}> }}>
<Dropdown> <Dropdown>
<ListItemIcon onClick={e => setAnchorEl(e.currentTarget)} sx={{ <ListItemIcon onClick={e => setAnchorEl(e.currentTarget)} sx={{

@ -3,7 +3,7 @@ import {LocaleContext} from "shared/locale";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import usePagination from "shared/hooks/pagination"; import usePagination from "shared/hooks/pagination";
import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table"; import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table";
import {Box, FormControl, FormGroup, FormLabel, Grid, IconButton, MenuItem, TextField} from "@mui/material"; import {Box, FormControl, FormGroup, FormLabel, Grid, IconButton, MenuItem, styled, TextField} from "@mui/material";
import {DateTimePicker} from "@mui/x-date-pickers"; import {DateTimePicker} from "@mui/x-date-pickers";
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
@ -12,12 +12,19 @@ import {format, toDate} from "date-fns";
import {ExpandLess, ExpandMore} from "@mui/icons-material"; import {ExpandLess, ExpandMore} from "@mui/icons-material";
import ViewContent from "../elements/view-content"; import ViewContent from "../elements/view-content";
const StyledLogMessage = styled(Box)(props => ({
alignSelf: "center",
"& pre": {
whiteSpace: "break-spaces"
}
}));
export default function LogView(props) { export default function LogView(props) {
// meta // meta
const api = props.api; const api = props.api;
const showDialog = props.showDialog; const showDialog = props.showDialog;
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext); const {translate: L, requestModules, currentLocale, toDateFns} = useContext(LocaleContext);
const pagination = usePagination(); const pagination = usePagination();
// data // data
@ -89,11 +96,11 @@ export default function LogView(props) {
</IconButton> </IconButton>
} }
</Box> </Box>
<Box alignSelf={"center"}> <StyledLogMessage>
<pre> <pre>
{entry.showDetails ? entry.message : lines[0]} {entry.showDetails ? entry.message : lines[0]}
</pre> </pre>
</Box> </StyledLogMessage>
</Box> </Box>
} }
return column; return column;
@ -130,7 +137,7 @@ export default function LogView(props) {
<FormGroup> <FormGroup>
<FormLabel>{L("logs.timestamp")}</FormLabel> <FormLabel>{L("logs.timestamp")}</FormLabel>
<FormControl> <FormControl>
<LocalizationProvider dateAdapter={AdapterDateFns}> <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={toDateFns()}>
<DateTimePicker label={L("logs.timestamp_placeholder") + "…"} <DateTimePicker label={L("logs.timestamp_placeholder") + "…"}
value={timestamp ? toDate(new Date()) : null} value={timestamp ? toDate(new Date()) : null}
format={L("general.datefns_datetime_format_precise")} format={L("general.datefns_datetime_format_precise")}

@ -88,7 +88,7 @@ export default function RouteEditView(props) {
api.addRoute(...args).then(res => { api.addRoute(...args).then(res => {
setSaving(false); setSaving(false);
if (res.success) { if (res.success) {
navigate("/admin/routes/" + res.routeId); navigate("/admin/route/" + res.routeId);
} else { } else {
showDialog(res.msg, L("routes.save_route_error")); showDialog(res.msg, L("routes.save_route_error"));
} }

@ -131,7 +131,7 @@ export default function RouteListView(props) {
</Button> </Button>
<Button variant={"outlined"} color={"success"} startIcon={<Add />} size={"small"} <Button variant={"outlined"} color={"success"} startIcon={<Add />} size={"small"}
disabled={!props.api.hasPermission("routes/add")} disabled={!props.api.hasPermission("routes/add")}
onClick={() => navigate("/admin/routes/new")} > onClick={() => navigate("/admin/route/new")} >
{L("general.add")} {L("general.add")}
</Button> </Button>
<Button variant={"outlined"} color={"info"} startIcon={<Cached />} size={"small"} <Button variant={"outlined"} color={"info"} startIcon={<Cached />} size={"small"}
@ -173,7 +173,7 @@ export default function RouteListView(props) {
<IconButton size={"small"} title={L("general.edit")} <IconButton size={"small"} title={L("general.edit")}
disabled={!api.hasPermission("routes/add")} disabled={!api.hasPermission("routes/add")}
color={"primary"} color={"primary"}
onClick={() => navigate("/admin/routes/" + id)}> onClick={() => navigate("/admin/route/" + id)}>
<Edit /> <Edit />
</IconButton> </IconButton>
<IconButton size={"small"} title={L("general.delete")} <IconButton size={"small"} title={L("general.delete")}

@ -82,7 +82,8 @@ export default function GpgKeyInput(props) {
}, [showDialog]); }, [showDialog]);
return <StyledGpgKeyInput {...other}> return <StyledGpgKeyInput {...other}>
<IconButton onClick={onOpenDialog}> <IconButton onClick={onOpenDialog}
disabled={!api.hasPermission(isConfigured ? "settings/removeGPG" : "settings/importGPG")}>
{ isConfigured ? <Delete color={"error"} /> : <Upload color={"success"} /> } { isConfigured ? <Delete color={"error"} /> : <Upload color={"success"} /> }
</IconButton> </IconButton>
<VisuallyHiddenInput ref={fileInputRef} type={"file"} onChange={e => { <VisuallyHiddenInput ref={fileInputRef} type={"file"} onChange={e => {

@ -73,6 +73,7 @@ export default function UserListView(props) {
new StringColumn(L("account.email"), "email"), new StringColumn(L("account.email"), "email"),
groupColumn, groupColumn,
new DateTimeColumn(L("account.registered_at"), "registeredAt"), new DateTimeColumn(L("account.registered_at"), "registeredAt"),
new DateTimeColumn(L("account.last_online"), "lastOnline"),
new BoolColumn(L("account.active"), "active", { align: "center" }), new BoolColumn(L("account.active"), "active", { align: "center" }),
new BoolColumn(L("account.confirmed"), "confirmed", { align: "center" }), new BoolColumn(L("account.confirmed"), "confirmed", { align: "center" }),
new ControlsColumn(L("general.controls"), [ new ControlsColumn(L("general.controls"), [

@ -32,7 +32,7 @@
} }
.pagination-controls { .pagination-controls {
margin-top: 6px; margin-top: 12px;
display: grid; display: grid;
grid-template-columns: 75px auto; grid-template-columns: 75px auto;
align-items: center; align-items: center;