Browse Source

[release] v0.7.0-unstable3

Yann Stepienik 2 years ago
parent
commit
6bc9e02e28
33 changed files with 733 additions and 320 deletions
  1. 1 1
      client/src/components/hostChip.jsx
  2. 1 1
      client/src/config.jsx
  3. 4 4
      client/src/isLoggedIn.jsx
  4. 1 1
      client/src/layout/MainLayout/Header/HeaderContent/index.jsx
  5. 3 3
      client/src/menu-items/dashboard.jsx
  6. 5 5
      client/src/menu-items/pages.jsx
  7. 1 1
      client/src/pages/authentication/Logoff.jsx
  8. 1 1
      client/src/pages/authentication/Signup.jsx
  9. 3 3
      client/src/pages/authentication/auth-forms/AuthLogin.jsx
  10. 1 1
      client/src/pages/authentication/auth-forms/AuthRegister.jsx
  11. 1 1
      client/src/pages/authentication/forgotPassword.jsx
  12. 3 3
      client/src/pages/authentication/newMFA.jsx
  13. 1 1
      client/src/pages/config/users/proxyman.jsx
  14. 1 1
      client/src/pages/config/users/usermanagement.jsx
  15. 36 11
      client/src/pages/market/listing.jsx
  16. 2 2
      client/src/pages/newInstall/newInstall.jsx
  17. 436 211
      client/src/pages/servapps/containers/docker-compose.jsx
  18. 2 0
      client/src/pages/servapps/containers/newService.jsx
  19. 22 11
      client/src/pages/servapps/containers/setup.jsx
  20. 21 8
      client/src/pages/servapps/containers/volumes.jsx
  21. 1 1
      client/src/pages/servapps/index.jsx
  22. 2 2
      client/src/pages/servapps/servapps.jsx
  23. 8 8
      client/src/routes/LoginRoutes.jsx
  24. 16 16
      client/src/routes/MainRoutes.jsx
  25. 22 1
      client/src/utils/indexs.js
  26. 116 2
      package-lock.json
  27. 6 3
      package.json
  28. 1 2
      readme.md
  29. 2 2
      src/authorizationserver/oauth2_discover.go
  30. 1 1
      src/docker/api_blueprint.go
  31. 2 2
      src/httpServer.go
  32. 4 4
      src/user/token.go
  33. 6 6
      src/utils/loggedIn.go

+ 1 - 1
client/src/components/hostChip.jsx

@@ -36,7 +36,7 @@ const HostChip = ({route, settings, style}) => {
         window.open(window.location.origin + route.PathPrefix, '_blank');
     }}
     onDelete={settings ? () => {
-      window.open('/ui/config-url/'+route.Name, '_blank');
+      window.open('/cosmos-ui/config-url/'+route.Name, '_blank');
     } : null}
     deleteIcon={settings ? <SettingOutlined /> : null}
   />

+ 1 - 1
client/src/config.jsx

@@ -1,7 +1,7 @@
 // ==============================|| THEME CONFIG  ||============================== //
 
 const config = {
-    defaultPath: '/ui',
+    defaultPath: '/cosmos-ui',
     fontFamily: `'Public Sans', sans-serif`,
     i18n: 'en',
     miniDrawer: false,

+ 4 - 4
client/src/isLoggedIn.jsx

@@ -10,13 +10,13 @@ const IsLoggedIn = () => useEffect(() => {
     API.auth.me().then((data) => {
         if(data.status != 'OK') {
             if(data.status == 'NEW_INSTALL') {
-                window.location.href = '/ui/newInstall';
+                window.location.href = '/cosmos-ui/newInstall';
             } else if (data.status == 'error' && data.code == "HTTP004") {
-                window.location.href = '/ui/login?redirect=' + redirectTo;
+                window.location.href = '/cosmos-ui/login?redirect=' + redirectTo;
             } else if (data.status == 'error' && data.code == "HTTP006") {
-                window.location.href = '/ui/loginmfa?redirect=' + redirectTo;
+                window.location.href = '/cosmos-ui/loginmfa?redirect=' + redirectTo;
             } else if (data.status == 'error' && data.code == "HTTP007") {
-                window.location.href = '/ui/newmfa?redirect=' + redirectTo;
+                window.location.href = '/cosmos-ui/newmfa?redirect=' + redirectTo;
             }
         }
     })

+ 1 - 1
client/src/layout/MainLayout/Header/HeaderContent/index.jsx

@@ -18,7 +18,7 @@ const HeaderContent = () => {
             {!matchesXs && <Search />}
             {matchesXs && <Box sx={{ width: '100%', ml: 1 }} />}
 
-            <Link href="/ui/logout" underline="none">
+            <Link href="/cosmos-ui/logout" underline="none">
                 <Chip label="Logout" />
             </Link>
             {/* <Notification /> */}

+ 3 - 3
client/src/menu-items/dashboard.jsx

@@ -17,7 +17,7 @@ const dashboard = {
             id: 'home',
             title: 'Home',
             type: 'item',
-            url: '/ui/',
+            url: '/cosmos-ui/',
             icon: icons.HomeOutlined,
             breadcrumbs: false
         },
@@ -25,7 +25,7 @@ const dashboard = {
             id: 'dashboard',
             title: 'Dashboard',
             type: 'item',
-            url: '/ui/dashboard',
+            url: '/cosmos-ui/dashboard',
             icon: DashboardOutlined,
             breadcrumbs: false
         },
@@ -33,7 +33,7 @@ const dashboard = {
             id: 'market',
             title: 'Market',
             type: 'item',
-            url: '/ui/market-listing',
+            url: '/cosmos-ui/market-listing',
             icon: AppstoreAddOutlined,
             breadcrumbs: false
         },

+ 5 - 5
client/src/menu-items/pages.jsx

@@ -19,35 +19,35 @@ const pages = {
             id: 'servapps',
             title: 'ServApps',
             type: 'item',
-            url: '/ui/servapps',
+            url: '/cosmos-ui/servapps',
             icon: AppstoreOutlined
         },
         {
             id: 'url',
             title: 'URLs',
             type: 'item',
-            url: '/ui/config-url',
+            url: '/cosmos-ui/config-url',
             icon: icons.NodeExpandOutlined,
         },
         {
             id: 'users',
             title: 'Users',
             type: 'item',
-            url: '/ui/config-users',
+            url: '/cosmos-ui/config-users',
             icon: icons.ProfileOutlined,
         },
         {
             id: 'openid',
             title: 'OpenID',
             type: 'item',
-            url: '/ui/openid-manage',
+            url: '/cosmos-ui/openid-manage',
             icon: PicLeftOutlined,
         },
         {
             id: 'config',
             title: 'Configuration',
             type: 'item',
-            url: '/ui/config-general',
+            url: '/cosmos-ui/config-general',
             icon: icons.SettingOutlined,
         }
     ]

+ 1 - 1
client/src/pages/authentication/Logoff.jsx

@@ -17,7 +17,7 @@ const Logout = () => {
       API.auth.logout()
        .then(() => {
           setTimeout(() => {
-            window.location.href = '/ui/login';
+            window.location.href = '/cosmos-ui/login';
           }, 2000);
         });
     },[]);

+ 1 - 1
client/src/pages/authentication/Signup.jsx

@@ -15,7 +15,7 @@ const Signup = () => (
             <Grid item xs={12}>
                 <Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
                     <Typography variant="h3">Sign up</Typography>
-                    <Typography component={Link} to="/ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
+                    <Typography component={Link} to="/cosmos-ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
                         Already have an account?
                     </Typography>
                 </Stack>

+ 3 - 3
client/src/pages/authentication/auth-forms/AuthLogin.jsx

@@ -53,14 +53,14 @@ const AuthLogin = () => {
     const notLogged = urlSearchParams.get('notlogged') == 1;
     const notLoggedAdmin = urlSearchParams.get('notlogged') == 2;
     const invalid = urlSearchParams.get('invalid') == 1;
-    const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui';
+    const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
 
     useEffect(() => {
         API.auth.me().then((data) => {
             if(data.status == 'OK') {
                 window.location.href = redirectTo;
             } else if(data.status == 'NEW_INSTALL') {
-                window.location.href = '/ui/newInstall';
+                window.location.href = '/cosmos-ui/newInstall';
             }
         });
 
@@ -190,7 +190,7 @@ const AuthLogin = () => {
                                         }
                                         label={<Typography variant="h6">Keep me sign in</Typography>}
                                     />*/}
-                                    {showResetPassword && <Link variant="h6" component={RouterLink} to="/ui/forgot-password" color="primary">
+                                    {showResetPassword && <Link variant="h6" component={RouterLink} to="/cosmos-ui/forgot-password" color="primary">
                                         Forgot Your Password?
                                     </Link>}
                                     {!showResetPassword &&  <Typography variant="h6">

+ 1 - 1
client/src/pages/authentication/auth-forms/AuthRegister.jsx

@@ -85,7 +85,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
                     }).then((res) => {
                         setStatus({ success: true });
                         setSubmitting(false);
-                        window.location.href = '/ui/login';
+                        window.location.href = '/cosmos-ui/login';
                     }).catch((err) => {
                         setStatus({ success: false });
                         setErrors({ submit: err.message });

+ 1 - 1
client/src/pages/authentication/forgotPassword.jsx

@@ -109,7 +109,7 @@ const ForgotPassword = () => {
                         variant="contained"
                         color="primary"
                         component={Link}
-                        to="/ui/login"
+                        to="/cosmos-ui/login"
                     >
                         Back to login
                     </Button>

+ 3 - 3
client/src/pages/authentication/newMFA.jsx

@@ -35,14 +35,14 @@ import { CosmosCollapse } from '../config/users/formShortcuts';
 
 const MFALoginForm = () => {
   const urlSearchParams = new URLSearchParams(window.location.search);
-  const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui';
+  const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
 
   useEffect(() => {
     API.auth.me().then((data) => {
         if(data.status == 'OK') {
             window.location.href = redirectTo;
         } else if(data.status == 'NEW_INSTALL') {
-            window.location.href = '/ui/newInstall';
+            window.location.href = '/cosmos-ui/newInstall';
         }
     });
   });  
@@ -149,7 +149,7 @@ const MFASetup = () => {
         <MFALoginForm />
       </Grid>
       <Grid item xs={12}>
-        <Link to="/ui/logout">
+        <Link to="/cosmos-ui/logout">
           <Typography variant="h5">Logout</Typography>
         </Link>
       </Grid>

+ 1 - 1
client/src/pages/config/users/proxyman.jsx

@@ -150,7 +150,7 @@ const ProxyManagement = () => {
       {routes && <PrettyTableView 
         data={routes}
         getKey={(r) => r.Name + r.Target + r.Mode}
-        linkTo={(r) => '/ui/config-url/' + r.Name}
+        linkTo={(r) => '/cosmos-ui/config-url/' + r.Name}
         columns={[
           { 
             title: '', 

+ 1 - 1
client/src/pages/config/users/usermanagement.jsx

@@ -57,7 +57,7 @@ const UserManagement = () => {
             formType: ""+formType,
         })
         .then((values) => {
-            let sendLink = window.location.origin + '/ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
+            let sendLink = window.location.origin + '/cosmos-ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
             setToAction({...values.data, nickname, sendLink, formType, formAction: formType === 2 ? 'invite them to the server' : 'let them reset their password'});
             setOpenInviteForm(true);
         });

+ 36 - 11
client/src/pages/market/listing.jsx

@@ -1,4 +1,4 @@
-import { Box, Stack } from "@mui/material";
+import { Box, CircularProgress, Stack } from "@mui/material";
 import { HomeBackground, TransparentHeader } from "../home";
 import { useEffect, useState } from "react";
 import * as API from "../../api";
@@ -70,7 +70,7 @@ function ShowcasesItem({ isDark, item }) {
             <Button className="CheckButton" color="primary" variant="contained">
               Install
             </Button>
-            <Link to={"/ui/market-listing/" + item.name} style={{
+            <Link to={"/cosmos-ui/market-listing/" + item.name} style={{
               textDecoration: 'none',
             }}>
               <Button className="CheckButton" color="primary" variant="outlined">
@@ -133,7 +133,6 @@ const MarketPage = () => {
   return <>
     <HomeBackground />
     <TransparentHeader />
-    
     {openedApp && <Box style={{
       position: 'fixed',
       top: 0,
@@ -143,7 +142,7 @@ const MarketPage = () => {
       zIndex: 1300,
       backgroundColor: 'rgba(0,0,0,0.5)',
     }}>
-      <Link to="/ui/market-listing" as={Box}
+      <Link to="/cosmos-ui/market-listing" as={Box}
        style={{
         position: 'fixed',
         top: 0,
@@ -169,7 +168,7 @@ const MarketPage = () => {
           }
         }}>
 
-          <Link to="/ui/market-listing" style={{
+          <Link to="/cosmos-ui/market-listing" style={{
             textDecoration: 'none',
           }}>
             <Button className="CheckButton" color="primary" variant="outlined">
@@ -199,7 +198,7 @@ const MarketPage = () => {
           }}></div>
 
           <div>
-          <DockerComposeImport installer defaultName={openedApp.name} dockerComposeInit={openedApp.compose} />
+          <DockerComposeImport installerInit defaultName={openedApp.name} dockerComposeInit={openedApp.compose} />
           </div>
         </Stack>
       </Stack>
@@ -207,7 +206,19 @@ const MarketPage = () => {
  
     <Stack style={{ position: 'relative' }} spacing={1}>
       <Stack style={{ height: '35vh' }} spacing={1}>
-        <Showcases showcase={showcase} isDark={isDark}/>
+        {(!showcase || !Object.keys(showcase).length) && <Box style={{
+          width: '100%',
+          height: '100%',
+          zIndex: 1000,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+        }}>
+          <CircularProgress
+            size={100} 
+          />
+        </Box>}
+        {showcase && showcase.length && <Showcases showcase={showcase} isDark={isDark}/>}
       </Stack>
 
       <Stack spacing={1} style={{
@@ -218,12 +229,26 @@ const MarketPage = () => {
         minHeight: 'calc(65vh - 80px)',
         padding: '24px',
       }}>
-        <Grid2 container spacing={{ xs: 1, sm: 1, md: 2 }}>
-          {apps && Object.keys(apps).length > 0 && apps[Object.keys(apps)[0]].map((app) => {
+        {(!apps || !Object.keys(apps).length) && <Box style={{
+          width: '100%',
+          height: '100%',
+          zIndex: 1000,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          marginTop: '150px',
+        }}>
+          <CircularProgress
+            size={100} 
+          />
+        </Box>}
+
+        {apps && Object.keys(apps).length > 0 && <Grid2 container spacing={{ xs: 1, sm: 1, md: 2 }}>
+          {apps[Object.keys(apps)[0]].map((app) => {
             return <Grid2 style={{
               ...gridAnim,
               cursor: 'pointer',
-            }} xs={12} sm={12} md={6} lg={4} xl={3} key={app.name} item><Link to={"/ui/market-listing/" + app.name} style={{
+            }} xs={12} sm={12} md={6} lg={4} xl={3} key={app.name} item><Link to={"/cosmos-ui/market-listing/" + app.name} style={{
               textDecoration: 'none',
             }}>
               <div key={app.name} style={appCardStyle(theme)}>
@@ -248,7 +273,7 @@ const MarketPage = () => {
               </Link>
             </Grid2>
           })}
-        </Grid2>
+        </Grid2>}
       </Stack>
     </Stack>
   </>

+ 2 - 2
client/src/pages/newInstall/newInstall.jsx

@@ -63,7 +63,7 @@ const NewInstall = () => {
             setStatus(res.data);
         } catch(error) {
             if(error.status == 401)
-                window.location.href = "/ui/login";
+                window.location.href = "/cosmos-ui/login";
         }
         if (typeof status !== 'undefined') {
             setTimeout(() => {
@@ -577,7 +577,7 @@ const NewInstall = () => {
                                     step: "5",
                                 })
                                 setTimeout(() => {
-                                    window.location.href = hostname + "/ui/login";
+                                    window.location.href = hostname + "/cosmos-ui/login";
                                 }, 500);
                             } else 
                                 setActiveStep(activeStep + 1)

+ 436 - 211
client/src/pages/servapps/containers/docker-compose.jsx

@@ -1,7 +1,7 @@
 // material-ui
 import * as React from 'react';
-import { Alert, Button, FormLabel, Stack, Typography } from '@mui/material';
-import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined, SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined, ArrowDownOutlined } from '@ant-design/icons';
+import { Alert, Button, Checkbox, FormControlLabel, FormLabel, Stack, Typography } from '@mui/material';
+import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined, SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined, ArrowDownOutlined, ConsoleSqlOutlined } from '@ant-design/icons';
 import Table from '@mui/material/Table';
 import TableBody from '@mui/material/TableBody';
 import TableCell from '@mui/material/TableCell';
@@ -26,6 +26,10 @@ import ResponsiveButton from '../../../components/responseiveButton';
 import UploadButtons from '../../../components/fileUpload';
 import NewDockerService from './newService';
 import yaml from 'js-yaml';
+import { CosmosCollapse, CosmosFormDivider, CosmosInputText } from '../../config/users/formShortcuts';
+import VolumeContainerSetup from './volumes';
+import DockerContainerSetup from './setup';
+import whiskers from 'whiskers';
 
 function checkIsOnline() {
   API.isOnline().then((res) => {
@@ -63,11 +67,15 @@ const preStyle = {
   marginRight: '0',
 }
 
+const isNewerVersion = (v1, v2) => {
+  return false;
+}
+
 const getHostnameFromName = (name) => {
   return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
 }
 
-const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultName }) => {
+const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaultName }) => {
   const [step, setStep] = useState(0);
   const [isLoading, setIsLoading] = useState(false);
   const [openModal, setOpenModal] = useState(false);
@@ -75,254 +83,371 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
   const [service, setService] = useState({});
   const [ymlError, setYmlError] = useState('');
   const [serviceName, setServiceName] = useState(defaultName || 'my-service');
-  const [hostnames, setHostnames] = useState([]);
+  const [hostnames, setHostnames] = useState({});
+  const [overrides, setOverrides] = useState({});
+  const [context, setContext] = useState({});
+  const [installer, setInstaller] = useState(installerInit);
 
   useEffect(() => {
-    const text = fetch(dockerComposeInit)
-      .then((res) => res.text())
-      .then((text) => {
-        setDockerCompose(text);
-    });
-  }, [dockerComposeInit]);
+    if (!openModal) {
+      return;
+    }
+    // fetch(dockerComposeInit)
+    //   .then((res) => res.text())
+    //   .then((text) => {
+    //     setDockerCompose(text);
+    // });
+    if(dockerComposeInit)
+    setDockerCompose(`
+      {
+        "cosmos-installer": {
+          "form": [
+            {
+              "name": "caca",
+              "label": "Caca?",
+              "type": "text"
+            }
+          ]
+        },
+        "services": {
+          "{ServiceName}": {
+            "image": "lscr.io/linuxserver/jellyfin:latest",
+            "container_name": "{ServiceName}",
+            "restart": "unless-stopped",
+            "environment": [
+              "PUID=1000",
+              "PGID=1000",
+              "TZ=auto"
+            ],
+            "labels": {
+              "cosmos-force-network-secured": "true",
+              "caca": "{Context.caca}"
+            },
+            "volumes": [{
+              "source": "{ServiceName}-config",
+              "target": "/config",
+              "type": "volume"
+            }],
+            "routes": [
+              {
+                "name": "{ServiceName}",
+                "description": "Expose {ServiceName} to the web",
+                "useHost": true,
+                "target": "http://{ServiceName}:8096",
+                "mode": "SERVAPP"
+              }
+            ]
+          }
+        }
+      }
+    `);
+  }, [openModal, dockerComposeInit]);
 
   useEffect(() => {
-    if(!openModal) {
+    if (!openModal || installer) {
       return;
     }
+
     setYmlError('');
     if (dockerCompose === '') {
       return;
     }
 
-    let isJson = dockerCompose.trim().startsWith('{') && dockerCompose.trim().endsWith('}');
+    let isJson = dockerCompose && dockerCompose.trim().startsWith('{') && dockerCompose.trim().endsWith('}');
 
     // if Json 
     if (isJson) {
       try {
         let doc = JSON.parse(dockerCompose);
-
-        if(installer) {
-          doc = JSON.parse(dockerCompose.replace(/\{\{self\}\}/gi, serviceName));
-
-          // check hostnames for each service
-          let hostnames = [];
-
-          if(doc.services)
-            Object.keys(doc.services).forEach((key) => {
-              if (doc.services[key].routes) {
-                let routeId = 0;
-                doc.services[key].routes.forEach((route) => {
-                  if (route.useHost) {
-                    let newRoute = Object.assign({}, route);
-                    if(route.useHost === true) {
-                      newRoute.host = getHostnameFromName(key + (routeId > 0 ? '-' + routeId : '')) 
-                    }
-                    hostnames.push(newRoute);
-                  }
-                });
-              }
-            });
-
-          setHostnames(hostnames);
+        if(typeof doc['cosmos-installer'] === 'object') {
+          setInstaller(true);
         }
-
         setService(doc);
       } catch (e) {
         setYmlError(e.message);
       }
     }
     else {
-    // if Yml
-    let doc;
-    let newService = {};
-    try {
-      doc = yaml.load(dockerCompose);
-
-      if (typeof doc === 'object' && doc !== null && Object.keys(doc).length > 0 &&
-        !doc.services && !doc.networks && !doc.volumes) {
-        doc = {
-          services: Object.assign({}, doc)
-        }
-      }
+      // if Yml
+      let doc;
+      let newService = {};
+      try {
+        doc = yaml.load(dockerCompose);
 
-      // convert to the proper format
-      if (doc.services) {
-        Object.keys(doc.services).forEach((key) => {
-
-          // convert volumes
-          if (doc.services[key].volumes) {
-            if(Array.isArray(doc.services[key].volumes)) {
-              let volumes = [];
-              doc.services[key].volumes.forEach((volume) => {
-                if (typeof volume === 'object') {
-                  volumes.push(volume);
-                } else {
-                  let volumeSplit = volume.split(':');
-                  let volumeObj = {
-                    Source: volumeSplit[0],
-                    Target: volumeSplit[1],
-                    Type: volume[0] === '/' ? 'bind' : 'volume',
-                  };
-                  volumes.push(volumeObj);
-                }
-              });
-              doc.services[key].volumes = volumes;
-            }
+        if (typeof doc === 'object' && doc !== null && Object.keys(doc).length > 0 &&
+          !doc.services && !doc.networks && !doc.volumes) {
+          doc = {
+            services: Object.assign({}, doc)
           }
+        }
 
-          // convert expose
-          if (doc.services[key].expose) {
-            doc.services[key].expose = doc.services[key].expose.map((port) => {
-              return '' + port;
-            })
-          }
+        // convert to the proper format
+        if (doc.services) {
+          Object.keys(doc.services).forEach((key) => {
 
-          //convert user
-          if (doc.services[key].user) {
-            doc.services[key].user = '' + doc.services[key].user;
-          }
+            // convert volumes
+            if (doc.services[key].volumes) {
+              if (Array.isArray(doc.services[key].volumes)) {
+                let volumes = [];
+                doc.services[key].volumes.forEach((volume) => {
+                  if (typeof volume === 'object') {
+                    volumes.push(volume);
+                  } else {
+                    let volumeSplit = volume.split(':');
+                    let volumeObj = {
+                      Source: volumeSplit[0],
+                      Target: volumeSplit[1],
+                      Type: volume[0] === '/' ? 'bind' : 'volume',
+                    };
+                    volumes.push(volumeObj);
+                  }
+                });
+                doc.services[key].volumes = volumes;
+              }
+            }
 
-          // convert labels: 
-          if (doc.services[key].labels) {
-            if (Array.isArray(doc.services[key].labels)) {
-              let labels = {};
-              doc.services[key].labels.forEach((label) => {
-                const [key, value] = label.split('=');
-                labels['' + key] = '' + value;
-              });
-              doc.services[key].labels = labels;
+            // convert expose
+            if (doc.services[key].expose) {
+              doc.services[key].expose = doc.services[key].expose.map((port) => {
+                return '' + port;
+              })
             }
-            if (typeof doc.services[key].labels == 'object') {
-              let labels = {};
-              Object.keys(doc.services[key].labels).forEach((keylabel) => {
-                labels['' + keylabel] = '' + doc.services[key].labels[keylabel];
-              });
-              doc.services[key].labels = labels;
+
+            //convert user
+            if (doc.services[key].user) {
+              doc.services[key].user = '' + doc.services[key].user;
             }
-          }
 
-          // convert environment
-          if (doc.services[key].environment) {
-            if (!Array.isArray(doc.services[key].environment)) {
-              let environment = [];
-              Object.keys(doc.services[key].environment).forEach((keyenv) => {
-                environment.push(keyenv + '=' + doc.services[key].environment[keyenv]);
-              });
-              doc.services[key].environment = environment;
+            // convert labels: 
+            if (doc.services[key].labels) {
+              if (Array.isArray(doc.services[key].labels)) {
+                let labels = {};
+                doc.services[key].labels.forEach((label) => {
+                  const [key, value] = label.split('=');
+                  labels['' + key] = '' + value;
+                });
+                doc.services[key].labels = labels;
+              }
+              if (typeof doc.services[key].labels == 'object') {
+                let labels = {};
+                Object.keys(doc.services[key].labels).forEach((keylabel) => {
+                  labels['' + keylabel] = '' + doc.services[key].labels[keylabel];
+                });
+                doc.services[key].labels = labels;
+              }
             }
-          }
 
-          // convert network
-          if (doc.services[key].networks) {
-            if (Array.isArray(doc.services[key].networks)) {
-              let networks = {};
-              doc.services[key].networks.forEach((network) => {
-                if (typeof network === 'object') {
-                  networks['' + network.name] = network;
-                }
-                else
-                  networks['' + network] = {};
-              });
-              doc.services[key].networks = networks;
+            // convert environment
+            if (doc.services[key].environment) {
+              if (!Array.isArray(doc.services[key].environment)) {
+                let environment = [];
+                Object.keys(doc.services[key].environment).forEach((keyenv) => {
+                  environment.push(keyenv + '=' + doc.services[key].environment[keyenv]);
+                });
+                doc.services[key].environment = environment;
+              }
             }
-          }
 
-          // ensure container_name
-          if (!doc.services[key].container_name) {
-            doc.services[key].container_name = key;
-          }
-        });
-      }
+            // convert network
+            if (doc.services[key].networks) {
+              if (Array.isArray(doc.services[key].networks)) {
+                let networks = {};
+                doc.services[key].networks.forEach((network) => {
+                  if (typeof network === 'object') {
+                    networks['' + network.name] = network;
+                  }
+                  else
+                    networks['' + network] = {};
+                });
+                doc.services[key].networks = networks;
+              }
+            }
 
-      // convert networks
-      if (doc.networks) {
-        if (Array.isArray(doc.networks)) {
-          let networks = {};
-          doc.networks.forEach((network) => {
-            if (typeof network === 'object') {
-              networks['' + network.name] = network;
+            // ensure container_name
+            if (!doc.services[key].container_name) {
+              doc.services[key].container_name = key;
             }
-            else
-              networks['' + network] = {};
           });
-          doc.networks = networks;
-        } else {
-          let networks = {};
-          Object.keys(doc.networks).forEach((key) => {
-            networks['' + key] = doc.networks[key] || {};
-          });
-          doc.networks = networks;
         }
-      }
 
-      // convert volumes
-      if (doc.volumes) {
-        if (Array.isArray(doc.volumes)) {
-          let volumes = {};
-          doc.volumes.forEach((volume) => {
-            if(!volume) {
-              volume = {};
-            }
-            if (typeof volume === 'object') {
-              volumes['' + volume.name] = volume;
-            }
-            else
-              volumes['' + volume] = {};
-          });
-          doc.volumes = volumes;
-        } else {
-          let volumes = {};
-          Object.keys(doc.volumes).forEach((key) => {
-            volumes['' + key] = doc.volumes[key] || {};
-          });
-          doc.volumes = volumes;
+        // convert networks
+        if (doc.networks) {
+          if (Array.isArray(doc.networks)) {
+            let networks = {};
+            doc.networks.forEach((network) => {
+              if (typeof network === 'object') {
+                networks['' + network.name] = network;
+              }
+              else
+                networks['' + network] = {};
+            });
+            doc.networks = networks;
+          } else {
+            let networks = {};
+            Object.keys(doc.networks).forEach((key) => {
+              networks['' + key] = doc.networks[key] || {};
+            });
+            doc.networks = networks;
+          }
         }
+
+        // convert volumes
+        if (doc.volumes) {
+          if (Array.isArray(doc.volumes)) {
+            let volumes = {};
+            doc.volumes.forEach((volume) => {
+              if (!volume) {
+                volume = {};
+              }
+              if (typeof volume === 'object') {
+                volumes['' + volume.name] = volume;
+              }
+              else
+                volumes['' + volume] = {};
+            });
+            doc.volumes = volumes;
+          } else {
+            let volumes = {};
+            Object.keys(doc.volumes).forEach((key) => {
+              volumes['' + key] = doc.volumes[key] || {};
+            });
+            doc.volumes = volumes;
+          }
+        }
+
+      } catch (e) {
+        console.log(e);
+        setYmlError(e.message);
+        return;
       }
 
-    } catch (e) {
-      console.log(e);
-      setYmlError(e.message);
-      return;
+      setService(doc);
     }
-
-    setService(doc);
-  }
   }, [openModal, dockerCompose]);
 
   useEffect(() => {
-    if(!openModal) {
+    setOverrides({});
+  }, [serviceName, hostnames, context]);
+
+  useEffect(() => {
+    if (!openModal || dockerCompose === '') {
       return;
     }
 
     try {
       if (installer) {
-        let doc = JSON.parse(dockerCompose.replace(/\{\{self\}\}/gi, serviceName));
+        const rendered = whiskers.render(dockerCompose, {
+          ServiceName: serviceName,
+          Hostnames: hostnames,
+          Context: context,
+        });
 
-        if(doc.services)
-          Object.keys(doc.services).forEach((key) => {
-            if (doc.services[key].routes) {
-              doc.services[key].routes.forEach((route, index) => {
-                doc.services[key].routes[index] = {
-                  ...hostnames[index],
-                  name: route.name,
-                  description: route.description,
-                };
+        const jsoned = JSON.parse(rendered);
+
+        if (jsoned.services) {
+          // GENERATE HOSTNAMES FORM
+          let newHostnames = {};
+          Object.keys(jsoned.services).forEach((key) => {
+            if (jsoned.services[key].routes) {
+              let routeId = 0;
+              jsoned.services[key].routes.forEach((route) => {
+                if (route.useHost) {
+                  let newRoute = Object.assign({}, route);
+                  if (route.useHost === true) {
+                    newRoute.host = getHostnameFromName(key + (routeId > 0 ? '-' + routeId : ''))
+                  }
+                  
+                  if(!newHostnames[key]) newHostnames[key] = {};
+
+
+                  if(!newHostnames[key][route.name]) {
+                    newHostnames[key][route.name] = newRoute;
+                    if(hostnames[key] && hostnames[key][route.name]) {
+                      newHostnames[key][route.name].host = hostnames[key][route.name].host;
+                    }
+                  }
+                }
               });
             }
           });
 
-        setService(doc);
+          if(JSON.stringify(newHostnames) !== JSON.stringify(hostnames))
+            setHostnames({...newHostnames});
+
+          let newVolumes = [];
+
+          Object.keys(jsoned.services).forEach((key) => {
+            // APPLY OVERRIDE
+            if (overrides[key]) {
+              // prevent customizing static volumes
+              if (jsoned.services[key].volumes && jsoned['cosmos-installer'] && jsoned['cosmos-installer']['frozen-volumes']) {
+                jsoned['cosmos-installer']['frozen-volumes'].forEach((volume) => {
+                  delete overrides[key].volumes;
+                });
+              }
+
+              jsoned.services[key] = {
+                ...jsoned.services[key],
+                ...overrides[key],
+              };
+            }
+
+            // APPLY HOSTNAMES
+            if (hostnames[key]) {
+              if (jsoned.services[key].routes) {
+                jsoned.services[key].routes.forEach((route) => {
+                  if (hostnames[key][route.name]) {
+                    route.host = hostnames[key][route.name].host;
+                  }
+                });
+              }
+            }
+
+            // CREATE NEW VOLUMES
+            if (jsoned.services[key].volumes) {
+              jsoned.services[key].volumes.forEach((volume) => {
+                if (typeof volume === 'object' && !volume.existing) {
+                  newVolumes.push(volume);
+                } else if (typeof volume === 'object' && volume.existing) {
+                  delete volume.existing;
+                }
+              });
+            }
+          });
+
+          if (newVolumes.length > 0) {
+            jsoned.volumes = jsoned.volumes || {};
+            newVolumes.forEach((volume) => {
+              jsoned.volumes[volume.source] = {
+              };
+            });
+          }
+        }
+
+        setService(jsoned);
       }
     } catch (e) {
       setYmlError(e.message);
       return;
     }
-  }, [openModal, installer, serviceName, hostnames]);
+  }, [openModal, dockerCompose, serviceName, hostnames, overrides]);
+
+  const openModalFunc = () => {
+    setOpenModal(true);
+    setStep(0);
+    setService({});
+    setYmlError(null);
+    setOverrides({});
+    setHostnames({});
+    setDockerCompose('');
+    setInstaller(installerInit);
+    setServiceName(defaultName || 'default-name');
+  }
 
   return <>
-    <Dialog open={openModal} onClose={() => setOpenModal(false)} >
+    <Dialog open={openModal} onClose={() => setOpenModal(false)}>
       <DialogTitle>{installer ? "Installation" : "Import Compose File"}</DialogTitle>
-      <DialogContent style={{width: '800px', maxWidth: '100%'}}>
+      <DialogContent style={{ width: '800px', maxWidth: '100%' }}>
         <DialogContentText>
           {step === 0 && !installer && <><Stack spacing={2}>
             <Alert severity="warning" icon={<WarningOutlined />}>
@@ -359,27 +484,119 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
               }}
               rows={20}></TextField>
           </Stack></>}
-          
+
           {step === 0 && installer && <><Stack spacing={2}>
-              <div style={{ color: 'red' }}>
-                {ymlError}
-              </div>
-              {!ymlError && (<><FormLabel>Choose your service name</FormLabel>
+            <div style={{ color: 'red' }}>
+              {ymlError}
+            </div>
+
+            {!ymlError && (<><FormLabel>Choose your service name</FormLabel>
+
               <TextField label="Service Name" value={serviceName} onChange={(e) => setServiceName(e.target.value)} />
-              {hostnames.map((hostname, index) => {
-                return <>
-                  <FormLabel>Choose URL for {hostname.name}</FormLabel>
-                  <div style={{ opacity: 0.9, fontSize: '0.8em', textDecoration: 'italic'}}
-                  >{hostname.description}</div>
-                  <TextField key={index} label="Hostname" value={hostname.host} onChange={(e) => {
-                    const newHostnames = [...hostnames];
-                    newHostnames[index].host = e.target.value;
-                    setHostnames(newHostnames);
-                  }} />
-                </>
+
+              {service['cosmos-installer'] && service['cosmos-installer'].form.map((formElement) => {
+                return formElement.type === 'checkbox' ? <FormControlLabel
+                  control={<Checkbox checked={context[formElement.name] || formElement.initialValue} onChange={(e) => {
+                    setContext({ ...context, [formElement.name]: e.target.checked });
+                  }
+                  } />}
+                  label={formElement.label}
+                /> : <TextField
+                  label={formElement.label}
+                  value={context[formElement.name] || formElement.initialValue}
+                  onChange={(e) => {
+                    setContext({ ...context, [formElement.name]: e.target.value });
+                  }
+                  } />
               })}
-              </>)}
+
+              {Object.keys(hostnames).map((serviceIndex) => {
+                const service = hostnames[serviceIndex];
+                return Object.keys(service).map((hostIndex) => {
+                  const hostname = service[hostIndex];
+                  return <>
+                    <FormLabel>Choose URL for {hostname.name}</FormLabel>
+                    <div style={{ opacity: 0.9, fontSize: '0.8em', textDecoration: 'italic' }}
+                    >{hostname.description}</div>
+                    <TextField key={serviceIndex + hostIndex} label="Hostname" value={hostname.host} onChange={(e) => {
+                      hostnames[serviceIndex][hostname.name].host = e.target.value;
+                      setHostnames({...hostnames});
+                    }} />
+                  </>
+                })
+              })}
+
+              {service && service.services && Object.values(service.services).map((value) => {
+                return <CosmosCollapse title={`Customize ${value.container_name}`}>
+                  <Stack spacing={2}>
+                    <DockerContainerSetup
+                      newContainer
+                      containerInfo={{
+                        Name: '',
+                        Image: '',
+                        Config: {
+                          Env: value.environment || [],
+                          Labels: value.labels || {},
+                        },
+                        HostConfig: {
+                          RestartPolicy: {},
+                        }
+                      }}
+                      OnChange={(containerInfo) => {
+                        setOverrides({
+                          ...overrides,
+                          [value.container_name]: {
+                            environment: containerInfo.envVars,
+                            labels: containerInfo.labels,
+                          }
+                        })
+                      }}
+                      noCard
+                      installer
+                    />
+                    <CosmosFormDivider title="Volumes" />
+                    <VolumeContainerSetup
+                      newContainer
+                      frozenVolumes={service['cosmos-installer'] && service['cosmos-installer']['frozen-volumes'] || []}
+                      containerInfo={{
+                        HostConfig: {
+                          Binds: [],
+                          Mounts: Object.keys(value.volumes).map(k => {
+                            return {
+                              Type: value.volumes[k].type || (k.startsWith('/') ? 'bind' : 'volume'),
+                              Source: value.volumes[k].source || "",
+                              Target: value.volumes[k].target || "",
+                            }
+                          }) || [],
+                        }
+                      }}
+                      OnChange={(containerInfo, volumes) => {
+                        setOverrides({
+                          ...overrides,
+                          [value.container_name]: {
+                            volumes: containerInfo.volumes.map((v, k) => {
+                              return {
+                                type: v.Type,
+                                source: v.Source,
+                                target: v.Target,
+                                existing: v.Type == 'volume' && volumes.find(v2 => v2.Source === v.Name),
+                              }
+                            })
+                          }
+                        })
+                      }}
+                      noCard
+                    />
+                  </Stack>
+                </CosmosCollapse>
+              })}
+
+            </>)}
           </Stack></>}
+          
+          {step === 0 && dockerComposeInit && dockerCompose == '' && <Stack spacing={2} alignItems={'center'} style={{paddingTop: '20px'}}>
+            <CircularProgress />
+          </Stack>}
 
           {step === 1 && <Stack spacing={2}>
             <NewDockerService service={service} refresh={refresh} />
@@ -392,6 +609,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
           setStep(0);
           setDockerCompose('');
           setYmlError('');
+          setInstaller(false);
         }}>Cancel</Button>
         <Button disabled={!dockerCompose || ymlError} onClick={() => {
           if (step === 0) {
@@ -406,14 +624,21 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
       </DialogActions>}
     </Dialog>
 
-    <ResponsiveButton
-      color="primary"
-      onClick={() => setOpenModal(true)}
-      variant={(installer ? "contained" : "outlined")} 
-      startIcon={(installer ? <ArrowDownOutlined /> : <ArrowUpOutlined />)}
-    >
-      {installer ? 'Install' : 'Import Compose File'}
-    </ResponsiveButton>
+    {(installerInit && service.minVersion && isNewerVersion(service.minVersion)) ?
+      <Alert severity="error" icon={<WarningOutlined />}>
+        This service requires a newer version of Cosmos. Please update Cosmos to install this service.
+      </Alert>
+      : <ResponsiveButton
+        color="primary"
+        onClick={() => {
+          openModalFunc();
+        }}
+        variant={(installerInit ? "contained" : "outlined")}
+        startIcon={(installerInit ? <ArrowDownOutlined /> : <ArrowUpOutlined />)}
+      >
+        {installerInit ? 'Install' : 'Import Compose File'}
+      </ResponsiveButton>
+    }
   </>;
 };
 

+ 2 - 0
client/src/pages/servapps/containers/newService.jsx

@@ -60,6 +60,8 @@ const NewDockerService = ({service, refresh}) => {
   const [openModal, setOpenModal] = React.useState(false);
   const preRef = React.useRef(null);
 
+  delete service['cosmos-installer'];
+
   React.useEffect(() => {
     // refreshContainer();
   }, []);

+ 22 - 11
client/src/pages/servapps/containers/setup.jsx

@@ -8,6 +8,7 @@ import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons';
 import * as API from '../../../api';
 import { LoadingButton } from '@mui/lab';
 import LogsInModal from '../../../components/logsInModal';
+import ResponsiveButton from '../../../components/responseiveButton';
 
 const containerInfoFrom = (values) => {
   const labels = {};
@@ -27,7 +28,7 @@ const containerInfoFrom = (values) => {
   return realvalues;
 }
 
-const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newContainer, OnForceSecure}) => {
+const DockerContainerSetup = ({noCard, containerInfo, installer, OnChange, refresh, newContainer, OnForceSecure}) => {
   const restartPolicies = [
     ['no', 'No Restart'],
     ['always', 'Always Restart'],
@@ -39,6 +40,13 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
   const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
   const padding = isMobile ? '6px 4px' : '12px 10px';
   const [latestImage, setLatestImage] = React.useState(containerInfo.Config.Image);
+  
+  const wrapCard = (children) => {
+    if (noCard) return children;
+    return <MainCard title="Docker Container Setup">
+      {children}
+    </MainCard>;
+  };
 
   return (
     <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
@@ -56,6 +64,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
           }),
           interactive: containerInfo.Config.Tty && containerInfo.Config.OpenStdin,
         }}
+        enableReinitialize
         validate={(values) => {
           const errors = {};
           if (!values.image) {
@@ -115,13 +124,14 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
               }}
             />
             <Stack spacing={2}>
-              <MainCard title={'Docker Container Setup'}>
+              {wrapCard(<>
               {containerInfo.State && containerInfo.State.Status !== 'running' && (
               <Alert severity="warning" style={{ marginBottom: '15px' }}>
                   This container is not running. Editing any settings will cause the container to start again.
                 </Alert>
               )}
                 <Grid container spacing={4}>
+                {!installer && <>
                     {newContainer && <CosmosInputText
                       name="name"
                       label="Name"
@@ -161,6 +171,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
                         }}
                       />
                   </Grid>}
+                  </>}
 
                   <CosmosFormDivider title={'Environment Variables'} />
                   <Grid item xs={12}>
@@ -206,8 +217,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
                         </Grid>
                       </Grid>
                     ))}
-                    <IconButton
-                      fullWidth
+                    <ResponsiveButton
                       variant="outlined"
                       color="primary"
                       size='large'
@@ -216,9 +226,10 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
                         newEnvVars.push({ key: '', value: '' });
                         formik.setFieldValue('envVars', newEnvVars);
                       }}
+                      startIcon={<PlusCircleOutlined />}
                     >
-                      <PlusCircleOutlined />
-                    </IconButton>
+                      Add
+                    </ResponsiveButton>
                   </Grid>
                   <CosmosFormDivider title={'Labels'} />
                   <Grid item xs={12}>
@@ -264,8 +275,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
                         </Grid>
                       </Grid>
                     ))}
-                    <IconButton
-                      fullWidth
+                    <ResponsiveButton
                       variant="outlined"
                       color="primary"
                       size='large'
@@ -274,12 +284,13 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
                         newLabels.push({ key: '', value: '' });
                         formik.setFieldValue('labels', newLabels);
                       }}
+                      startIcon={<PlusCircleOutlined />}
                     >
-                      <PlusCircleOutlined />
-                    </IconButton>
+                      Add
+                    </ResponsiveButton>
                   </Grid>
                 </Grid>
-              </MainCard>
+              </>)}
               {!newContainer && <MainCard>
                 <Stack direction="column" spacing={2}>
                   {formik.errors.submit && (

+ 21 - 8
client/src/pages/servapps/containers/volumes.jsx

@@ -12,7 +12,7 @@ import { NetworksColumns } from '../networks';
 import NewNetworkButton from '../createNetwork';
 import ResponsiveButton from '../../../components/responseiveButton';
 
-const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange }) => {
+const VolumeContainerSetup = ({ noCard, containerInfo, frozenVolumes = [], refresh, newContainer, OnChange }) => {
   const restartPolicies = [
     ['no', 'No Restart'],
     ['always', 'Always Restart'],
@@ -44,6 +44,13 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
       });
   };
 
+  const wrapCard = (children) => {
+    if (noCard) return children;
+    return <MainCard title="Volume Mounts">
+      {children}
+    </MainCard>
+  };
+
   return (<Stack spacing={2}>
     <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
       <Formik
@@ -60,6 +67,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
             })
           ])
         }}
+        enableReinitialize
         validate={(values) => {
           const errors = {};
           // check unique
@@ -70,7 +78,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
           if (unique.length !== volumes.length) {
             errors.submit = 'Mounts must have unique targets';
           }
-          OnChange && OnChange(values);
+          OnChange && OnChange(values, volumes);
           return errors;
         }}
         onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
@@ -95,8 +103,8 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
       >
         {(formik) => (
           <form noValidate onSubmit={formik.handleSubmit}>
-            <MainCard title={'Volume Mounts'}>
-            {containerInfo.State && containerInfo.State.Status !== 'running' && (
+            {wrapCard(<>
+            {!newContainer && containerInfo.State && containerInfo.State.Status !== 'running' && (
             <Alert severity="warning" style={{ marginBottom: '15px' }}>
                 This container is not running. Editing any settings will cause the container to start again.
               </Alert>
@@ -129,6 +137,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
                               <div style={{fontWeight: 'bold', wordSpace: 'nowrap', overflow:'hidden', textOverflow:'ellipsis', maxWidth: '100px'}}>
                                 <TextField
                                   className="px-2 my-2"
+                                  disabled={frozenVolumes.includes(r.Source)}
                                   variant="outlined"
                                   name='Type'
                                   id='Type'
@@ -155,6 +164,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
                                     variant="outlined"
                                     name='Source'
                                     id='Source'
+                                    disabled={frozenVolumes.includes(r.Source)}
                                     style={{minWidth: '200px'}}
                                     value={r.Source}
                                     onChange={(e) => {
@@ -168,6 +178,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
                                     variant="outlined"
                                     name='Source'
                                     id='Source'
+                                    disabled={frozenVolumes.includes(r.Source)}
                                     select
                                     style={{minWidth: '200px'}}
                                     value={r.Source}
@@ -177,9 +188,9 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
                                       formik.setFieldValue('volumes', newVolumes);
                                     }}
                                   >
-                                    {volumes.map((volume) => (
-                                      <MenuItem key={volume.Id} value={volume.Name}>
-                                        {volume.Name}
+                                    {[...volumes, r].map((volume) => (
+                                      <MenuItem key={volume.Id || "last"} value={volume.Name || volume.Source}>
+                                        {volume.Name || volume.Source}
                                       </MenuItem>
                                     ))}
                                   </TextField>
@@ -195,6 +206,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
                                     variant="outlined"
                                     name='Target'
                                     id='Target'
+                                    disabled={frozenVolumes.includes(r.Source)}
                                     style={{minWidth: '200px'}}
                                     value={r.Target}
                                     onChange={(e) => {
@@ -211,6 +223,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
                               return (<Button
                                 variant="outlined"
                                 color="primary"
+                                disabled={frozenVolumes.includes(r.Source)}
                                 onClick={() => {
                                   const newVolumes = [...formik.values.volumes];
                                   newVolumes.splice(newVolumes.indexOf(r), 1);
@@ -252,7 +265,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
                   </Stack>
                 </Grid>
               </Grid>
-            </MainCard>
+            </>)}
           </form>
         )}
       </Formik>

+ 1 - 1
client/src/pages/servapps/index.jsx

@@ -17,7 +17,7 @@ const ServappsIndex = () => {
   return <div>
     <IsLoggedIn />
     
-    <PrettyTabbedView path="/ui/servapps/:tab" tabs={[
+    <PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={[
         {
           title: 'Containers',
           children: <ServeApps />,

+ 2 - 2
client/src/pages/servapps/servapps.jsx

@@ -150,7 +150,7 @@ const ServeApps = () => {
         <ResponsiveButton variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
           refreshServeApps();
         }}>Refresh</ResponsiveButton>
-        <Link to="/ui/servapps/new-service">
+        <Link to="/cosmos-ui/servapps/new-service">
           <ResponsiveButton
             variant="contained" 
             startIcon={<AppstoreAddOutlined />}
@@ -303,7 +303,7 @@ const ServeApps = () => {
                 </Stack>
               </Stack>
               <div>
-                <Link to={`/ui/servapps/containers/${app.Names[0].replace('/', '')}`}>
+                <Link to={`/cosmos-ui/servapps/containers/${app.Names[0].replace('/', '')}`}>
                   <Button variant="outlined" color="primary" fullWidth>Details</Button>
                 </Link>
               </div>

+ 8 - 8
client/src/routes/LoginRoutes.jsx

@@ -22,35 +22,35 @@ const LoginRoutes = {
     element: <MinimalLayout />,
     children: [
         {
-            path: '/ui/login',
+            path: '/cosmos-ui/login',
             element: <AuthLogin />
         },
         {
-            path: '/ui/register',
+            path: '/cosmos-ui/register',
             element: <AuthRegister />
         },
         {
-            path: '/ui/logout',
+            path: '/cosmos-ui/logout',
             element: <Logout />
         },
         {
-            path: '/ui/newInstall',
+            path: '/cosmos-ui/newInstall',
             element: <NewInstall />
         },
         {
-            path: '/ui/newmfa',
+            path: '/cosmos-ui/newmfa',
             element: <NewMFA />
         },
         {
-            path: '/ui/openid',
+            path: '/cosmos-ui/openid',
             element: <OpenID />
         },
         {
-            path: '/ui/loginmfa',
+            path: '/cosmos-ui/loginmfa',
             element: <MFALogin />
         },
         {
-            path: '/ui/forgot-password',
+            path: '/cosmos-ui/forgot-password',
             element: <ForgotPassword />
         },
     ]

+ 16 - 16
client/src/routes/MainRoutes.jsx

@@ -39,60 +39,60 @@ const MainRoutes = {
     children: [
         {
             path: '/',
-            // redirect to /ui
-            element: <Navigate to="/ui" />
+            // redirect to /cosmos-ui
+            element: <Navigate to="/cosmos-ui" />
         },
         {
-            path: '/ui/logo',
-            // redirect to /ui
+            path: '/cosmos-ui/logo',
+            // redirect to /cosmos-ui
             element: <Navigate to={logo} />
         },
         {
-            path: '/ui',
+            path: '/cosmos-ui',
             element: <HomePage />
         },
         {
-            path: '/ui/dashboard',
+            path: '/cosmos-ui/dashboard',
             element: <DashboardDefault />
         },
         {
-            path: '/ui/servapps',
+            path: '/cosmos-ui/servapps',
             element: <ServeAppsIndex />
         },
         {
-            path: '/ui/config-users',
+            path: '/cosmos-ui/config-users',
             element: <UserManagement />
         },
         {
-            path: '/ui/config-general',
+            path: '/cosmos-ui/config-general',
             element: <ConfigManagement />
         },
         {
-            path: '/ui/servapps/new-service',
+            path: '/cosmos-ui/servapps/new-service',
             element: <NewDockerServiceForm />
         },
         {
-            path: '/ui/config-url',
+            path: '/cosmos-ui/config-url',
             element: <ProxyManagement />
         },
         {
-            path: '/ui/config-url/:routeName',
+            path: '/cosmos-ui/config-url/:routeName',
             element: <RouteConfigPage />,
         },
         {
-            path: '/ui/servapps/containers/:containerName',
+            path: '/cosmos-ui/servapps/containers/:containerName',
             element: <ContainerIndex />,
         },
         {
-            path: '/ui/openid-manage',
+            path: '/cosmos-ui/openid-manage',
             element: <OpenIdList />,
         },
         {
-            path: '/ui/market-listing/',
+            path: '/cosmos-ui/market-listing/',
             element: <MarketPage />
         },
         {
-            path: '/ui/market-listing/:appName',
+            path: '/cosmos-ui/market-listing/:appName',
             element: <MarketPage />
         }
     ]

+ 22 - 1
client/src/utils/indexs.js

@@ -25,4 +25,25 @@ export function isDomain(hostname) {
   }
 
   return true;
-}
+}
+
+export function debounce(func, wait, immediate) {
+  var timeout;
+
+  return () => {
+      var context = this, args = arguments;
+
+      var later = () => {
+          timeout = null;
+          if (!immediate) func.apply(context, args);
+      };
+
+      var callNow = immediate && !timeout;
+
+      clearTimeout(timeout);
+
+      timeout = setTimeout(later, wait);
+
+      if (callNow) func.apply(context, args);
+  };
+};

+ 116 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cosmos-server",
-  "version": "0.7.0-unstable",
+  "version": "0.7.0-unstable2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cosmos-server",
-      "version": "0.7.0-unstable",
+      "version": "0.7.0-unstable2",
       "dependencies": {
         "@ant-design/colors": "^6.0.0",
         "@ant-design/icons": "^4.7.0",
@@ -24,6 +24,7 @@
         "apexcharts": "^3.35.5",
         "bcryptjs": "^2.4.3",
         "browserslist": "^4.21.7",
+        "dot": "^1.1.3",
         "formik": "^2.2.9",
         "framer-motion": "^7.3.6",
         "history": "^5.3.0",
@@ -52,6 +53,8 @@
         "typescript": "4.8.3",
         "vite": "^4.2.0",
         "web-vitals": "^3.0.2",
+        "whiskers": "^0.4.0",
+        "whiskers.js": "^1.0.0",
         "yup": "^0.32.11"
       },
       "devDependencies": {
@@ -4391,6 +4394,11 @@
       "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==",
       "dev": true
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
     "node_modules/available-typed-arrays": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@@ -4411,6 +4419,15 @@
         "node": ">=4"
       }
     },
+    "node_modules/axios": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+      "dependencies": {
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
+      }
+    },
     "node_modules/axobject-query": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -4749,6 +4766,17 @@
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
       "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/comma-separated-tokens": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
@@ -4997,6 +5025,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/diff-sequences": {
       "version": "29.4.3",
       "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz",
@@ -5048,6 +5084,17 @@
         "csstype": "^3.0.2"
       }
     },
+    "node_modules/dot": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dot/-/dot-1.1.3.tgz",
+      "integrity": "sha512-/nt74Rm+PcfnirXGEdhZleTwGC2LMnuKTeeTIlI82xb5loBBoXNYzr2ezCroPSMtilK8EZIfcNZwOcHN+ib1Lg==",
+      "engines": [
+        "node >=0.2.6"
+      ],
+      "bin": {
+        "dottojs": "bin/dot-packer"
+      }
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.4.419",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.419.tgz",
@@ -6107,6 +6154,25 @@
       "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
       "dev": true
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/for-each": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -6115,6 +6181,19 @@
         "is-callable": "^1.1.3"
       }
     },
+    "node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/format": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
@@ -7687,6 +7766,25 @@
         "node": ">=8.6"
       }
     },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/mimic-fn": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -9923,6 +10021,22 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/whiskers": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/whiskers/-/whiskers-0.4.0.tgz",
+      "integrity": "sha512-pTygA/fE6RIMOp3AwUy7E9jrdpqUEa4k5VCdJIBZ/64kNtiMuCTCYC6fzbiUhjxN32zX+qZQlZACMC/un5HS7A==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/whiskers.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/whiskers.js/-/whiskers.js-1.0.0.tgz",
+      "integrity": "sha512-IzdNdA2jTPrTEnjQkySSOW90rtb++vJUjkHdwwsjqIeGndnizM3Mo+5DEVZ3iy7Su3itR92i9+EFtYamIT3Zxw==",
+      "dependencies": {
+        "axios": "^0.27.2"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

+ 6 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cosmos-server",
-  "version": "0.7.0-unstable2",
+  "version": "0.7.0-unstable3",
   "description": "",
   "main": "test-server.js",
   "bugs": {
@@ -24,6 +24,7 @@
     "apexcharts": "^3.35.5",
     "bcryptjs": "^2.4.3",
     "browserslist": "^4.21.7",
+    "dot": "^1.1.3",
     "formik": "^2.2.9",
     "framer-motion": "^7.3.6",
     "history": "^5.3.0",
@@ -52,11 +53,13 @@
     "typescript": "4.8.3",
     "vite": "^4.2.0",
     "web-vitals": "^3.0.2",
+    "whiskers": "^0.4.0",
+    "whiskers.js": "^1.0.0",
     "yup": "^0.32.11"
   },
   "scripts": {
     "client": "vite",
-    "client-build": "vite build --base=/ui/",
+    "client-build": "vite build --base=/cosmos-ui/",
     "start": "env CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
     "build": "sh build.sh",
     "dev": "npm run build && npm run start",
@@ -64,7 +67,7 @@
     "dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host  --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
     "dockerdev": "npm run dockerdevbuild && npm run dockerdevrun",
     "dockerdevclient": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun",
-    "demo": "vite build --base=/ui/ --mode demo",
+    "demo": "vite build --base=/cosmos-ui/ --mode demo",
     "devdemo": "vite --mode demo"
   },
   "eslintConfig": {

+ 1 - 2
readme.md

@@ -5,7 +5,6 @@
 <!-- sponsors -->
 <h3 align="center">Thanks to the sponsors:</h3></br>
 <p align="center"><a href="https://github.com/zarevskaya"><img src="https://avatars.githubusercontent.com/zarevskaya" style="border-radius:48px" width="48" height="48" alt="zarev" title="zarev" /></a>
-<a href="https://github.com/DrMxrcy"><img src="https://avatars.githubusercontent.com/DrMxrcy" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
 </p><!-- /sponsors -->
 
 ---
@@ -24,7 +23,7 @@ Cosmos is a self-hosted platform for running server applications securely and wi
   <a target="_blank" href="https://cosmos-cloud.io/doc">
     <img height="44px" src="https://raw.githubusercontent.com/azukaar/Cosmos-Server/master/icons/doc.png" />
   </a>
-  <a target="_blank" href="https://cosmos-cloud.io/ui">
+  <a target="_blank" href="https://cosmos-cloud.io/cosmos-ui">
     <img height="44px" src="https://raw.githubusercontent.com/azukaar/Cosmos-Server/master/icons/demo.png" />
   </a>
   <br/>

+ 2 - 2
src/authorizationserver/oauth2_discover.go

@@ -72,7 +72,7 @@ func discoverEndpoint(rw http.ResponseWriter, req *http.Request) {
 
 	json.NewEncoder(rw).Encode(&oidcConfiguration{
 		Issuer:                                 hostname,
-		AuthURL:                                realHostname + "/ui/openid",
+		AuthURL:                                realHostname + "/cosmos-ui/openid",
 		TokenURL:                               hostname + "/oauth2/token",
 		JWKsURI:                                hostname + "/.well-known/jwks.json",
 		RevocationEndpoint:                     hostname + "/oauth2/revoke",
@@ -96,7 +96,7 @@ func discoverEndpoint(rw http.ResponseWriter, req *http.Request) {
 		BackChannelLogoutSessionSupported:      true,
 		FrontChannelLogoutSupported:            true,
 		FrontChannelLogoutSessionSupported:     true,
-		EndSessionEndpoint:                     hostname + "/ui/logout",
+		EndSessionEndpoint:                     hostname + "/cosmos-ui/logout",
 		RequestObjectSigningAlgValuesSupported: []string{"RS256"},
 		CodeChallengeMethodsSupported:          []string{"plain", "S256"},
 	})

+ 1 - 1
src/docker/api_blueprint.go

@@ -384,7 +384,7 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
 		if containerConfig.Env != nil {
 			for i, env := range containerConfig.Env {
 				if strings.HasPrefix(env, "TZ=") {
-					if strings.TrimPrefix(env, "TZ=") == "" {
+					if strings.TrimPrefix(env, "TZ=") == "auto" {
 						if os.Getenv("TZ") != "" {
 							containerConfig.Env[i] = "TZ=" + os.Getenv("TZ")
 						} else {

+ 2 - 2
src/httpServer.go

@@ -302,12 +302,12 @@ func StartServer() {
 		fs = utils.EnsureHostname(fs)
 	}
 
-	router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs))
+	router.PathPrefix("/cosmos-ui").Handler(http.StripPrefix("/cosmos-ui", fs))
 
 	router = proxy.BuildFromConfig(router, HTTPConfig.ProxyConfig)
 	
 	router.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-    http.Redirect(w, r, "/ui", http.StatusMovedPermanently)
+    http.Redirect(w, r, "/cosmos-ui", http.StatusMovedPermanently)
 	}))
 
 

+ 4 - 4
src/user/token.go

@@ -152,7 +152,7 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err
 	}
 
 	requestURL := req.URL.Path
-	isSettingMFA := strings.HasPrefix(requestURL, "/ui/loginmfa") || strings.HasPrefix(requestURL, "/ui/newmfa") || strings.HasPrefix(requestURL, "/api/mfa")
+	isSettingMFA := strings.HasPrefix(requestURL, "/cosmos-ui/loginmfa") || strings.HasPrefix(requestURL, "/cosmos-ui/newmfa") || strings.HasPrefix(requestURL, "/api/mfa")
 
 	userInBase.MFAState = 0
 
@@ -201,15 +201,15 @@ func logOutUser(w http.ResponseWriter, req *http.Request) {
 }
 
 func redirectToReLogin(w http.ResponseWriter, req *http.Request) {
-	http.Redirect(w, req, "/ui/login?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
+	http.Redirect(w, req, "/cosmos-ui/login?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
 }
 
 func redirectToLoginMFA(w http.ResponseWriter, req *http.Request) {
-	http.Redirect(w, req, "/ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
+	http.Redirect(w, req, "/cosmos-ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
 }
 
 func redirectToNewMFA(w http.ResponseWriter, req *http.Request) {
-	http.Redirect(w, req, "/ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
+	http.Redirect(w, req, "/cosmos-ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
 }
 
 func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mfaDone bool) {

+ 6 - 6
src/utils/loggedIn.go

@@ -15,15 +15,15 @@ func LoggedInOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
 
 	if !isUserLoggedIn || userNickname == "" {
 		Error("LoggedInOnlyWithRedirect: User is not logged in", nil)
-		http.Redirect(w, req, "/ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound)
+		http.Redirect(w, req, "/cosmos-ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound)
 		return errors.New("User not logged in")
 	}
 
 	if(mfa == 1) {
-		http.Redirect(w, req, "/ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
+		http.Redirect(w, req, "/cosmos-ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
 		return errors.New("User requires MFA")
 	} else if(mfa == 2) {
-		http.Redirect(w, req, "/ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
+		http.Redirect(w, req, "/cosmos-ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
 		return errors.New("User requires MFA Setup")
 	}
 
@@ -39,7 +39,7 @@ func AdminOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
 
 	if !isUserLoggedIn || userNickname == "" {
 		Error("AdminLoggedInOnlyWithRedirect: User is not logged in", nil)
-		http.Redirect(w, req, "/ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound)
+		http.Redirect(w, req, "/cosmos-ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound)
 		return errors.New("User is not logged")
 	}
 
@@ -50,10 +50,10 @@ func AdminOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
 	}
 
 	if(mfa == 1) {
-		http.Redirect(w, req, "/ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
+		http.Redirect(w, req, "/cosmos-ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
 		return errors.New("User requires MFA")
 	} else if(mfa == 2) {
-		http.Redirect(w, req, "/ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
+		http.Redirect(w, req, "/cosmos-ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
 		return errors.New("User requires MFA Setup")
 	}