From 822d4bc0577fd69390d67a1fd5303838f60a6d49 Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Wed, 29 Mar 2023 21:38:50 +0100 Subject: [PATCH] v0.0.9 setup wizard --- .gitignore | 1 + client/src/api/docker.jsx | 12 +- client/src/api/index.jsx | 26 +- client/src/api/wrap.js | 4 +- .../src/assets/images/auth/AuthBackground.jsx | 2 +- client/src/isLoggedIn.jsx | 5 +- .../src/pages/authentication/AuthWrapper.jsx | 18 +- .../authentication/auth-forms/AuthLogin.jsx | 2 + .../src/pages/config/users/formShortcuts.jsx | 87 +++- client/src/pages/config/users/routeman.jsx | 2 +- client/src/pages/dashboard/index.jsx | 56 ++- client/src/pages/newInstall/newInstall.jsx | 466 ++++++++++++++++++ client/src/routes/LoginRoutes.jsx | 8 +- client/src/routes/MainRoutes.jsx | 2 +- client/src/utils/container-security.jsx | 12 + package.json | 2 +- readme.md | 4 +- src/docker/api_newDB.go | 45 ++ src/docker/bootstrap.go | 4 +- src/docker/docker.go | 99 +--- src/docker/events.go | 2 +- src/docker/network.go | 2 +- src/docker/run.go | 127 +++++ src/httpServer.go | 8 +- src/newInstall.go | 188 +++++++ src/status.go | 53 ++ src/user/create.go | 7 +- src/user/delete.go | 7 +- src/user/edit.go | 7 +- src/user/get.go | 9 +- src/user/list.go | 7 +- src/user/login.go | 7 +- src/user/register.go | 7 +- src/user/resend.go | 7 +- src/user/token.go | 38 +- src/utils/db.go | 29 +- src/utils/types.go | 2 + src/utils/utils.go | 12 +- 38 files changed, 1243 insertions(+), 133 deletions(-) create mode 100644 client/src/pages/newInstall/newInstall.jsx create mode 100644 client/src/utils/container-security.jsx create mode 100644 src/docker/api_newDB.go create mode 100644 src/docker/run.go create mode 100644 src/newInstall.go create mode 100644 src/status.go diff --git a/.gitignore b/.gitignore index 4a25363..02bc050 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ static client/dist client/.vite config_dev.json +config_dev.old.json tests todo.txt LICENCE \ No newline at end of file diff --git a/client/src/api/docker.jsx b/client/src/api/docker.jsx index 1b7540d..cd09631 100644 --- a/client/src/api/docker.jsx +++ b/client/src/api/docker.jsx @@ -8,7 +8,17 @@ function list() { }, })) } - + +const newDB = () => { + return wrap(fetch('/cosmos/api/newDB', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + })) +} + export { list, + newDB, }; \ No newline at end of file diff --git a/client/src/api/index.jsx b/client/src/api/index.jsx index 459db23..c91a787 100644 --- a/client/src/api/index.jsx +++ b/client/src/api/index.jsx @@ -2,9 +2,33 @@ import * as auth from './authentication.jsx'; import * as users from './users.jsx'; import * as config from './config.jsx'; import * as docker from './docker.jsx'; + +import wrap from './wrap'; + +const getStatus = () => { + return wrap(fetch('/cosmos/api/status', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + })) +} + +const newInstall = (req) => { + return wrap(fetch('/cosmos/api/newInstall', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(req) + })) +} + export { auth, users, config, - docker + docker, + getStatus, + newInstall, }; \ No newline at end of file diff --git a/client/src/api/wrap.js b/client/src/api/wrap.js index 278c448..0d71f7f 100644 --- a/client/src/api/wrap.js +++ b/client/src/api/wrap.js @@ -13,7 +13,9 @@ export default function wrap(apicall) { return rep; } snackit(rep.message); - throw new Error(rep.message); + const e = new Error(rep.message); + e.status = response.status; + throw e; }); } diff --git a/client/src/assets/images/auth/AuthBackground.jsx b/client/src/assets/images/auth/AuthBackground.jsx index ad0ac4d..d229d98 100644 --- a/client/src/assets/images/auth/AuthBackground.jsx +++ b/client/src/assets/images/auth/AuthBackground.jsx @@ -9,7 +9,7 @@ import logo from '../icons/cosmos.png'; const AuthBackground = () => { const theme = useTheme(); return ( - + Cosmos ); diff --git a/client/src/isLoggedIn.jsx b/client/src/isLoggedIn.jsx index 04821b9..8593627 100644 --- a/client/src/isLoggedIn.jsx +++ b/client/src/isLoggedIn.jsx @@ -6,7 +6,10 @@ const isLoggedIn = () => useEffect(() => { console.log("CHECK LOGIN") API.auth.me().then((data) => { if(data.status != 'OK') { - window.location.href = '/ui/login'; + if(data.status == 'NEW_INSTALL') { + window.location.href = '/ui/newInstall'; + } else + window.location.href = '/ui/login'; } }); }, []); diff --git a/client/src/pages/authentication/AuthWrapper.jsx b/client/src/pages/authentication/AuthWrapper.jsx index cdcea5a..2dc40c9 100644 --- a/client/src/pages/authentication/AuthWrapper.jsx +++ b/client/src/pages/authentication/AuthWrapper.jsx @@ -10,11 +10,16 @@ import AuthFooter from '../../components/cards/AuthFooter'; // assets import AuthBackground from '../../assets/images/auth/AuthBackground'; +import { useTheme } from '@mui/material/styles'; // ==============================|| AUTHENTICATION - WRAPPER ||============================== // -const AuthWrapper = ({ children }) => ( - +const AuthWrapper = ({ children }) => { + const theme = useTheme(); + const darkMode = theme.palette.mode === 'dark'; + + return ( container justifyContent="center" alignItems="center" - sx={{ minHeight: { xs: 'calc(100vh - 134px)', md: 'calc(100vh - 112px)' } }} + sx={{ + minHeight: { + xs: 'calc(100vh - 134px)', + md: 'calc(100vh - 112px)' + } + }} > {children} @@ -46,7 +56,7 @@ const AuthWrapper = ({ children }) => ( -); +}; AuthWrapper.propTypes = { children: PropTypes.node diff --git a/client/src/pages/authentication/auth-forms/AuthLogin.jsx b/client/src/pages/authentication/auth-forms/AuthLogin.jsx index 52203de..534e893 100644 --- a/client/src/pages/authentication/auth-forms/AuthLogin.jsx +++ b/client/src/pages/authentication/auth-forms/AuthLogin.jsx @@ -57,6 +57,8 @@ const AuthLogin = () => { API.auth.me().then((data) => { if(data.status == 'OK') { window.location.href = redirectTo; + } else if(data.status == 'NEW_INSTALL') { + window.location.href = '/ui/newInstall'; } }); }); diff --git a/client/src/pages/config/users/formShortcuts.jsx b/client/src/pages/config/users/formShortcuts.jsx index baed73b..beca404 100644 --- a/client/src/pages/config/users/formShortcuts.jsx +++ b/client/src/pages/config/users/formShortcuts.jsx @@ -15,13 +15,19 @@ import { AccordionDetails, Accordion, Chip, + Box, + FormControl, + IconButton, + InputAdornment, } from '@mui/material'; import { Field } from 'formik'; import { DownOutlined, UpOutlined } from '@ant-design/icons'; +import { strengthColor, strengthIndicator } from '../../../utils/password-strength'; +import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; -export const CosmosInputText = ({ name, type, placeholder, label, formik }) => { +export const CosmosInputText = ({ name, type, placeholder, onChange, label, formik }) => { return {label} @@ -31,7 +37,12 @@ export const CosmosInputText = ({ name, type, placeholder, label, formik }) => { value={formik.values[name]} name={name} onBlur={formik.handleBlur} - onChange={formik.handleChange} + onChange={(...e) => { + if (onChange) { + onChange(...e); + } + formik.handleChange(...e); + }} placeholder={placeholder} fullWidth error={Boolean(formik.touched[name] && formik.errors[name])} @@ -45,6 +56,78 @@ export const CosmosInputText = ({ name, type, placeholder, label, formik }) => { } +export const CosmosInputPassword = ({ name, type, placeholder, onChange, label, formik }) => { + const [level, setLevel] = React.useState(); + const [showPassword, setShowPassword] = React.useState(false); + const handleClickShowPassword = () => { + setShowPassword(!showPassword); + }; + + const handleMouseDownPassword = (event) => { + event.preventDefault(); + }; + + const changePassword = (value) => { + const temp = strengthIndicator(value); + setLevel(strengthColor(temp)); + }; + + React.useEffect(() => { + changePassword(''); + }, []); + + return + + {label} + { + changePassword(e.target.value); + formik.handleChange(e); + }} + placeholder="********" + endAdornment={ + + + {showPassword ? : } + + + } + fullWidth + error={Boolean(formik.touched[name] && formik.errors[name])} + /> + {formik.touched[name] && formik.errors[name] && ( + + {formik.errors[name]} + + )} + + + + + + + + + {level?.label} + + + + + + +} + export const CosmosSelect = ({ name, label, formik, options }) => { return diff --git a/client/src/pages/config/users/routeman.jsx b/client/src/pages/config/users/routeman.jsx index af604f3..44ed941 100644 --- a/client/src/pages/config/users/routeman.jsx +++ b/client/src/pages/config/users/routeman.jsx @@ -42,7 +42,7 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute }) initialValues={{ Name: routeConfig.Name, Description: routeConfig.Description, - Mode: routeConfig.Mode, + Mode: routeConfig.Mode || "SERVAPP", Target: routeConfig.Target, UseHost: routeConfig.UseHost, AuthEnabled: routeConfig.AuthEnabled, diff --git a/client/src/pages/dashboard/index.jsx b/client/src/pages/dashboard/index.jsx index f21b8c8..fb20c17 100644 --- a/client/src/pages/dashboard/index.jsx +++ b/client/src/pages/dashboard/index.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; // material-ui import { @@ -36,6 +36,9 @@ import avatar3 from '../../assets/images/users/avatar-3.png'; import avatar4 from '../../assets/images/users/avatar-4.png'; import isLoggedIn from '../../isLoggedIn'; +import * as API from '../../api'; +import AnimateButton from '../../components/@extended/AnimateButton'; + // avatar style const avatarSX = { width: 36, @@ -77,10 +80,59 @@ const DashboardDefault = () => { isLoggedIn(); + const [coStatus, setCoStatus] = useState(null); + const [isCreatingDB, setIsCreatingDB] = useState(false); + + const refreshStatus = () => { + API.getStatus().then((res) => { + setCoStatus(res.data); + }); + } + + useEffect(() => { + refreshStatus(); + }, []); + + const setupDB = () => { + setIsCreatingDB(true); + API.docker.newDB().then((res) => { + refreshStatus(); + }); + } + return ( <>
- Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord! + + {coStatus && !coStatus.database && ( + + No Database is setup for Cosmos! User Management and Authentication will not work.
+ You can either setup the database, or disable user management in the configuration panel.
+
+ )} + + {coStatus && coStatus.letsencrypt && ( + + 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. + + )} + + {coStatus && coStatus.domain && ( + + 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. + + )} + + {coStatus && !coStatus.docker && ( + + Docker is not connected! Please check your docker connection.
+ Did you forget to add
-v /var/run/docker.sock:/var/run/docker.sock
to your docker run command?
+ if your docker daemon is running somewhere else, please add
-e DOCKER_HOST=...
to your docker run command. +
+ )} + + Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord! +
diff --git a/client/src/pages/newInstall/newInstall.jsx b/client/src/pages/newInstall/newInstall.jsx new file mode 100644 index 0000000..47e6e8a --- /dev/null +++ b/client/src/pages/newInstall/newInstall.jsx @@ -0,0 +1,466 @@ +import { Link } from 'react-router-dom'; + +import * as Yup from 'yup'; + +// material-ui +import { Alert, Button, CircularProgress, FormControl, FormHelperText, Grid, Stack, Typography } from '@mui/material'; + +// ant-ui icons +import { CheckCircleOutlined, LeftOutlined, QuestionCircleFilled, QuestionCircleOutlined, RightOutlined } from '@ant-design/icons'; + +// project import +import AuthWrapper from '../authentication/AuthWrapper'; +import { useEffect, useState } from 'react'; + +import * as API from '../../api'; +import { Formik } from 'formik'; +import { CosmosInputPassword, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts'; +import AnimateButton from '../../components/@extended/AnimateButton'; +import { Box } from '@mui/system'; +// ================================|| LOGIN ||================================ // + +const NewInstall = () => { + const [activeStep, setActiveStep] = useState(0); + const [status, setStatus] = useState(null); + const [counter, setCounter] = useState(0); + const refreshStatus = async () => { + try { + const res = await API.getStatus() + setStatus(res.data); + } catch(error) { + if(error.status == 401) + window.location.href = "/ui/login"; + } + if (typeof status !== 'undefined') { + setTimeout(() => { + setCounter(counter + 1); + }, 2000); + } + } + + useEffect(() => { + refreshStatus(); + }, [counter]); + + useEffect(() => { + if(activeStep == 4 && status && !status.database) { + setActiveStep(5); + } + }, [activeStep, status]); + + const steps = [ + { + label: 'Welcome! 💖', + component:
+ First of all, thanks a lot for trying out Cosmos! And Welcome to the setup wizard. + This wizard will guide you through the setup of Cosmos. It will take about 2-3 minutes and you will be ready to go. +
, + nextButtonLabel: () => { + return 'Start'; + } + }, + { + label: 'Docker 🐋 (step 1/4)', + component: +
+ 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. +
+ {(status && status.docker) ? + + Docker is installed and running. + : + + Docker is not connected! Please check your docker connection.
+ Did you forget to add
-v /var/run/docker.sock:/var/run/docker.sock
to your docker run command?
+ if your docker daemon is running somewhere else, please add
-e DOCKER_HOST=...
to your docker run command. +
+ } + {(status && status.docker) ? ( +
+
+ +
+
+ ) : (<>
+ Rechecking Docker Status... +
+
+
+
)} +
, + nextButtonLabel: () => { + return status && status.docker ? 'Next' : 'Skip'; + } + }, + { + label: 'Database 🗄️ (step 2/4)', + component: +
+ 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. +
+ {(status && status.database) ? + + Database is connected. + : + <> + Database is not connected! + +
+ { + }} + onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { + try { + setSubmitting(true); + const res = await API.newInstall({ + step: "2", + MongoDBMode: values.DBMode, + MongoDB: values.MongoDB, + }); + if(res.status == "OK") + setStatus({ success: true }); + } catch (error) { + setStatus({ success: false }); + setErrors({ submit: error.message }); + setSubmitting(false); + } + }}> + {(formik) => ( +
+ + + {formik.values.DBMode === "Provided" && ( + <> + + + )} + {formik.errors.submit && ( + + {formik.errors.submit} + + )} + + + + +
+ )} +
+
+ + } + {(status && status.database) ? ( +
+
+ +
+
+ ) : (<>
+ Rechecking Database Status... +
+
+
+
)} +
, + nextButtonLabel: () => { + return (status && status.database) ? 'Next' : ''; + } + }, + { + label: 'HTTPS 🌐 (step 3/4)', + component: ( +
+ It is recommended to use Let's Encrypt to automatically provide HTTPS Certificates. + This requires a valid domain name pointing to this server. If you don't have one, you can use a self-signed certificate. + If you enable HTTPS, it will be effective after the next restart. +
+
+ {status &&
+ HTTPS Certificate Mode is currently: {status.HTTPSCertificateMode} +
} +
+
+ { + try { + setSubmitting(true); + const res = await API.newInstall({ + step: "3", + HTTPSCertificateMode: values.HTTPSCertificateMode, + SSLEmail: values.SSLEmail, + TLSKey: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSKey : '', + TLSCert: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSCert : '', + Hostname: values.Hostname, + }); + if(res.status == "OK") + setStatus({ success: true }); + } catch (error) { + setStatus({ success: false }); + setErrors({ submit: error.message }); + setSubmitting(false); + } + }}> + {(formik) => ( +
+ + + {formik.values.HTTPSCertificateMode === "LETSENCRYPT" && ( + <> + + + )} + {formik.values.HTTPSCertificateMode === "PROVIDED" && ( + <> + + + + )} + + + + {formik.errors.submit && ( + + {formik.errors.submit} + + )} + + + + +
+ )} +
+
+
), + nextButtonLabel: () => { + return status ? 'Next' : 'Skip'; + } + }, + { + label: 'Admin Account 🔑 (step 4/4)', + component:
+ +
+ Create a local admin account to manage your server. Email is optional and used for notifications and password recovery. +
+ { + try { + setSubmitting(true); + const res = await API.newInstall({ + step: "4", + nickname: values.nickname, + password: values.password, + email: values.email, + }); + if(res.status == "OK") { + setStatus({ success: true }); + setActiveStep(5); + } + } catch (error) { + setStatus({ success: false }); + setErrors({ submit: error.message }); + setSubmitting(false); + } + }}> + {(formik) => ( +
+ + + + + + {formik.errors.submit && ( + + {formik.errors.submit} + + )} + + + + +
+ )} +
+
+
, + nextButtonLabel: () => { + return ''; + } + }, + { + label: 'Finish 🎉', + component:
+ 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 Discord server and we'll be happy to help! +
, + nextButtonLabel: () => { + return 'Apply and Restart'; + } + } + ]; + + return + + + + {steps[activeStep].label} + + + + {steps[activeStep].component} + + {/*JSON.stringify(status)*/} + +
+ *': { flexGrow: 1 } }}> + + + + +
+
+
+}; + +export default NewInstall; diff --git a/client/src/routes/LoginRoutes.jsx b/client/src/routes/LoginRoutes.jsx index 63d2195..5894a02 100644 --- a/client/src/routes/LoginRoutes.jsx +++ b/client/src/routes/LoginRoutes.jsx @@ -5,6 +5,7 @@ import { lazy } from 'react'; import Loadable from '../components/Loadable'; import MinimalLayout from '../layout/MinimalLayout'; import Logout from '../pages/authentication/Logoff'; +import NewInstall from '../pages/newInstall/newInstall'; // render - login const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login'))); @@ -27,7 +28,12 @@ const LoginRoutes = { { path: '/ui/logout', element: - } + }, + { + path: '/ui/newInstall', + // redirect to /ui + element: + }, ] }; diff --git a/client/src/routes/MainRoutes.jsx b/client/src/routes/MainRoutes.jsx index 3878a1d..aab4463 100644 --- a/client/src/routes/MainRoutes.jsx +++ b/client/src/routes/MainRoutes.jsx @@ -51,7 +51,7 @@ const MainRoutes = { { path: '/ui/config/proxy', element: - } + }, ] }; diff --git a/client/src/utils/container-security.jsx b/client/src/utils/container-security.jsx new file mode 100644 index 0000000..134727f --- /dev/null +++ b/client/src/utils/container-security.jsx @@ -0,0 +1,12 @@ + +function checkSec(containers, name) { + const container = containers.find((container) => container.Names[0] === '/' + p1_.split(":")[0]) + + if (container) { + + } +} + +const containerSecurityText = ({containers, name}) => { + +} \ No newline at end of file diff --git a/package.json b/package.json index 63efbcd..ffeb68c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.0.8", + "version": "0.0.9", "description": "", "main": "test-server.js", "bugs": { diff --git a/readme.md b/readme.md index 63a1ab1..8860590 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with application * **Anti-Bot** 🤖❌ protections such as Captcha and IP rate limiting * **Anti-DDOS** 🔥⛔️ protections such as variable timeouts/throttling, IP rate limiting and IP blacklisting * **Proper User Management** 🪪 ❎ to invite your friends and family to your applications without awkardly sharing credentials. Let them request a password change with an email rather than having you unlock their account manually! - * **Container Management** 🧱🔧 to easily manage your containers and their settings, keep them up to date as well as audit their security. + * **Container Management** 🐋🔧 to easily manage your containers and their settings, keep them up to date as well as audit their security. * **Modular** 🧩📦 to easily add new features and integrations, but also run only the features you need (for example No docker, no Databases, or no HTTPS) * **Visible Source** 📖📝 for full transparency and trust @@ -68,6 +68,8 @@ Installation is simple using Docker: docker run -d -p 80:80 -p 443:443 --name cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest ``` +make sure you expose the right ports (by default 80 / 443). It is best to keep those ports intacts, as Cosmos is meant to run as your reverse proxy. Trying to setup Cosmos behind another reverse proxy is possible but will only create headaches. + you can use `latest-arm64` for arm architecture (ex: NAS or Raspberry) You can thing tweak the config file accordingly. Some settings can be changed before end with env var. [see here](https://github.com/azukaar/Cosmos-Server/wiki/Configuration). diff --git a/src/docker/api_newDB.go b/src/docker/api_newDB.go new file mode 100644 index 0000000..576ff0f --- /dev/null +++ b/src/docker/api_newDB.go @@ -0,0 +1,45 @@ +package docker + +import ( + "net/http" + "encoding/json" + "time" + "os" + + "github.com/azukaar/cosmos-server/src/utils" +) + +func restart() { + time.Sleep(3 * time.Second) + os.Exit(0) +} + +func NewDBRoute(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + if(req.Method == "GET") { + costr, err := NewDB() + + if err != nil { + utils.Error("NewDB: Error while creating new DB", err) + utils.HTTPError(w, "Error while creating new DB", http.StatusInternalServerError, "DB001") + return + } + + config := utils.GetBaseMainConfig() + config.MongoDB = costr + utils.SaveConfigTofile(config) + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + + go restart() + } else { + utils.Error("UserList: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/docker/bootstrap.go b/src/docker/bootstrap.go index 0de4602..87efc46 100644 --- a/src/docker/bootstrap.go +++ b/src/docker/bootstrap.go @@ -6,7 +6,7 @@ import ( ) func BootstrapAllContainersFromTags() []error { - errD := connect() + errD := Connect() if errD != nil { return []error{errD} } @@ -32,7 +32,7 @@ func BootstrapAllContainersFromTags() []error { func BootstrapContainerFromTags(containerID string) error { - errD := connect() + errD := Connect() if errD != nil { return errD } diff --git a/src/docker/docker.go b/src/docker/docker.go index a9cc5bb..aae5bdb 100644 --- a/src/docker/docker.go +++ b/src/docker/docker.go @@ -2,15 +2,14 @@ package docker import ( "context" - "fmt" "errors" "github.com/azukaar/cosmos-server/src/utils" "github.com/docker/docker/client" - natting "github.com/docker/go-connections/nat" + // natting "github.com/docker/go-connections/nat" "github.com/docker/docker/api/types/container" - network "github.com/docker/docker/api/types/network" + // network "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types" ) @@ -35,11 +34,14 @@ func getIdFromName(name string) (string, error) { return "", errors.New("Container not found") } -func connect() error { +var DockerIsConnected = false + +func Connect() error { if DockerClient == nil { ctx := context.Background() client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { + DockerIsConnected = false return err } defer client.Close() @@ -49,8 +51,10 @@ func connect() error { ping, err := DockerClient.Ping(DockerContext) if ping.APIVersion != "" && err == nil { + DockerIsConnected = true utils.Log("Docker Connected") } else { + DockerIsConnected = false utils.Error("Docker Connection - Cannot ping Daemon. Is it running?", nil) return errors.New("Docker Connection - Cannot ping Daemon. Is it running?") } @@ -64,91 +68,8 @@ func connect() error { return nil } -func runContainer(imagename string, containername string, port string, inputEnv []string) error { - errD := connect() - if errD != nil { - utils.Error("Docker Connect", errD) - return errD - } - - // Define a PORT opening - newport, err := natting.NewPort("tcp", port) - if err != nil { - fmt.Println("Unable to create docker port") - return err - } - - // Configured hostConfig: - // https://godoc.org/github.com/docker/docker/api/types/container#HostConfig - hostConfig := &container.HostConfig{ - PortBindings: natting.PortMap{ - newport: []natting.PortBinding{ - { - HostIP: "0.0.0.0", - HostPort: port, - }, - }, - }, - RestartPolicy: container.RestartPolicy{ - Name: "always", - }, - LogConfig: container.LogConfig{ - Type: "json-file", - Config: map[string]string{}, - }, - } - - // Define Network config (why isn't PORT in here...?: - // https://godoc.org/github.com/docker/docker/api/types/network#NetworkingConfig - networkConfig := &network.NetworkingConfig{ - EndpointsConfig: map[string]*network.EndpointSettings{}, - } - gatewayConfig := &network.EndpointSettings{ - Gateway: "gatewayname", - } - networkConfig.EndpointsConfig["bridge"] = gatewayConfig - - // Define ports to be exposed (has to be same as hostconfig.portbindings.newport) - exposedPorts := map[natting.Port]struct{}{ - newport: struct{}{}, - } - - // Configuration - // https://godoc.org/github.com/docker/docker/api/types/container#Config - config := &container.Config{ - Image: imagename, - Env: inputEnv, - ExposedPorts: exposedPorts, - Hostname: fmt.Sprintf("%s-hostnameexample", imagename), - } - - //archi := runtime.GOARCH - - // Creating the actual container. This is "nil,nil,nil" in every example. - cont, err := DockerClient.ContainerCreate( - context.Background(), - config, - hostConfig, - networkConfig, - nil, - containername, - ) - - if err != nil { - utils.Error("Docker Container Create", err) - return err - } - - // Run the actual container - DockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) - utils.Log("Container created " + cont.ID) - - - return nil -} - func EditContainer(containerID string, newConfig types.ContainerJSON) (string, error) { - errD := connect() + errD := Connect() if errD != nil { return "", errD } @@ -212,7 +133,7 @@ func EditContainer(containerID string, newConfig types.ContainerJSON) (string, e } func ListContainers() ([]types.Container, error) { - errD := connect() + errD := Connect() if errD != nil { return nil, errD } diff --git a/src/docker/events.go b/src/docker/events.go index 3f7d0e0..2235985 100644 --- a/src/docker/events.go +++ b/src/docker/events.go @@ -9,7 +9,7 @@ import ( ) func DockerListenEvents() error { - errD := connect() + errD := Connect() if errD != nil { utils.Error("Docker did not connect. Not listening", errD) return errD diff --git a/src/docker/network.go b/src/docker/network.go index 3c3011f..69d98d8 100644 --- a/src/docker/network.go +++ b/src/docker/network.go @@ -58,7 +58,7 @@ func CreateCosmosNetwork() (string, error) { } func ConnectToSecureNetwork(containerConfig types.ContainerJSON) (bool, error) { - errD := connect() + errD := Connect() if errD != nil { utils.Error("Docker Connect", errD) return false, errD diff --git a/src/docker/run.go b/src/docker/run.go new file mode 100644 index 0000000..150dcd2 --- /dev/null +++ b/src/docker/run.go @@ -0,0 +1,127 @@ +package docker + +import ( + "github.com/azukaar/cosmos-server/src/utils" + "io" + "os" + // "github.com/docker/docker/client" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types" +) + +func NewDB() (string, error) { + mongoUser := "cosmos-" + utils.GenerateRandomString(5) + mongoPass := utils.GenerateRandomString(24) + monHost := "cosmos-mongo-" + utils.GenerateRandomString(3) + + err := RunContainer( + "mongo:latest", + monHost, + []string{ + "MONGO_INITDB_ROOT_USERNAME=" + mongoUser, + "MONGO_INITDB_ROOT_PASSWORD=" + mongoPass, + }, + ) + + if err != nil { + return "", err + } + + return "mongodb://"+mongoUser+":"+mongoPass+"@"+monHost+":27017", nil +} + +func RunContainer(imagename string, containername string, inputEnv []string) error { + errD := Connect() + if errD != nil { + utils.Error("Docker Connect", errD) + return errD + } + + pull, errPull := DockerClient.ImagePull(DockerContext, imagename, types.ImagePullOptions{}) + if errPull != nil { + utils.Error("Docker Pull", errPull) + return errPull + } + io.Copy(os.Stdout, pull) + + + // Define a PORT opening + // newport, err := natting.NewPort("tcp", port) + // if err != nil { + // fmt.Println("Unable to create docker port") + // return err + // } + + // Configured hostConfig: + // https://godoc.org/github.com/docker/docker/api/types/container#HostConfig + hostConfig := &container.HostConfig{ + // PortBindings: natting.PortMap{ + // newport: []natting.PortBinding{ + // { + // HostIP: "0.0.0.0", + // HostPort: port, + // }, + // }, + // }, + RestartPolicy: container.RestartPolicy{ + Name: "always", + }, + // LogConfig: container.LogConfig{ + // Type: "json-file", + // Config: map[string]string{}, + // }, + } + + // Define Network config + // https://godoc.org/github.com/docker/docker/api/types/network#NetworkingConfig + + + + // networkConfig := &network.NetworkingConfig{ + // EndpointsConfig: map[string]*network.EndpointSettings{}, + // } + // gatewayConfig := &network.EndpointSettings{ + // Gateway: "gatewayname", + // } + // networkConfig.EndpointsConfig["bridge"] = gatewayConfig + + // Define ports to be exposed (has to be same as hostconfig.portbindings.newport) + // exposedPorts := map[natting.Port]struct{}{ + // newport: struct{}{}, + // } + + // Configuration + // https://godoc.org/github.com/docker/docker/api/types/container#Config + config := &container.Config{ + Image: imagename, + Env: inputEnv, + Hostname: containername, + Labels: map[string]string{ + "cosmos-force-network-secured": "true", + }, + // ExposedPorts: exposedPorts, + } + + //archi := runtime.GOARCH + + // Creating the actual container. This is "nil,nil,nil" in every example. + cont, err := DockerClient.ContainerCreate( + DockerContext, + config, + hostConfig, + nil, + nil, + containername, + ) + + if err != nil { + utils.Error("Docker Container Create", err) + return err + } + + // Run the actual container + DockerClient.ContainerStart(DockerContext, cont.ID, types.ContainerStartOptions{}) + utils.Log("Container created " + cont.ID) + + return nil +} \ No newline at end of file diff --git a/src/httpServer.go b/src/httpServer.go index 79fa768..ff86b18 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -37,6 +37,7 @@ func startHTTPServer(router *mux.Router) { func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) { config := utils.GetMainConfig() + // check if Docker overwrite Hostname serverHostname := "0.0.0.0" //utils.GetMainConfig().HTTPConfig.Hostname // if os.Getenv("HOSTNAME") != "" { @@ -50,13 +51,16 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) { cfg.SSLEmail = config.HTTPConfig.SSLEmail cfg.HTTPAddress = serverHostname+":"+serverPortHTTP cfg.TLSAddress = serverHostname+":"+serverPortHTTPS + cfg.FailedToRenewCertificate = func(err error) { + utils.Error("Failed to renew certificate", err) + } var certReloader *simplecert.CertReloader var errSimCert error if(config.HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) { certReloader, errSimCert = simplecert.Init(cfg, nil) if errSimCert != nil { - utils.Fatal("simplecert init failed: ", errSimCert) + utils.Fatal("simplecert init failed", errSimCert) } } @@ -185,6 +189,8 @@ func StartServer() { srapi := router.PathPrefix("/cosmos").Subrouter() + srapi.HandleFunc("/api/status", StatusRoute) + srapi.HandleFunc("/api/newInstall", NewInstallRoute) srapi.HandleFunc("/api/login", user.UserLogin) srapi.HandleFunc("/api/logout", user.UserLogout) srapi.HandleFunc("/api/register", user.UserRegister) diff --git a/src/newInstall.go b/src/newInstall.go new file mode 100644 index 0000000..7abeb81 --- /dev/null +++ b/src/newInstall.go @@ -0,0 +1,188 @@ +package main + +import ( + "net/http" + "encoding/json" + "time" + "os" + "golang.org/x/crypto/bcrypt" + + "github.com/azukaar/cosmos-server/src/utils" + "github.com/azukaar/cosmos-server/src/docker" +) + +func waitForDB() { + time.Sleep(1 * time.Second) + err := utils.DB() + if err != nil { + utils.Warn("DB Not ready yet") + waitForDB() + } +} + +type NewInstallJSON struct { + MongoDBMode string `json:"mongodbMode"` + MongoDB string `json:"mongodb"` + HTTPSCertificateMode string `json:"httpsCertificateMode"` + TLSCert string `json:"tlsCert"` + TLSKey string `json:"tlsKey"` + Nickname string `json:"nickname"` + Password string `json:"password"` + Email string `json:"email"` + Hostname string `json:"hostname"` + Step string `json:"step"` + SSLEmail string `json:"sslEmail",validate:"if=HTTPSCertificateMode==LetsEncrypt,email"` +} + +type AdminJSON struct { + Nickname string `validate:"required,min=3,max=32,alphanum"` + Password string `validate:"required,min=8,max=128,containsany=!@#$%^&*()_+,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789"` +} + +func NewInstallRoute(w http.ResponseWriter, req *http.Request) { + if !utils.GetMainConfig().NewInstall { + utils.Error("Status: not a new New install", nil) + utils.HTTPError(w, "New install", http.StatusForbidden, "NI001") + return + } + + if(req.Method == "POST") { + var request NewInstallJSON + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("NewInstall: Invalid User Request", err1) + utils.HTTPError(w, "New Install: Invalid User Request" + err1.Error(), + http.StatusInternalServerError, "NI001") + return + } + + errV := utils.Validate.Struct(request) + if errV != nil { + utils.Error("NewInstall: Invalid User Request", errV) + utils.HTTPError(w, "New Install: Invalid User Request " + errV.Error(), + http.StatusInternalServerError, "NI001") + return + } + + newConfig := utils.GetBaseMainConfig() + + if(request.Step == "2") { + utils.Log("NewInstall: Step Database") + // User Management & Mongo DB + if(request.MongoDBMode == "DisableUserManagement") { + utils.Log("NewInstall: Disable User Management") + newConfig.DisableUserManagement = true + utils.SaveConfigTofile(newConfig) + utils.LoadBaseMainConfig(newConfig) + } else if (request.MongoDBMode == "Provided") { + utils.Log("NewInstall: DB Provided") + newConfig.DisableUserManagement = false + newConfig.MongoDB = request.MongoDB + utils.SaveConfigTofile(newConfig) + utils.LoadBaseMainConfig(newConfig) + } else if (request.MongoDBMode == "Create"){ + utils.Log("NewInstall: Create DB") + newConfig.DisableUserManagement = false + strco, err := docker.NewDB() + if err != nil { + utils.Error("NewInstall: Error creating MongoDB", err) + utils.HTTPError(w, "New Install: Error creating MongoDB " + err.Error(), + http.StatusInternalServerError, "NI001") + return + } + newConfig.MongoDB = strco + utils.SaveConfigTofile(newConfig) + utils.LoadBaseMainConfig(newConfig) + utils.Log("NewInstall: MongoDB created, waiting for it to be ready") + waitForDB() + } else { + utils.Log("NewInstall: Invalid MongoDBMode") + utils.Error("NewInstall: Invalid MongoDBMode", nil) + utils.HTTPError(w, "New Install: Invalid MongoDBMode", + http.StatusInternalServerError, "NI001") + return + } + } else if (request.Step == "3") { + // HTTPS Certificate Mode & Certs & Let's Encrypt + newConfig.HTTPConfig.HTTPSCertificateMode = request.HTTPSCertificateMode + newConfig.HTTPConfig.SSLEmail = request.SSLEmail + newConfig.HTTPConfig.TLSCert = request.TLSCert + newConfig.HTTPConfig.TLSKey = request.TLSKey + + // Hostname + newConfig.HTTPConfig.Hostname = request.Hostname + + utils.SaveConfigTofile(newConfig) + utils.LoadBaseMainConfig(newConfig) + } else if (request.Step == "4") { + + adminObj := AdminJSON{ + Nickname: request.Nickname, + Password: request.Password, + } + + errV2 := utils.Validate.Struct(adminObj) + if errV2 != nil { + utils.Error("NewInstall: Invalid User Request", errV2) + utils.HTTPError(w, errV2.Error(), http.StatusInternalServerError, "UL001") + return + } + + // Admin User + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } + + nickname := utils.Sanitize(request.Nickname) + hashedPassword, err2 := bcrypt.GenerateFromPassword([]byte(request.Password), 14) + + if err2 != nil { + utils.Error("NewInstall: Error hashing password", err2) + utils.HTTPError(w, "New Install: Error hashing password " + err2.Error(), + http.StatusInternalServerError, "NI001") + return + } + + // pre-remove every users + _, err4 := c.DeleteMany(nil, map[string]interface{}{}) + if err4 != nil { + utils.Error("NewInstall: Error deleting users", err4) + utils.HTTPError(w, "New Install: Error deleting users " + err4.Error(), + http.StatusInternalServerError, "NI001") + return + } + + _, err3 := c.InsertOne(nil, map[string]interface{}{ + "Nickname": nickname, + "Email": request.Email, + "Password": hashedPassword, + "Role": utils.ADMIN, + "PasswordCycle": 0, + "CreatedAt": time.Now(), + "RegisteredAt": time.Now(), + }) + + if err3 != nil { + utils.Error("NewInstall: Error creating admin user", err3) + utils.HTTPError(w, "New Install: Error creating admin user " + err3.Error(), + http.StatusInternalServerError, "NI001") + return + } + } else if (request.Step == "5") { + newConfig.NewInstall = false + utils.SaveConfigTofile(newConfig) + os.Exit(0) + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + } else { + utils.Error("UserList: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/status.go b/src/status.go new file mode 100644 index 0000000..de6ce38 --- /dev/null +++ b/src/status.go @@ -0,0 +1,53 @@ +package main + +import ( + "net/http" + "encoding/json" + + "github.com/azukaar/cosmos-server/src/utils" + "github.com/azukaar/cosmos-server/src/docker" +) + +func StatusRoute(w http.ResponseWriter, req *http.Request) { + if !utils.GetMainConfig().NewInstall && (utils.AdminOnly(w, req) != nil) { + return + } + + if(req.Method == "GET") { + utils.Log("API: Status") + + databaseStatus := true + + if(!utils.GetMainConfig().DisableUserManagement) { + err := utils.DB() + if err != nil { + utils.Error("Status: Database error", err) + databaseStatus = false + } + } else { + utils.Log("Status: User management is disabled, skipping database check") + } + + if(!docker.DockerIsConnected) { + ed := docker.Connect() + if ed != nil { + utils.Error("Status: Docker error", ed) + } + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": map[string]interface{}{ + "database": databaseStatus, + "docker": docker.DockerIsConnected, + "letsencrypt": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode == "LETSENCRYPT" && utils.GetMainConfig().HTTPConfig.SSLEmail == "", + "domain": utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0", + "HTTPSCertificateMode": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode, + }, + }) + } else { + utils.Error("UserList: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/user/create.go b/src/user/create.go index 1a55664..cfde478 100644 --- a/src/user/create.go +++ b/src/user/create.go @@ -43,7 +43,12 @@ func UserCreate(w http.ResponseWriter, req *http.Request) { nickname := utils.Sanitize(request.Nickname) email := utils.Sanitize(request.Email) - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } user := utils.User{} diff --git a/src/user/delete.go b/src/user/delete.go index b368991..8432c1b 100644 --- a/src/user/delete.go +++ b/src/user/delete.go @@ -18,7 +18,12 @@ func UserDelete(w http.ResponseWriter, req *http.Request) { if(req.Method == "DELETE") { - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } utils.Debug("UserDeletion: Deleting user " + nickname) diff --git a/src/user/edit.go b/src/user/edit.go index 080df13..6dfcb08 100644 --- a/src/user/edit.go +++ b/src/user/edit.go @@ -36,7 +36,12 @@ func UserEdit(w http.ResponseWriter, req *http.Request) { return } - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } utils.Debug("UserEdit: Edit user " + nickname) diff --git a/src/user/get.go b/src/user/get.go index a7d776f..2eb3058 100644 --- a/src/user/get.go +++ b/src/user/get.go @@ -17,11 +17,16 @@ func UserGet(w http.ResponseWriter, req *http.Request) { if utils.AdminOrItselfOnly(w, req, nickname) != nil { return - } + } if(req.Method == "GET") { - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } utils.Debug("UserGet: Get user " + nickname) diff --git a/src/user/list.go b/src/user/list.go index 1280266..fc61cfa 100644 --- a/src/user/list.go +++ b/src/user/list.go @@ -24,7 +24,12 @@ func UserList(w http.ResponseWriter, req *http.Request) { } if(req.Method == "GET") { - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } utils.Debug("UserList: List user ") diff --git a/src/user/login.go b/src/user/login.go index df77d45..84b52a9 100644 --- a/src/user/login.go +++ b/src/user/login.go @@ -28,7 +28,12 @@ func UserLogin(w http.ResponseWriter, req *http.Request) { return } - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } nickname := utils.Sanitize(request.Nickname) password := request.Password diff --git a/src/user/register.go b/src/user/register.go index a22e33c..cb0098a 100644 --- a/src/user/register.go +++ b/src/user/register.go @@ -50,7 +50,12 @@ func UserRegister(w http.ResponseWriter, req *http.Request) { return } - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } user := utils.User{} diff --git a/src/user/resend.go b/src/user/resend.go index 253fd42..da1d4b1 100644 --- a/src/user/resend.go +++ b/src/user/resend.go @@ -31,7 +31,12 @@ func UserResendInviteLink(w http.ResponseWriter, req *http.Request) { utils.Debug("Re-Sending an invite to " + nickname) - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } user := utils.User{} diff --git a/src/user/token.go b/src/user/token.go index f934d96..6c8a2e6 100644 --- a/src/user/token.go +++ b/src/user/token.go @@ -7,9 +7,30 @@ import ( "errors" "strings" "time" + "encoding/json" ) func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, error) { + // if(utils.DB != nil) { + // return utils.User{ + // Nickname: "noname", + // Role: utils.ADMIN, + // }, nil + // } + + // if new install + if utils.GetMainConfig().NewInstall { + // check route + if req.URL.Path != "/cosmos/api/status" && req.URL.Path != "/cosmos/api/newInstall" { + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "NEW_INSTALL", + }) + return utils.User{}, errors.New("New install") + } else { + return utils.User{}, nil + } + } + cookie, err := req.Cookie("jwttoken") if err != nil { @@ -63,7 +84,12 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err userInBase := utils.User{} - c := utils.GetCollection(utils.GetRootAppId(), "users") + c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return utils.User{}, errCo + } errDB := c.FindOne(nil, map[string]interface{}{ "Nickname": nickname, @@ -101,9 +127,11 @@ func logOutUser(w http.ResponseWriter) { Domain: utils.GetMainConfig().HTTPConfig.Hostname, } - http.SetCookie(w, &cookie) + if(utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0") { + cookie.Domain = "" + } - // TODO: Remove all other cookies from apps + http.SetCookie(w, &cookie) // TODO: logout every other device if asked by increasing passwordcycle } @@ -151,6 +179,10 @@ func SendUserToken(w http.ResponseWriter, user utils.User) { Domain: utils.GetMainConfig().HTTPConfig.Hostname, } + if(utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0") { + cookie.Domain = "" + } + http.SetCookie(w, &cookie) // http.SetCookie(w, &cookie2) } \ No newline at end of file diff --git a/src/utils/db.go b/src/utils/db.go index 4babe41..f0c0d7c 100644 --- a/src/utils/db.go +++ b/src/utils/db.go @@ -3,6 +3,7 @@ package utils import ( "context" "os" + "errors" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" @@ -11,26 +12,35 @@ import ( var client *mongo.Client -func DB() { - Log("Connecting to the database...") +func DB() error { + if(GetBaseMainConfig().DisableUserManagement) { + return errors.New("User Management is disabled") + } uri := MainConfig.MongoDB + "/?retryWrites=true&w=majority" - + + if(client != nil && client.Ping(context.TODO(), readpref.Primary()) == nil) { + return nil + } + + Log("(Re) Connecting to the database...") + var err error client, err = mongo.Connect(context.TODO(), options.Client().ApplyURI(uri)) if err != nil { - Fatal("DB", err) + return err } defer func() { }() // Ping the primary if err := client.Ping(context.TODO(), readpref.Primary()); err != nil { - Fatal("DB", err) + return err } Log("Successfully connected to the database.") + return nil } func Disconnect() { @@ -39,9 +49,12 @@ func Disconnect() { } } -func GetCollection(applicationId string, collection string) *mongo.Collection { +func GetCollection(applicationId string, collection string) (*mongo.Collection, error) { if client == nil { - DB() + errCo := DB() + if errCo != nil { + return nil, errCo + } } name := os.Getenv("MONGODB_NAME"); if name == "" { @@ -52,7 +65,7 @@ func GetCollection(applicationId string, collection string) *mongo.Collection { c := client.Database(name).Collection(applicationId + "_" + collection) - return c + return c, nil } // func query(q string) (*sql.Rows, error) { diff --git a/src/utils/types.go b/src/utils/types.go index 07989ea..584085f 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -74,6 +74,8 @@ type Config struct { LoggingLevel LoggingLevel `validate:"oneof=DEBUG INFO WARNING ERROR"` MongoDB string HTTPConfig HTTPConfig + DisableUserManagement bool + NewInstall bool } type HTTPConfig struct { diff --git a/src/utils/utils.go b/src/utils/utils.go index be51a97..122bd25 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -16,6 +16,7 @@ var IsHTTPS = false var DefaultConfig = Config{ LoggingLevel: "INFO", + NewInstall: true, HTTPConfig: HTTPConfig{ HTTPSCertificateMode: "DISABLED", GenerateMissingAuthCert: true, @@ -278,5 +279,14 @@ func GetAllHostnames() []string { hostnames = append(hostnames, proxy.Host) } } - return hostnames + // remove doubles + seen := make(map[string]bool) + uniqueHostnames := []string{} + for _, hostname := range hostnames { + if _, ok := seen[hostname]; !ok { + seen[hostname] = true + uniqueHostnames = append(uniqueHostnames, hostname) + } + } + return uniqueHostnames } \ No newline at end of file