소스 검색

v0.1.0 servapps management

Yann Stepienik 2 년 전
부모
커밋
cc2c749250

+ 1 - 1
.nvmrc

@@ -1 +1 @@
-16
+16

+ 2 - 1
build arm64.sh

@@ -3,4 +3,5 @@ env GOARCH=arm64 go build -o build/cosmos src/*.go
 if [ $? -ne 0 ]; then
     exit 1
 fi
-cp -r static build/
+cp -r static build/
+cp package.json build/

+ 6 - 1
build.sh

@@ -3,4 +3,9 @@ go build -o build/cosmos src/*.go
 if [ $? -ne 0 ]; then
     exit 1
 fi
-cp -r static build/
+cp -r static build/
+echo '{' > build/meta.json
+cat package.json | grep -E '"version"' >> build/meta.json
+echo '  "buildDate": "'`date`'",' >> build/meta.json
+echo '  "built from": "'`hostname`'"' >> build/meta.json
+echo '}' >> build/meta.json

+ 10 - 0
client/src/api/docker.jsx

@@ -8,6 +8,15 @@ function list() {
     },
   }))
 }
+
+function secure(id, res) {
+  return wrap(fetch('/cosmos/api/servapps/' + id + '/secure/'+res, {
+    method: 'GET',
+    headers: {
+        'Content-Type': 'application/json'
+    },
+  }))
+}
     
 const newDB = () => {
   return wrap(fetch('/cosmos/api/newDB', {
@@ -21,4 +30,5 @@ const newDB = () => {
 export {
   list,
   newDB,
+  secure
 };

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

@@ -19,7 +19,7 @@ const AuthWrapper = ({ children }) => {
     const darkMode = theme.palette.mode === 'dark';
 
     return <Box sx={{ minHeight: '100vh', 
-        background:  darkMode ? 'none' : '#f0efef' }}>
+        background:  darkMode ? 'none' : '#fafafb' }}>
         <AuthBackground />
         <Grid
             container

+ 18 - 9
client/src/pages/config/users/containerPicker.jsx

@@ -31,7 +31,7 @@ import CircularProgress from '@mui/material/CircularProgress';
 
 import * as API  from '../../../api';
 
-export function CosmosContainerPicker({formik}) {
+export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
   const [open, setOpen] = React.useState(false);
   const [containers, setContainers] = React.useState([]);
   const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
@@ -43,11 +43,13 @@ export function CosmosContainerPicker({formik}) {
   const name = "Target"
   const label = "Container Name"
   let targetResult = {
-    container: "container",
+    container: 'null',
     port: "",
     protocol: "http",
   }
+
   let preview = formik.values[name];
+
   if(preview && preview.includes("://") && preview.includes(":")) {
     let p1_ = preview.split("://")[1]
     targetResult = {
@@ -83,6 +85,12 @@ export function CosmosContainerPicker({formik}) {
     }
   }
 
+  React.useEffect(() => {
+    if(lockTarget) {
+      onContainerChange(TargetContainer)
+    }
+  }, [])
+
   React.useEffect(() => {
     let active = true;
 
@@ -93,6 +101,7 @@ export function CosmosContainerPicker({formik}) {
     (async () => {
       const res = await API.docker.list()
       setContainers(res.data);
+      
 
       let names = res.data.map((container) => container.Names[0])
 
@@ -118,27 +127,27 @@ export function CosmosContainerPicker({formik}) {
     <Autocomplete
       id={name + "-autocomplete"}
       open={open}
+      disabled={lockTarget}
       onOpen={() => {
-        setOpen(true);
+        !lockTarget && setOpen(true);
       }}
       onClose={() => {
-        setOpen(false);
+        !lockTarget && setOpen(false);
       }}
       onChange={(event, newValue) => {
-        onContainerChange(newValue)
+        !lockTarget && onContainerChange(newValue)
       }}
       isOptionEqualToValue={(option, value) => {
-        console.log(option.Names[0], value.Names[0])
-        return option.Names[0] === value.Names[0]
+        return !lockTarget && (option.Names[0] === value.Names[0])
       }}
       getOptionLabel={(option) => {
-        return option.Names[0]
+        return !lockTarget ? option.Names[0] : TargetContainer.Names[0]
       }}
       options={containers}
       loading={loading}
       freeSolo={true}
       placeholder={"Please select a container"}
-      defaultValue={targetResult.containerObject}
+      defaultValue={lockTarget ? TargetContainer : targetResult.containerObject}
       renderInput={(params) => (
         <TextField
           {...params}

+ 8 - 5
client/src/pages/config/users/formShortcuts.jsx

@@ -27,9 +27,9 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
 
 import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
 
-export const CosmosInputText = ({ name, type, placeholder, onChange, label, formik }) => {
+export const CosmosInputText = ({ name, style, type, placeholder, onChange, label, formik }) => {
   return <Grid item xs={12}>
-    <Stack spacing={1}>
+    <Stack spacing={1} style={style}>
       <InputLabel htmlFor={name}>{label}</InputLabel>
       <OutlinedInput
         id={name}
@@ -128,7 +128,7 @@ export const CosmosInputPassword = ({ name, type, placeholder, onChange, label,
   </Grid>
 }
 
-export const CosmosSelect = ({ name, label, formik, options }) => {
+export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
   return <Grid item xs={12}>
     <Stack spacing={1}>
       <InputLabel htmlFor={name}>{label}</InputLabel>
@@ -137,6 +137,7 @@ export const CosmosSelect = ({ name, label, formik, options }) => {
         variant="outlined"
         name={name}
         id={name}
+        disabled={disabled}
         select
         value={formik.values[name]}
         onChange={formik.handleChange}
@@ -158,7 +159,7 @@ export const CosmosSelect = ({ name, label, formik, options }) => {
   </Grid>;
 }
 
-export const CosmosCheckbox = ({ name, label, formik }) => {
+export const CosmosCheckbox = ({ name, label, formik, style }) => {
   return <Grid item xs={12}>
     <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
       <Field
@@ -167,6 +168,7 @@ export const CosmosCheckbox = ({ name, label, formik }) => {
         as={FormControlLabel}
         control={<Checkbox size="large" />}
         label={label}
+        style={style}
       />
     </Stack>
   </Grid>
@@ -182,7 +184,8 @@ export const CosmosCollapse = ({ children, title }) => {
             aria-controls="panel1a-content"
             id="panel1a-header"
           >
-            <Typography>{title}</Typography>
+            <Typography variant="h6">
+              {title}</Typography>
           </AccordionSummary>
           <AccordionDetails>
             {children}

+ 4 - 3
client/src/pages/config/users/proxyman.jsx

@@ -95,6 +95,7 @@ const ProxyManagement = () => {
         routes.push({
           Name: 'New Route',
           Description: 'New Route',
+          Mode: "SERVAPP",
           UseHost: false,
           Host: '',
           UsePathPrefix: false,
@@ -103,8 +104,7 @@ const ProxyManagement = () => {
           ThrottlePerMinute: 100,
           CORSOrigin: '',
           StripPathPrefix: false,
-          Static: false,
-          SPAMode: false,
+          AuthEnabled: false,
         });
         updateRoutes(routes);
     }}>Create</Button>
@@ -114,7 +114,8 @@ const ProxyManagement = () => {
     {config && <>
       <RestartModal openModal={openModal} setOpenModal={setOpenModal} />
       {routes && routes.map((route,key) => (<>
-        <RouteManagement routeConfig={route} setRouteConfig={(newRoute) => {
+        <RouteManagement routeConfig={route}
+          setRouteConfig={(newRoute) => {
             routes[key] = newRoute;
           }}
           up={() => up(key)}

+ 24 - 8
client/src/pages/config/users/routeman.jsx

@@ -33,7 +33,7 @@ import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, Cos
 import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined  } from '@ant-design/icons';
 import { CosmosContainerPicker } from './containerPicker';
 
-const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute }) => {
+const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockTarget=false, setRouteConfig, up, down, deleteRoute }) => {
   const [confirmDelete, setConfirmDelete] = React.useState(false);
 
   return <div style={{ maxWidth: '1000px', margin: '' }}>
@@ -67,6 +67,7 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
         {(formik) => (
           <form noValidate onSubmit={formik.handleSubmit}>
             <MainCard title={
+              noControls ? 'New Route' :
               <div>{routeConfig.Name} &nbsp;
                 <Chip label={<UpOutlined />} onClick={() => up()}/> &nbsp;
                 <Chip label={<DownOutlined />} onClick={() => down()}/> &nbsp;
@@ -93,11 +94,15 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
                 <CosmosCollapse title="Settings">
                   <Grid container spacing={2}>
                     <CosmosFormDivider title={'Target Type'}/>
+                    <Grid item xs={12}>
+                    <Alert color='info'>What are you trying to access with this route?</Alert>
+                    </Grid>
 
                     <CosmosSelect
                       name="Mode"
                       label="Mode"
                       formik={formik}
+                      disabled={lockTarget}
                       options={[
                         ["SERVAPP", "ServApp - Docker Container"],
                         ["PROXY", "Proxy"],
@@ -109,20 +114,24 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
                     <CosmosFormDivider title={'Target Settings'}/>
 
                     {
-                      formik.values.Mode === "SERVAPP" ? 
+                      (formik.values.Mode === "SERVAPP")? 
                       <CosmosContainerPicker
                         formik={formik}
+                        lockTarget={lockTarget} 
+                        TargetContainer={TargetContainer}
                       />
                       :  <CosmosInputText
-                      name="Target"
-                      label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
-                      placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
-                      formik={formik}
-                    />
+                        name="Target"
+                        label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
+                        placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
+                        formik={formik}
+                      />
                     }
                     
                     <CosmosFormDivider title={'Source'}/>
-
+                    <Grid item xs={12}>
+                    <Alert color='info'>What URL do you want to access your target from?</Alert>
+                    </Grid>
                     <CosmosCheckbox
                       name="UseHost"
                       label="Use Host"
@@ -134,6 +143,7 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
                       label="Host"
                       placeholder="Host"
                       formik={formik}
+                      style={{paddingLeft: '20px'}}
                     />}
 
                     <CosmosCheckbox
@@ -147,16 +157,22 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
                       label="Path Prefix"
                       placeholder="Path Prefix"
                       formik={formik}
+                      style={{paddingLeft: '20px'}}
                     />}
 
                     {formik.values.UsePathPrefix && <CosmosCheckbox
                       name="StripPathPrefix"
                       label="Strip Path Prefix"
                       formik={formik}
+                      style={{paddingLeft: '20px'}}
                     />}
 
                     <CosmosFormDivider title={'Security'}/>
                     
+                    <Grid item xs={12}>
+                    <Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
+                    </Grid>
+
                     <CosmosCheckbox
                       name="AuthEnabled"
                       label="Authentication Required"

+ 266 - 7
client/src/pages/servapps/servapps.jsx

@@ -1,17 +1,276 @@
 // material-ui
-import { Alert, Typography } from '@mui/material';
-import { useState } from 'react';
+import { AppstoreAddOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
+import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material';
+import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
+import { Stack } from '@mui/system';
+import { useEffect, useState } from 'react';
+import Paper from '@mui/material/Paper';
+import { styled } from '@mui/material/styles';
 
-// project import
-import MainCard from '../../components/MainCard';
+import * as API from '../../api';
+import isLoggedIn from '../../isLoggedIn';
+import RestartModal from '../config/users/restart';
+import RouteManagement from '../config/users/routeman';
 
-// ==============================|| SAMPLE PAGE ||============================== //
+const Item = styled(Paper)(({ theme }) => ({
+  backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
+  ...theme.typography.body2,
+  padding: theme.spacing(1),
+  textAlign: 'center',
+  color: theme.palette.text.secondary,
+}));
 
 const ServeApps = () => {
-  const {serveApps, setServeApps} = useState([]);
+  isLoggedIn();
+
+  const [serveApps, setServeApps] = useState([]);
+  const [isUpdating, setIsUpdating] = useState({});
+  const [search, setSearch] = useState("");
+  const [config, setConfig] = useState(null);
+  const [openModal, setOpenModal] = useState(false);
+  const [newRoute, setNewRoute] = useState(null);
+  const [openRestartModal, setOpenRestartModal] = useState(false);
+
+  const hasCosmosNetwork = (containerName) => {
+    const container = serveApps.find((app) => {
+      return app.Names[0].replace('/', '') === containerName.replace('/', '');
+    });
+    return container && container.NetworkSettings.Networks && Object.keys(container.NetworkSettings.Networks).some((network) => {
+      if(network.startsWith('cosmos-network'))
+        return true;
+    })
+  }
+
+  const refreshServeApps = () => {
+    API.docker.list().then((res) => {
+      setServeApps(res.data);
+    });
+    API.config.get().then((res) => {
+      setConfig(res.data);
+    });
+    setIsUpdating({});
+  };
+
+  const setIsUpdatingId = (id, value) => {
+    setIsUpdating({
+      ...isUpdating,
+      [id]: value
+    });
+  }
+
+  const getContainersRoutes = (containerName) => {
+    return config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
+      return route.Mode == "SERVAPP" && (
+        route.Target.startsWith(containerName) ||
+        route.Target.split('://')[1].startsWith(containerName)
+      )
+    })
+  }
+
+  useEffect(() => {
+    refreshServeApps();
+  }, []);
+  
+  function updateRoutes() {
+    let con = {
+      ...config,
+      HTTPConfig: {
+        ...config.HTTPConfig,
+        ProxyConfig: {
+          ...config.HTTPConfig.ProxyConfig,
+          Routes: [
+            ...config.HTTPConfig.ProxyConfig.Routes,
+            newRoute,
+          ]
+        },
+      },
+    };
+    
+    API.config.set(con).then((res) => {
+      setOpenModal(false);
+      setOpenRestartModal(true);
+    });
+  }
+  const gridAnim = {
+    transition: 'all 0.2s ease',
+    opacity: 1,
+    transform: 'translateY(0px)',
+    '&.MuiGrid2-item--hidden': {
+      opacity: 0,
+      transform: 'translateY(-20px)',
+    },
+  };
 
   return <div>
-    <Alert severity="info">Implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord!</Alert>
+    <RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
+    <Dialog open={openModal} onClose={() => setOpenModal(false)}>
+        <DialogTitle>Connect ServApp</DialogTitle>
+            {openModal && <>
+            <DialogContent>
+                <DialogContentText>
+                  <Stack spacing={2}>
+                    <div>
+                      Welcome to the Connect Wizard. This interface will help you expose your ServApp securely to the internet.
+                    </div>
+                    <div>
+                        {openModal && !hasCosmosNetwork(openModal.Names[0]) && <Alert severity="warning">This ServApp does not appear to be connected to a Cosmos Network, so the hostname might not be accessible. The easiest way to fix this is to check the box "Force Secure Network" or manually create a sub-network in Docker.</Alert>}
+                    </div>
+                    <div>
+                        <RouteManagement TargetContainer={openModal} 
+                          routeConfig={{
+                            Target: "http://"+openModal.Names[0] + ":8080",
+                            Mode: "SERVAPP",
+                            Name: openModal.Names[0].replace('/', ''),
+                            Description: "Expose " + openModal.Names[0].replace('/', '') + " to the internet",
+                            UseHost: false,
+                            Host: '',
+                            UsePathPrefix: false,
+                            PathPrefix: '',
+                            Timeout: 30000,
+                            ThrottlePerMinute: 100,
+                            CORSOrigin: '',
+                            StripPathPrefix: false,
+                            AuthEnabled: false,
+                          }} 
+                          setRouteConfig={(_newRoute) => {
+                            setNewRoute(_newRoute);
+                          }}
+                          up={() => {}}
+                          down={() => {}}
+                          deleteRoute={() => {}}
+                          noControls
+                          lockTarget
+                        />
+                    </div>
+                  </Stack>
+                </DialogContentText>
+            </DialogContent>
+            <DialogActions>
+                <Button onClick={() => setOpenModal(false)}>Cancel</Button>
+                <Button onClick={() => {
+                  updateRoutes()
+                }}>Connect</Button>
+            </DialogActions>
+        </>}
+    </Dialog>
+
+    <Stack spacing={2}>
+      <Stack direction="row" spacing={2}>
+        <Input placeholder="Search"
+          value={search}
+          startAdornment={
+            <InputAdornment position="start">
+              <SearchOutlined />
+            </InputAdornment>
+          }
+          onChange={(e) => {
+            setSearch(e.target.value);
+          }}
+        />
+        <Button variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
+          refreshServeApps();
+        }}>Refresh</Button>
+        <Tooltip title="This is not implemented yet.">
+          <span style={{ cursor: 'not-allowed' }}>
+            <Button variant="contained" startIcon={<AppstoreAddOutlined />} disabled>Start ServApp</Button>
+          </span>
+        </Tooltip>
+      </Stack>
+
+      <Grid2 container  spacing={2}>
+        {serveApps && serveApps.filter(app => search.length < 2 || app.Names[0].includes(search)).map((app) => {
+          return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4}>
+            <Item>
+            <Stack justifyContent='space-around' direction="column" spacing={2} padding={2} divider={<Divider orientation="horizontal" flexItem />}>
+              <Stack direction="row" spacing={2} alignItems="center">
+                <Typography variant="body2" color="text.secondary">
+                  {
+                    ({
+                      "created": <Chip label="Created" color="warning" />,
+                      "restarting": <Chip label="Restarting" color="warning" />,
+                      "running": <Chip label="Running" color="success" />,
+                      "removing": <Chip label="Removing" color="error" />,
+                      "paused": <Chip label="Paused" color="info" />,
+                      "exited": <Chip label="Exited" color="error" />,
+                      "dead": <Chip label="Dead" color="error" />,
+                    })[app.State]
+                  }
+                </Typography>
+                <Stack direction="column" spacing={0} alignItems="flex-start">
+                  <Typography  variant="h5" color="text.secondary">
+                    {app.Names[0].replace('/', '')}&nbsp;
+                  </Typography>
+                  <Typography style={{ fontSize: '80%' }} color="text.secondary">
+                    {app.Image}
+                  </Typography>
+                </Stack>
+              </Stack>
+              <Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
+                <Typography  variant="h6" color="text.secondary">
+                  Ports
+                </Typography> 
+                <Stack margin={1} direction="row" spacing={1}>
+                  {app.Ports.map((port) => {
+                    return <Tooltip title={port.PublicPort ? 'Warning, this port is publicly accessible' : ''}>
+                      <Chip style={{ fontSize: '80%' }} label={":" + port.PrivatePort} color={port.PublicPort ? 'warning' : 'default'} />
+                    </Tooltip>
+                  })}
+                </Stack>
+              </Stack>
+              <Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
+                <Typography  variant="h6" color="text.secondary">
+                  Networks
+                </Typography> 
+                <Stack margin={1} direction="row" spacing={1}>
+                  {app.NetworkSettings.Networks && Object.keys(app.NetworkSettings.Networks).map((network) => {
+                    return <Chip style={{ fontSize: '80%' }} label={network} color={network === 'bridge' ? 'warning' : 'default'} />
+                  })}
+                </Stack>
+              </Stack>
+              {isUpdating[app.Id] ? <div>
+                <CircularProgress color="inherit" />
+              </div> :
+              <Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
+                <Typography  variant="h6" color="text.secondary">
+                  Settings
+                </Typography> 
+                <Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
+                  <Checkbox
+                    checked={app.Labels['cosmos-force-network-secured'] === 'true'}
+                    onChange={(e) => {
+                      setIsUpdatingId(app.Id, true);
+                      API.docker.secure(app.Id, e.target.checked).then(() => {
+                        setTimeout(() => {
+                          setIsUpdatingId(app.Id, false);
+                          refreshServeApps();
+                        }, 3000);
+                      })
+                    }}
+                  /> Force Secure Network
+                </Stack></Stack>}
+              <Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
+                <Typography  variant="h6" color="text.secondary">
+                  Proxies
+                </Typography>
+                <Stack spacing={2} direction="row">
+                  {getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
+                    return <Chip label={route.Host + route.PathPrefix} color="info" />
+                  })}
+                  {getContainersRoutes(app.Names[0].replace('/', '')).length == 0 &&
+                    <Chip label="No Proxy Setup" />}
+                </Stack>
+              </Stack>
+              <Stack>
+                <Button variant="contained" color="primary" onClick={() => {
+                  setOpenModal(app);
+                }}>Connect</Button>
+              </Stack>
+            </Stack>
+          </Item></Grid2>
+        })
+        }
+      </Grid2>
+    </Stack>
   </div>
 }
 

+ 2 - 0
client/src/themes/index.jsx

@@ -18,6 +18,8 @@ export default function ThemeCustomization({ children }) {
         window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ?
              'dark' : 'light');
 
+             console.log(theme)
+
     // eslint-disable-next-line react-hooks/exhaustive-deps
     const themeTypography = Typography(`'Public Sans', sans-serif`);
     const themeCustomShadows = useMemo(() => CustomShadows(theme), [theme]);

+ 1 - 1
client/src/themes/overrides/Button.jsx

@@ -3,7 +3,7 @@
 export default function Button(theme) {
     const disabledStyle = {
         '&.Mui-disabled': {
-            backgroundColor: theme.palette.grey[200]
+            backgroundColor: theme.palette.grey[400]
         }
     };
 

+ 20 - 18
client/src/themes/palette.jsx

@@ -55,24 +55,26 @@ const Palette = (mode) => {
             }
         }
     } : {
-        mode,
-        common: {
-            black: '#000',
-            white: '#fff'
-        },
-        ...paletteColor,
-        text: {
-            primary: paletteColor.grey[700],
-            secondary: paletteColor.grey[500],
-            disabled: paletteColor.grey[400]
-        },
-        action: {
-            disabled: paletteColor.grey[300]
-        },
-        divider: paletteColor.grey[200],
-        background: {
-            paper: paletteColor.grey[0],
-            default: paletteColor.grey.A50
+        palette: {
+            mode,
+            common: {
+                black: '#000',
+                white: '#fff'
+            },
+            ...paletteColor,
+            text: {
+                primary: paletteColor.grey[700],
+                secondary: paletteColor.grey[600],
+                disabled: paletteColor.grey[500]
+            },
+            action: {
+                disabled: paletteColor.grey[300]
+            },
+            divider: paletteColor.grey[200],
+            background: {
+                paper: paletteColor.grey[0],
+                default: paletteColor.grey.A50
+            }
         }
     });
 };

+ 1 - 0
go.mod

@@ -64,6 +64,7 @@ require (
 	github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
 	github.com/imdario/mergo v0.3.14 // indirect
 	github.com/jarcoal/httpmock v1.0.7 // indirect
+	github.com/jasonlvhit/gocron v0.0.1 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/joho/godotenv v1.5.1 // indirect
 	github.com/json-iterator/go v1.1.10 // indirect

+ 5 - 0
go.sum

@@ -187,6 +187,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
 github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
+github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
 github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k=
 github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
@@ -302,6 +303,8 @@ github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h
 github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
 github.com/jarcoal/httpmock v1.0.7 h1:d1a2VFpSdm5gtjhCPWsQHSnx8+5V3ms5431YwvmkuNk=
 github.com/jarcoal/httpmock v1.0.7/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
+github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
+github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -391,7 +394,9 @@ github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4r
 github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cosmos-server",
-  "version": "0.0.9",
+  "version": "0.1.0",
   "description": "",
   "main": "test-server.js",
   "bugs": {

+ 69 - 0
src/CRON.go

@@ -0,0 +1,69 @@
+package main
+
+import (
+	"github.com/jasonlvhit/gocron"
+	"io/ioutil"
+	"net/http"
+	"github.com/azukaar/cosmos-server/src/utils"
+	"os"
+	"path/filepath"
+	"encoding/json"
+)
+
+type Version struct {
+	Version string `json:"version"`
+}
+
+func checkVersion() {
+
+	ex, err := os.Executable()
+	if err != nil {
+			panic(err)
+	}
+	exPath := filepath.Dir(ex)
+
+	pjs, errPR := os.Open(exPath + "/meta.json")
+	if errPR != nil {
+		utils.Error("checkVersion", errPR)
+		return
+	}
+
+	packageJson, _ := ioutil.ReadAll(pjs)
+
+	utils.Debug("checkVersion" + string(packageJson))
+
+	var version Version
+	errJ := json.Unmarshal(packageJson, &version)
+	if errJ != nil {
+		utils.Error("checkVersion", errJ)
+		return
+	}
+
+	myVersion := version.Version
+	
+	response, err := http.Get("https://comos-technologies.com/versions/" + myVersion)
+	if err != nil {
+		utils.Error("checkVersion", err)
+		return
+	}
+
+	defer response.Body.Close()
+
+	body, err := ioutil.ReadAll(response.Body)
+	if err != nil {
+		utils.Error("checkVersion", err)
+		return
+	}
+
+	if string(body) != myVersion {
+		utils.Log("New version available: " + string(body))
+		// update
+	} else {
+		utils.Log("No new version available")
+	}
+}
+
+func CRON() {
+	gocron.Every(1).Day().At("00:00").Do(checkVersion)
+	<-gocron.Start()
+}

+ 1 - 0
src/configapi/set.go

@@ -33,6 +33,7 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
 		config := utils.GetBaseMainConfig()
 		request.HTTPConfig.AuthPrivateKey = config.HTTPConfig.AuthPrivateKey
 		request.HTTPConfig.TLSKey = config.HTTPConfig.TLSKey
+		request.NewInstall = config.NewInstall
 
 		utils.SaveConfigTofile(request)
 

+ 4 - 3
src/docker/api_secureContainer.go

@@ -15,7 +15,8 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
 	}
 
 	vars := mux.Vars(req)
-	containerName := utils.Sanitize(vars["container"])
+	containerName := utils.Sanitize(vars["containerId"])
+	status := utils.Sanitize(vars["status"])
 	
 	if(req.Method == "GET") {
 		container, err := DockerClient.ContainerInspect(DockerContext, containerName)
@@ -26,10 +27,10 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
 		}
 
 		AddLabels(container, map[string]string{
-			"cosmos-force-network-secured": "true",
+			"cosmos-force-network-secured": status,
 		});
 
-		utils.Log("API: Add Force network secured: " + containerName)
+		utils.Log("API: Set Force network secured "+status+" : " + containerName)
 
 		_, errEdit := EditContainer(container.ID, container)
 		if errEdit != nil {

+ 3 - 1
src/docker/docker.go

@@ -138,7 +138,9 @@ func ListContainers() ([]types.Container, error) {
 		return nil, errD
 	}
 
-	containers, err := DockerClient.ContainerList(DockerContext, types.ContainerListOptions{})
+	containers, err := DockerClient.ContainerList(DockerContext, types.ContainerListOptions{
+		All: true,
+	})
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
src/httpServer.go

@@ -202,7 +202,7 @@ func StartServer() {
 	srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
 	srapi.HandleFunc("/api/users", user.UsersRoute)
 	
-	srapi.HandleFunc("/api/servapps/{container}/secure", docker.SecureContainerRoute)
+	srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute)
 	srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
 
 	srapi.Use(tokenMiddleware)

+ 2 - 0
src/index.go

@@ -14,6 +14,8 @@ func main() {
 
 		LoadConfig()
 		
+		go CRON()
+
 		docker.Test()
 
 		docker.DockerListenEvents()

+ 1 - 1
src/proxy/buildFromConfig.go

@@ -15,7 +15,7 @@ func BuildFromConfig(router *mux.Router, config utils.ProxyConfig) *mux.Router {
 
 	for i := len(config.Routes)-1; i >= 0; i-- {
 		routeConfig := config.Routes[i]
-		RouterGen(routeConfig, router, RouteTo(routeConfig.Target))
+		RouterGen(routeConfig, router, RouteTo(routeConfig))
 	}
 	
 	return router

+ 25 - 14
src/proxy/routeTo.go

@@ -4,6 +4,7 @@ import (
 	"net/http"
 	"net/http/httputil"    
 	"net/url"
+	spa "github.com/roberthodgen/spa-server"
 	"github.com/azukaar/cosmos-server/src/utils"
 	// "io/ioutil"
 	// "io"
@@ -20,7 +21,6 @@ func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
 
 	proxy := httputil.NewSingleHostReverseProxy(url)
 
-	// upgrade the request to websocket
 	proxy.ModifyResponse = func(resp *http.Response) error {
 		utils.Debug("Response from backend: " + resp.Status)
 		utils.Debug("URL was " + resp.Request.URL.String())
@@ -30,20 +30,31 @@ func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
 	return proxy, nil
 }
 
-// ProxyRequestHandler handles the http request using proxy
-func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
-	return func(w http.ResponseWriter, r *http.Request) {
-			proxy.ServeHTTP(w, r)
-	}
-}
 
-func RouteTo(destination string) *httputil.ReverseProxy /*func(http.ResponseWriter, *http.Request)*/ {
+func RouteTo(route utils.ProxyRouteConfig) http.Handler /*func(http.ResponseWriter, *http.Request)*/ {
 	// initialize a reverse proxy and pass the actual backend server url here
-	proxy, err := NewProxy(destination)
-	if err != nil {
-			panic(err)
-	}
 
-	// create a handler function which uses the reverse proxy
-	return proxy //ProxyRequestHandler(proxy)
+	destination := route.Target
+	routeType := route.Mode
+
+	if(routeType == "SERVAPP" || routeType == "PROXY") {
+		proxy, err := NewProxy(destination)
+		if err != nil {
+				utils.Error("Create Route", err)
+		}
+
+		// create a handler function which uses the reverse proxy
+		return proxy
+	}  else if (routeType == "STATIC") {
+		return http.FileServer(http.Dir(destination))
+	}  else if (routeType == "SPA") {
+		return spa.SpaHandler(destination, "index.html")	
+	} else if(routeType == "REDIRECT") {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			http.Redirect(w, r, destination, 302)
+		})
+	} else {
+		utils.Error("Invalid route type", nil)
+		return nil
+	}
 }

+ 15 - 7
src/proxy/routerGen.go

@@ -2,7 +2,6 @@ package proxy
 
 import (
 	"net/http"
-	"net/http/httputil"  
 	"github.com/gorilla/mux"
 	"time"
 	"github.com/azukaar/cosmos-server/src/utils" 
@@ -44,10 +43,7 @@ func tokenMiddleware(enabled bool) func(next http.Handler) http.Handler {
 	}
 }
 
-func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *httputil.ReverseProxy) *mux.Route {
-	var realDestination http.Handler
-	realDestination = destination
-
+func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination http.Handler) *mux.Route {
 	origin := router.Methods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD")
 
 	if(route.UseHost) {
@@ -55,11 +51,17 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *ht
 	}
 
 	if(route.UsePathPrefix) {
+		if(route.PathPrefix != "" && route.PathPrefix[0] != '/') {
+			utils.Error("PathPrefix must start with a /", nil)
+		}
 		origin = origin.PathPrefix(route.PathPrefix)
 	}
 	
 	if(route.UsePathPrefix && route.StripPathPrefix) {
-		realDestination = http.StripPrefix(route.PathPrefix, destination)
+		if(route.PathPrefix != "" && route.PathPrefix[0] != '/') {
+			utils.Error("PathPrefix must start with a /", nil)
+		}
+		destination = http.StripPrefix(route.PathPrefix, destination)
 	}
 	timeout := route.Timeout
 	
@@ -83,6 +85,10 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *ht
 		}
 	}
 
+	if(route.UsePathPrefix && !route.StripPathPrefix && (route.Mode == "STATIC" || route.Mode == "SPA")) {
+		utils.Warn("PathPrefix is used, but StripPathPrefix is false. The route mode is " + (string)(route.Mode) + ". This will likely cause issues with the route. Ignore this warning if you know what you are doing.")
+	}
+
 	origin.Handler(
 		tokenMiddleware(route.AuthEnabled)(
 		utils.CORSHeader(originCORS)(
@@ -95,7 +101,9 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *ht
 					http.StatusTooManyRequests, "HTTP003")
 				return 
 			}),
-		)(realDestination)))))
+		)(destination)))))
+
+	utils.Log("Added route: ["+ (string)(route.Mode)  + "] " + route.Host + route.PathPrefix + " to " + route.Target + "")
 
 	return origin
 }