Yann Stepienik пре 2 година
родитељ
комит
e93e45d4df

+ 8 - 5
changelog.md

@@ -1,13 +1,16 @@
 ## Version 0.3.0
+ - Implement 2 FA
+ - Implement SMTP to Send Email (password reset / invites)
+ - Add homepage
+ - DNS challenge for letsencrypt
  - Set Max nb simulatneous connections per user 
+ - Admin only routes (See in security tab)
  - Set Global Max nb simulatneous connections
- - Display nickname on invite page
- - Reset self-signed certificates when hostnames changes
  - Block based on geo-locations
  - Block common bots
- - DNS challenge for letsencrypt
- - Implement 2 FA
- - Implement SMTP to Send Email (password reset / invites)
+ - Display nickname on invite page
+ - Reset self-signed certificates when hostnames changes
+ - Edit user emails
  - Show loading on user rows on actions
 
 ## Version 0.2.0

BIN
client/src/assets/images/wallpaper(1).png


BIN
client/src/assets/images/wallpaper.png


+ 6 - 0
client/src/components/countrySelect.jsx

@@ -451,6 +451,12 @@ export default function CountrySelect({name, label, formik}) {
       onChange={(event, value) => {
         formik.setFieldValue(name, value)
       }}
+      filterOptions={(options, state) => {
+        const inputValue = state.inputValue.toUpperCase();
+        return options.filter((option) => {
+          return countries[option].label.toUpperCase().includes(inputValue)
+        })
+      }}
       error={Boolean(formik.touched[name] && formik.errors[name])}
       getOptionLabel={(option) => <div style={{verticalAlign: 'middle'}}><img
         loading="lazy"

+ 1 - 1
client/src/components/tableView/prettyTableView.jsx

@@ -62,7 +62,6 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
               })
               .map((row, key) => (
                 <TableRow
-                  onClick={() => onRowClick && onRowClick(row, key)}
                   key={getKey(row)}
                   sx={{
                     cursor: 'pointer',
@@ -81,6 +80,7 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
                 
                     (!column.screenMin || screenMin[column.screenMin]) && <TableCell 
                       component={(linkTo && !column.clickable) ? Link : 'td'}
+                      onClick={() => !column.clickable && onRowClick && onRowClick(row, key)}
                       to={linkTo && linkTo(row, key)}
                       className={column.underline ? 'emphasis' : ''}
                       sx={{

+ 7 - 6
client/src/menu-items/dashboard.jsx

@@ -1,5 +1,5 @@
 // assets
-import { HomeOutlined, AppstoreOutlined } from '@ant-design/icons';
+import { HomeOutlined, AppstoreOutlined, DashboardOutlined } from '@ant-design/icons';
 
 // icons
 const icons = {
@@ -17,16 +17,17 @@ const dashboard = {
             id: 'home',
             title: 'Home',
             type: 'item',
-            url: '/ui',
+            url: '/ui/',
             icon: icons.HomeOutlined,
             breadcrumbs: false
         },
         {
-            id: 'servapps',
-            title: 'ServApps',
+            id: 'dashboard',
+            title: 'Dashboard',
             type: 'item',
-            url: '/ui/servapps',
-            icon: AppstoreOutlined
+            url: '/ui/dashboard',
+            icon: DashboardOutlined,
+            breadcrumbs: false
         },
     ]
 };

+ 9 - 2
client/src/menu-items/pages.jsx

@@ -1,5 +1,5 @@
 // assets
-import { ProfileOutlined, SettingOutlined, NodeExpandOutlined} from '@ant-design/icons';
+import { ProfileOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
 
 // icons
 const icons = {
@@ -15,6 +15,13 @@ const pages = {
     title: 'Management',
     type: 'group',
     children: [
+        {
+            id: 'servapps',
+            title: 'ServApps',
+            type: 'item',
+            url: '/ui/servapps',
+            icon: AppstoreOutlined
+        },
         {
             id: 'url',
             title: 'URLs',
@@ -24,7 +31,7 @@ const pages = {
         },
         {
             id: 'users',
-            title: 'Manage Users',
+            title: 'Users',
             type: 'item',
             url: '/ui/config-users',
             icon: icons.ProfileOutlined,

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

@@ -51,6 +51,7 @@ const AuthLogin = () => {
     // TODO: Extract ?redirect=<URL> to redirect to a specific page after login
     const urlSearchParams = new URLSearchParams(window.location.search);
     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';
 
@@ -78,6 +79,11 @@ const AuthLogin = () => {
                 <br />
             </Grid>}
 
+            { notLoggedAdmin &&<Grid container spacing={2} justifyContent="center">
+                <Alert severity="error">You need to be Admin</Alert>
+                <br />
+            </Grid>}
+
             { invalid &&<Grid container spacing={2} justifyContent="center">
                 <Alert severity="error">You have been disconnected. Please login to continue</Alert>
                 <br />

+ 7 - 0
client/src/pages/config/routes/routeSecurity.jsx

@@ -30,6 +30,7 @@ const RouteSecurity = ({ routeConfig }) => {
           MaxBandwith: routeConfig.MaxBandwith,
           BlockAPIAbuse: routeConfig.BlockAPIAbuse,
           BlockCommonBots: routeConfig.BlockCommonBots,
+          AdminOnly: routeConfig.AdminOnly,
           _SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
           _SmartShield_PolicyStrictness: (routeConfig.SmartShield ? routeConfig.SmartShield.PolicyStrictness : 0),
           _SmartShield_PerUserTimeBudget: (routeConfig.SmartShield ? routeConfig.SmartShield.PerUserTimeBudget : 0),
@@ -97,6 +98,12 @@ const RouteSecurity = ({ routeConfig }) => {
                       formik={formik}
                     />
 
+                    <CosmosCheckbox
+                      name="AdminOnly"
+                      label="Admin only"
+                      formik={formik}
+                    />
+
                     <CosmosFormDivider title={'Smart Shield'} />
 
                     <CosmosCheckbox

+ 8 - 0
client/src/pages/config/users/configman.jsx

@@ -59,6 +59,7 @@ const ConfigManagement = () => {
           LoggingLevel: config.LoggingLevel,
           RequireMFA: config.RequireMFA,
           GeoBlocking: config.BlockedCountries,
+          AutoUpdate: config.AutoUpdate,
 
           Hostname: config.HTTPConfig.Hostname,
           GenerateMissingTLSCert: config.HTTPConfig.GenerateMissingTLSCert,
@@ -89,6 +90,7 @@ const ConfigManagement = () => {
               MongoDB: values.MongoDB,
               LoggingLevel: values.LoggingLevel,
               RequireMFA: values.RequireMFA,
+              AutoUpdate: values.AutoUpdate,
               BlockedCountries: values.GeoBlocking,
               HTTPConfig: {
                 ...config.HTTPConfig,
@@ -150,6 +152,12 @@ const ConfigManagement = () => {
                     formik={formik}
                     helperText="Require MFA for all users"
                   />
+                  
+                  <CosmosCheckbox
+                    label="Auto Update Cosmos"
+                    name="AutoUpdate"
+                    formik={formik}
+                  />
 
                   <Grid item xs={12}>
                     <Stack spacing={1}>

+ 33 - 0
client/src/pages/config/users/usermanagement.jsx

@@ -29,6 +29,7 @@ const UserManagement = () => {
     const [openCreateForm, setOpenCreateForm] = React.useState(false);
     const [openDeleteForm, setOpenDeleteForm] = React.useState(false);
     const [openInviteForm, setOpenInviteForm] = React.useState(false);
+    const [openEditEmail, setOpenEditEmail] = React.useState(false);
     const [toAction, setToAction] = React.useState(null);
     const [loadingRow, setLoadingRow] = React.useState(null);
 
@@ -120,6 +121,35 @@ const UserManagement = () => {
                 }}>Delete</Button>
             </DialogActions>
         </Dialog>
+        
+        <Dialog open={openEditEmail} onClose={() => setOpenEditEmail(false)}>
+            <DialogTitle>Edit Email</DialogTitle>
+            <DialogContent>
+                <DialogContentText>
+                    Use this form to invite edit {openEditEmail}'s Email.
+                </DialogContentText>
+                <TextField
+                    autoFocus
+                    margin="dense"
+                    id="c-email-edit"
+                    label="Email Address"
+                    type="email"
+                    fullWidth
+                    variant="standard"
+                />
+            </DialogContent>
+            <DialogActions>
+                <Button onClick={() => setOpenEditEmail(false)}>Cancel</Button>
+                <Button onClick={() => {
+                    API.users.edit(openEditEmail, {
+                        email: document.getElementById('c-email-edit').value,
+                    }).then(() => {
+                        setOpenEditEmail(false);
+                        refresh();
+                    });
+                }}>Edit</Button>
+            </DialogActions>
+        </Dialog>
 
         <Dialog open={openCreateForm} onClose={() => setOpenCreateForm(false)}>
             <DialogTitle>Create User</DialogTitle>
@@ -174,6 +204,9 @@ const UserManagement = () => {
 
         {!isLoading && rows && (<PrettyTableView 
             data={rows}
+            onRowClick = {(r) => {
+                setOpenEditEmail(r.nickname);
+            }}
             getKey={(r) => r.nickname}
             columns={[
                 {

+ 6 - 0
client/src/pages/dashboard/index.jsx

@@ -116,6 +116,12 @@ const DashboardDefault = () => {
                     </Alert>
                 )}
 
+                {coStatus && coStatus.newVersionAvailable && (
+                    <Alert severity="warning">
+                        A new version of Cosmos is available! Please update to the latest version to get the latest features and bug fixes.
+                    </Alert>
+                )}
+
                 {coStatus && coStatus.needsRestart && (
                     <Alert severity="warning">
                         You have made changes to the configuration that require a restart to take effect. Please restart Cosmos to apply the changes.

+ 151 - 0
client/src/pages/home/index.jsx

@@ -0,0 +1,151 @@
+import { useParams } from "react-router";
+import Back from "../../components/back";
+import { Alert, Box, CircularProgress, Grid, Stack, useTheme } from "@mui/material";
+import { useEffect, useState } from "react";
+import * as API  from "../../api";
+import wallpaper from '../../assets/images/wallpaper.png';
+import Grid2 from "@mui/material/Unstable_Grid2/Grid2";
+import { getFaviconURL } from "../../utils/routes";
+import { Link } from "react-router-dom";
+import { getFullOrigin } from "../../utils/routes";
+import IsLoggedIn from "../../isLoggedIn";
+
+
+const HomeBackground = () => {
+    const theme = useTheme();
+    return (
+        <Box sx={{ position: 'fixed', float: 'left',  overflow: 'hidden',  zIndex: 0, top: 0, left: 0, right: 0, bottom: 0 }}>
+            <img src={wallpaper} style={{ display:'inline'}} alt="Cosmos" width="100%" height="100%" />
+        </Box>
+    );
+};
+
+const blockStyle = {
+    margin: 0, 
+    whiteSpace: 'nowrap',
+    overflow: 'hidden',
+    textOverflow: 'ellipsis',
+    maxWidth: '78%',
+    verticalAlign: 'middle',
+}
+
+const HomePage = () => {
+  const { routeName } = useParams();
+  const [config, setConfig] = useState(null);
+  const [coStatus, setCoStatus] = useState(null);
+
+  const refreshStatus = () => {
+      API.getStatus().then((res) => {
+          setCoStatus(res.data);
+      });
+  }
+
+  const refreshConfig = () => {
+    API.config.get().then((res) => {
+      setConfig(res.data);
+    });
+  };
+  
+  let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []);
+
+  useEffect(() => {
+    refreshConfig();
+    refreshStatus();
+  }, []);
+  
+
+  return <Stack spacing={2} >
+    <IsLoggedIn />
+    <HomeBackground />
+    <style>
+        {`header {
+            background: rgba(0.2,0.2,0.2,0.2) !important;
+            border-bottom-color: rgba(0.4,0.4,0.4,0.4) !important;
+            color: white !important;
+        }
+
+        header .MuiChip-label  {
+            color: #eee !important;
+        }
+
+        header .MuiButtonBase-root {
+            color: #eee !important;
+            background: rgba(0.2,0.2,0.2,0.2) !important;
+        }
+
+        .app {
+            transition: background 0.1s ease-in-out;
+            transition: transform 0.1s ease-in-out;
+        }
+        
+        .app:hover {
+            cursor: pointer;
+            background: rgba(0.4,0.4,0.4,0.4) !important;
+            transform: scale(1.05);
+        }
+    `}
+    </style>
+    <Stack style={{zIndex: 2}} spacing={1}>
+        {coStatus && !coStatus.database && (
+            <Alert severity="error">
+                No Database is setup for Cosmos! User Management and Authentication will not work.<br />
+                You can either setup the database, or disable user management in the configuration panel.<br />
+            </Alert>
+        )}
+
+        {coStatus && coStatus.letsencrypt && (
+            <Alert severity="error">
+                You have enabled Let's Encrypt for automatic HTTPS Certificate. You need to provide the configuration with an email address to use for Let's Encrypt in the configs.
+            </Alert>
+        )}
+
+        {coStatus && coStatus.newVersionAvailable && (
+            <Alert severity="warning">
+                A new version of Cosmos is available! Please update to the latest version to get the latest features and bug fixes.
+            </Alert>
+        )}
+
+        {coStatus && coStatus.needsRestart && (
+            <Alert severity="warning">
+                You have made changes to the configuration that require a restart to take effect. Please restart Cosmos to apply the changes.
+            </Alert>
+        )}
+
+        {coStatus && coStatus.domain && (
+            <Alert severity="error">
+                You are using localhost or 0.0.0.0 as a hostname in the configuration. It is recommended that you use a domain name instead.
+            </Alert>
+        )}
+
+        {coStatus && !coStatus.docker && (
+            <Alert severity="error">
+                Docker is not connected! Please check your docker connection.<br/>
+                Did you forget to add <pre>-v /var/run/docker.sock:/var/run/docker.sock</pre> to your docker run command?<br />
+                if your docker daemon is running somewhere else, please add <pre>-e DOCKER_HOST=...</pre> to your docker run command.
+            </Alert>
+        )}
+    </Stack>
+    
+    <Grid2 container spacing={2} style={{zIndex: 2}}>
+        {config && config.HTTPConfig.ProxyConfig.Routes.map((route) => {
+            return <Grid2 item xs={12} sm={6} md={4} lg={3} xl={2} key={route.Name}>
+                <Box className='app' style={{padding: 10, color: 'white', background: 'rgba(0,0,0,0.35)', borderRadius: 5}}>
+                    <Link to={getFullOrigin(route)} target="_blank" style={{textDecoration: 'none', color: 'white'}}>
+                        <Stack direction="row" spacing={2} alignItems="center">
+                            <img src={getFaviconURL(route)} width="64px" />
+
+                            <div style={{width: '100%'}}>
+                                <h3 style={blockStyle}>{route.Name}</h3>
+                                <p style={blockStyle}>{route.Description}</p>
+                                <p style={blockStyle}>{route.Target}</p>
+                            </div>
+                        </Stack>
+                    </Link>
+                </Box>
+            </Grid2>
+        })}
+    </Grid2>
+  </Stack>
+}
+
+export default HomePage;

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

@@ -63,7 +63,7 @@ const NewInstall = () => {
             label: 'Docker 🐋 (step 1/4)',
             component: <Stack item xs={12} spacing={2}>
                 <div>
-                    <QuestionCircleOutlined /> Cosmos is using docker to run applications. It is optionnal, but Cosmos will run in reverse-proxy-only mode if it cannot connect to Docker.
+                    <QuestionCircleOutlined /> Cosmos is using docker to run applications. It is optional, but Cosmos will run in reverse-proxy-only mode if it cannot connect to Docker.
                 </div>
                 {(status && status.docker) ? 
                     <Alert severity="success">
@@ -98,7 +98,7 @@ const NewInstall = () => {
             label: 'Database 🗄️ (step 2/4)',
             component:  <Stack item xs={12} spacing={2}>
                 <div>
-                <QuestionCircleOutlined /> Cosmos is using a MongoDB database to store all the data. It is optionnal, but Authentication as well as the UI will not work without a database.
+                <QuestionCircleOutlined /> Cosmos is using a MongoDB database to store all the data. It is optional, but Authentication as well as the UI will not work without a database.
                 </div>
                 {(status && status.database) ? 
                     <Alert severity="success">
@@ -412,7 +412,7 @@ const NewInstall = () => {
                 Well done! You have successfully installed Cosmos. You can now login to your server using the admin account you created.
                 If you have changed the hostname, don't forget to use that URL to access your server after the restart.
                 If you have are running into issues, check the logs for any error messages and edit the file in the /config folder. 
-                If you still don't manage, please join our <a href="https://discord.gg/PwMWwsrwHA">Discord server</a> and we'll be happy to help!
+                If you still don't manage, please join our <a target="_blank" href="https://discord.gg/PwMWwsrwHA">Discord server</a> and we'll be happy to help!
             </div>,
             nextButtonLabel: () => {
                 return 'Apply and Restart';

+ 5 - 0
client/src/routes/MainRoutes.jsx

@@ -10,6 +10,7 @@ import ServeApps from '../pages/servapps/servapps';
 import { Navigate } from 'react-router';
 import RouteConfigPage from '../pages/config/routeConfigPage';
 import logo from '../assets/images/icons/cosmos.png';
+import HomePage from '../pages/home';
 
 
 // render - dashboard
@@ -43,6 +44,10 @@ const MainRoutes = {
         },
         {
             path: '/ui',
+            element: <HomePage />
+        },
+        {
+            path: '/ui/dashboard',
             element: <DashboardDefault />
         },
         {

+ 1 - 1
client/src/utils/routes.jsx

@@ -33,7 +33,7 @@ const addProtocol = (url) => {
 }
 
 export const getOrigin = (route) => {
-  return (route.UseHost ? route.Host : '') + (route.UsePathPrefix ? route.PathPrefix : '');
+  return (route.UseHost ? route.Host : window.location.origin) + (route.UsePathPrefix ? route.PathPrefix : '');
 }
 
 export const getFullOrigin = (route) => {

+ 1 - 0
go.mod

@@ -34,6 +34,7 @@ require (
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.0 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Microsoft/go-winio v0.6.0 // indirect
 	github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
 	github.com/PuerkitoBio/goquery v1.6.0 // indirect

+ 2 - 0
go.sum

@@ -76,6 +76,8 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
 github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
 github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cosmos-server",
-  "version": "0.3.0-unstable13",
+  "version": "0.3.0-unstable14",
   "description": "",
   "main": "test-server.js",
   "bugs": {

+ 36 - 4
src/CRON.go

@@ -1,21 +1,46 @@
 package main
 
 import (
-	"github.com/jasonlvhit/gocron"
 	"io/ioutil"
 	"net/http"
 	"github.com/azukaar/cosmos-server/src/utils"
-	// "github.com/azukaar/cosmos-server/src/docker"
 	"os"
 	"path/filepath"
 	"encoding/json"
+
+	"github.com/jasonlvhit/gocron"
+	"github.com/Masterminds/semver"
 )
 
 type Version struct {
 	Version string `json:"version"`
 }
 
+// compareSemver compares two semantic version strings.
+// Returns:
+//   0 if v1 == v2
+//   1 if v1 > v2
+//  -1 if v1 < v2
+//   error if there's a problem parsing either version string
+func compareSemver(v1, v2 string) (int, error) {
+	ver1, err := semver.NewVersion(v1)
+	if err != nil {
+		utils.Error("compareSemver 1 " + v1, err)
+		return 0, err
+	}
+
+	ver2, err := semver.NewVersion(v2)
+	if err != nil {
+		utils.Error("compareSemver 2 " + v2, err)
+		return 0, err
+	}
+
+	return ver1.Compare(ver2), nil
+}
+
+
 func checkVersion() {
+	utils.NewVersionAvailable = false
 
 	ex, err := os.Executable()
 	if err != nil {
@@ -56,9 +81,16 @@ func checkVersion() {
 		return
 	}
 
-	if string(body) != myVersion {
+	cp, errc := compareSemver(myVersion, string(body))
+
+	if errc != nil {
+		utils.Error("checkVersion", errc)
+		return
+	}
+
+	if cp == -1 {
 		utils.Log("New version available: " + string(body))
-		// update
+		utils.NewVersionAvailable = true
 	} else {
 		utils.Log("No new version available")
 	}

+ 20 - 2
src/configapi/get.go

@@ -7,9 +7,11 @@ import (
 )
 
 func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
-	if utils.AdminOnly(w, req) != nil {
+	if utils.LoggedInOnly(w, req) != nil {
 		return
-	} 
+	}
+
+	isAdmin := utils.IsAdmin(req)
 
 	if(req.Method == "GET") {
 		config := utils.ReadConfigFromFile()
@@ -18,6 +20,22 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
 		config.HTTPConfig.AuthPrivateKey = ""
 		config.HTTPConfig.TLSKey = ""
 
+		if !isAdmin {
+			config.MongoDB = "***"
+			config.EmailConfig.Password = "***"
+			config.EmailConfig.Username = "***"
+			config.EmailConfig.Host = "***"
+
+			// filter admin only routes
+			filteredRoutes := make([]utils.ProxyRouteConfig, 0)
+			for _, route := range config.HTTPConfig.ProxyConfig.Routes {
+				if !route.AdminOnly {
+					filteredRoutes = append(filteredRoutes, route)
+				}
+			}
+			config.HTTPConfig.ProxyConfig.Routes = filteredRoutes
+		}
+
 		json.NewEncoder(w).Encode(map[string]interface{}{
 			"status": "OK",
 			"data": config,

+ 2 - 0
src/index.go

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

+ 6 - 3
src/proxy/routerGen.go

@@ -12,7 +12,7 @@ import (
 	"github.com/gorilla/mux"
 )
 
-func tokenMiddleware(enabled bool) func(next http.Handler) http.Handler {
+func tokenMiddleware(enabled bool, adminOnly bool) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			r.Header.Del("x-cosmos-user")
@@ -25,6 +25,7 @@ func tokenMiddleware(enabled bool) func(next http.Handler) http.Handler {
 				return
 			}
 
+
 			r.Header.Set("x-cosmos-user", u.Nickname)
 			r.Header.Set("x-cosmos-role", strconv.Itoa((int)(u.Role)))
 			r.Header.Set("x-cosmos-mfa", strconv.Itoa((int)(u.MFAState)))
@@ -37,7 +38,9 @@ func tokenMiddleware(enabled bool) func(next http.Handler) http.Handler {
 			// Replace the token with a application speicfic one
 			r.Header.Set("x-cosmos-token", "1234567890")
 
-			if enabled {
+			if enabled && adminOnly {
+				utils.AdminOnlyWithRedirect(w, r)
+			} else if enabled {
 				utils.LoggedInOnlyWithRedirect(w, r)
 			}
 
@@ -116,7 +119,7 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
 		destination = utils.BlockPostWithoutReferer(destination)
 	}
 
-	destination = tokenMiddleware(route.AuthEnabled)(utils.CORSHeader(originCORS)((destination)))
+	destination = tokenMiddleware(route.AuthEnabled, route.AdminOnly)(utils.CORSHeader(originCORS)((destination)))
 
 	origin.Handler(destination)
 

+ 2 - 1
src/status.go

@@ -9,7 +9,7 @@ import (
 )
 
 func StatusRoute(w http.ResponseWriter, req *http.Request) {
-	if !utils.GetMainConfig().NewInstall && (utils.AdminOnly(w, req) != nil) {
+	if !utils.GetMainConfig().NewInstall && (utils.LoggedInOnly(w, req) != nil) {
 		return
 	}
 
@@ -44,6 +44,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
 				"domain": utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0",
 				"HTTPSCertificateMode": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode,
 				"needsRestart": utils.NeedsRestart,
+				"newVersionAvailable": utils.NewVersionAvailable,
 			},
 		})
 	} else {

+ 35 - 0
src/utils/loggedIn.go

@@ -29,6 +29,36 @@ func LoggedInOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
 	return nil
 }
 
+func AdminOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
+	userNickname := req.Header.Get("x-cosmos-user")
+	role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
+	mfa, _ := strconv.Atoi(req.Header.Get("x-cosmos-mfa"))
+	isUserLoggedIn := role > 0
+	isUserAdmin := role > 1
+
+	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)
+		return errors.New("User is not logged")
+	}
+
+	if isUserLoggedIn && !isUserAdmin {
+		Error("AdminLoggedInOnly: User is not Authorized", nil)
+		HTTPError(w, "User not Authorized", http.StatusUnauthorized, "HTTP004")
+		return errors.New("User is not Admin")
+	}
+
+	if(mfa == 1) {
+		http.Redirect(w, req, "/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)
+		return errors.New("User requires MFA Setup")
+	}
+
+	return nil
+}
+
 func LoggedInWeakOnly(w http.ResponseWriter, req *http.Request) error {
 	userNickname := req.Header.Get("x-cosmos-user")
 	role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
@@ -97,6 +127,11 @@ func AdminOnly(w http.ResponseWriter, req *http.Request) error {
 	return nil
 }
 
+func IsAdmin(req *http.Request) bool {
+	role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
+	return role > 1
+}
+
 func AdminOrItselfOnly(w http.ResponseWriter, req *http.Request, nickname string) error {
 	userNickname := req.Header.Get("x-cosmos-user")
 	role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))

+ 2 - 0
src/utils/types.go

@@ -84,6 +84,7 @@ type Config struct {
 	BlockedCountries []string
 	ServerCountry string
 	RequireMFA bool
+	AutoUpdate bool
 }
 
 type HTTPConfig struct {
@@ -139,6 +140,7 @@ type ProxyRouteConfig struct {
 	StripPathPrefix bool
 	MaxBandwith int64
 	AuthEnabled bool
+	AdminOnly bool
 	Target  string `validate:"required"`
 	SmartShield SmartShieldPolicy
 	Mode ProxyMode

+ 2 - 0
src/utils/utils.go

@@ -19,12 +19,14 @@ import (
 var BaseMainConfig Config
 var MainConfig Config
 var IsHTTPS = false
+var NewVersionAvailable = false
 
 var NeedsRestart = false
 
 var DefaultConfig = Config{
 	LoggingLevel: "INFO",
 	NewInstall:   true,
+	AutoUpdate:	  true,
 	// By default we block all countries that have a high amount of attacks
 	// Note that Cosmos wont block the country of origin of the server even if it is in this list
 	BlockedCountries: []string{