[release] v0.22.0-unstable03

This commit is contained in:
Yann Stepienik
2026-03-16 17:05:20 +00:00
parent 103e41c5a4
commit daeef5d7e4
141 changed files with 2516 additions and 1040 deletions
+6
View File
@@ -1,6 +1,12 @@
## Version 0.22
- Adding feature to create API key to directly query the Cosmos API
- Refactor the entire permission system for future work on roles and fine-grained permissions
- You can now create multiple admin users
- Fixed issue with post-install on compose import
- Support for mounting network disk
- Fix regression with static/spa folders
- Fix issue with Constellation sometime not connecting right away on new network
- Sync more settings accross servers in Constellations (Rclone and api token/permissions configs)
## Version 0.21.7
- Fix issue with Constellation routing HTTP tunnels to the wrong protocol
+36
View File
@@ -0,0 +1,36 @@
import wrap from './wrap';
function list() {
return wrap(fetch('/cosmos/api/api-tokens', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function create(request) {
return wrap(fetch('/cosmos/api/api-tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
}))
}
function remove(name) {
return wrap(fetch('/cosmos/api/api-tokens', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name }),
}))
}
export {
list,
create,
remove,
};
+2 -2
View File
@@ -183,12 +183,12 @@ function createVolume(values) {
}))
}
function attachTerminal(containerId) {
function attachTerminal(containerId, readonly) {
let protocol = 'ws://';
if (window.location.protocol === 'https:') {
protocol = 'wss://';
}
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/attach');
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/attach' + (readonly ? '?readonly=true' : ''));
}
function createTerminal(containerId) {
+3
View File
@@ -9,6 +9,7 @@ import * as _storage from './storage';
import * as _cron from './cron';
import * as _rclone from './rclone';
import * as _backups from './backup';
import * as _apiTokens from './apiTokens';
import * as authDemo from './authentication.demo';
import * as usersDemo from './users.demo';
@@ -255,6 +256,7 @@ let storage = _storage;
let cron = _cron;
let rclone = _rclone;
let backups = _backups;
let apiTokens = _apiTokens;
if(isDemo) {
auth = authDemo;
@@ -298,4 +300,5 @@ export {
restartServer,
forceAutoUpdate,
backups,
apiTokens,
};
+11
View File
@@ -43,6 +43,16 @@ function list() {
}))
}
function listRemotes() {
return wrapRClone(fetch('/cosmos/rclone/config/listremotes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
}))
}
// New function to update a provider
function update(data) {
return wrapRClone(fetch('/cosmos/rclone/config/update', {
@@ -115,6 +125,7 @@ function restart() {
export {
create,
list,
listRemotes,
deleteRemote,
update,
coreStats,
+26
View File
@@ -0,0 +1,26 @@
import React from "react";
import { Tooltip } from "@mui/material";
import { useClientInfos } from "../utils/hooks";
import { useTranslation } from "react-i18next";
const PermissionGuard = ({ children, permission, alwaysShow }) => {
const { t } = useTranslation();
const { hasPermission, hasRolePermission } = useClientInfos();
const hasPerm = hasPermission(permission);
const hasRole = hasRolePermission(permission);
if (!hasRole && !alwaysShow) return null;
if (hasPerm) return children;
return (
<Tooltip title={t('sudo.required')}>
<span style={{ cursor: 'not-allowed' }}>
<span style={{ pointerEvents: 'none', filter: 'grayscale(0.5)', opacity: 0.65 }}>
{React.cloneElement(children, { onClick: undefined, onChange: undefined })}
</span>
</span>
</Tooltip>
);
};
export default PermissionGuard;
+3 -1
View File
@@ -6,6 +6,8 @@ import { Alert, Input, Stack, useMediaQuery, useTheme } from '@mui/material';
import { ApiOutlined, SendOutlined } from '@ant-design/icons';
import ResponsiveButton from './responseiveButton';
import { useTranslation } from 'react-i18next';
import PermissionGuard from './permissionGuard';
import { PERM_RESOURCES } from '../utils/permissions';
import { Terminal } from '@xterm/xterm'
import '@xterm/xterm/css/xterm.css'
@@ -227,7 +229,7 @@ const TerminalComponent = ({refresh, connectButtons}) => {
) :
<>
{connectButtons && connectButtons.map((button, index) => (
<ResponsiveButton key={index} variant="contained" onClick={() => button.onClick(connect)}>{button.label}</ResponsiveButton>
<PermissionGuard key={index} permission={PERM_RESOURCES}><ResponsiveButton variant="contained" onClick={() => button.onClick(connect)}>{button.label}</ResponsiveButton></PermissionGuard>
))}
</>
}
@@ -20,8 +20,7 @@ const NavItem = ({ item, level }) => {
const dispatch = useDispatch();
const menu = useSelector((state) => state.menu);
const { drawerOpen, openItem } = menu;
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
// active menu item on page load
useEffect(() => {
@@ -35,7 +34,7 @@ const NavItem = ({ item, level }) => {
// eslint-disable-next-line
}, []);
if (item.adminOnly && !isAdmin) {
if (item.permission !== undefined && !hasPermission(item.permission)) {
return null;
}
@@ -32,6 +32,8 @@ import { BellOutlined, ClockCircleOutlined, CloseOutlined, CloseSquareOutlined,
import * as API from '../../../../api';
import { redirectToLocal } from '../../../../utils/indexs';
import { useClientInfos } from '../../../../utils/hooks';
import { PERM_RESOURCES, PERM_RESOURCES_READ } from '../../../../utils/permissions';
import PermissionGuard from '../../../../components/permissionGuard';
// sx styles
const avatarSX = {
@@ -60,8 +62,8 @@ const getStatus = (job) => {
const Jobs = () => {
const { t, i18n } = useTranslation();
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES_READ);
const theme = useTheme();
const matchesXs = useMediaQuery(theme.breakpoints.down('md'));
const [jobs, setJobs] = useState([]);
@@ -238,20 +240,20 @@ const Jobs = () => {
/>
<ListItemSecondaryAction>
{(job.Running) ? <IconButton disabled={!job.Cancellable} variant="outlined" color="error" onClick={() => {
{(job.Running) ? <PermissionGuard permission={PERM_RESOURCES}><IconButton disabled={!job.Cancellable} variant="outlined" color="error" onClick={() => {
API.cron.stop(job.Scheduler, job.Name).then(() => {
refreshJobs();
});
}}>
<CloseSquareOutlined />
</IconButton> : ''}
{(!job.Running) ? <IconButton variant="outlined" color="primary" onClick={() => {
</IconButton></PermissionGuard> : ''}
{(!job.Running) ? <PermissionGuard permission={PERM_RESOURCES}><IconButton variant="outlined" color="primary" onClick={() => {
API.cron.run(job.Scheduler, job.Name).then(() => {
refreshJobs();
});
}}>
<PlaySquareOutlined />
</IconButton> : ''}
</IconButton></PermissionGuard> : ''}
</ListItemSecondaryAction>
</ListItemButton>
<ListItemButton
@@ -7,36 +7,37 @@ import React from "react";
import RestartModal from "../../../../pages/config/users/restart";
import { ConfirmModalDirect } from "../../../../components/confirmModal";
import { useClientInfos } from "../../../../utils/hooks";
import { PERM_ADMIN } from "../../../../utils/permissions";
import PermissionGuard from "../../../../components/permissionGuard";
const RestartMenu = () => {
const { t } = useTranslation();
const [openResartModal, setOpenRestartModal] = React.useState(false);
const [openRestartServerModal, setOpenRestartServerModal] = React.useState(false);
const [status, setStatus] = React.useState({});
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
React.useEffect(() => {
API.getStatus().then((res) => {
setStatus(res.data);
});
}, []);
const restartServer = API.restartServer;
return <PermissionGuard permission={PERM_ADMIN}>
<>
<RestartModal openModal={openResartModal} setOpenModal={setOpenRestartModal} />
<RestartModal openModal={openRestartServerModal} setOpenModal={setOpenRestartModal} isHostMachine/>
return isAdmin ? <>
<RestartModal openModal={openResartModal} setOpenModal={setOpenRestartModal} />
<RestartModal openModal={openRestartServerModal} setOpenModal={setOpenRestartModal} isHostMachine/>
<MenuButton size="medium" icon={<PoweroffOutlined />}>
<MenuItem disabled={status.containerized} onClick={() => setOpenRestartServerModal(true)}>
<ListItemText>{t('global.restartServer')}</ListItemText>
</MenuItem>
<MenuItem disabled={false} onClick={() => setOpenRestartModal(true)}>
<ListItemText>{t('global.restartCosmos')}</ListItemText>
</MenuItem>
</MenuButton>
</> : <></>;
<MenuButton size="medium" icon={<PoweroffOutlined />}>
<MenuItem disabled={status.containerized} onClick={() => setOpenRestartServerModal(true)}>
<ListItemText>{t('global.restartServer')}</ListItemText>
</MenuItem>
<MenuItem disabled={false} onClick={() => setOpenRestartModal(true)}>
<ListItemText>{t('global.restartCosmos')}</ListItemText>
</MenuItem>
</MenuButton>
</>
</PermissionGuard>;
};
export default RestartMenu;
@@ -18,8 +18,7 @@ import { useTranslation } from 'react-i18next';
const SudoModal = () => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const {userRole, role} = useClientInfos();
const canSudo = role !== "2" && userRole === "2";
const { needsSudo } = useClientInfos();
const {t} = useTranslation();
const formik = useFormik({
@@ -50,7 +49,7 @@ const SudoModal = () => {
};
return (
canSudo ?
needsSudo ?
<>
<Chip
label="Admin"
@@ -6,13 +6,14 @@ import TerminalComponent from "../../../../components/terminal";
import * as API from '../../../../api';
import { useTranslation } from "react-i18next";
import { useClientInfos } from "../../../../utils/hooks";
import { PERM_ADMIN } from "../../../../utils/permissions";
import PermissionGuard from "../../../../components/permissionGuard";
const TerminalHeader = () => {
const [open, setOpen] = React.useState(false);
const [status, setStatus] = React.useState(false);
const { t, Trans } = useTranslation();
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
useEffect(() => {
API.getStatus().then((res) => {
@@ -24,7 +25,8 @@ const TerminalHeader = () => {
setOpen((prevOpen) => !prevOpen);
};
return isAdmin ? (<>
return <PermissionGuard permission={PERM_ADMIN}>
<>
<Tooltip title={t('mgmt.servapps.containers.terminal.enabled')}>
<IconButton
disableRipple
@@ -37,7 +39,7 @@ const TerminalHeader = () => {
<BorderOuterOutlined />
</IconButton>
</Tooltip>
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="md" fullWidth={true}>
<DialogTitle>{t('mgmt.servapps.containers.terminal.enabled')}</DialogTitle>
{(!status || !status.containerized) ? <DialogContentText>
@@ -62,7 +64,8 @@ const TerminalHeader = () => {
}}>Close</Button>
</DialogActions>
</Dialog>
</>) : null
</>
</PermissionGuard>
}
export default TerminalHeader;
+2 -1
View File
@@ -1,5 +1,6 @@
// assets
import { HomeOutlined, AppstoreOutlined, DashboardOutlined, AppstoreAddOutlined, CheckOutlined } from '@ant-design/icons';
import { PERM_ADMIN_READ } from '../utils/permissions';
import { isDomain } from '../utils/indexs';
// icons
@@ -31,7 +32,7 @@ const dashboard = {
url: '/cosmos-ui/monitoring',
icon: DashboardOutlined,
breadcrumbs: false,
adminOnly: true
permission: PERM_ADMIN_READ
},
{
id: 'market',
+7 -6
View File
@@ -1,5 +1,6 @@
// assets
import { ProfileOutlined, FolderOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined, ClockCircleOutlined, CloudServerOutlined} from '@ant-design/icons';
import { PERM_RESOURCES_READ, PERM_USERS_READ, PERM_CONFIGURATION_READ } from '../utils/permissions';
import ConstellationIcon from '../assets/images/icons/constellation.png';
import ConstellationWhiteIcon from '../assets/images/icons/constellation_white.png';
import { DarkModeSwitch } from '../utils/indexs';
@@ -24,7 +25,7 @@ const pages = {
type: 'item',
url: '/cosmos-ui/servapps',
icon: AppstoreOutlined,
adminOnly: true
permission: PERM_RESOURCES_READ
},
{
id: 'backups',
@@ -32,7 +33,7 @@ const pages = {
type: 'item',
url: '/cosmos-ui/backups',
icon: CloudServerOutlined,
adminOnly: true
permission: PERM_RESOURCES_READ
},
{
id: 'url',
@@ -47,7 +48,7 @@ const pages = {
type: 'item',
url: '/cosmos-ui/storage',
icon: icons.FolderOutlined,
adminOnly: true
permission: PERM_RESOURCES_READ
},
{
id: 'constellation',
@@ -66,7 +67,7 @@ const pages = {
type: 'item',
url: '/cosmos-ui/config-users',
icon: icons.ProfileOutlined,
adminOnly: true
permission: PERM_USERS_READ
},
{
id: 'openid',
@@ -74,7 +75,7 @@ const pages = {
type: 'item',
url: '/cosmos-ui/openid-manage',
icon: PicLeftOutlined,
adminOnly: true
permission: PERM_CONFIGURATION_READ
},
{
id: 'cron',
@@ -82,7 +83,7 @@ const pages = {
type: 'item',
url: '/cosmos-ui/cron',
icon: ClockCircleOutlined,
adminOnly: true
permission: PERM_RESOURCES_READ
},
{
id: 'config',
+14 -9
View File
@@ -6,6 +6,8 @@ import * as yup from "yup";
import * as API from '../../api';
import ResponsiveButton from "../../components/responseiveButton";
import { PlusCircleOutlined } from "@ant-design/icons";
import PermissionGuard from "../../components/permissionGuard";
import { PERM_RESOURCES } from "../../utils/permissions";
import { crontabToText } from "../../utils/indexs";
import { Trans, useTranslation } from 'react-i18next';
import { FilePickerButton } from '../../components/filePicker';
@@ -197,16 +199,19 @@ const BackupDialog = ({ refresh, data, preSource, preName }) => {
{open && <BackupDialogInternal preSource={preSource} preName={preName} refresh={refresh} open={open} setOpen={setOpen} data={data} />}
<div>
{!data ? (
<ResponsiveButton
onClick={() => setOpen(true)}
variant="contained"
size="small"
startIcon={<PlusCircleOutlined />}
>
{t('mgmt.backup.newBackup')}
</ResponsiveButton>
<PermissionGuard permission={PERM_RESOURCES}>
<ResponsiveButton
onClick={() => setOpen(true)}
variant="contained"
startIcon={<PlusCircleOutlined />}
>
{t('mgmt.backup.newBackup')}
</ResponsiveButton>
</PermissionGuard>
) : (
<div onClick={() => setOpen(true)}>{t('global.edit')}</div>
<PermissionGuard permission={PERM_RESOURCES}>
<div onClick={() => setOpen(true)}>{t('global.edit')}</div>
</PermissionGuard>
)}
</div>
</>
+21 -14
View File
@@ -10,11 +10,14 @@ import ResponsiveButton from "../../components/responseiveButton";
import { useTranslation } from "react-i18next";
import { ConfirmModalDirect } from "../../components/confirmModal";
import BackupDialog, { BackupDialogInternal } from "./backupDialog";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
export const Backups = ({pathFilters}) => {
const { t } = useTranslation();
// const [isAdmin, setIsAdmin] = useState(false);
let isAdmin = true;
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [config, setConfig] = useState(null);
const [backups, setBackups] = useState([]);
const [loading, setLoading] = useState(false);
@@ -131,18 +134,22 @@ export const Backups = ({pathFilters}) => {
</ListItemIcon>
<ListItemText>{t('global.open')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading} onClick={() => setEditOpened(r)}>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.edit')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading} onClick={() => tryDeleteBackup(r.Name)}>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.delete')}</ListItemText>
</MenuItem>
<PermissionGuard permission={PERM_RESOURCES}>
<MenuItem disabled={loading} onClick={() => setEditOpened(r)}>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.edit')}</ListItemText>
</MenuItem>
</PermissionGuard>
<PermissionGuard permission={PERM_RESOURCES}>
<MenuItem disabled={loading} onClick={() => tryDeleteBackup(r.Name)}>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.delete')}</ListItemText>
</MenuItem>
</PermissionGuard>
</MenuButton>
</div>
}
+21 -16
View File
@@ -11,11 +11,14 @@ import { useTranslation } from "react-i18next";
import { ConfirmModalDirect } from "../../components/confirmModal";
import BackupDialog, { BackupDialogInternal } from "./backupDialog";
import {simplifyNumber} from '../dashboard/components/utils';
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
export const Repositories = () => {
const { t } = useTranslation();
// const [isAdmin, setIsAdmin] = useState(false);
let isAdmin = true;
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [repositories, setRepositories] = useState(null);
const [repoStats, setRepoStats] = useState({});
const [loading, setLoading] = useState(false);
@@ -148,20 +151,22 @@ export const Repositories = () => {
size="small"
variant="outlined"
/>
<ResponsiveButton
variant="outlined"
color="warning"
size="small"
startIcon={unlocking === r.id ? <CircularProgress size={16} /> : null}
disabled={unlocking === r.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleUnlock(r.id);
}}
>
Unlock
</ResponsiveButton>
<PermissionGuard permission={PERM_RESOURCES}>
<ResponsiveButton
variant="outlined"
color="warning"
size="small"
startIcon={unlocking === r.id ? <CircularProgress size={16} /> : null}
disabled={unlocking === r.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleUnlock(r.id);
}}
>
Unlock
</ResponsiveButton>
</PermissionGuard>
</Stack>;
},
},
+359
View File
@@ -0,0 +1,359 @@
import React, { useEffect, useState } from "react";
import * as API from "../../api";
import PrettyTableView from "../../components/tableView/prettyTableView";
import { PlusOutlined, ReloadOutlined, CopyOutlined, DeleteOutlined, SearchOutlined } from "@ant-design/icons";
import {
Alert,
Button,
Chip,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
ListItemIcon,
ListItemText,
MenuItem,
Stack,
TextField,
Tooltip,
} from "@mui/material";
import { CosmosCheckbox, CosmosInputText } from "./users/formShortcuts";
import { FormikProvider, useFormik } from "formik";
import { LoadingButton } from "@mui/lab";
import ResponsiveButton from "../../components/responseiveButton";
import MenuButton from "../../components/MenuButton";
import { useClientInfos } from "../../utils/hooks";
import { PERM_ADMIN } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
import dayjs from "dayjs";
import { useTranslation } from "react-i18next";
import EventExplorerStandalone from "../dashboard/eventsExplorerStandalone";
import { ConfirmModalDirect } from "../../components/confirmModal";
const CreateTokenDialog = ({ open, onClose, onCreated, t }) => {
const formik = useFormik({
initialValues: {
name: "",
description: "",
readOnly: false,
ipWhitelist: "",
restrictToConstellation: false,
},
validateOnChange: false,
onSubmit: async (values, { setErrors, setSubmitting }) => {
setSubmitting(true);
const request = {
...values,
ipWhitelist: values.ipWhitelist
? values.ipWhitelist.split(",").map((s) => s.trim()).filter(Boolean)
: [],
};
try {
const res = await API.apiTokens.create(request);
setSubmitting(false);
onCreated(res.data);
} catch (err) {
setErrors({ submit: err.message });
setSubmitting(false);
}
},
});
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<FormikProvider value={formik}>
<form onSubmit={formik.handleSubmit}>
<DialogTitle>{t('mgmt.config.apiTokens.createTitle')}</DialogTitle>
<DialogContent>
<Stack spacing={2} style={{ marginTop: "10px" }}>
<TextField
fullWidth
required
id="name"
name="name"
label={t('mgmt.config.apiTokens.tokenName')}
value={formik.values.name}
onChange={formik.handleChange}
error={formik.touched.name && Boolean(formik.errors.name)}
helperText={formik.touched.name && formik.errors.name}
/>
<TextField
fullWidth
id="description"
name="description"
label={t('global.description')}
value={formik.values.description}
onChange={formik.handleChange}
/>
<CosmosCheckbox
formik={formik}
name="readOnly"
label={t('mgmt.config.apiTokens.readOnly')}
/>
<TextField
fullWidth
id="ipWhitelist"
name="ipWhitelist"
label={t('mgmt.config.apiTokens.ipWhitelist')}
value={formik.values.ipWhitelist}
onChange={formik.handleChange}
helperText={t('mgmt.config.apiTokens.ipWhitelistHelper')}
/>
<CosmosCheckbox
formik={formik}
name="restrictToConstellation"
label={t('mgmt.config.apiTokens.restrictToConstellation')}
/>
{formik.errors.submit && (
<Alert severity="error">{formik.errors.submit}</Alert>
)}
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('global.cancelAction')}</Button>
<LoadingButton
color="primary"
variant="contained"
type="submit"
loading={formik.isSubmitting}
>
{t('global.createAction')}
</LoadingButton>
</DialogActions>
</form>
</FormikProvider>
</Dialog>
);
};
const TokenRevealDialog = ({ tokenData, onClose, t }) => {
const [copied, setCopied] = useState(false);
if (!tokenData) return null;
const handleCopy = () => {
navigator.clipboard.writeText(tokenData.token);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Dialog open={!!tokenData} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{t('mgmt.config.apiTokens.tokenCreated')}</DialogTitle>
<DialogContent>
<Stack spacing={2} style={{ marginTop: "10px" }}>
<Alert severity="warning">
{t('mgmt.config.apiTokens.copyWarning')}
</Alert>
<Stack direction="row" spacing={1} alignItems="center">
<TextField
fullWidth
value={tokenData.token}
InputProps={{ readOnly: true }}
size="small"
sx={{ fontFamily: "monospace" }}
/>
<Tooltip title={copied ? t('global.copied') : t('global.copyFilenameSuffix')}>
<IconButton onClick={handleCopy}>
<CopyOutlined />
</IconButton>
</Tooltip>
</Stack>
</Stack>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onClose}>
{t('global.done')}
</Button>
</DialogActions>
</Dialog>
);
};
const TokenEventsDialog = ({ tokenName, onClose, t }) => {
if (!tokenName) return null;
return (
<Dialog open={!!tokenName} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>{t('mgmt.config.apiTokens.events', { name: tokenName })}</DialogTitle>
<DialogContent>
<EventExplorerStandalone
initLevel="info"
initSearch={`{"object":"token@${tokenName}"}`}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('global.close')}</Button>
</DialogActions>
</Dialog>
);
};
const permissionLabelKeys = {
1: "mgmt.config.apiTokens.permAdminRead",
2: "mgmt.config.apiTokens.permAdmin",
10: "mgmt.config.apiTokens.permUsersRead",
11: "mgmt.config.apiTokens.permUsers",
20: "mgmt.config.apiTokens.permResourcesRead",
21: "mgmt.config.apiTokens.permResources",
30: "mgmt.config.apiTokens.permConfigRead",
31: "mgmt.config.apiTokens.permConfig",
};
const formatPermissions = (permissions, t) => {
if (!permissions || permissions.length === 0) return [];
return permissions
.filter((p) => p < 100)
.map((p) => permissionLabelKeys[p] ? t(permissionLabelKeys[p]) : t('mgmt.config.apiTokens.permUnknown', { id: p }));
};
const ConfigAPITokens = () => {
const { t } = useTranslation();
const [tokens, setTokens] = useState(null);
const [loading, setLoading] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [revealedToken, setRevealedToken] = useState(null);
const [monitorToken, setMonitorToken] = useState(null);
const [confirmDelete, setConfirmDelete] = useState(null);
const { hasPermission } = useClientInfos();
const refresh = async () => {
setLoading(true);
try {
const res = await API.apiTokens.list();
setTokens(res.data || {});
} catch (err) {
setTokens({});
}
setLoading(false);
};
useEffect(() => {
refresh();
}, []);
return (
<Stack spacing={2}>
<CreateTokenDialog
t={t}
open={createOpen}
onClose={() => setCreateOpen(false)}
onCreated={(data) => {
setCreateOpen(false);
setRevealedToken(data);
refresh();
}}
/>
<TokenRevealDialog
t={t}
tokenData={revealedToken}
onClose={() => setRevealedToken(null)}
/>
<TokenEventsDialog
t={t}
tokenName={monitorToken}
onClose={() => setMonitorToken(null)}
/>
{confirmDelete && (
<ConfirmModalDirect
callback={() => {
API.apiTokens.remove(confirmDelete).then(() => refresh());
}}
content={t('mgmt.config.apiTokens.deleteConfirm', { name: confirmDelete })}
onClose={() => setConfirmDelete(null)}
/>
)}
<Stack direction="row" spacing={2}>
<PermissionGuard permission={PERM_ADMIN}>
<ResponsiveButton
variant="contained"
startIcon={<PlusOutlined />}
onClick={() => setCreateOpen(true)}
>
{t('mgmt.config.apiTokens.newToken')}
</ResponsiveButton>
</PermissionGuard>
<ResponsiveButton
variant="outlined"
startIcon={<ReloadOutlined />}
onClick={refresh}
>
{t('global.refresh')}
</ResponsiveButton>
</Stack>
{tokens === null ? (
<center>
<CircularProgress color="inherit" size={20} />
</center>
) : Object.keys(tokens).length === 0 ? (
<Alert severity="info">{t('mgmt.config.apiTokens.noTokens')}</Alert>
) : (
<PrettyTableView
data={Object.values(tokens)}
getKey={(r) => r.name}
columns={[
{
title: t('global.name'),
field: (r) => r.name,
},
{
title: t('global.owner'),
field: (r) => r.owner || "-",
},
{
title: t('global.permissions'),
field: (r) => {
const labels = formatPermissions(r.permissions, t);
// For now, simplify to Full Access / Read Only based on whether PERM_ADMIN (1) is present
const hasAdmin = r.permissions && r.permissions.includes(PERM_ADMIN);
const displayLabel = hasAdmin ? t('mgmt.config.apiTokens.fullAccess') : t('mgmt.config.apiTokens.readOnly');
const displayColor = hasAdmin ? "warning" : "info";
return (
<Tooltip title={labels.join(", ") || t('global.none')}>
<Chip
label={displayLabel}
size="small"
color={displayColor}
sx={{ cursor: "pointer" }}
/>
</Tooltip>
);
},
},
{
title: t('global.createdAt'),
field: (r) =>
r.createdAt
? dayjs(r.createdAt).format("L, LT")
: "-",
},
{
title: "",
field: (r) => (
<MenuButton>
<MenuItem onClick={() => setMonitorToken(r.name)}>
<ListItemIcon><SearchOutlined /></ListItemIcon>
<ListItemText>{t('global.monitor')}</ListItemText>
</MenuItem>
<PermissionGuard permission={PERM_ADMIN}>
<MenuItem onClick={() => setConfirmDelete(r.name)} sx={{ color: "error.main" }}>
<ListItemIcon><DeleteOutlined style={{ color: "inherit" }} /></ListItemIcon>
<ListItemText>{t('global.delete')}</ListItemText>
</MenuItem>
</PermissionGuard>
</MenuButton>
),
},
]}
/>
)}
</Stack>
);
};
export default ConfigAPITokens;
+10 -6
View File
@@ -17,6 +17,8 @@ import { CosmosCheckbox, CosmosInputPassword, CosmosInputText } from './users/fo
import { LoadingButton } from '@mui/lab';
import { useTranslation } from 'react-i18next';
import { FilePickerButton } from '../../components/filePicker';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_ADMIN } from '../../utils/permissions';
// License issuer's public key for signature verification
const LICENSE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
@@ -60,12 +62,14 @@ const ConfigGeneral = ({ formik, config, status, isAdmin }) => {
{status && !status.containerized && <>
<Grid item xs={3}>
<LoadingButton loading={isCheckingUpdate} variant="outlined" color="primary" onClick={() => {
setIsCheckingUpdate(true);
API.forceAutoUpdate().then(() => {
setIsCheckingUpdate(false);
})
}}>{t('mgmt.config.general.forceAutoUpdateButton')}</LoadingButton>
<PermissionGuard permission={PERM_ADMIN}>
<LoadingButton loading={isCheckingUpdate} variant="outlined" color="primary" onClick={() => {
setIsCheckingUpdate(true);
API.forceAutoUpdate().then(() => {
setIsCheckingUpdate(false);
})
}}>{t('mgmt.config.general.forceAutoUpdateButton')}</LoadingButton>
</PermissionGuard>
</Grid>
<CosmosCheckbox
+28 -14
View File
@@ -14,7 +14,9 @@ import RestartModal from './users/restart';
import { ArrowDownOutlined, DeleteOutlined, SyncOutlined } from '@ant-design/icons';
import { LoadingButton } from '@mui/lab';
import { useClientInfos } from '../../utils/hooks';
import { PERM_CONFIGURATION, PERM_ADMIN_READ } from '../../utils/permissions';
import ConfirmModal from '../../components/confirmModal';
import PermissionGuard from '../../components/permissionGuard';
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
import { useTranslation } from 'react-i18next';
@@ -24,10 +26,11 @@ import ConfigHTTP from './configHTTP';
import ConfigHTTPS from './configHTTPS';
import ConfigExternal from './configExternal';
import ConfigAppearance from './configAppearance';
import ConfigAPITokens from './configAPITokens';
const SaveButton = ({ formik, saveLabel, isAdmin }) => {
if (!isAdmin) return null;
const SaveButton = ({ formik, saveLabel }) => {
return (
<PermissionGuard permission={PERM_CONFIGURATION}>
<MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
@@ -48,6 +51,7 @@ const SaveButton = ({ formik, saveLabel, isAdmin }) => {
</LoadingButton>
</Grid>
</MainCard>
</PermissionGuard>
);
};
@@ -58,8 +62,9 @@ const ConfigManagement = () => {
const [openModal, setOpenModal] = React.useState(false);
const [openResartModal, setOpenRestartModal] = React.useState(false);
const [saveLabel, setSaveLabel] = React.useState(t('global.saveAction'));
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_CONFIGURATION);
const canViewAdmin = hasPermission(PERM_ADMIN_READ);
function refresh() {
API.config.get().then((res) => {
@@ -83,18 +88,20 @@ const ConfigManagement = () => {
refresh();
}}>{t('mgmt.config.header.refreshButton.refreshLabel')}</Button>
{isAdmin && <Button variant="outlined" color="primary"
{canViewAdmin && <Button variant="outlined" color="primary"
startIcon={<ArrowDownOutlined />} href={"/cosmos/_logs"}>
{t('mgmt.config.header.downloadLogsButton.downloadLogsLabel')}
</Button>}
<ConfirmModal variant="outlined" color="warning" startIcon={<DeleteOutlined />} callback={() => {
API.metrics.reset().then(() => {
refresh();
});
}}
label={t('mgmt.config.header.purgeMetricsButton.purgeMetricsLabel')}
content={t('mgmt.config.header.purgeMetricsButton.purgeMetricsPopUp.cofirmAction')} />
<PermissionGuard permission={PERM_CONFIGURATION}>
<ConfirmModal variant="outlined" color="warning" startIcon={<DeleteOutlined />} callback={() => {
API.metrics.reset().then(() => {
refresh();
});
}}
label={t('mgmt.config.header.purgeMetricsButton.purgeMetricsLabel')}
content={t('mgmt.config.header.purgeMetricsButton.purgeMetricsPopUp.cofirmAction')} />
</PermissionGuard>
</Stack>
{!isAdmin && <div>
@@ -102,9 +109,9 @@ const ConfigManagement = () => {
</Alert>
</div>}
<SaveButton formik={formik} saveLabel={saveLabel} isAdmin={isAdmin} />
<SaveButton formik={formik} saveLabel={saveLabel} />
{content}
<SaveButton formik={formik} saveLabel={saveLabel} isAdmin={isAdmin} />
<SaveButton formik={formik} saveLabel={saveLabel} />
</Stack>
</div>
);
@@ -336,6 +343,13 @@ const ConfigManagement = () => {
children: wrapTab(formik, <ConfigExternal formik={formik} />),
url: "/external",
},
{
title: "API",
children: <div style={{ maxWidth: "1000px", margin: "auto" }}>
<ConfigAPITokens />
</div>,
url: "/api",
},
]}
/>
</form>
@@ -15,6 +15,8 @@ import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from
import { snackit } from '../../../api/wrap';
import { IsRouteSocketProxy } from '../../../utils/routes';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_CONFIGURATION } from '../../../utils/permissions';
const RouteSecurity = ({ routeConfig, config }) => {
const { t } = useTranslation();
@@ -237,7 +239,7 @@ const RouteSecurity = ({ routeConfig, config }) => {
</>}
</Grid>
</MainCard>
<MainCard ><Button
<MainCard ><PermissionGuard permission={PERM_CONFIGURATION}><Button
fullWidth
disableElevation
size="large"
@@ -246,7 +248,7 @@ const RouteSecurity = ({ routeConfig, config }) => {
color="primary"
>
{t('global.saveAction')}
</Button></MainCard>
</Button></PermissionGuard></MainCard>
</Stack>
</form>
)}
+4 -2
View File
@@ -20,6 +20,8 @@ import { IsRouteSocketProxy, ValidateRouteSchema, getHostnameFromName, sanitizeR
import { isDomain } from '../../../utils/indexs';
import { useTranslation } from 'react-i18next';
import { FilePickerButton } from '../../../components/filePicker';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_CONFIGURATION } from '../../../utils/permissions';
const Hide = ({ children, h }) => {
return h ? <div style={{ display: 'none' }}>
@@ -414,7 +416,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
</CosmosCollapse>
</Grid>
</MainCard>
{submitButton && <MainCard ><Button
{submitButton && <MainCard ><PermissionGuard permission={PERM_CONFIGURATION}><Button
fullWidth
disableElevation
size="large"
@@ -423,7 +425,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
color="primary"
>
{t('global.saveAction')}
</Button></MainCard>}
</Button></PermissionGuard></MainCard>}
</Stack>
</form>
)}
+13 -9
View File
@@ -44,6 +44,8 @@ import MiniPlotComponent from '../../dashboard/components/mini-plot';
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
import { useTranslation } from 'react-i18next';
import { useClientInfos } from '../../../utils/hooks';
import { PERM_CONFIGURATION } from '../../../utils/permissions';
import PermissionGuard from '../../../components/permissionGuard';
const stickyButton = {
position: 'fixed',
@@ -73,8 +75,8 @@ const ProxyManagement = () => {
const [openNewModal, setOpenNewModal] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const [tunnels, setTunnels] = React.useState([]);
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_CONFIGURATION);
function setRouteEnabled(key) {
return (event) => {
@@ -212,9 +214,11 @@ const ProxyManagement = () => {
return <div style={{ maxWidth: "1200px", margin: "auto" }}>
<Stack direction="row" spacing={1} style={{ marginBottom: '20px' }}>
{isAdmin && <Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
setOpenNewModal(true);
}}>{t('global.createAction')}</Button>}&nbsp;&nbsp;
<PermissionGuard permission={PERM_CONFIGURATION}>
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
setOpenNewModal(true);
}}>{t('global.createAction')}</Button>
</PermissionGuard>&nbsp;&nbsp;
<Button variant="outlined" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>{t('global.refresh')}</Button>
@@ -290,7 +294,7 @@ const ProxyManagement = () => {
verticalAlign: 'middle',
paddingRight: '5px',
}} />
</Tooltip> : <RouteActions
</Tooltip> : <PermissionGuard permission={PERM_CONFIGURATION}><RouteActions
route={r}
routeKey={routes.indexOf(r)}
up={(event) => up(event, routes.indexOf(r))}
@@ -299,7 +303,7 @@ const ProxyManagement = () => {
moveToBottom={(event) => moveToBottom(event, routes.indexOf(r))}
deleteRoute={(event) => deleteRoute(event, routes.indexOf(r))}
duplicateRoute={(event) => duplicateRoute(event, routes.indexOf(r))}
/>,
/></PermissionGuard>,
style: {
textAlign: 'right',
}
@@ -328,7 +332,7 @@ const ProxyManagement = () => {
{submitErrors.map((err) => {
return <Alert severity="error">{err}</Alert>
})}
<AnimateButton>
<PermissionGuard permission={PERM_CONFIGURATION}>
<Button
className='shinyButton'
disableElevation
@@ -349,7 +353,7 @@ const ProxyManagement = () => {
>
{t('mgmt.config.proxy.saveChangesButton')}
</Button>
</AnimateButton>
</PermissionGuard>
</Stack>
</MainCard>
</div>
@@ -1,6 +1,6 @@
// material-ui
import * as React from 'react';
import { Button, LinearProgress, Stack, Typography } from '@mui/material';
import { Button, LinearProgress, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from '@mui/material';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -24,6 +24,8 @@ import { useEffect, useState } from 'react';
import PrettyTableView from '../../../components/tableView/prettyTableView';
import { Trans, useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_USERS } from '../../../utils/permissions';
const UserManagement = () => {
const { t } = useTranslation();
@@ -31,12 +33,13 @@ const UserManagement = () => {
const [openCreateForm, setOpenCreateForm] = React.useState(false);
const [openDeleteForm, setOpenDeleteForm] = React.useState(false);
const [openInviteForm, setOpenInviteForm] = React.useState(false);
const [openEditEmail, setOpenEditEmail] = React.useState(false);
const [openEditUser, setOpenEditUser] = React.useState(null);
const [editRole, setEditRole] = React.useState(1);
const [createRole, setCreateRole] = React.useState(1);
const [toAction, setToAction] = React.useState(null);
const [loadingRow, setLoadingRow] = React.useState(null);
const [status, setStatus] = React.useState(null);
const roles = ['Guest', 'User', 'Admin']
const [roles, setRoles] = React.useState({0: 'Guest', 1: 'User', 2: 'Admin'});
const [rows, setRows] = useState(null);
@@ -52,6 +55,16 @@ const UserManagement = () => {
API.getStatus().then((res) => {
setStatus(res.data);
});
API.config.get().then((res) => {
const configRoles = {1: 'User', 2: 'Admin'};
if (res.data && res.data.Roles) {
Object.keys(res.data.Roles).forEach((k) => {
configRoles[k] = res.data.Roles[k].name;
});
}
setRoles(configRoles);
});
}
useEffect(() => {
@@ -128,11 +141,11 @@ const UserManagement = () => {
</DialogActions>
</Dialog>
<Dialog open={openEditEmail} onClose={() => setOpenEditEmail(false)}>
<Dialog open={!!openEditUser} onClose={() => setOpenEditUser(null)}>
<DialogTitle>{t('mgmt.usermgmt.editEmailTitle')}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('mgmt.usermgmt.editEmailText', { user: openEditEmail })}
{t('mgmt.usermgmt.editEmailText', { user: openEditUser ? openEditUser.nickname : '' })}
</DialogContentText>
<TextField
autoFocus
@@ -142,15 +155,23 @@ const UserManagement = () => {
type="email"
fullWidth
variant="standard"
defaultValue={openEditUser ? openEditUser.email : ''}
/>
<FormControl fullWidth variant="standard" margin="dense">
<InputLabel>Role</InputLabel>
<Select value={editRole} onChange={(e) => setEditRole(e.target.value)}>
{Object.entries(roles).map(([id, name]) => <MenuItem key={id} value={Number(id)}>{name}</MenuItem>)}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenEditEmail(false)}>{t('global.cancelAction')}</Button>
<Button onClick={() => setOpenEditUser(null)}>{t('global.cancelAction')}</Button>
<Button onClick={() => {
API.users.edit(openEditEmail, {
email: document.getElementById('c-email-edit').value,
}).then(() => {
setOpenEditEmail(false);
const payload = { role: editRole };
const emailVal = document.getElementById('c-email-edit').value;
if (emailVal) payload.email = emailVal;
API.users.edit(openEditUser.nickname, payload).then(() => {
setOpenEditUser(null);
refresh();
});
}}>{t('global.edit')}</Button>
@@ -181,6 +202,12 @@ const UserManagement = () => {
fullWidth
variant="standard"
/>
<FormControl fullWidth variant="standard" margin="dense">
<InputLabel>Role</InputLabel>
<Select value={createRole} onChange={(e) => setCreateRole(e.target.value)}>
{Object.entries(roles).map(([id, name]) => <MenuItem key={id} value={Number(id)}>{name}</MenuItem>)}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenCreateForm(false)}>{t('global.cancelAction')}</Button>
@@ -188,6 +215,7 @@ const UserManagement = () => {
API.users.create({
nickname: document.getElementById('c-nickname').value,
email: document.getElementById('c-email').value,
role: createRole,
}).then(() => {
setOpenCreateForm(false);
refresh();
@@ -199,9 +227,9 @@ const UserManagement = () => {
<Stack direction="row" spacing={2}>
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
<PermissionGuard permission={PERM_USERS}><Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
setOpenCreateForm(true)
}} disabled={(status && rows) ? (rows.length >= status.LicenceNumber) : false}>{t('global.createAction')}</Button>
}} disabled={(status && rows) ? (rows.length >= status.LicenceNumber) : false}>{t('global.createAction')}</Button></PermissionGuard>
<Button variant="outlined" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
@@ -223,7 +251,8 @@ const UserManagement = () => {
{!isLoading && rows && (<PrettyTableView
data={rows}
onRowClick = {(r) => {
setOpenEditEmail(r.nickname);
setOpenEditUser(r);
setEditRole(r.role);
}}
getKey={(r) => r.nickname}
columns={[
@@ -239,12 +268,9 @@ const UserManagement = () => {
const isRegistered = new Date(r.registeredAt).getTime() > 0;
const inviteExpired = new Date(r.registerKeyExp).getTime() < new Date().getTime();
return <>{isRegistered ? (r.role > 1 ? <Chip
icon={<KeyOutlined />}
label={t('mgmt.usermgmt.adminLabel')}
/> : <Chip
icon={<UserOutlined />}
label={t('global.user')}
return <>{isRegistered ? (<Chip
icon={r.role === 2 ? <KeyOutlined /> : <UserOutlined />}
label={roles[r.role] || t('global.user')}
/>) : (
inviteExpired ? <Chip
icon={<ExclamationCircleOutlined />}
@@ -288,34 +314,34 @@ const UserManagement = () => {
}
return <>{isRegistered ?
(<Button variant="contained" color="primary" onClick={
(<PermissionGuard permission={PERM_USERS}><Button variant="contained" color="primary" onClick={
() => {
setLoadingRow(r.nickname);
sendlink(r.nickname, 1);
}
}>{t('mgmt.usermgmt.sendPasswordResetButton')}</Button>) :
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
}>{t('mgmt.usermgmt.sendPasswordResetButton')}</Button></PermissionGuard>) :
(<PermissionGuard permission={PERM_USERS}><Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
() => {
setLoadingRow(r.nickname);
sendlink(r.nickname, 2);
}
} color="primary">{t('newInstall.usermgmt.inviteUser.resendInviteButton')}</Button>)
} color="primary">{t('newInstall.usermgmt.inviteUser.resendInviteButton')}</Button></PermissionGuard>)
}
&nbsp;&nbsp;<Button variant="contained" color="error" onClick={
&nbsp;&nbsp;<PermissionGuard permission={PERM_USERS}><Button variant="contained" color="error" onClick={
() => {
setLoadingRow(r.nickname);
setToAction(r.nickname);
setOpenDeleteForm(true);
}
}>{t('global.delete')}</Button>
&nbsp;&nbsp;<Button variant="contained" color="error" onClick={
}>{t('global.delete')}</Button></PermissionGuard>
&nbsp;&nbsp;<PermissionGuard permission={PERM_USERS}><Button variant="contained" color="error" onClick={
() => {
setLoadingRow(r.nickname);
API.users.reset2FA(r.nickname).then(() => {
refresh();
});
}
}>{t('mgmt.usermgmt.reset2faButton')}</Button></>
}>{t('mgmt.usermgmt.reset2faButton')}</Button></PermissionGuard></>
}
},
]}
+20 -16
View File
@@ -17,6 +17,8 @@ import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from
import { DownloadFile } from '../../api/downloadButton';
import QRCode from 'qrcode';
import { useClientInfos } from '../../utils/hooks';
import { PERM_RESOURCES } from '../../utils/permissions';
import PermissionGuard from '../../components/permissionGuard';
import { useTranslation } from 'react-i18next';
import { json } from 'react-router';
@@ -26,8 +28,8 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices, canCreateManage
const [isDone, setIsDone] = useState(null);
const [nextIP, setNextIP] = useState("");
const canvasRef = React.useRef(null);
const {role, nickname} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission, nickname } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const fetchNextIP = async () => {
try {
@@ -301,20 +303,22 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices, canCreateManage
</Formik>
</Dialog>
<ResponsiveButton
color="primary"
onClick={() => {
setIsDone(null);
fetchNextIP();
setOpenModal(true);
}}
variant={
"contained"
}
startIcon={<PlusCircleFilled />}
>
{t('mgmt.constellation.setup.addDeviceTitle')}
</ResponsiveButton>
<PermissionGuard permission={PERM_RESOURCES}>
<ResponsiveButton
color="primary"
onClick={() => {
setIsDone(null);
fetchNextIP();
setOpenModal(true);
}}
variant={
"contained"
}
startIcon={<PlusCircleFilled />}
>
{t('mgmt.constellation.setup.addDeviceTitle')}
</ResponsiveButton>
</PermissionGuard>
</>;
};
+9 -4
View File
@@ -15,16 +15,19 @@ import { isDomain } from "../../utils/indexs";
import ConfirmModal from "../../components/confirmModal";
import UploadButtons from "../../components/fileUpload";
import { useTranslation } from 'react-i18next';
import { useClientInfos } from "../../utils/hooks";
import { PERM_CONFIGURATION } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
export const ConstellationDNS = () => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_CONFIGURATION);
const [config, setConfig] = useState(null);
const refreshConfig = async () => {
let configAsync = await API.config.get();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
};
useEffect(() => {
@@ -147,7 +150,8 @@ export const ConstellationDNS = () => {
}}>{t('mgmt.constellation.dns.resetButton')}</Button>
</Stack>
<LoadingButton
<PermissionGuard permission={PERM_CONFIGURATION}>
<LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
@@ -155,7 +159,8 @@ export const ConstellationDNS = () => {
color="primary"
>
{t('global.saveAction')}
</LoadingButton>
</LoadingButton>
</PermissionGuard>
</Stack>
</form>
)}
+3 -2
View File
@@ -8,6 +8,7 @@ import * as API from '../../api';
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
import { useClientInfos } from '../../utils/hooks';
import { PERM_RESOURCES_READ } from '../../utils/permissions';
import { useTranslation } from 'react-i18next';
@@ -17,8 +18,8 @@ import VPNSalesPage from './free';
const ConstellationIndex = () => {
const { t } = useTranslation();
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES_READ);
const [coStatus, setCoStatus] = React.useState(null);
const refreshStatus = () => {
+30 -28
View File
@@ -16,6 +16,8 @@ import { isDomain } from "../../utils/indexs";
import ConfirmModal from "../../components/confirmModal";
import UploadButtons from "../../components/fileUpload";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
import { Trans, useTranslation } from 'react-i18next';
import ResyncDeviceModal from "./resyncDevice";
import VPNSalesPage from "./free";
@@ -27,8 +29,8 @@ export const ConstellationVPN = ({ freeVersion }) => {
const [users, setUsers] = useState(null);
const [devices, setDevices] = useState(null);
const [resynDevice, setResyncDevice] = useState(null); // [nickname, deviceName]
const { role } = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [ping, setPing] = useState(0);
const [coStatus, setCoStatus] = React.useState(null);
const [devicePingStatus, setDevicePingStatus] = useState({}); // {deviceName: 'loading' | 'success' | 'error'}
@@ -246,8 +248,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
</Stack>
{enableLoading
? <CircularProgress size={24} sx={{ mr: 1 }} />
: <Switch
disabled={!isAdmin}
: <PermissionGuard permission={PERM_RESOURCES} alwaysShow><Switch
checked={!!constellationEnabled}
onChange={async (e) => {
setEnableLoading(true);
@@ -259,12 +260,12 @@ export const ConstellationVPN = ({ freeVersion }) => {
}, 1500);
}}
color="success"
/>
/></PermissionGuard>
}
</Stack>
</Stack>
{isAdmin && constellationEnabled && <>
{constellationEnabled && <PermissionGuard permission={PERM_RESOURCES}>
<Box sx={{ borderTop: '1px solid', borderColor: 'divider', mt: 2, pt: 2 }}>
<Stack spacing={1} direction="row" flexWrap="wrap">
<Button
@@ -351,7 +352,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
/>
</Stack>
</Box>
</>}
</PermissionGuard>}
</MainCard>}
{(isAdmin || !config.ConstellationConfig.ThisDeviceName) && <div>
@@ -469,20 +470,21 @@ export const ConstellationVPN = ({ freeVersion }) => {
</Stack>} />
</>}
{isAdmin && (!freeVersion || config.ConstellationConfig.ThisDeviceName) && <><LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{config.ConstellationConfig.ThisDeviceName
? t('global.saveAction')
: t('mgmt.constellation.setup.createConstellation')}
</LoadingButton>
</>}
{(!freeVersion || config.ConstellationConfig.ThisDeviceName) && <PermissionGuard permission={PERM_RESOURCES}>
<LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{config.ConstellationConfig.ThisDeviceName
? t('global.saveAction')
: t('mgmt.constellation.setup.createConstellation')}
</LoadingButton>
</PermissionGuard>}
{isAdmin && <><UploadButtons
<PermissionGuard permission={PERM_RESOURCES}><UploadButtons
accept=".yml,.yaml"
label={t('mgmt.constellation.setup.externalConfig.label')}
variant="outlined"
@@ -494,7 +496,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
refreshConfig();
}, 1000);
}}
/></>}
/></PermissionGuard>
</Stack>
</form>
)}
@@ -650,7 +652,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
/>;
}
return blocked ? <Chip
return blocked ? <PermissionGuard permission={PERM_RESOURCES} alwaysShow><Chip
label={
<div>
<Switch size="small" style={{ verticalAlign: "middle", marginRight: 4 }} />
@@ -660,7 +662,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
color="error"
onClick={() => toggleFirewallBlock(r.deviceName, blocked)}
style={{ cursor: 'pointer' }}
/> : <Chip
/></PermissionGuard> : <PermissionGuard permission={PERM_RESOURCES} alwaysShow><Chip
label={
<div>
<Switch
@@ -682,7 +684,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
color="success"
onClick={() => toggleFirewallBlock(r.deviceName, blocked)}
style={{ cursor: 'pointer' }}
/>;
/></PermissionGuard>;
},
},
{
@@ -690,15 +692,15 @@ export const ConstellationVPN = ({ freeVersion }) => {
clickable: true,
field: (r) => {
return <>
<Tooltip title={t('mgmt.constellation.resyncDevice')}>
<PermissionGuard permission={PERM_RESOURCES}>
<IconButton onClick={() => setResyncDevice([r.nickname, r.deviceName])}>
<SyncOutlined />
</IconButton>
</Tooltip>
<DeleteButton onDelete={async () => {
</PermissionGuard>
<PermissionGuard permission={PERM_RESOURCES}><DeleteButton onDelete={async () => {
await API.constellation.block(r.nickname, r.deviceName, true);
refreshConfig();
}}></DeleteButton>
}}></DeleteButton></PermissionGuard>
</>
}
}
+14 -9
View File
@@ -19,6 +19,9 @@ import JobLogsDialog from "./jobLogs";
import NewJobDialog from "./newJob";
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
const getStatus = (job) => {
if (job.Running) return 'running';
@@ -29,7 +32,8 @@ const getStatus = (job) => {
export const CronManager = () => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [config, setConfig] = useState(null);
const [cronJobs, setCronJobs] = useState([]);
const [mergeDialog, setMergeDialog] = useState(null);
@@ -72,7 +76,6 @@ export const CronManager = () => {
let _cronJobs = await API.cron.list();
let configAsync = await API.config.get();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
setCronJobs(_cronJobs.data);
setLoading(false);
};
@@ -93,9 +96,9 @@ export const CronManager = () => {
}} />}
<Stack spacing={2}>
<Stack direction="row" spacing={2}>
<ResponsiveButton variant="contained" startIcon={<PlusOutlined />} onClick={() => {
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton variant="contained" startIcon={<PlusOutlined />} onClick={() => {
setNewJob(true);
}}>{t('mgmt.cron.newCronTitle')}</ResponsiveButton>
}}>{t('mgmt.cron.newCronTitle')}</ResponsiveButton></PermissionGuard>
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
refresh();
}}>{t('global.refresh')}</ResponsiveButton>
@@ -160,14 +163,16 @@ export const CronManager = () => {
setJobLogs(r);
}}><SearchOutlined /></IconButton></Tooltip>
{scheduler == "Custom" && <>
<Tooltip title={t('global.edit')}><IconButton onClick={() => {
setNewJob(r);
}}><EditOutlined /></IconButton></Tooltip>
<Tooltip title={t('global.delete')}>
<PermissionGuard permission={PERM_RESOURCES}>
<Tooltip title={t('global.edit')}><IconButton onClick={() => {
setNewJob(r);
}}><EditOutlined /></IconButton></Tooltip>
</PermissionGuard>
<PermissionGuard permission={PERM_RESOURCES}>
<DeleteIconButton onDelete={() => {
deleteCronJob(r);
}} />
</Tooltip>
</PermissionGuard>
</>}
</>
}
+3 -2
View File
@@ -12,6 +12,7 @@ import { getFullOrigin } from "../../utils/routes";
import { DashboardIcon } from "../../utils/servapp-icon";
import Chart from 'react-apexcharts';
import { useClientInfos } from "../../utils/hooks";
import { PERM_ADMIN_READ } from "../../utils/permissions";
import { FormaterForMetric, formatDate } from "../dashboard/components/utils";
import MiniPlotComponent from "../dashboard/components/mini-plot";
import Migrate014 from "./migrate014";
@@ -98,8 +99,8 @@ const HomePage = () => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const isMd = useMediaQuery(theme.breakpoints.up('md'));
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_ADMIN_READ);
const [metrics, setMetrics] = useState(null);
const blockStyle = {
+14 -17
View File
@@ -13,6 +13,8 @@ import DockerComposeImport from '../servapps/containers/docker-compose';
import { AppstoreAddOutlined, SearchOutlined, WarningOutlined } from "@ant-design/icons";
import ResponsiveButton from "../../components/responseiveButton";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
import EditSourcesModal from "./sources";
import { PersistentCheckbox } from "../../components/persistentInput";
import { useTranslation } from 'react-i18next';
@@ -97,9 +99,7 @@ function ShowcasesItem({ isDark, item, isAdmin }) {
overflow: 'hidden',
}}></p>
<Stack direction="row" spacing={2} justifyContent="flex-start">
{isAdmin && <div>
<DockerComposeImport installerInit defaultName={item.name} dockerComposeInit={item.compose} />
</div>}
<DockerComposeImport installerInit defaultName={item.name} dockerComposeInit={item.compose} />
<Link to={"/cosmos-ui/market-listing/cosmos-cloud/" + item.name} style={{
textDecoration: 'none',
}}>
@@ -141,8 +141,8 @@ const MarketPage = () => {
const { appName, appStore } = useParams();
const [search, setSearch] = useState("");
const [filterDups, setFilterDups] = useState(false);
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const backgroundStyle = isDark ? {
backgroundColor: '#141414',
@@ -294,12 +294,7 @@ const MarketPage = () => {
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(openedApp?.translation?.[i18n?.resolvedLanguage]?.longDescription || openedApp?.translation?.[i18n?.resolvedLanguage.substr?.(0,2)]?.longDescription || openedApp.longDescription) }}></div>
{isAdmin ? <div>
<DockerComposeImport installerInit defaultName={openedApp.name} dockerComposeInit={openedApp.compose} />
</div> : <div style={{
color: 'orange',
fontStyle: 'italic',
}}>{t('navigation.market.mustBeAdmin')}</div>}
<DockerComposeImport installerInit defaultName={openedApp.name} dockerComposeInit={openedApp.compose} />
</Stack>
</Stack>
</Box>}
@@ -344,12 +339,14 @@ const MarketPage = () => {
}}
/>
<Link to="/cosmos-ui/servapps/new-service">
<ResponsiveButton
variant="contained"
startIcon={<AppstoreAddOutlined />}
>{t('navigation.market.startServAppButton')}</ResponsiveButton>
</Link>
<PermissionGuard permission={PERM_RESOURCES}>
<Link to="/cosmos-ui/servapps/new-service" style={{ textDecoration: 'none' }}>
<ResponsiveButton
variant="contained"
startIcon={<AppstoreAddOutlined />}
>{t('navigation.market.startServAppButton')}</ResponsiveButton>
</Link>
</PermissionGuard>
<DockerComposeImport refresh={() => { }} />
<EditSourcesModal onSave={refresh} />
<PersistentCheckbox name="filterDups" label={t('navigation.market.filterDuplicateCheckbox')} value={filterDups} onChange={setFilterDups} />
+7 -5
View File
@@ -36,6 +36,8 @@ import { DeleteButton } from '../../components/delete';
import OpenIdEditModal from './openid-edit';
import bcrypt from 'bcryptjs';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_CONFIGURATION } from '../../utils/permissions';
const stickyButton = {
position: 'fixed',
@@ -129,10 +131,10 @@ const OpenIdList = () => {
return <div style={{ maxWidth: "1200px", margin: "auto" }}>
<Stack direction="row" spacing={1} style={{ marginBottom: '20px' }}>
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
<PermissionGuard permission={PERM_CONFIGURATION}><Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
setClientId(null);
setOpenNewModal(true);
}}>{t('global.createAction')}</Button>&nbsp;&nbsp;
}}>{t('global.createAction')}</Button></PermissionGuard>&nbsp;&nbsp;
<Button variant="outlined" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>{t('global.refresh')}</Button>
@@ -223,10 +225,10 @@ const OpenIdList = () => {
},
{
title: '', clickable: true, field: (r, k) => <>
<Button variant="contained" color="primary" startIcon={<ArrowRightOutlined />} onClick={() => {
<PermissionGuard permission={PERM_CONFIGURATION}><Button variant="contained" color="primary" startIcon={<ArrowRightOutlined />} onClick={() => {
generateNewSecret(r.id)
}}>{t('mgmt.openId.resetSecret')}</Button>&nbsp;&nbsp;
<DeleteButton onDelete={(event) => deleteClient(event, k)} />
}}>{t('mgmt.openId.resetSecret')}</Button></PermissionGuard>&nbsp;&nbsp;
<PermissionGuard permission={PERM_CONFIGURATION}><DeleteButton onDelete={(event) => deleteClient(event, k)} /></PermissionGuard>
</>,
},
]}
+22 -20
View File
@@ -5,6 +5,8 @@ import * as API from '../../api';
import LogsInModal from '../../components/logsInModal';
import DeleteModal from './deleteModal';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES } from '../../utils/permissions';
const GetActions = ({
Id,
@@ -67,70 +69,70 @@ const GetActions = ({
{
t: t('mgmt.servapps.actionBar.update') + (isStack ? ', go the stack details to update' : ', Click to Update'),
if: ['update_available'],
es: <IconButton className="shinyButton" style={{cursor: 'not-allowed'}} color='primary' onClick={()=>{}} size={isMiniMobile ? 'medium' : 'large'}>
es: <PermissionGuard permission={PERM_RESOURCES}><IconButton className="shinyButton" style={{cursor: 'not-allowed'}} color='primary' onClick={()=>{}} size={isMiniMobile ? 'medium' : 'large'}>
<UpCircleOutlined />
</IconButton>,
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
</IconButton></PermissionGuard>,
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
<UpCircleOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.noUpdate'),
if: ['update_not_available'],
hideStack: true,
e: <IconButton onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
<UpCircleOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.start'),
if: ['exited', 'created'],
e: <IconButton onClick={() => {doTo('start')}} size={isMiniMobile ? 'medium' : 'large'}>
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => {doTo('start')}} size={isMiniMobile ? 'medium' : 'large'}>
<PlaySquareOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.unpause'),
if: ['paused'],
e: <IconButton onClick={() => {doTo('unpause')}} size={isMiniMobile ? 'medium' : 'large'}>
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => {doTo('unpause')}} size={isMiniMobile ? 'medium' : 'large'}>
<PlaySquareOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.pause'),
if: ['running'],
e: <IconButton onClick={() => {doTo('pause')}} size={isMiniMobile ? 'medium' : 'large'}>
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => {doTo('pause')}} size={isMiniMobile ? 'medium' : 'large'}>
<PauseCircleOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.stop'),
if: ['paused', 'restarting', 'running'],
e: <IconButton onClick={() => {doTo('stop')}} size={isMiniMobile ? 'medium' : 'large'} variant="outlined">
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => {doTo('stop')}} size={isMiniMobile ? 'medium' : 'large'} variant="outlined">
<StopOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.restart'),
if: ['exited', 'running', 'paused', 'created', 'restarting'],
e: <IconButton onClick={() => doTo('restart')} size={isMiniMobile ? 'medium' : 'large'}>
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => doTo('restart')} size={isMiniMobile ? 'medium' : 'large'}>
<ReloadOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.recreate'),
if: ['exited', 'running', 'paused', 'created', 'restarting'],
hideStack: true,
e: <IconButton onClick={() => doTo('recreate')} color="error" size={isMiniMobile ? 'medium' : 'large'}>
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => doTo('recreate')} color="error" size={isMiniMobile ? 'medium' : 'large'}>
<RollbackOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('mgmt.servapps.actionBar.kill'),
if: ['running', 'paused', 'created', 'restarting'],
e: <IconButton onClick={() => doTo('kill')} color="error" size={isMiniMobile ? 'medium' : 'large'}>
e: <PermissionGuard permission={PERM_RESOURCES}><IconButton onClick={() => doTo('kill')} color="error" size={isMiniMobile ? 'medium' : 'large'}>
<CloseSquareOutlined />
</IconButton>
</IconButton></PermissionGuard>
},
{
t: t('global.delete'),
@@ -1,41 +1,24 @@
import React from 'react';
import { Alert, Checkbox, Chip, CircularProgress, Stack, Typography, useMediaQuery } from '@mui/material';
import MainCard from '../../../components/MainCard';
import { ContainerOutlined, DashboardOutlined, DesktopOutlined, InfoCircleOutlined, NodeExpandOutlined, PlayCircleOutlined, PlusCircleOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons';
import { getFaviconURL, getContainersRoutes } from '../../../utils/routes';
import HostChip from '../../../components/hostChip';
import ExposeModal from '../exposeModal';
import * as API from '../../../api';
import RestartModal from '../../config/users/restart';
import GetActions from '../actionBar';
import { ServAppIcon } from '../../../utils/servapp-icon';
import MiniPlotComponent from '../../dashboard/components/mini-plot';
import UploadButtons from '../../../components/fileUpload';
import NewDockerService from './newService';
const info = {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
padding: '10px',
borderRadius: '5px',
}
import { PERM_CREDENTIALS_READ } from '../../../utils/permissions';
import { useClientInfos } from '../../../utils/hooks';
import { useTranslation } from 'react-i18next';
const ContainerComposeEdit = ({ containerInfo, config, refresh, updatesAvailable, selfName }) => {
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
const [openModal, setOpenModal] = React.useState(false);
const [openRestartModal, setOpenRestartModal] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const { Name, Config, NetworkSettings, State } = containerInfo;
const Image = Config.Image;
const IPAddress = NetworkSettings.Networks?.[Object.keys(NetworkSettings.Networks)[0]]?.IPAddress;
const Health = State.Health;
const healthStatus = Health ? Health.Status : 'Healthy';
const healthIconColor = healthStatus === 'Healthy' ? 'green' : 'red';
const routes = getContainersRoutes(config, Name.replace('/', ''));
const { hasPermission, hasRolePermission } = useClientInfos();
const { t } = useTranslation();
const { Name } = containerInfo;
const [exportedCompose, setExportedCompose] = React.useState(null);
React.useEffect(() => {
if (!hasPermission(PERM_CREDENTIALS_READ)) return;
API.docker.exportContainer(Name.replace('/', '')).then((res) => {
setExportedCompose({
services: {
@@ -49,11 +32,23 @@ const ContainerComposeEdit = ({ containerInfo, config, refresh, updatesAvailable
setIsUpdating(false);
})) : (() => {setIsUpdating(false);});
if (!hasPermission(PERM_CREDENTIALS_READ)) {
return (
<div style={{ maxWidth: '1000px', width: '100%', margin: 'auto', padding: '20px 0' }}>
<Alert severity="warning">
{hasRolePermission(PERM_CREDENTIALS_READ)
? t('mgmt.servapps.compose.credentialsRequired')
: t('mgmt.servapps.compose.credentialsDenied')}
</Alert>
</div>
);
}
return (
<div style={{ maxWidth: '1000px', width: '100%', margin: 'auto' }}>
{exportedCompose && <NewDockerService edit service={exportedCompose} refresh={refreshAll} />}
{exportedCompose && <NewDockerService edit service={exportedCompose} refresh={refreshAll} />}
</div>
);
};
export default ContainerComposeEdit;
export default ContainerComposeEdit;
@@ -36,6 +36,8 @@ import { CosmosContainerPicker } from '../../config/users/containerPicker';
import { randomString } from '../../../utils/indexs';
import { useTranslation } from 'react-i18next';
import { FilePickerButton } from '../../../components/filePicker';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES } from '../../../utils/permissions';
function checkIsOnline() {
API.isOnline().then((res) => {
@@ -1094,16 +1096,18 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
</DialogActions>)}
</Dialog>
<ResponsiveButton
color="primary"
onClick={() => {
openModalFunc();
}}
variant={(installerInit ? "contained" : "outlined")}
startIcon={(installerInit ? <ArrowDownOutlined /> : <ArrowUpOutlined />)}
>
{installerInit ? t('mgmt.servapps.compose.installButton') : t('mgmt.servapps.importComposeFileButton')}
</ResponsiveButton>
<PermissionGuard permission={PERM_RESOURCES}>
<ResponsiveButton
color="primary"
onClick={() => {
openModalFunc();
}}
variant={(installerInit ? "contained" : "outlined")}
startIcon={(installerInit ? <ArrowDownOutlined /> : <ArrowUpOutlined />)}
>
{installerInit ? t('mgmt.servapps.compose.installButton') : t('mgmt.servapps.importComposeFileButton')}
</ResponsiveButton>
</PermissionGuard>
</>;
};
@@ -57,7 +57,7 @@ const Logs = ({ containerInfo }) => {
ws.current.close();
}
ws.current = API.docker.attachTerminal(Name.slice(1));
ws.current = API.docker.attachTerminal(Name.slice(1), true);
ws.current.onmessage = (event) => {
if(event.data === '_PONG_') {
@@ -13,6 +13,8 @@ import NewNetworkButton from '../createNetwork';
import LinkContainersButton from '../linkContainersButton';
import { useTranslation } from 'react-i18next';
import { CosmosContainerPicker } from '../../config/users/containerPicker';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES } from '../../../utils/permissions';
const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange, OnConnect, OnDisconnect }) => {
const { t } = useTranslation();
@@ -234,7 +236,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
</Grid>
</Grid>
))}
<IconButton
<PermissionGuard permission={PERM_RESOURCES}><IconButton
fullWidth
variant="outlined"
color="primary"
@@ -250,7 +252,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
}}
>
<PlusCircleOutlined />
</IconButton>
</IconButton></PermissionGuard>
</div>
<div>
<Stack direction="column" spacing={2}>
@@ -259,7 +261,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
{!newContainer && <LoadingButton
{!newContainer && <PermissionGuard permission={PERM_RESOURCES}><LoadingButton
fullWidth
disableElevation
disabled={formik.errors.submit}
@@ -270,7 +272,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
color="primary"
>
{t('mgmt.servApps.networks.updatePortsButton')}
</LoadingButton>}
</LoadingButton></PermissionGuard>}
</Stack>
</div>
</Stack>
@@ -304,13 +306,13 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
data={networks}
sort={(a, b) => a.Name > b.Name}
buttons={[
<NewNetworkButton refresh={refreshAll} />,
<LinkContainersButton
refresh={refreshAll}
<PermissionGuard permission={PERM_RESOURCES}><NewNetworkButton refresh={refreshAll} /></PermissionGuard>,
<PermissionGuard permission={PERM_RESOURCES}><LinkContainersButton
refresh={refreshAll}
originContainer={containerInfo.Name.replace('/', '')}
newContainer={newContainer}
OnConnect={OnConnect}
/>,
/></PermissionGuard>,
]}
onRowClick={() => { }}
getKey={(r) => r.Id}
@@ -330,14 +332,14 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
title: '',
field: (r) => {
const isConnected = containerInfo.NetworkSettings.Networks[r.Name];
return (<Button
return (<PermissionGuard permission={PERM_RESOURCES}><Button
variant="outlined"
color="primary"
onClick={() => {
isConnected ? disconnect(r.Name) : connect(r.Name);
}}>
{isConnected ? t('mgmt.servapps.containers.terminal.disconnectButton') : t('mgmt.servapps.containers.terminal.connectButton')}
</Button>)
</Button></PermissionGuard>)
}
}
]}
@@ -12,6 +12,8 @@ import { ServAppIcon } from '../../../utils/servapp-icon';
import MiniPlotComponent from '../../dashboard/components/mini-plot';
import UploadButtons from '../../../components/fileUpload';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES } from '../../../utils/permissions';
const info = {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
@@ -162,19 +164,21 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s
/> Isolate Container Network
</Stack> */}
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
<Checkbox
checked={Config.Labels['cosmos-auto-update'] === 'true' ||
(selfName && Name.replace('/', '') == selfName && config.AutoUpdate)}
disabled={isUpdating}
onChange={(e) => {
setIsUpdating(true);
API.docker.autoUpdate(Name.replace('/', ''), e.target.checked).then(() => {
setTimeout(() => {
refreshAll();
}, 3000);
})
}}
/> {t('mgmt.servApps.autoUpdateCheckbox')}
<PermissionGuard permission={PERM_RESOURCES} alwaysShow>
<Checkbox
checked={Config.Labels['cosmos-auto-update'] === 'true' ||
(selfName && Name.replace('/', '') == selfName && config.AutoUpdate)}
disabled={isUpdating}
onChange={(e) => {
setIsUpdating(true);
API.docker.autoUpdate(Name.replace('/', ''), e.target.checked).then(() => {
setTimeout(() => {
refreshAll();
}, 3000);
})
}}
/>
</PermissionGuard> {t('mgmt.servApps.autoUpdateCheckbox')}
</Stack>
<strong><NodeExpandOutlined /> URLs</strong>
<div>
@@ -182,18 +186,20 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s
return <HostChip route={route} settings style={{margin: '5px'}}/>
})}
<br />
<Chip
label={t('mgmt.servApps.newChip.newLabel')}
color="primary"
style={{paddingRight: '4px', margin: '5px'}}
deleteIcon={<PlusCircleOutlined />}
onClick={() => {
addNewRoute();
}}
onDelete={() => {
addNewRoute();
}}
/>
<PermissionGuard permission={PERM_RESOURCES}>
<Chip
label={t('mgmt.servApps.newChip.newLabel')}
color="primary"
style={{paddingRight: '4px', margin: '5px'}}
deleteIcon={<PlusCircleOutlined />}
onClick={() => {
addNewRoute();
}}
onDelete={() => {
addNewRoute();
}}
/>
</PermissionGuard>
</div>
<strong><DashboardOutlined /> {t('menu-items.navigation.monitoringTitle')}</strong>
<div style={{ width: '96%' }}>
+18 -14
View File
@@ -27,6 +27,8 @@ import { LoadingButton } from "@mui/lab";
import LogsInModal from "../../../components/logsInModal";
import ResponsiveButton from "../../../components/responseiveButton";
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES } from '../../../utils/permissions';
const containerInfoFrom = (values) => {
const labels = {};
@@ -483,20 +485,22 @@ const DockerContainerSetup = ({
{t('mgmt.servapps.newContainer.imageUpdateWarning')}
</Alert>
)}
<LoadingButton
fullWidth
disableElevation
disabled={formik.errors.submit}
loading={formik.isSubmitting}
size="large"
type="submit"
variant="contained"
color="primary"
>
{formik.values.image !== latestImage
? t('mgmt.servapps.newContainer.pullImageButton')
: t('mgmt.servapps.newContainer.updateContainerButton')}
</LoadingButton>
<PermissionGuard permission={PERM_RESOURCES}>
<LoadingButton
fullWidth
disableElevation
disabled={formik.errors.submit}
loading={formik.isSubmitting}
size="large"
type="submit"
variant="contained"
color="primary"
>
{formik.values.image !== latestImage
? t('mgmt.servapps.newContainer.pullImageButton')
: t('mgmt.servapps.newContainer.updateContainerButton')}
</LoadingButton>
</PermissionGuard>
</Stack>
</MainCard>
)}
@@ -6,6 +6,9 @@ import { Alert, Input, Stack, useMediaQuery, useTheme } from '@mui/material';
import { ApiOutlined, SendOutlined } from '@ant-design/icons';
import ResponsiveButton from '../../../components/responseiveButton';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES, PERM_ADMIN } from '../../../utils/permissions';
import { useClientInfos } from '../../../utils/hooks';
import { Terminal } from '@xterm/xterm'
import '@xterm/xterm/css/xterm.css'
@@ -14,6 +17,7 @@ import TerminalComponent from '../../../components/terminal';
const DockerTerminal = ({containerInfo, refresh}) => {
const { t } = useTranslation();
const { hasPermission, hasRolePermission } = useClientInfos();
const { Name, Config, NetworkSettings, State } = containerInfo;
const isInteractive = Config.Tty;
@@ -27,6 +31,18 @@ const DockerTerminal = ({containerInfo, refresh}) => {
});
};
if (!hasPermission(PERM_ADMIN)) {
return (
<div style={{ maxWidth: '1000px', width: '100%', margin: 'auto', padding: '20px 0' }}>
<Alert severity="warning">
{hasRolePermission(PERM_ADMIN)
? t('sudo.required')
: t('mgmt.servapps.containers.terminal.terminalAccessDenied')}
</Alert>
</div>
);
}
return (
<div className="terminal-container" style={{
background: '#000',
@@ -37,7 +53,7 @@ const DockerTerminal = ({containerInfo, refresh}) => {
{(!isInteractive) && (
<Alert severity="warning">
{t('mgmt.servapps.containers.terminal.terminalNotInteractiveWarning')}
<Button onClick={() => makeInteractive()}>{t('mgmt.servapps.containers.terminal.ttyEnableButton')}</Button>
<PermissionGuard permission={PERM_RESOURCES}><Button onClick={() => makeInteractive()}>{t('mgmt.servapps.containers.terminal.ttyEnableButton')}</Button></PermissionGuard>
</Alert>
)}
@@ -23,6 +23,8 @@ import { useTranslation } from 'react-i18next';
import { FilePickerButton } from "../../../components/filePicker";
import { Backups } from "../../backups/backups";
import BackupDialog from "../../backups/backupDialog";
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES } from '../../../utils/permissions';
const VolumeContainerSetup = ({
noCard,
@@ -168,7 +170,7 @@ const VolumeContainerSetup = ({
getKey={(r) => r.Id}
fullWidth
buttons={[
<ResponsiveButton
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton
startIcon={<PlusCircleOutlined />}
variant="outlined"
color="primary"
@@ -187,7 +189,7 @@ const VolumeContainerSetup = ({
}}
>
{t('mgmt.servapps.newContainer.volumes.newMountButton')}
</ResponsiveButton>,
</ResponsiveButton></PermissionGuard>,
]}
columns={[
{
@@ -315,7 +317,7 @@ const VolumeContainerSetup = ({
field: (r) => {
return (
<Stack direction="row" spacing={2}>
<Button
<PermissionGuard permission={PERM_RESOURCES}><Button
variant="outlined"
color="primary"
disabled={frozenVolumes.includes(r.Source)}
@@ -334,7 +336,7 @@ const VolumeContainerSetup = ({
}}
>
{t('global.unmount')}
</Button>
</Button></PermissionGuard>
{!newContainer && containerInfo.Name && (r.Target ? <BackupDialog preName={`${containerInfo.Name.replace("/", "").replace("/", "-")}-${r.Target.replace("/", "").replaceAll("/", "_")}`} preSource={formatSource(r.Source)} refresh={() => setTimeout(refreshAll, 1500)} /> : null)}
</Stack>
);
@@ -362,18 +364,20 @@ const VolumeContainerSetup = ({
</Grid>
)}
{!newContainer && (
<LoadingButton
fullWidth
disableElevation
disabled={formik.errors.submit}
loading={formik.isSubmitting}
size="large"
type="submit"
variant="contained"
color="primary"
>
{t('mgmt.servapps.newContainer.volumes.updateVolumesButton')}
</LoadingButton>
<PermissionGuard permission={PERM_RESOURCES}>
<LoadingButton
fullWidth
disableElevation
disabled={formik.errors.submit}
loading={formik.isSubmitting}
size="large"
type="submit"
variant="contained"
color="primary"
>
{t('mgmt.servapps.newContainer.volumes.updateVolumesButton')}
</LoadingButton>
</PermissionGuard>
)}
</Stack>
</Grid>
+4 -2
View File
@@ -15,6 +15,8 @@ import * as API from '../../api';
import { CosmosCheckbox } from '../config/users/formShortcuts';
import ResponsiveButton from '../../components/responseiveButton';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES } from '../../utils/permissions';
const NewNetworkButton = ({ fullWidth, refresh }) => {
@@ -150,13 +152,13 @@ const NewNetworkButton = ({ fullWidth, refresh }) => {
</DialogActions>
</FormikProvider>
</Dialog>
<ResponsiveButton
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton
fullWidth={fullWidth}
onClick={() => setIsOpened(true)}
startIcon={<PlusCircleOutlined />}
>
{t('mgmt.servapps.networks.list.newNetwork')}
</ResponsiveButton>
</ResponsiveButton></PermissionGuard>
</>
);
};
+4 -2
View File
@@ -20,6 +20,8 @@ import { PlusCircleOutlined } from '@ant-design/icons';
import { LoadingButton } from '@mui/lab';
import * as API from '../../api';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES } from '../../utils/permissions';
const NewVolumeButton = ({ fullWidth, refresh }) => {
const { t } = useTranslation();
@@ -107,13 +109,13 @@ const NewVolumeButton = ({ fullWidth, refresh }) => {
</DialogActions>
</FormikProvider>
</Dialog>
<Button
<PermissionGuard permission={PERM_RESOURCES}><Button
fullWidth={fullWidth}
onClick={() => setIsOpened(true)}
startIcon={<PlusCircleOutlined />}
>
{t('mgmt.servApps.volumes.newVolumeTitle')}
</Button>
</Button></PermissionGuard>
</>
);
};
+13 -9
View File
@@ -7,6 +7,8 @@ import { DeleteButton } from '../../components/delete';
import { CosmosCheckbox } from '../config/users/formShortcuts';
import { getContaienrsJobs, getContainersRoutes } from '../../utils/routes';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES } from '../../utils/permissions';
const DeleteModal = ({Ids, containers, refreshServApps, setIsUpdatingId, config}) => {
const { t } = useTranslation();
@@ -268,15 +270,17 @@ const DeleteModal = ({Ids, containers, refreshServApps, setIsUpdatingId, config}
</Dialog>
</>}
<IconButton onClick={() => {
setIsOpen(true);
setIsDeleting(false);
setFailed([]);
setDeleted([]);
setIgnored([]);
}} color="error" size='large'>
<DeleteOutlined />
</IconButton>
<PermissionGuard permission={PERM_RESOURCES}>
<IconButton onClick={() => {
setIsOpen(true);
setIsDeleting(false);
setFailed([]);
setDeleted([]);
setIgnored([]);
}} color="error" size='large'>
<DeleteOutlined />
</IconButton>
</PermissionGuard>
</>
}
@@ -19,6 +19,8 @@ import { CosmosCheckbox } from '../config/users/formShortcuts';
import ResponsiveButton from '../../components/responseiveButton';
import { CosmosContainerPicker } from '../config/users/containerPicker';
import { randomString } from '../../utils/indexs';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES } from '../../utils/permissions';
const LinkContainersButton = ({ fullWidth, refresh, originContainer, newContainer, OnConnect }) => {
const { t } = useTranslation();
@@ -101,13 +103,13 @@ const LinkContainersButton = ({ fullWidth, refresh, originContainer, newContaine
</DialogActions>
</FormikProvider>
</Dialog>
<ResponsiveButton
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton
fullWidth={fullWidth}
onClick={() => setIsOpened(true)}
startIcon={<PlusCircleOutlined />}
>
{t('mgmt.servApps.container.network.linkContainerButton')}
</ResponsiveButton>
</ResponsiveButton></PermissionGuard>
</>
);
};
+4 -2
View File
@@ -8,6 +8,8 @@ import PrettyTableView from '../../components/tableView/prettyTableView';
import NewNetworkButton from './createNetwork';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES } from '../../utils/permissions';
export const NetworksColumns = (theme, isDark, t) => [
{
@@ -92,7 +94,7 @@ const NetworkManagementList = () => {
clickable: true,
field: (r) => (
<>
<Button
<PermissionGuard permission={PERM_RESOURCES}><Button
variant="contained"
color="error"
startIcon={<DeleteOutlined />}
@@ -112,7 +114,7 @@ const NetworkManagementList = () => {
}}
>
{tryDelete === r.Id ? t('global.confirmDeletion') : t('global.delete')}
</Button>
</Button></PermissionGuard>
</>
),
},
+32 -24
View File
@@ -23,6 +23,8 @@ import MiniPlotComponent from '../dashboard/components/mini-plot';
import { DownloadFile } from '../../api/downloadButton';
import { useTheme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES, PERM_CREDENTIALS_READ } from '../../utils/permissions';
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(20,24,35,0.7)' : 'rgba(255,255,255)',
@@ -254,21 +256,25 @@ const ServApps = ({stack}) => {
<ResponsiveButton variant="secondary" startIcon={<RollbackOutlined />}>Back</ResponsiveButton>
</Link>}
{!stack && <>
<Link to="/cosmos-ui/servapps/new-service">
<ResponsiveButton
variant="contained"
startIcon={<AppstoreAddOutlined />}
>{t('navigation.market.startServAppButton')}</ResponsiveButton>
</Link>
<PermissionGuard permission={PERM_RESOURCES}>
<Link to="/cosmos-ui/servapps/new-service" style={{ textDecoration: 'none' }}>
<ResponsiveButton
variant="contained"
startIcon={<AppstoreAddOutlined />}
>{t('navigation.market.startServAppButton')}</ResponsiveButton>
</Link>
</PermissionGuard>
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
refreshServApps();
}}>{t('global.refresh')}</ResponsiveButton>
<DockerComposeImport refresh={refreshServApps}/>
<DownloadFile
filename={'backup.cosmos-compose.json'}
label={t('mgmt.servApps.exportDockerBackupButton.exportDockerBackupLabel')}
contentGetter={API.config.getBackup}
/>
<PermissionGuard permission={PERM_CREDENTIALS_READ}>
<DownloadFile
filename={'backup.cosmos-compose.json'}
label={t('mgmt.servApps.exportDockerBackupButton.exportDockerBackupLabel')}
contentGetter={API.config.getBackup}
/>
</PermissionGuard>
</>}
</Stack>
@@ -362,19 +368,21 @@ const ServApps = ({stack}) => {
<Typography variant="h6" color="text.secondary">
{t('menu-items.management.urls')}
</Typography>
<Chip
label={t('mgmt.servApps.newChip.newLabel')}
color="primary"
size="small"
style={{paddingRight: '4px'}}
deleteIcon={<PlusCircleOutlined />}
onClick={() => {
setOpenModal(app.app);
}}
onDelete={() => {
setOpenModal(app.app);
}}
/>
<PermissionGuard permission={PERM_RESOURCES}>
<Chip
label={t('mgmt.servApps.newChip.newLabel')}
color="primary"
size="small"
style={{paddingRight: '4px'}}
deleteIcon={<PlusCircleOutlined />}
onClick={() => {
setOpenModal(app.app);
}}
onDelete={() => {
setOpenModal(app.app);
}}
/>
</PermissionGuard>
</Stack>
<Stack style={noOver} spacing={1} direction="row" alignItems="center" >
{getContainersRoutes(config, app.name.replace('/', '')).map((route) => {
+4 -2
View File
@@ -16,6 +16,8 @@ import PrettyTableView from '../../components/tableView/prettyTableView';
import NewVolumeButton from './createVolumes';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import PermissionGuard from '../../components/permissionGuard';
import { PERM_RESOURCES } from '../../utils/permissions';
const VolumeManagementList = () => {
const { t } = useTranslation();
@@ -84,7 +86,7 @@ const VolumeManagementList = () => {
clickable: true,
field: (r) => (
<>
<Button
<PermissionGuard permission={PERM_RESOURCES}><Button
variant="contained"
color="error"
startIcon={<DeleteOutlined />}
@@ -104,7 +106,7 @@ const VolumeManagementList = () => {
}}
>
{tryDelete === r.Name ? t('global.confirmDeletion') : t('global.delete')}
</Button>
</Button></PermissionGuard>
</>
),
},
+21 -16
View File
@@ -30,6 +30,9 @@ import ResponsiveButton from "../../components/responseiveButton";
import SMARTDialog, { CompleteDataSMARTDisk, diskChip, diskColor, getSMARTDef, temperatureChip } from "./smart";
import { useTranslation } from 'react-i18next';
import VMWarning from "./vmWarning";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
const diskStyle = {
width: "100%",
@@ -56,17 +59,19 @@ const FormatButton = ({disk, refresh, disabled}) => {
const [passwordConfirm, setPasswordConfirm] = useState(false);
return <>
<LoadingButton
disabled={disabled}
loading={loading}
onClick={() => {setPasswordConfirm(true); setLoading(true)}}
variant="outlined"
color="error"
size="small"
startIcon={<CloseCircleOutlined />}
>
{t('mgmt.storage.formatButton')}
</LoadingButton>
<PermissionGuard permission={PERM_RESOURCES}>
<LoadingButton
disabled={disabled}
loading={loading}
onClick={() => {setPasswordConfirm(true); setLoading(true)}}
variant="outlined"
color="error"
size="small"
startIcon={<CloseCircleOutlined />}
>
{t('mgmt.storage.formatButton')}
</LoadingButton>
</PermissionGuard>
{passwordConfirm && <FormatModal
OnClose={() => {
@@ -183,11 +188,11 @@ const Disk = ({disk, refresh, SetSMARTDialogOpened, containerized}) => {
</Stack>
<Stack spacing={2} direction="column" justifyContent={"center"}>
{(disk.type == "disk" || disk.type == "part") ? <FormatButton disabled={containerized} disk={disk} refresh={refresh}/> : ""}
{disk.mountpoint ? <MountDiskDialog disabled={containerized} disk={disk} unmount={true} refresh={refresh} /> : ""}
{(
(disk.type == "part" || (disk.type == "disk" && (!disk.children || !disk.children.length))) &&
(disk.type == "part" || (disk.type == "disk" && (!disk.children || !disk.children.length))) &&
disk.fstype &&
disk.fstype !== "swap" &&
disk.fstype !== "linux_raid_member" &&
@@ -208,7 +213,8 @@ const Disk = ({disk, refresh, SetSMARTDialogOpened, containerized}) => {
export const StorageDisks = () => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [config, setConfig] = useState(null);
const [disks, setDisks] = useState([]);
const [containerized, setContainerized] = useState(false);
@@ -224,7 +230,6 @@ export const StorageDisks = () => {
});
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
setDisks(disksData);
setContainerized(status.data.containerized);
};
+3 -2
View File
@@ -8,6 +8,7 @@ import * as API from '../../api';
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
import { useClientInfos } from '../../utils/hooks';
import { PERM_RESOURCES_READ } from '../../utils/permissions';
import { StorageMounts } from './mounts';
import { StorageDisks } from './disks';
import { StorageMerges } from './merges';
@@ -20,8 +21,8 @@ import RCloneServePage from './rclone/rclone_serve_config';
const StorageIndex = () => {
const { t } = useTranslation();
const {role} = useClientInfos();
const isAdmin = role === "2";
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES_READ);
const [coStatus, setCoStatus] = React.useState(null);
const refreshStatus = () => {
+12 -8
View File
@@ -9,6 +9,8 @@ import * as API from '../../api';
import { MountPicker } from "./mountPicker";
import ResponsiveButton from "../../components/responseiveButton";
import { PlusCircleOutlined } from "@ant-design/icons";
import PermissionGuard from "../../components/permissionGuard";
import { PERM_RESOURCES } from "../../utils/permissions";
import { Trans, useTranslation } from 'react-i18next';
const MergerDialogInternal = ({ refresh, open, setOpen, data }) => {
@@ -129,14 +131,16 @@ const MergerDialog = ({ refresh, disabled }) => {
return <>
{open && <MergerDialogInternal refresh={refresh} open={open} setOpen={setOpen}/>}
<ResponsiveButton
disabled={disabled}
onClick={() => {setOpen(true);}}
variant="contained"
startIcon={<PlusCircleOutlined />}
size="small"
>{t('mgmt.storage.newMerge.newMergeButton')}</ResponsiveButton>
<PermissionGuard permission={PERM_RESOURCES}>
<ResponsiveButton
disabled={disabled}
onClick={() => {setOpen(true);}}
variant="contained"
startIcon={<PlusCircleOutlined />}
size="small"
>{t('mgmt.storage.newMerge.newMergeButton')}</ResponsiveButton>
</PermissionGuard>
</>
}
+12 -7
View File
@@ -18,10 +18,14 @@ import ResponsiveButton from "../../components/responseiveButton";
import MenuButton from "../../components/MenuButton";
import { useTranslation } from 'react-i18next';
import VMWarning from "./vmWarning";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
export const StorageMerges = () => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [config, setConfig] = useState(null);
const [mounts, setMounts] = useState([]);
const [mergeDialog, setMergeDialog] = useState(null);
@@ -35,7 +39,6 @@ export const StorageMerges = () => {
let status = await API.getStatus();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
setMounts(mountsData.data);
setContainerized(status.data.containerized);
setLoading(false);
@@ -83,11 +86,13 @@ export const StorageMerges = () => {
title: '',
clickable:true,
field: (r) => <>
<DeleteIconButton disabled={containerized} onDelete={() => {
API.storage.mounts.unmount({ mountPoint: r.path, permanent: true }).then(() => {
refresh();
});
}} />
<PermissionGuard permission={PERM_RESOURCES}>
<DeleteIconButton disabled={containerized} onDelete={() => {
API.storage.mounts.unmount({ mountPoint: r.path, permanent: true }).then(() => {
refresh();
});
}} />
</PermissionGuard>
</>
},
]}
+11 -7
View File
@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
import * as API from '../../api';
import { DownCircleOutlined, UpCircleOutlined } from "@ant-design/icons";
import PermissionGuard from "../../components/permissionGuard";
import { PERM_RESOURCES } from "../../utils/permissions";
const MountDiskDialogInternal = ({disk, unmount, refresh, open, setOpen }) => {
const { t } = useTranslation();
@@ -114,13 +116,15 @@ const MountDiskDialog = ({ disk, unmount, refresh, disabled }) => {
return <>
{open && <MountDiskDialogInternal disk={disk} unmount={unmount} refresh={refresh} open={open} setOpen={setOpen}/>}
<Button
onClick={() => {setOpen(true);}}
variant="outlined"
size="small"
disabled={disabled}
startIcon={unmount ? <DownCircleOutlined /> : <UpCircleOutlined />}
>{unmount ? t('global.unmount') : t('global.mount')}</Button>
<PermissionGuard permission={PERM_RESOURCES}>
<Button
onClick={() => {setOpen(true);}}
variant="outlined"
size="small"
disabled={disabled}
startIcon={unmount ? <DownCircleOutlined /> : <UpCircleOutlined />}
>{unmount ? t('global.unmount') : t('global.mount')}</Button>
</PermissionGuard>
</>
}
+22 -17
View File
@@ -19,10 +19,14 @@ import MountDialog, { MountDialogInternal } from "./mountDialog";
import ResponsiveButton from "../../components/responseiveButton";
import { useTranslation } from 'react-i18next';
import VMWarning from "./vmWarning";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
export const StorageMounts = () => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [config, setConfig] = useState(null);
const [mounts, setMounts] = useState([]);
const [mountDialog, setMountDialog] = useState(null);
@@ -36,7 +40,6 @@ export const StorageMounts = () => {
let status = await API.getStatus();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
setMounts(mountsData.data);
setLoading(false);
setContainerized(status.data.containerized);
@@ -54,7 +57,7 @@ export const StorageMounts = () => {
data={mounts}
getKey={(r) => `${r.device} - ${refresh.path}`}
buttons={[
<ResponsiveButton startIcon={<PlusCircleOutlined />} disabled={containerized} variant="contained" onClick={() => setMountDialog({data: null, unmount: false})}>{t('mgmt.storage.newMount.newMountButton')}</ResponsiveButton>,
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton startIcon={<PlusCircleOutlined />} disabled={containerized} variant="contained" onClick={() => setMountDialog({data: null, unmount: false})}>{t('mgmt.storage.newMount.newMountButton')}</ResponsiveButton></PermissionGuard>,
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
refresh();
}}>{t('global.refresh')}</ResponsiveButton>
@@ -80,20 +83,22 @@ export const StorageMounts = () => {
title: '',
field: (r) => <>
<div style={{position: 'relative'}}>
<MenuButton>
<MenuItem disabled={!r.device.startsWith('/dev/') || loading || containerized} onClick={() => setMountDialog({data: r, unmount: false})}>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText >{t('global.edit')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => setMountDialog({data: r, unmount: true})}>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText >{t('global.unmount')}</ListItemText>
</MenuItem>
</MenuButton>
<PermissionGuard permission={PERM_RESOURCES}>
<MenuButton>
<MenuItem disabled={!r.device.startsWith('/dev/') || loading || containerized} onClick={() => setMountDialog({data: r, unmount: false})}>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText >{t('global.edit')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => setMountDialog({data: r, unmount: true})}>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText >{t('global.unmount')}</ListItemText>
</MenuItem>
</MenuButton>
</PermissionGuard>
</div>
</>
},
+39 -34
View File
@@ -19,6 +19,9 @@ import diskIcon from '../../assets/images/icons/disk.svg';
import ResponsiveButton from "../../components/responseiveButton";
import { useTranslation } from 'react-i18next';
import VMWarning from "./vmWarning";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
const getStatus = (status) => {
if (!status) {
@@ -59,7 +62,8 @@ const cleanStatus = (status) => {
export const Parity = () => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [config, setConfig] = useState(null);
const [parities, setParities] = useState([]);
const [loading, setLoading] = useState(false);
@@ -73,7 +77,6 @@ export const Parity = () => {
let configAsync = await API.config.get();
let status = await API.getStatus();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
setParities(paritiesData.data);
setLoading(false);
@@ -205,38 +208,40 @@ export const Parity = () => {
title: '',
field: (r) => {
return <div style={{position: 'relative'}}>
<MenuButton>
<MenuItem disabled={loading || containerized} onClick={() => setEditOpened(r)}>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.edit')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => sync(r.Name)}>
<ListItemIcon>
<CloudOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.list.syncText')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => scrub(r.Name)}>
<ListItemIcon>
<CompassOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.list.scrubText')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => fix(r.Name)}>
<ListItemIcon>
<CloudOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.list.fixText')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => tryDeleteRaid(r.Name)}>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.delete')}</ListItemText>
</MenuItem>
</MenuButton>
<PermissionGuard permission={PERM_RESOURCES}>
<MenuButton>
<MenuItem disabled={loading || containerized} onClick={() => setEditOpened(r)}>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.edit')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => sync(r.Name)}>
<ListItemIcon>
<CloudOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.list.syncText')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => scrub(r.Name)}>
<ListItemIcon>
<CompassOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.list.scrubText')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => fix(r.Name)}>
<ListItemIcon>
<CloudOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.list.fixText')}</ListItemText>
</MenuItem>
<MenuItem disabled={loading || containerized} onClick={() => tryDeleteRaid(r.Name)}>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.delete')}</ListItemText>
</MenuItem>
</MenuButton>
</PermissionGuard>
</div>
}
}
+36 -31
View File
@@ -12,6 +12,9 @@ import ResponsiveButton from "../../components/responseiveButton";
import { useTranslation } from 'react-i18next';
import VMWarning from "./vmWarning";
import { RaidDialog, RaidDialogInternal } from "./raidDialog";
import { useClientInfos } from "../../utils/hooks";
import { PERM_RESOURCES } from "../../utils/permissions";
import PermissionGuard from "../../components/permissionGuard";
const getRAIDStatus = (details) => {
if (!details || !details["Array State"]) {
@@ -43,7 +46,8 @@ const cleanRAIDStatus = (details) => {
export const RAIDArrays = () => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const { hasPermission } = useClientInfos();
const isAdmin = hasPermission(PERM_RESOURCES);
const [config, setConfig] = useState(null);
const [raids, setRaids] = useState([]);
const [loading, setLoading] = useState(false);
@@ -58,7 +62,6 @@ export const RAIDArrays = () => {
let configAsync = await API.config.get();
let status = await API.getStatus();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
setRaids(raidsData.data);
setLoading(false);
setContainerized(status.data.containerized);
@@ -174,35 +177,37 @@ export const RAIDArrays = () => {
title: '',
field: (r) => {
return <div style={{position: 'relative'}}>
<MenuButton>
<MenuItem
disabled={loading || containerized}
onClick={() => resizeRaid(r.device.split('/').pop())}
>
<ListItemIcon>
<CloudOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.raid.resize')}</ListItemText>
</MenuItem>
<MenuItem
disabled={loading || containerized}
onClick={() => setEditOpened(r)}
>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.edit')}</ListItemText>
</MenuItem>
<MenuItem
disabled={loading || containerized}
onClick={() => tryDeleteRaid(r.device.split('/').pop())}
>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.delete')}</ListItemText>
</MenuItem>
</MenuButton>
<PermissionGuard permission={PERM_RESOURCES}>
<MenuButton>
<MenuItem
disabled={loading || containerized}
onClick={() => resizeRaid(r.device.split('/').pop())}
>
<ListItemIcon>
<CloudOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('mgmt.storage.raid.resize')}</ListItemText>
</MenuItem>
<MenuItem
disabled={loading || containerized}
onClick={() => setEditOpened(r)}
>
<ListItemIcon>
<EditOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.edit')}</ListItemText>
</MenuItem>
<MenuItem
disabled={loading || containerized}
onClick={() => tryDeleteRaid(r.device.split('/').pop())}
>
<ListItemIcon>
<DeleteOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t('global.delete')}</ListItemText>
</MenuItem>
</MenuButton>
</PermissionGuard>
</div>
}
}
+12 -8
View File
@@ -7,6 +7,8 @@ import * as yup from 'yup';
import * as API from '../../api';
import ResponsiveButton from "../../components/responseiveButton";
import { PlusCircleOutlined } from "@ant-design/icons";
import PermissionGuard from "../../components/permissionGuard";
import { PERM_RESOURCES } from "../../utils/permissions";
import { MountPicker } from "./mountPicker";
import { Trans } from 'react-i18next';
@@ -234,14 +236,16 @@ export const RaidDialog = ({ refresh, disabled }) => {
return (
<>
<ResponsiveButton
variant="contained"
startIcon={<PlusCircleOutlined />}
onClick={() => setOpen(true)}
disabled={disabled}
>
{t('mgmt.storage.raid.create')}
</ResponsiveButton>
<PermissionGuard permission={PERM_RESOURCES}>
<ResponsiveButton
variant="contained"
startIcon={<PlusCircleOutlined />}
onClick={() => setOpen(true)}
disabled={disabled}
>
{t('mgmt.storage.raid.create')}
</ResponsiveButton>
</PermissionGuard>
<RaidDialogInternal
refresh={refresh}
open={open}
@@ -15,6 +15,8 @@ import RCloneTransfers from './rclone-transfers';
import MiniPlotComponent from '../../dashboard/components/mini-plot';
import { FilePickerButton } from '../../../components/filePicker';
import VMWarning from '../vmWarning';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES, PERM_CREDENTIALS_READ } from '../../../utils/permissions';
const RClonePage = ({coStatus, containerized}) => {
const { t } = useTranslation();
@@ -35,7 +37,7 @@ const RClonePage = ({coStatus, containerized}) => {
}
const refresh = (delay = false) => {
API.rclone.list().then((response) => {
API.rclone.listRemotes().then((response) => {
setProviders(response);
if(delay) {
setTimeout(() => {
@@ -83,14 +85,14 @@ const RClonePage = ({coStatus, containerized}) => {
data={Object.keys(providers).map(key => ({name: key, ...providers[key]}))}
getKey={(r, k) => `${r.name + k}`}
buttons={[
<ResponsiveButton startIcon={<PlusCircleOutlined />} variant="contained" onClick={() => setConfigModal(true)}>{t('mgmt.storage.rclone.create')}</ResponsiveButton>,
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton startIcon={<PlusCircleOutlined />} variant="contained" onClick={() => setConfigModal(true)}>{t('mgmt.storage.rclone.create')}</ResponsiveButton></PermissionGuard>,
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
refresh();
}}>{t('global.refresh')}</ResponsiveButton>,
<RCloneTransfers refreshStats={refreshStats} transferring={coreStats.transferring ? coreStats.transferring : []}/>,
<ResponsiveButton variant="outlined" startIcon={<SettingOutlined />} onClick={() => {
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton variant="outlined" startIcon={<SettingOutlined />} onClick={() => {
API.rclone.restart()
}}>{t('mgmt.storage.rclone.remountAll')}</ResponsiveButton>
}}>{t('mgmt.storage.rclone.remountAll')}</ResponsiveButton></PermissionGuard>
]}
columns={[
{
@@ -159,17 +161,17 @@ const RClonePage = ({coStatus, containerized}) => {
title: '',
field: (r) => <>
<div style={{position: 'relative'}}>
<DeleteIconButton onDelete={() => {
<PermissionGuard permission={PERM_RESOURCES}><DeleteIconButton onDelete={() => {
API.rclone.deleteRemote(r.name).then(() => {
refresh();
});
}} />
<IconButton variant="outlined"
}} /></PermissionGuard>
<PermissionGuard permission={PERM_CREDENTIALS_READ}><IconButton variant="outlined"
onClick={() => {
setConfigModal(r);
}}>
<EditOutlined />
</IconButton>
</IconButton></PermissionGuard>
<FilePickerButton storage={r.name} />
</div>
</>,
@@ -8,6 +8,8 @@ import ResponsiveButton from '../../../components/responseiveButton';
import { DeleteIconButton } from '../../../components/delete';
import VMWarning from '../vmWarning';
import RCloneNewServeConfig from './rclone_serve_new';
import PermissionGuard from '../../../components/permissionGuard';
import { PERM_RESOURCES } from '../../../utils/permissions';
const RCloneServePage = ({containerized}) => {
const { t } = useTranslation();
@@ -36,13 +38,13 @@ const RCloneServePage = ({containerized}) => {
data={(config.RemoteStorage ? (config.RemoteStorage.Shares || []) : [])}
getKey={(r, k) => `${r.name + k}`}
buttons={[
<ResponsiveButton startIcon={<PlusCircleOutlined />} variant="contained" onClick={() => setConfigModal(true)}>{t('mgmt.storage.rclone.create-serve')}</ResponsiveButton>,
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton startIcon={<PlusCircleOutlined />} variant="contained" onClick={() => setConfigModal(true)}>{t('mgmt.storage.rclone.create-serve')}</ResponsiveButton></PermissionGuard>,
<ResponsiveButton variant="outlined" startIcon={<ReloadOutlined />} onClick={() => {
refresh();
}}>{t('global.refresh')}</ResponsiveButton>,
<ResponsiveButton variant="outlined" startIcon={<SettingOutlined />} onClick={() => {
<PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton variant="outlined" startIcon={<SettingOutlined />} onClick={() => {
API.rclone.restart()
}}>{t('mgmt.storage.rclone.remountAll')}</ResponsiveButton>
}}>{t('mgmt.storage.rclone.remountAll')}</ResponsiveButton></PermissionGuard>
]}
columns={[
{
@@ -57,7 +59,7 @@ const RCloneServePage = ({containerized}) => {
title: '',
field: (r) => <>
<div style={{position: 'relative'}}>
<DeleteIconButton onDelete={() => {
<PermissionGuard permission={PERM_RESOURCES}><DeleteIconButton onDelete={() => {
return API.config.get().then(({data}) => {
let shares = data.RemoteStorage.Shares || [];
shares = shares.filter((s) => s.Name !== r.Name);
@@ -70,13 +72,13 @@ const RCloneServePage = ({containerized}) => {
}
})
});
}} />
<IconButton variant="outlined"
}} /></PermissionGuard>
<PermissionGuard permission={PERM_RESOURCES}><IconButton variant="outlined"
onClick={() => {
setConfigModal(r);
}}>
<EditOutlined />
</IconButton>
</IconButton></PermissionGuard>
</div>
</>,
}
+5 -3
View File
@@ -9,6 +9,8 @@ import * as API from '../../api';
import { MountPicker } from "./mountPicker";
import ResponsiveButton from "../../components/responseiveButton";
import { PlusCircleOutlined } from "@ant-design/icons";
import PermissionGuard from "../../components/permissionGuard";
import { PERM_RESOURCES } from "../../utils/permissions";
import { crontabToText } from "../../utils/indexs";
import { MountPickerEntry } from "./mountPickerEntry";
import { json } from "react-router";
@@ -195,14 +197,14 @@ const SnapRAIDDialog = ({ refresh, data, disabled }) => {
{open && <SnapRAIDDialogInternal refresh={refresh} open={open} setOpen={setOpen} data={data} />}
<div>
{!data ? <ResponsiveButton
{!data ? <PermissionGuard permission={PERM_RESOURCES}><ResponsiveButton
onClick={() => {setOpen(true);}}
variant="contained"
size="small"
disabled={disabled}
startIcon={<PlusCircleOutlined />}
>{t('mgmt.storage.snapraid.createParity.newDisks')}</ResponsiveButton> :
<div onClick={() => setOpen(true)}>{t('global.edit')}</div>}
>{t('mgmt.storage.snapraid.createParity.newDisks')}</ResponsiveButton></PermissionGuard> :
<PermissionGuard permission={PERM_RESOURCES}><div onClick={() => setOpen(true)}>{t('global.edit')}</div></PermissionGuard>}
</div>
</>
}
+30 -24
View File
@@ -1,6 +1,7 @@
import React from 'react';
import { useCookies } from 'react-cookie';
import { logout } from '../api/authentication';
import { SUDO_PERMISSIONS } from './permissions';
const isDemo = import.meta.env.MODE === 'demo';
@@ -9,43 +10,48 @@ function useClientInfos() {
if(isDemo) return {
nickname: "Demo",
role: "2",
userRole: "2"
hasPermission: () => true,
hasRolePermission: () => true,
needsSudo: false,
};
let clientInfos = null;
try {
// Try to parse the cookie into a JavaScript object
clientInfos = cookies['client-infos'].split(',');
if(clientInfos.length <= 3) {
const parts = cookies['client-infos'].split(',');
// Format: nickname,perm1-perm2-...,sudoUntil
if(parts.length < 3) {
window.location.href = '/cosmos-ui/logout';
}
let res = {
nickname: clientInfos[0],
userRole: clientInfos[1],
role: clientInfos[2]
};
// If role is admin, check if the timeout has expired
const nickname = parts[0];
const permissions = parts[1].split('-').map(Number);
const sudoUntilTs = parseInt(parts[2], 10);
if(clientInfos.length > 3) {
let roleUntil = new Date(parseInt(clientInfos[3], 10) * 1000);
let currentDate = new Date();
if(roleUntil < currentDate && res.role == "2") {
res.role = "1";
}
// Determine sudo state from timestamp
let isSudoed = false;
if(sudoUntilTs > 0) {
isSudoed = new Date(sudoUntilTs * 1000) > new Date();
}
return res;
const hasSudoPerms = permissions.some(p => SUDO_PERMISSIONS.includes(p));
const needsSudo = hasSudoPerms && !isSudoed;
return {
nickname,
hasPermission: (perm) => {
if(!permissions.includes(perm)) return false;
if(SUDO_PERMISSIONS.includes(perm)) return isSudoed;
return true;
},
hasRolePermission: (perm) => permissions.includes(perm),
needsSudo,
};
} catch (error) {
console.error('Error parsing client-infos cookie:', error);
return {
nickname: "",
role: "2"
hasPermission: () => true,
hasRolePermission: () => true,
needsSudo: false,
};
}
}
+32 -1
View File
@@ -44,6 +44,7 @@
"global.confirmAction": "Confirm",
"global.confirmDelete": "Confirm Delete",
"global.confirmDeletion": "Are you sure?",
"global.copied": "Copied!",
"global.copyFilenameSuffix": "Copy",
"global.createAction": "Create",
"global.duplicate": "Duplicate",
@@ -51,6 +52,7 @@
"global.delete": "Delete",
"global.default": "Default",
"global.description": "Description",
"global.done": "Done",
"global.driver": "Driver",
"global.edit": "Edit",
"global.emailInvalidValidation": "Must be a valid email",
@@ -67,6 +69,7 @@
"global.moveToTop": "Move to Top",
"global.moveUp": "Move Up",
"global.name.validation": "Name is required",
"global.monitor": "Monitor",
"global.nameTitle": "Name",
"global.network": "Network",
"global.networks": "Networks",
@@ -74,7 +77,10 @@
"global.next": "Next",
"global.nicknameLabel": "Nickname",
"global.nicknameRequiredValidation": "Nickname is required",
"global.none": "None",
"global.normal": "Normal",
"global.owner": "Owner",
"global.permissions": "Permissions",
"global.refresh": "Refresh",
"global.refreshPage": "Refresh Page",
"global.required": "Required",
@@ -125,6 +131,28 @@
"menu-items.support.discord": "Discord",
"menu-items.support.docsTitle": "Documentation",
"menu-items.support.github": "Github",
"mgmt.config.apiTokens.copyWarning": "Copy this token now. It will not be shown again.",
"mgmt.config.apiTokens.createTitle": "Create API Token",
"mgmt.config.apiTokens.deleteConfirm": "Are you sure you want to delete the token \"{{name}}\"?",
"mgmt.config.apiTokens.events": "Events — {{name}}",
"mgmt.config.apiTokens.fullAccess": "Full Access",
"mgmt.config.apiTokens.ipWhitelist": "IP Whitelist (comma-separated)",
"mgmt.config.apiTokens.ipWhitelistHelper": "Leave empty to allow all IPs",
"mgmt.config.apiTokens.newToken": "New Token",
"mgmt.config.apiTokens.noTokens": "No API tokens created yet.",
"mgmt.config.apiTokens.permAdminRead": "Admin Read",
"mgmt.config.apiTokens.permAdmin": "Admin",
"mgmt.config.apiTokens.permUsersRead": "Users Read",
"mgmt.config.apiTokens.permUsers": "Users",
"mgmt.config.apiTokens.permResourcesRead": "Resources Read",
"mgmt.config.apiTokens.permResources": "Resources",
"mgmt.config.apiTokens.permConfigRead": "Configuration Read",
"mgmt.config.apiTokens.permConfig": "Configuration",
"mgmt.config.apiTokens.permUnknown": "Permission {{id}}",
"mgmt.config.apiTokens.readOnly": "Read Only",
"mgmt.config.apiTokens.restrictToConstellation": "Restrict to Constellation network",
"mgmt.config.apiTokens.tokenCreated": "Token Created",
"mgmt.config.apiTokens.tokenName": "Token Name",
"mgmt.config.appearance.appDetailsOnHomepageCheckbox.appDetailsOnHomepageLabel": "Show Application Details on Homepage",
"mgmt.config.appearance.primaryColorSlider": "Primary Color",
"mgmt.config.appearance.resetColorsButton.resetColorsLabel": "Reset Colors",
@@ -1083,5 +1111,8 @@
],
"trust.platforms.notAvailable": "Platform-specific instructions not available. Please check the instructions below for your platform.",
"sudo.title": "Enable Admin Privileges",
"sudo.description": "You are about to enable admin privileges. Please enter your password to continue. This will expire after 2h of inactivity."
"sudo.description": "You are about to enable admin privileges. Please enter your password to continue. This will expire after 2h of inactivity.",
"sudo.required": "Enable Admin privileges with the button on the top right to use this function",
"mgmt.servapps.compose.credentialsRequired": "The Compose editor requires access to container credentials (environment variables, secrets). Please enable Admin privileges using the button on the top right to view and edit the Compose configuration.",
"mgmt.servapps.compose.credentialsDenied": "The Compose editor requires access to container credentials (environment variables, secrets). Your current role does not have permission to view credentials."
}
+16
View File
@@ -0,0 +1,16 @@
// Permission constants — must match src/utils/types.go
export const PERM_ADMIN_READ = 1;
export const PERM_ADMIN = 2;
export const PERM_USERS_READ = 10;
export const PERM_USERS = 11;
export const PERM_RESOURCES_READ = 20;
export const PERM_RESOURCES = 21;
export const PERM_CONFIGURATION_READ = 30;
export const PERM_CONFIGURATION = 31;
export const PERM_CREDENTIALS_READ = 40;
export const PERM_LOGIN = 100;
export const PERM_LOGIN_WEAK = 101;
// Global: permissions that require sudo for ANY role
// Must match SudoPermissions in types.go
export const SUDO_PERMISSIONS = [PERM_ADMIN, PERM_USERS, PERM_RESOURCES, PERM_CONFIGURATION, PERM_CREDENTIALS_READ];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.22.0-unstable02",
"version": "0.22.0-unstable03",
"description": "",
"main": "test-server.js",
"bugs": {
+1 -1
View File
@@ -312,7 +312,7 @@ func detectCallbackEndpoint(w http.ResponseWriter, req *http.Request) {
return
}
userMod.SendUserToken(w, req, userInBase, true, utils.GUEST)
userMod.SendUserToken(w, req, userInBase, true, false)
// redirect to user page
// TODO: redirect to RIGHT PAGE
+2 -2
View File
@@ -9,11 +9,11 @@ func authEndpoint(rw http.ResponseWriter, req *http.Request) {
// This context will be passed to all methods.
ctx := req.Context()
if utils.LoggedInOnly(rw, req) != nil {
if utils.CheckPermissions(rw, req, utils.PERM_LOGIN) != nil {
return
}
nickname := req.Header.Get("x-cosmos-user")
nickname := utils.GetAuthContext(req).Nickname
hostname := utils.GetMainConfig().HTTPConfig.Hostname
if utils.IsHTTPS {
+1 -1
View File
@@ -85,7 +85,7 @@ func userInfosEndpoint(rw http.ResponseWriter, req *http.Request) {
baseToken.Email = user.Email
}
if user.Role == utils.ADMIN {
if utils.HasPermission(req, utils.PERM_ADMIN_READ) {
baseToken.Role = "admin"
} else {
baseToken.Role = "user"
+12 -12
View File
@@ -11,7 +11,7 @@ import (
)
func AddBackupRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -120,7 +120,7 @@ func AddBackupRoute(w http.ResponseWriter, req *http.Request) {
}
func EditBackupRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -159,7 +159,7 @@ func EditBackupRoute(w http.ResponseWriter, req *http.Request) {
}
func RemoveBackupRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -238,7 +238,7 @@ func RemoveBackupRoute(w http.ResponseWriter, req *http.Request) {
}
func ListSnapshotsRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -277,7 +277,7 @@ func ListSnapshotsRoute(w http.ResponseWriter, req *http.Request) {
}
func ListFoldersRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -319,7 +319,7 @@ func ListFoldersRoute(w http.ResponseWriter, req *http.Request) {
}
func RestoreBackupRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -394,7 +394,7 @@ func RestoreBackupRoute(w http.ResponseWriter, req *http.Request) {
}
func ListSnapshotsRouteFromRepo(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -433,7 +433,7 @@ func ListSnapshotsRouteFromRepo(w http.ResponseWriter, req *http.Request) {
}
func ListRepos(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -467,7 +467,7 @@ func ListRepos(w http.ResponseWriter, req *http.Request) {
}
func RepoStatsRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -505,7 +505,7 @@ func RepoStatsRoute(w http.ResponseWriter, req *http.Request) {
}
func ForgetSnapshotRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -538,7 +538,7 @@ func ForgetSnapshotRoute(w http.ResponseWriter, req *http.Request) {
}
func StatsRepositorySubfolderRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -579,7 +579,7 @@ func StatsRepositorySubfolderRoute(w http.ResponseWriter, req *http.Request) {
}
func UnlockRepositoryRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+233
View File
@@ -0,0 +1,233 @@
package configapi
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/http"
"regexp"
"time"
"github.com/azukaar/cosmos-server/src/constellation"
"github.com/azukaar/cosmos-server/src/utils"
)
var validTokenName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
type CreateAPITokenRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
ReadOnly bool `json:"readOnly"`
IPWhitelist []string `json:"ipWhitelist,omitempty"`
RestrictToConstellation bool `json:"restrictToConstellation"`
}
type DeleteAPITokenRequest struct {
Name string `json:"name"`
}
func APITokenRoute(w http.ResponseWriter, req *http.Request) {
if req.Method == "GET" {
listAPITokens(w, req)
} else if req.Method == "POST" {
createAPIToken(w, req)
} else if req.Method == "DELETE" {
deleteAPIToken(w, req)
} else {
utils.Error("APITokenRoute: Method not allowed "+req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
}
}
func listAPITokens(w http.ResponseWriter, req *http.Request) {
if utils.CheckPermissions(w, req, utils.PERM_ADMIN_READ) != nil {
return
}
config := utils.GetMainConfig()
tokens := make(map[string]interface{})
for name, tok := range config.APITokens {
tokens[name] = map[string]interface{}{
"name": tok.Name,
"description": tok.Description,
"owner": tok.Owner,
"permissions": tok.Permissions,
"ipWhitelist": tok.IPWhitelist,
"restrictToConstellation": tok.RestrictToConstellation,
"createdAt": tok.CreatedAt,
}
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": tokens,
})
}
func createAPIToken(w http.ResponseWriter, req *http.Request) {
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
return
}
var request CreateAPITokenRequest
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
utils.Error("CreateAPIToken: Invalid request", err)
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "AT010")
return
}
if request.Name == "" {
utils.HTTPError(w, "Token name is required", http.StatusBadRequest, "AT011")
return
}
if len(request.Name) > 64 || !validTokenName.MatchString(request.Name) {
utils.HTTPError(w, "Token name must be 1-64 characters, alphanumeric, dash, or underscore", http.StatusBadRequest, "AT014")
return
}
// Generate raw token: "cosmos_" + base64url(32 random bytes)
rawBytes := make([]byte, 32)
if _, err := rand.Read(rawBytes); err != nil {
utils.Error("CreateAPIToken: Failed to generate random bytes", err)
utils.HTTPError(w, "Internal error", http.StatusInternalServerError, "AT012")
return
}
rawToken := "cosmos_" + base64.RawURLEncoding.EncodeToString(rawBytes)
// Hash for storage
h := sha256.Sum256([]byte(rawToken))
tokenHash := hex.EncodeToString(h[:])
// Build permissions array
var permissions []utils.Permission
if request.ReadOnly {
permissions = []utils.Permission{
utils.PERM_ADMIN_READ,
utils.PERM_USERS_READ,
utils.PERM_RESOURCES_READ,
utils.PERM_CONFIGURATION_READ,
}
} else {
permissions = []utils.Permission{
utils.PERM_ADMIN_READ,
utils.PERM_ADMIN,
utils.PERM_USERS_READ,
utils.PERM_USERS,
utils.PERM_RESOURCES_READ,
utils.PERM_RESOURCES,
utils.PERM_CONFIGURATION_READ,
utils.PERM_CONFIGURATION,
utils.PERM_CREDENTIALS_READ,
}
}
owner := utils.GetAuthContext(req).Nickname
tokenConfig := utils.APITokenConfig{
Name: request.Name,
Description: request.Description,
Owner: owner,
TokenHash: tokenHash,
Permissions: permissions,
IPWhitelist: request.IPWhitelist,
RestrictToConstellation: request.RestrictToConstellation,
CreatedAt: time.Now(),
}
utils.ConfigLock.Lock()
defer utils.ConfigLock.Unlock()
config := utils.ReadConfigFromFile()
if config.APITokens == nil {
config.APITokens = make(map[string]utils.APITokenConfig)
}
if _, exists := config.APITokens[request.Name]; exists {
utils.HTTPError(w, "Token with this name already exists", http.StatusConflict, "AT013")
return
}
config.APITokens[request.Name] = tokenConfig
utils.SetBaseMainConfig(config)
utils.TouchDatabase()
go constellation.SendNewDBSyncMessage()
utils.TriggerEvent(
"cosmos.api.token.created",
"API Token created",
"important",
"token@"+request.Name,
map[string]interface{}{
"tokenName": request.Name,
"owner": owner,
"readOnly": request.ReadOnly,
},
)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{
"token": rawToken,
"name": request.Name,
},
})
}
func deleteAPIToken(w http.ResponseWriter, req *http.Request) {
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
return
}
var request DeleteAPITokenRequest
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
utils.Error("DeleteAPIToken: Invalid request", err)
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "AT020")
return
}
if request.Name == "" {
utils.HTTPError(w, "Token name is required", http.StatusBadRequest, "AT021")
return
}
utils.ConfigLock.Lock()
defer utils.ConfigLock.Unlock()
config := utils.ReadConfigFromFile()
if config.APITokens == nil {
utils.HTTPError(w, "Token not found", http.StatusNotFound, "AT022")
return
}
if _, exists := config.APITokens[request.Name]; !exists {
utils.HTTPError(w, "Token not found", http.StatusNotFound, "AT022")
return
}
delete(config.APITokens, request.Name)
utils.SetBaseMainConfig(config)
utils.TouchDatabase()
go constellation.SendNewDBSyncMessage()
utils.TriggerEvent(
"cosmos.api.token.deleted",
"API Token deleted",
"important",
"token@"+request.Name,
map[string]interface{}{
"tokenName": request.Name,
},
)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
}
+2 -2
View File
@@ -26,7 +26,7 @@ type DashboardRoute struct {
}
func DashboardApiGet(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -36,7 +36,7 @@ func DashboardApiGet(w http.ResponseWriter, req *http.Request) {
return
}
isAdmin := utils.IsAdmin(req)
isAdmin := utils.HasPermission(req, utils.PERM_ADMIN)
config := utils.ReadConfigFromFile()
// Get containers if docker is available
+1 -1
View File
@@ -17,7 +17,7 @@ type DNSConfigRequest struct {
}
func ConfigApiDNS(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
return
}
+8 -4
View File
@@ -9,11 +9,12 @@ import (
)
func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
return
}
isAdmin := utils.IsAdmin(req)
isAdmin := utils.HasPermission(req, utils.PERM_CONFIGURATION_READ)
canReadCredentials := utils.HasPermission(req, utils.PERM_CREDENTIALS_READ)
if(req.Method == "GET") {
config := utils.ReadConfigFromFile()
@@ -22,7 +23,7 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
config.HTTPConfig.AuthPrivateKey = ""
config.HTTPConfig.TLSKey = ""
if !isAdmin {
if !canReadCredentials {
config.MongoDB = "***"
config.EmailConfig.Password = "***"
config.EmailConfig.Username = "***"
@@ -32,7 +33,10 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
config.HTTPConfig.DNSChallengeConfig = map[string]string{}
config.Licence = "***"
config.ServerToken = "***"
config.APITokens = map[string]utils.APITokenConfig{}
}
if !isAdmin {
// filter admin only routes
filteredRoutes := make([]utils.ProxyRouteConfig, 0)
for _, route := range config.HTTPConfig.ProxyConfig.Routes {
@@ -58,7 +62,7 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
}
func BackupFileApiGet(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_CREDENTIALS_READ) != nil {
return
}
+1 -1
View File
@@ -15,7 +15,7 @@ type UpdateRouteRequest struct {
}
func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
return
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
func ConfigApiRestart(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
return
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
return
}
+1 -1
View File
@@ -43,7 +43,7 @@ func DeviceBlock(w http.ResponseWriter, req *http.Request) {
nickname := utils.Sanitize(request.Nickname)
deviceName := utils.Sanitize(request.DeviceName)
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
if utils.CheckPermissionsOrSelf(w, req, nickname, utils.PERM_RESOURCES) != nil {
return
}
+2 -2
View File
@@ -59,12 +59,12 @@ func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
nickname := utils.Sanitize(request.Nickname)
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
if utils.CheckPermissionsOrSelf(w, req, nickname, utils.PERM_RESOURCES) != nil {
return
}
// Non-admin users can only create client devices
if !utils.IsAdmin(req) && request.IsLighthouse {
if !utils.HasPermission(req, utils.PERM_RESOURCES) && request.IsLighthouse {
utils.Error("DeviceCreation: Non-admin users can only create client devices", nil)
utils.HTTPError(w, "Device Creation Error: Only administrators can create lighthouse devices",
http.StatusForbidden, "DC006")
+1 -1
View File
@@ -22,7 +22,7 @@ func DeviceEdit_API(w http.ResponseWriter, req *http.Request) {
return
}
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+3 -3
View File
@@ -14,11 +14,11 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
return
}
if utils.LoggedInOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
return
}
isAdmin := utils.IsAdmin(req)
isAdmin := utils.HasPermission(req, utils.PERM_RESOURCES_READ)
// Connect to the collection
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
@@ -49,7 +49,7 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
}
} else {
// If not admin, get user's devices based on their nickname
nickname := req.Header.Get("x-cosmos-user")
nickname := utils.GetAuthContext(req).Nickname
cursor, err := c.Find(nil, map[string]interface{}{"Nickname": nickname})
defer cursor.Close(nil)
if err != nil {
+1 -1
View File
@@ -16,7 +16,7 @@ func DevicePing(w http.ResponseWriter, req *http.Request) {
return
}
if utils.LoggedInOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
return
}
+1 -1
View File
@@ -92,7 +92,7 @@ func API_GetNextIP(w http.ResponseWriter, req *http.Request) {
return
}
if utils.LoggedInOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
return
}
+5 -5
View File
@@ -55,7 +55,7 @@ func CheckConstellationToken(req *http.Request) error {
}
func API_GetConfig(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -81,7 +81,7 @@ func API_GetConfig(w http.ResponseWriter, req *http.Request) {
}
func API_Restart(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -102,7 +102,7 @@ func API_Restart(w http.ResponseWriter, req *http.Request) {
}
func API_Reset(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -122,7 +122,7 @@ func API_Reset(w http.ResponseWriter, req *http.Request) {
}
func API_GetLogs(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -145,7 +145,7 @@ func API_GetLogs(w http.ResponseWriter, req *http.Request) {
}
func API_Ping(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
return
}
+3 -3
View File
@@ -11,14 +11,14 @@ import (
)
func API_NewConstellation(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
if req.Method == "POST" {
utils.ConfigLock.Lock()
defer utils.ConfigLock.Unlock()
utils.Log("API_NewConstellation: creating new Constellation")
var request struct {
@@ -145,7 +145,7 @@ func API_NewConstellation(w http.ResponseWriter, req *http.Request) {
}
func API_ConnectToExisting(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+1 -1
View File
@@ -13,7 +13,7 @@ func TunnelList(w http.ResponseWriter, req *http.Request) {
return
}
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+47
View File
@@ -18,6 +18,8 @@ type SyncPayload struct {
CART string `json:"caCrt"`
CAKey string `json:"caKey"`
DNS SyncDNSPayload `json:"dns"`
RcloneConfig string `json:"rcloneConfig"`
ConfigData SyncConfigPayload `json:"configData"`
LastEdited int64 `json:"lastEdited"`
}
@@ -29,6 +31,11 @@ type SyncDNSPayload struct {
CustomDNSEntries []utils.ConstellationDNSEntry `json:"customDNSEntries"`
}
type SyncConfigPayload struct {
APITokens map[string]utils.APITokenConfig `json:"apiTokens,omitempty"`
Roles map[utils.Role]utils.RoleConfig `json:"roles,omitempty"`
}
type SyncRequestPayload struct {
EditedAt int64 `json:"editedAt"`
}
@@ -82,6 +89,13 @@ func MakeSyncPayload(rawPayload string) string {
CAKey = base64.StdEncoding.EncodeToString(caKeyData)
}
// Read rclone config
rcloneConfig := ""
rcloneConfigData, err := ioutil.ReadFile(utils.CONFIGFOLDER + "rclone.conf")
if err == nil {
rcloneConfig = base64.StdEncoding.EncodeToString(rcloneConfigData)
}
// Create payload
constellationConfig := utils.GetMainConfig().ConstellationConfig
@@ -99,6 +113,11 @@ func MakeSyncPayload(rawPayload string) string {
DNSAdditionalBlocklists: constellationConfig.DNSAdditionalBlocklists,
CustomDNSEntries: constellationConfig.CustomDNSEntries,
},
RcloneConfig: rcloneConfig,
ConfigData: SyncConfigPayload{
APITokens: utils.GetMainConfig().APITokens,
Roles: utils.GetMainConfig().Roles,
},
LastEdited: utils.GetFileLastModifiedTime(dbPath).Unix(),
}
@@ -192,6 +211,22 @@ func ReceiveSyncPayload(rawPayload string) bool {
}
}
// Write rclone config if present
needRcloneRemount := false
if payload.RcloneConfig != "" {
rcloneConfigData, err := base64.StdEncoding.DecodeString(payload.RcloneConfig)
if err != nil {
utils.Error("Constellation: ReceiveSyncPayload: Failed to decode rclone config", err)
} else {
err = ioutil.WriteFile(utils.CONFIGFOLDER+"rclone.conf", rcloneConfigData, 0644)
if err != nil {
utils.Error("Constellation: ReceiveSyncPayload: Failed to write rclone config file", err)
} else {
needRcloneRemount = true
}
}
}
// Update auth keys and DNS config
config := utils.ReadConfigFromFile()
config.HTTPConfig.AuthPrivateKey = payload.AuthPrivateKey
@@ -201,10 +236,22 @@ func ReceiveSyncPayload(rawPayload string) bool {
config.ConstellationConfig.DNSBlockBlacklist = payload.DNS.DNSBlockBlacklist
config.ConstellationConfig.DNSAdditionalBlocklists = payload.DNS.DNSAdditionalBlocklists
config.ConstellationConfig.CustomDNSEntries = payload.DNS.CustomDNSEntries
if payload.ConfigData.APITokens != nil {
config.APITokens = payload.ConfigData.APITokens
}
if payload.ConfigData.Roles != nil {
config.Roles = payload.ConfigData.Roles
}
utils.SetBaseMainConfig(config)
utils.CloseEmbeddedDB()
if needRcloneRemount && utils.InitRemoteStorage != nil {
go utils.InitRemoteStorage()
}
lastDBSyncTime = time.Now()
utils.TriggerEvent(
+6 -6
View File
@@ -9,7 +9,7 @@ import (
)
func ListJobs(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -58,7 +58,7 @@ type JobRequestJSON struct {
func StopJobRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -93,7 +93,7 @@ func StopJobRoute(w http.ResponseWriter, req *http.Request) {
}
func DeleteJobRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -132,7 +132,7 @@ func DeleteJobRoute(w http.ResponseWriter, req *http.Request) {
}
func RunJobRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -170,7 +170,7 @@ func RunJobRoute(w http.ResponseWriter, req *http.Request) {
}
func GetJobRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -215,7 +215,7 @@ func GetJobRoute(w http.ResponseWriter, req *http.Request) {
}
func GetRunningJobsRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
+1 -1
View File
@@ -19,7 +19,7 @@ var clients = make(map[*websocket.Conn]bool) // connected clients
// listenJobs handles new WebSocket requests from clients
func listenJobs(w http.ResponseWriter, r *http.Request) {
if utils.AdminOnly(w, r) != nil {
if utils.CheckPermissions(w, r, utils.PERM_RESOURCES_READ) != nil {
return
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
)
func CheckDNSRoute(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
return
}
@@ -48,7 +48,7 @@ func CheckDNSRoute(w http.ResponseWriter, req *http.Request) {
func GetDNSRoute(w http.ResponseWriter, req *http.Request) {
if !utils.GetMainConfig().NewInstall && (utils.LoggedInOnly(w, req) != nil) {
if !utils.GetMainConfig().NewInstall && (utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil) {
return
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
func AutoUpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+6 -12
View File
@@ -220,7 +220,7 @@ func Rollback(actions []DockerServiceCreateRollback , OnLog func(string)) {
}
func CreateServiceRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -953,6 +953,7 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
Name: container.Name,
DependsOn: container.DependsOn,
NetworkMode: string(hostConfig.NetworkMode),
PostInstall: container.PostInstall,
}
}
@@ -1007,6 +1008,9 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
// if post install
if len(container.PostInstall) > 0 {
utils.Log(fmt.Sprintf("Container %s started. Running %d post install commands...", container.Name, len(container.PostInstall)))
OnLog(fmt.Sprintf("Container %s started. Running %d post install commands...\n", container.Name, len(container.PostInstall)))
// run post install commands
for _, cmd := range container.PostInstall {
utils.Log(fmt.Sprintf("Running post install command: %s", cmd))
@@ -1034,19 +1038,9 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
Rollback(rollbackActions, OnLog)
return err
}
defer response.Close()
// run the command
err = DockerClient.ContainerExecStart(DockerContext, execResponse.ID, doctype.ExecStartCheck{})
if err != nil {
utils.Error("CreateService: Post Install", err)
OnLog(utils.DoErr("Rolling back changes because of -- Post install error: "+err.Error()))
Rollback(rollbackActions, OnLog)
return err
}
// read the output
out, _ := ioutil.ReadAll(response.Reader)
response.Close()
OnLog(fmt.Sprintf("----> %s", out))
}
+2 -2
View File
@@ -13,7 +13,7 @@ import (
var maxLimit = 1000
func ListContainersRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -45,7 +45,7 @@ func ListContainersRoute(w http.ResponseWriter, req *http.Request) {
}
func ExportContainerRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_CREDENTIALS_READ) != nil {
return
}
+16 -1
View File
@@ -4,13 +4,14 @@ import (
"context"
"encoding/json"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/azukaar/cosmos-server/src/utils"
)
func GetContainerRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -34,6 +35,20 @@ func GetContainerRoute(w http.ResponseWriter, req *http.Request) {
return
}
// Mask env var values if user lacks PERM_CREDENTIALS_READ
if !utils.HasPermission(req, utils.PERM_CREDENTIALS_READ) && container.Config != nil {
masked := make([]string, len(container.Config.Env))
for i, env := range container.Config.Env {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
masked[i] = parts[0] + "=***"
} else {
masked[i] = env
}
}
container.Config.Env = masked
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": container,
+1 -1
View File
@@ -79,7 +79,7 @@ func FilterLogs(logReader io.Reader, searchQuery string, limit int) []LogOutput
}
func GetContainerLogsRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
)
func InspectImageRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
)
func ManageContainerRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+1 -1
View File
@@ -13,7 +13,7 @@ type migrateToHostMode struct {
}
func MigrateToHostModeRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+6 -6
View File
@@ -13,7 +13,7 @@ import (
)
func ListNetworksRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -46,7 +46,7 @@ func ListNetworksRoute(w http.ResponseWriter, req *http.Request) {
func DeleteNetworkRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -127,7 +127,7 @@ func NetworkRoutes(w http.ResponseWriter, req *http.Request) {
}
func AttachNetwork(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -162,7 +162,7 @@ func AttachNetwork(w http.ResponseWriter, req *http.Request) {
}
func DetachNetwork(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -203,7 +203,7 @@ func DetachNetwork(w http.ResponseWriter, req *http.Request) {
}
func ListContainerNetworks(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
return
}
@@ -257,7 +257,7 @@ type createNetworkPayload struct {
}
func CreateNetworkRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
+24 -10
View File
@@ -42,8 +42,16 @@ func splitIntoChunks(input string) []string {
}
func TerminalRoute(w http.ResponseWriter, r *http.Request) {
if utils.AdminOnly(w, r) != nil {
return
readOnly := r.URL.Query().Get("readonly") == "true"
if readOnly {
if utils.CheckPermissions(w, r, utils.PERM_RESOURCES_READ) != nil {
return
}
} else {
if utils.CheckPermissions(w, r, utils.PERM_RESOURCES) != nil {
return
}
}
utils.Log("Attempting to attach container")
@@ -83,6 +91,12 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
var resp types.HijackedResponse
if readOnly && action == "new" {
utils.Error("Cannot create new shell in read-only mode", nil)
http.Error(w, "Cannot create new shell in read-only mode", http.StatusForbidden)
return
}
if action == "new" {
execConfig := types.ExecConfig{
Tty: true,
@@ -95,14 +109,14 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
execStart := types.ExecStartCheck{
Tty: true,
}
execResp, errExec := DockerClient.ContainerExecCreate(ctx, containerID, execConfig)
if errExec != nil {
utils.Error("ContainerExecCreate failed: ", errExec)
http.Error(w, "ContainerExecCreate failed: "+errExec.Error(), http.StatusInternalServerError)
return
}
resp, err = DockerClient.ContainerExecAttach(ctx, execResp.ID, execStart)
if err != nil {
utils.Error("ContainerExecAttach failed: ", err)
@@ -111,17 +125,17 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
}
// Start bash if it exists
resp.Conn.Write([]byte("bash\n"))
resp.Conn.Write([]byte("bash\n"))
utils.Log("Created new shell and attached to it in container " + containerID)
} else {
options := conttype.AttachOptions{
Stream: true,
Stdin: true,
Stdin: !readOnly,
Stdout: true,
Stderr: true,
}
// Attach to the container
resp, err = DockerClient.ContainerAttach(ctx, containerID, options)
if err != nil {
@@ -129,7 +143,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
http.Error(w, "ContainerAttach failed: "+err.Error(), http.StatusInternalServerError)
return
}
utils.Log("Attached to existing process in container " + containerID)
}
defer resp.Close()
@@ -205,7 +219,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) {
utils.Error("Failed to write to websocket: ", err)
return
}
} else {
} else if !readOnly {
utils.Debug("Writing message to container " + string(message))
_, err := resp.Conn.Write(message)
if err != nil {
+3 -3
View File
@@ -12,7 +12,7 @@ import (
)
func PullImageIfMissing(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -75,7 +75,7 @@ func PullImageIfMissing(w http.ResponseWriter, req *http.Request) {
}
func PullImage(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}
@@ -131,7 +131,7 @@ func PullImage(w http.ResponseWriter, req *http.Request) {
}
func CanUpdateImageRoute(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
return
}

Some files were not shown because too many files have changed in this diff Show More