mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-10-12 02:11:26 +00:00
[release] v0.22.0-unstable03
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>}
|
||||
<PermissionGuard permission={PERM_CONFIGURATION}>
|
||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||
setOpenNewModal(true);
|
||||
}}>{t('global.createAction')}</Button>
|
||||
</PermissionGuard>
|
||||
<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>)
|
||||
}
|
||||
<Button variant="contained" color="error" onClick={
|
||||
<PermissionGuard permission={PERM_USERS}><Button variant="contained" color="error" onClick={
|
||||
() => {
|
||||
setLoadingRow(r.nickname);
|
||||
setToAction(r.nickname);
|
||||
setOpenDeleteForm(true);
|
||||
}
|
||||
}>{t('global.delete')}</Button>
|
||||
<Button variant="contained" color="error" onClick={
|
||||
}>{t('global.delete')}</Button></PermissionGuard>
|
||||
<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></>
|
||||
}
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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>
|
||||
</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
}}>{t('global.createAction')}</Button></PermissionGuard>
|
||||
<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>
|
||||
<DeleteButton onDelete={(event) => deleteClient(event, k)} />
|
||||
}}>{t('mgmt.openId.resetSecret')}</Button></PermissionGuard>
|
||||
<PermissionGuard permission={PERM_CONFIGURATION}><DeleteButton onDelete={(event) => deleteClient(event, k)} /></PermissionGuard>
|
||||
</>,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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%' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.22.0-unstable02",
|
||||
"version": "0.22.0-unstable03",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user