[skip ci] v0.2.0-unstable temp commit

This commit is contained in:
Yann Stepienik 2023-04-27 19:29:26 +01:00
parent 7c36e39ac0
commit 963a1c7699
36 changed files with 2461 additions and 137 deletions

7
.gitignore vendored
View file

@ -11,4 +11,9 @@ tests
todo.txt
LICENCE
tokens.json
.vscode
<<<<<<< Updated upstream
.vscode
cosmos_gray.png
=======
cosmos_gray.png
>>>>>>> Stashed changes

View file

@ -4,6 +4,9 @@ if [ $? -ne 0 ]; then
exit 1
fi
cp -r static build/
mkdir build/images
cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png
cp client/src/assets/images/icons/cosmos_gray.png cosmos_gray.png
echo '{' > build/meta.json
cat package.json | grep -E '"version"' >> build/meta.json
echo ' "buildDate": "'`date`'",' >> build/meta.json

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="2048px" height="2048px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 2048 2048"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<![CDATA[
.fil2 {fill:none}
.fil3 {fill:none;fill-rule:nonzero}
.fil1 {fill:#78909C}
.fil0 {fill:#B0BEC5}
.fil4 {fill:#F57C00}
.fil6 {fill:#FFA726}
.fil5 {fill:white}
]]>
</style>
<clipPath id="id0">
<path d="M1024 0c565.541,0 1024,458.459 1024,1024 0,565.541 -458.459,1024 -1024,1024 -565.541,0 -1024,-458.459 -1024,-1024 0,-565.541 458.459,-1024 1024,-1024z"/>
</clipPath>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path class="fil0" d="M1024 0c565.541,0 1024,458.459 1024,1024 0,565.541 -458.459,1024 -1024,1024 -565.541,0 -1024,-458.459 -1024,-1024 0,-565.541 458.459,-1024 1024,-1024z"/>
<g style="clip-path:url(#id0)">
<g id="_308718424">
<g>
<polygon id="_3030812801" class="fil1" points="1011.64,604.271 2147.52,1740.15 2149.55,1742.49 2151.47,1744.93 2153.27,1747.46 2154.95,1750.07 2156.5,1752.77 2157.93,1755.54 2159.22,1758.39 2160.38,1761.32 2161.4,1764.3 2162.27,1767.36 2162.99,1770.47 2163.56,1773.63 2163.97,1776.84 2164.22,1780.1 2164.31,1783.4 2164.31,1789.54 1028.42,653.653 1028.42,647.522 1028.34,644.221 1028.09,640.961 1027.68,637.746 1027.11,634.583 1026.39,631.473 1025.52,628.423 1024.5,625.434 1023.34,622.512 1022.05,619.661 1020.62,616.884 1019.07,614.188 1017.39,611.573 1015.58,609.047 1013.67,606.611 "/>
<polygon id="_303081616" class="fil1" points="1500.87,665.717 2636.75,1801.6 2638.66,1804.15 2640.32,1806.87 2641.72,1809.76 2642.83,1812.8 2643.65,1815.96 2644.16,1819.23 2644.33,1822.6 2644.33,1877.73 1508.45,741.843 1508.45,686.718 1508.27,683.35 1507.77,680.076 1506.95,676.914 1505.84,673.88 1504.44,670.991 1502.78,668.265 "/>
<polygon id="_303081808" class="fil1" points="1576.86,753.922 2712.75,1889.8 2714.68,1892.35 2716.35,1895.08 2717.72,1897.97 2718.78,1901 2719.49,1904.16 2719.85,1907.42 2719.82,1910.78 1583.94,774.897 1583.96,771.539 1583.61,768.273 1582.89,765.116 1581.84,762.086 1580.47,759.198 1578.8,756.471 "/>
<polygon id="_303081712" class="fil1" points="1583.94,774.897 2719.82,1910.78 2675.73,2567.71 1539.85,1431.82 "/>
<polygon id="_303081952" class="fil1" points="1539.85,1431.82 2675.73,2567.71 2675.36,2571.06 2674.7,2574.33 2673.78,2577.49 2672.6,2580.52 2671.18,2583.4 2669.52,2586.13 2667.64,2588.68 2665.55,2591.03 2663.26,2593.18 2660.79,2595.09 2658.14,2596.75 2655.32,2598.15 2652.35,2599.27 2649.25,2600.09 2646.01,2600.6 2642.66,2600.77 1677.12,2600.77 1673.77,2600.6 1670.54,2600.09 1667.44,2599.27 1664.49,2598.16 1661.68,2596.75 1659.04,2595.09 1656.58,2593.18 520.699,1457.3 523.163,1459.21 525.802,1460.87 528.605,1462.27 531.561,1463.39 534.659,1464.21 537.889,1464.72 541.238,1464.89 1506.78,1464.89 1510.13,1464.72 1513.37,1464.21 1516.47,1463.39 1519.44,1462.27 1522.25,1460.87 1524.9,1459.2 1527.38,1457.29 1529.67,1455.15 1531.76,1452.8 1533.64,1450.25 1535.3,1447.52 1536.72,1444.63 1537.9,1441.6 1538.82,1438.45 1539.48,1435.18 "/>
</g>
<path id="_308718448" class="fil1" d="M545.337 583.111l418.676 0c35.4272,0 64.4115,28.9843 64.4115,64.4115l0 6.13111 446.955 0c18.1878,0 33.0662,14.8784 33.0662,33.065l0 55.1245 42.424 -0.0106299c18.1866,-0.00472441 34.2803,14.9445 33.0662,33.065l-44.0882 656.926c-1.21536,18.1217 -14.9043,33.0662 -33.0662,33.0662l-965.544 0c-18.1665,0 -31.6607,-14.9339 -33.065,-33.0662l-44.0882 -568.75c-1.40551,-18.1311 14.8795,-33.065 33.065,-33.065l15.9815 0 0 -214.693c0,-17.713 14.4921,-32.2051 32.2051,-32.2051z"/>
</g>
</g>
<path class="fil2" d="M1024 0c565.541,0 1024,458.459 1024,1024 0,565.541 -458.459,1024 -1024,1024 -565.541,0 -1024,-458.459 -1024,-1024 0,-565.541 458.459,-1024 1024,-1024z"/>
<path class="fil3" d="M1488.93 870.905c0.103937,0.00708662 0.408662,0.108662 0.517323,0.168898"/>
<path class="fil4" d="M545.337 583.111l418.677 0c35.426,0 64.4103,28.9843 64.4103,64.4103l0 6.13229 446.955 0c18.1866,0 33.0662,14.8784 33.0662,33.065l0 496.002 -515.293 0 0 -70.5426 -480.022 0 0 -496.862c0,-17.713 14.4921,-32.2051 32.2051,-32.2051z"/>
<rect class="fil5" x="605.17" y="688.924" width="811.237" height="423.254"/>
<path class="fil6" d="M497.15 830.009l323.712 0 214.244 -88.0477 515.764 -0.12874c18.1866,-0.00354331 34.2803,14.9445 33.065,33.065l-44.087 656.926c-1.21536,18.1217 -14.9043,33.0662 -33.0662,33.0662l-965.544 0c-18.1665,0 -31.6607,-14.9339 -33.065,-33.0662l-44.0882 -568.75c-1.40433,-18.1323 14.8784,-33.065 33.065,-33.065z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,41 @@
import { SettingOutlined } from "@ant-design/icons";
import { Chip } from "@mui/material";
import { useEffect, useState } from "react";
import { getOrigin, getFullOrigin } from "../utils/routes";
import { useTheme } from '@mui/material/styles';
const HostChip = ({route, settings}) => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const [isOnline, setIsOnline] = useState(null);
const url = getOrigin(route)
useEffect(() => {
fetch(getFullOrigin(route), {
method: 'HEAD',
mode: 'no-cors',
}).then((res) => {
setIsOnline(true);
}).catch((err) => {
setIsOnline(false);
});
}, [url]);
return <Chip
label={((isOnline == null) ? "⚪" : (isOnline ? "🟢 " : "🔴 ")) + url}
color="secondary"
style={{paddingRight: '4px'}}
onClick={() => {
if(route.UseHost)
window.open(window.location.origin.split("://")[0] + "://" + route.Host + route.PathPrefix, '_blank');
else
window.open(window.location.origin + route.PathPrefix, '_blank');
}}
onDelete={settings ? () => {
window.open('/ui/config-url#'+route.Name, '_blank');
} : null}
deleteIcon={settings ? <SettingOutlined /> : null}
/>
}
export default HostChip;

View file

@ -0,0 +1,132 @@
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
import { Card, Chip, Stack, Tooltip } from "@mui/material";
import { useState } from "react";
import { useTheme } from '@mui/material/styles';
let routeImages = {
"SERVAPP": {
label: "ServApp",
icon: "🐳",
backgroundColor: "#0db7ed",
color: "white",
colorDark: "black",
},
"STATIC": {
label: "Static",
icon: "📁",
backgroundColor: "#f9d71c",
color: "black",
colorDark: "black",
},
"REDIRECT": {
label: "Redir",
icon: "🔀",
backgroundColor: "#2c3e50",
color: "white",
colorDark: "white",
},
"PROXY": {
label: "Proxy",
icon: "🔗",
backgroundColor: "#2ecc71",
color: "white",
colorDark: "black",
},
"SPA": {
label: "SPA",
icon: "🌐",
backgroundColor: "#e74c3c",
color: "white",
colorDark: "black",
},
}
export const RouteMode = ({route}) => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
let c = routeImages[route.Mode.toUpperCase()];
return <>
<Chip
icon={<span>{c.icon}</span>}
label={c.label}
sx={{
backgroundColor: c.backgroundColor,
color: isDark ? c.colorDark : c.color,
paddingLeft: "5px",
alignItems: "right",
}}
></Chip>
</>
}
export const RouteSecurity = ({route}) => {
return <div style={{fontWeight: 'bold', fontSize: '110%'}}>
<Tooltip title={route.AuthEnabled ? "Authentication is enabled" : "Authentication is disabled"}>
<div style={{display: 'inline-block'}}>
{route.AuthEnabled ?
<LockOutlined style={{color: 'green'}} /> :
<LockOutlined style={{color: 'red'}} />
}
</div>
</Tooltip>
&nbsp;
<Tooltip title={route.ThrottlePerMinute ? "Throttling is enabled" : "Throttling is disabled"}>
<div style={{display: 'inline-block'}}>
{route.ThrottlePerMinute ?
<DashboardOutlined style={{color: 'green'}} /> :
<DashboardOutlined style={{color: 'red'}} />
}
</div>
</Tooltip>
&nbsp;
<Tooltip title={route.Timeout ? "Timeout is enabled" : "Timeout is disabled"}>
<div style={{display: 'inline-block'}}>
{route.Timeout ?
<ClockCircleOutlined style={{color: 'green'}} /> :
<ClockCircleOutlined style={{color: 'red'}} />
}
</div>
</Tooltip>
</div>
}
export const RouteActions = ({route, routeKey, up, down, deleteRoute}) => {
const [confirmDelete, setConfirmDelete] = useState(false);
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const miniChip = {
width: '30px',
height: '20px',
display: 'inline-block',
textAlign: 'center',
cursor: 'pointer',
color: theme.palette.text.secondary,
fontSize: '12px',
lineHeight: '20px',
padding: '0px',
borderRadius: '0px',
background: isDark ? 'rgba(255, 255, 255, 0.03)' : '',
fontWeight: 'bold',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
}
}
return <>
<Stack direction={'row'} spacing={2} alignItems={'center'} justifyContent={'right'}>
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={() => deleteRoute()}/>)}
<Tooltip title='Routes with the lowest priority are matched first'>
<Stack direction={'column'} spacing={0}>
<Card sx={{...miniChip, borderBottom: 'none'}} onClick={() => up()}><UpOutlined /></Card>
<Card sx={{...miniChip, cursor: 'auto'}}>{routeKey}</Card>
<Card sx={{...miniChip, borderTop: 'none'}} onClick={() => down()}><DownOutlined /></Card>
</Stack>
</Tooltip>
</Stack>
</>;
}

View file

@ -0,0 +1,84 @@
import * as React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { Input, InputAdornment, Stack, TextField } from '@mui/material';
import { SearchOutlined } from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
const PrettyTableView = ({ getKey, data, columns, onRowClick }) => {
const [search, setSearch] = React.useState('');
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
return (
<Stack direction="column" spacing={2}>
<Input placeholder="Search"
value={search}
style={{
width: '250px',
}}
startAdornment={
<InputAdornment position="start">
<SearchOutlined />
</InputAdornment>
}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<TableContainer style={{background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell>{column.title}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data
.filter((row) => {
if (!search || search.length <= 2) return true;
let found = false;
columns.forEach((column) => {
if (column.search && column.search(row).toLowerCase().includes(search.toLowerCase())) {
found = true;
}
})
return found;
})
.map((row, key) => (
<TableRow
onClick={() => onRowClick && onRowClick(row, key)}
key={getKey(row)}
sx={{
cursor: 'pointer',
borderLeft: 'transparent solid 5px',
'&:last-child td, &:last-child th': { border: 0 },
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
borderColor: 'gray',
},
}}
>
{columns.map((column) => (
<TableCell
style={column.style}
>{column.field(row, key)}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Stack>
)
}
export default PrettyTableView;

View file

@ -2,16 +2,17 @@
import * as API from './api';
import { useEffect } from 'react';
const isLoggedIn = () => useEffect(() => {
const IsLoggedIn = () => useEffect(() => {
console.log("CHECK LOGIN")
API.auth.me().then((data) => {
if(data.status != 'OK') {
if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall';
} else
} else if (data.status == 'error' && data.code == "HTTP004") {
window.location.href = '/ui/login';
}
}
});
}, []);
export default isLoggedIn;
export default IsLoggedIn;

View file

@ -0,0 +1,194 @@
import * as React from 'react';
import MainCard from '../../../components/MainCard';
import { Formik } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Grid,
FormHelperText,
Chip,
} from '@mui/material';
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from './formShortcuts';
import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons';
import { CosmosContainerPicker } from './containerPicker';
import { ValidateRoute } from './users/routeman';
const RouteConfig = ({route, key, lockTarget, TargetContainer, setRouteConfig}) => {
return (<div style={{ maxWidth: '1000px', margin: '' }}>
{route && <>
<Formik
initialValues={{
Name: route.Name,
Description: route.Description,
Mode: route.Mode || "SERVAPP",
Target: route.Target,
UseHost: route.UseHost,
AuthEnabled: route.AuthEnabled,
Host: route.Host,
UsePathPrefix: route.UsePathPrefix,
PathPrefix: route.PathPrefix,
StripPathPrefix: route.StripPathPrefix,
Timeout: route.Timeout,
ThrottlePerMinute: route.ThrottlePerMinute,
CORSOrigin: route.CORSOrigin,
}}
validationSchema={ValidateRoute}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
return false;
}}
// validate={(values) => {
// setRouteConfig(values);
// }}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<MainCard name={route.Name} title={route.Name}>
<Grid container spacing={2}>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<CosmosInputText
name="Name"
label="Name"
placeholder="Name"
formik={formik}
/>
<CosmosInputText
name="Description"
label="Description"
placeholder="Description"
formik={formik}
/>
<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"],
["STATIC", "Static Folder"],
["SPA", "Single Page Application"],
["REDIRECT", "Redirection"]
]}
/>
<CosmosFormDivider title={'Target Settings'}/>
{
(formik.values.Mode === "SERVAPP")?
<CosmosContainerPicker
formik={formik}
lockTarget={lockTarget}
TargetContainer={TargetContainer}
onTargetChange={() => {
setRouteConfig(formik.values);
}}
/>
: <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}
/>
}
<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"
formik={formik}
/>
{formik.values.UseHost && <CosmosInputText
name="Host"
label="Host"
placeholder="Host"
formik={formik}
style={{paddingLeft: '20px'}}
/>}
<CosmosCheckbox
name="UsePathPrefix"
label="Use Path Prefix"
formik={formik}
/>
{formik.values.UsePathPrefix && <CosmosInputText
name="PathPrefix"
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"
formik={formik}
/>
<CosmosInputText
name="Timeout"
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
placeholder="Timeout"
type="number"
formik={formik}
/>
<CosmosInputText
name="ThrottlePerMinute"
label="Maximum number of requests Per Minute (0 for no limit, at least 100 or less recommended)"
placeholder="Throttle Per Minute"
type="number"
formik={formik}
/>
<CosmosInputText
name="CORSOrigin"
label="Custom CORS Origin (Recommended to leave blank)"
placeholder="CORS Origin"
formik={formik}
/>
</Grid>
</CosmosCollapse>
</Grid>
</MainCard>
</form>
)}
</Formik>
</>
}
</div>)
}
export default RouteConfig;

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../IsLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';
@ -32,7 +32,6 @@ import { CosmosInputText, CosmosSelect } from './formShortcuts';
const ConfigManagement = () => {
isLoggedIn();
const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
@ -47,6 +46,7 @@ const ConfigManagement = () => {
}, []);
return <div style={{maxWidth: '1000px', margin: ''}}>
<IsLoggedIn />
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button><br /><br />

View file

@ -39,7 +39,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
const [isOnBridge, setIsOnBridge] = React.useState(false);
const [options, setOptions] = React.useState(null);
const [portsOptions, setPortsOptions] = React.useState([]);
const [portsOptions, setPortsOptions] = React.useState(null);
const loading = options === null;
const name = "Target"
@ -78,35 +78,37 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
})
setPortsOptions(portsTemp)
targetResult.port = '80'
if(portsTemp.length > 0) {
// pick best default port
// set default to first port
targetResult.port = portsTemp[0]
// first, check if a manual override exists
let override = Object.keys(defaultport.overrides).find((key) => {
let keyMatch = new RegExp(key, "i");
return newContainer.Image.match(keyMatch) && portsTemp.includes(defaultport.overrides[key])
});
if(override) {
targetResult.port = defaultport.overrides[override]
} else {
// if not, check the default list of common ports
let priorityList = defaultport.priority;
priorityList.find((_portReg) => {
return portsTemp.find((portb) => {
let portReg = new RegExp(_portReg, "i");
if(portb.toString().match(portReg)) {
targetResult.port = portb
return true;
}
if(targetResult.port == '') {
targetResult.port = '80'
if(portsTemp.length > 0) {
// pick best default port
// set default to first port
targetResult.port = portsTemp[0]
// first, check if a manual override exists
let override = Object.keys(defaultport.overrides).find((key) => {
let keyMatch = new RegExp(key, "i");
return newContainer.Image.match(keyMatch) && portsTemp.includes(defaultport.overrides[key])
});
if(override) {
targetResult.port = defaultport.overrides[override]
} else {
// if not, check the default list of common ports
let priorityList = defaultport.priority;
priorityList.find((_portReg) => {
return portsTemp.find((portb) => {
let portReg = new RegExp(_portReg, "i");
if(portb.toString().match(portReg)) {
targetResult.port = portb
return true;
}
})
})
})
}
}
}
formik.setFieldValue(name, getTarget());
if(newContainer.NetworkSettings.Networks["bridge"]) {
@ -151,6 +153,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
if (active) {
setOptions([...names]);
}
if (targetResult.container !== 'null') {
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container))
}
@ -217,7 +220,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
)}
/>}
{(portsOptions.length > 0) ? (<>
{(portsOptions) ? (<>
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
<Autocomplete
className="px-2 my-2"
@ -245,7 +248,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
</>) : ''}
{(portsOptions.length > 0) ? (<>
{(portsOptions) ? (<>
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
<TextField
type="text"

View file

@ -1,9 +1,10 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../IsLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';
import * as Yup from 'yup';
import { useTheme } from '@mui/material/styles';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import {
Alert,
@ -23,6 +24,7 @@ import {
Collapse,
TextField,
MenuItem,
Chip,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
@ -30,20 +32,29 @@ import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import RouteManagement, {ValidateRoute} from './routeman';
import { map } from 'lodash';
import { getFaviconURL, sanitizeRoute } from '../../../utils/routes';
import PrettyTableView from '../../../components/tableView/prettyTableView';
import HostChip from '../../../components/hostChip';
import {RouteActions, RouteMode, RouteSecurity} from '../../../components/routeComponents';
const stickyButton = {
position: 'fixed',
bottom: '20px',
width: '100%',
maxWidth: '1000px',
// left: '20px',
// right: '20px',
width: '300px',
boxShadow: '0px 0px 10px 0px rgba(0,0,0,0.50)',
right: '20px',
}
function shorten(test) {
if (test.length > 75) {
return test.substring(0, 75) + '...';
}
return test;
}
const ProxyManagement = () => {
isLoggedIn();
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
const [error, setError] = React.useState(null);
@ -118,7 +129,8 @@ const ProxyManagement = () => {
}
let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []);
return <div style={{ maxWidth: '1000px', margin: '' }}>
return <div style={{ }}>
<IsLoggedIn />
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button>&nbsp;&nbsp;
@ -145,10 +157,49 @@ const ProxyManagement = () => {
{config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routes && routes.map((route,key) => (<>
{routes && <PrettyTableView
data={routes}
getKey={(r) => r.Name + r.Target + r.Mode}
columns={[
{
title: '',
field: (r) => <img src={getFaviconURL(r)} width="64px" />,
style: {
textAlign: 'center',
},
},
{ title: 'URL',
search: (r) => r.Name + ' ' + r.Description,
field: (r) => <>
<div style={{display:'inline-block', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
<div style={{display:'inline-block', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
</>
},
// { title: 'Description', field: (r) => shorten(r.Description), style:{fontSize: '90%', opacity: '90%'} },
{ title: 'Origin', search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => <HostChip route={r} /> },
// { title: 'Mode', field: (r) => <RouteMode route={r} /> },
{ title: 'Target', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
{ title: 'Security', field: (r) => <RouteSecurity route={r} />,
style: {minWidth: '70px'} },
{ title: '', field: (r, k) => <RouteActions
route={r}
routeKey={k}
up={() => up(k)}
down={() => down(k)}
deleteRoute={() => deleteRoute(k)}
/>,
style: {
textAlign: 'right',
}
},
]}
/>}
{/* {routes && routes.map((route,key) => (<>
<RouteManagement key={route.Name} routeConfig={route}
setRouteConfig={(newRoute) => {
routes[key] = newRoute;
routes[key] = sanitizeRoute(newRoute);
setNeedSave(true);
}}
up={() => up(key)}
@ -156,7 +207,7 @@ const ProxyManagement = () => {
deleteRoute={() => deleteRoute(key)}
/>
<br /><br />
</>))}
</>))} */}
{routes && needSave && <>
<div>
@ -207,7 +258,7 @@ const ProxyManagement = () => {
variant="contained"
color="primary"
>
Save
Save Changes
</Button>
</AnimateButton>
</Stack>

View file

@ -20,7 +20,7 @@ import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../IsLoggedIn';
import { useEffect, useState } from 'react';
const RestartModal = ({openModal, setOpenModal}) => {

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../IsLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';

View file

@ -20,7 +20,7 @@ import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../IsLoggedIn';
import { useEffect, useState } from 'react';
const UserManagement = () => {
@ -34,8 +34,6 @@ const UserManagement = () => {
const [rows, setRows] = useState([]);
isLoggedIn();
function refresh() {
setIsLoading(true);
API.users.list()
@ -62,6 +60,7 @@ const UserManagement = () => {
return <>
{openInviteForm ? <Dialog open={openInviteForm} onClose={() => setOpenInviteForm(false)}>
<IsLoggedIn />
<DialogTitle>Invite User</DialogTitle>
<DialogContent>
<DialogContentText>

View file

@ -34,7 +34,7 @@ import avatar1 from '../../assets/images/users/avatar-1.png';
import avatar2 from '../../assets/images/users/avatar-2.png';
import avatar3 from '../../assets/images/users/avatar-3.png';
import avatar4 from '../../assets/images/users/avatar-4.png';
import isLoggedIn from '../../isLoggedIn';
import IsLoggedIn from '../../IsLoggedIn';
import * as API from '../../api';
import AnimateButton from '../../components/@extended/AnimateButton';
@ -78,8 +78,6 @@ const DashboardDefault = () => {
const [value, setValue] = useState('today');
const [slot, setSlot] = useState('week');
isLoggedIn();
const [coStatus, setCoStatus] = useState(null);
const [isCreatingDB, setIsCreatingDB] = useState(false);
@ -102,6 +100,7 @@ const DashboardDefault = () => {
return (
<>
<IsLoggedIn />
<div>
<Stack spacing={1}>
{coStatus && !coStatus.database && (
@ -117,6 +116,12 @@ const DashboardDefault = () => {
</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.

View file

@ -8,9 +8,11 @@ import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import * as API from '../../api';
import isLoggedIn from '../../isLoggedIn';
import IsLoggedIn from '../../IsLoggedIn';
import RestartModal from '../config/users/restart';
import RouteManagement, { ValidateRoute } from '../config/users/routeman';
import { sanitizeRoute } from '../../utils/routes';
import HostChip from '../../components/hostChip';
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
@ -20,11 +22,14 @@ const Item = styled(Paper)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const noOver = {overflowX: 'auto', width: "100%"}
const noOver = {
overflowX: 'auto',
width: "100%",
maxWidth: "800px",
height: "50px"
}
const ServeApps = () => {
isLoggedIn();
const [serveApps, setServeApps] = useState([]);
const [isUpdating, setIsUpdating] = useState({});
const [search, setSearch] = useState("");
@ -71,10 +76,12 @@ const ServeApps = () => {
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)
)
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
return route.Mode == "SERVAPP" && reg.test(route.Target)
// (
// route.Target.startsWith(containerName) ||
// route.Target.split('://')[1].startsWith(containerName)
// )
})) || [];
}
@ -112,7 +119,12 @@ const ServeApps = () => {
},
};
const getHostnameFromName = (name) => {
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
}
return <div>
<IsLoggedIn />
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Expose ServApp</DialogTitle>
@ -134,7 +146,7 @@ const ServeApps = () => {
Name: openModal.Names[0].replace('/', ''),
Description: "Expose " + openModal.Names[0].replace('/', '') + " to the internet",
UseHost: true,
Host: openModal.Names[0].replace('/', '') + '.' + window.location.origin.split('://')[1],
Host: getHostnameFromName(openModal.Names[0]),
UsePathPrefix: false,
PathPrefix: '',
Timeout: 30000,
@ -144,7 +156,7 @@ const ServeApps = () => {
AuthEnabled: false,
}}
setRouteConfig={(_newRoute) => {
setNewRoute(_newRoute);
setNewRoute(sanitizeRoute(_newRoute));
}}
up={() => {}}
down={() => {}}
@ -237,9 +249,9 @@ const ServeApps = () => {
Ports
</Typography>
<Stack style={noOver} margin={1} direction="row" spacing={1}>
{app.Ports.map((port) => {
{app.Ports.filter(p => p.IP != '::').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'} />
<Chip style={{ fontSize: '80%' }} label={port.PrivatePort + (port.PublicPort ? (":" + port.PublicPort) : '')} color={port.PublicPort ? 'warning' : 'default'} />
</Tooltip>
})}
</Stack>
@ -281,22 +293,7 @@ const ServeApps = () => {
</Typography>
<Stack spacing={2} direction="row">
{getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
return <><Chip
label={route.Host + route.PathPrefix}
color="secondary"
style={{paddingRight: '4px'}}
onClick={() => {
if(route.UseHost)
window.open(window.location.origin.split("://")[0] + "://" + route.Host + route.PathPrefix, '_blank');
else
window.open(window.location.origin + route.PathPrefix, '_blank');
}}
onDelete={() => {
window.open('/ui/config-url#'+route.Name, '_blank');
}}
deleteIcon={<SettingOutlined />}
/>
</>
return <HostChip route={route} settings/>
})}
{/* {getContainersRoutes(app.Names[0].replace('/', '')).length == 0 && */}
<Chip

View file

@ -30,7 +30,7 @@ const Palette = (mode) => {
colors.grey = [...greyPrimary, ...greyAscent, ...greyConstant];
const paletteColor = ThemeOption(colors);
const paletteColor = ThemeOption(colors, mode === 'dark');
return createTheme(mode === 'dark' ? {
palette: {

View file

@ -2,7 +2,7 @@
import { purple, pink, deepPurple } from '@mui/material/colors';
const Theme = (colors) => {
const Theme = (colors, darkMode) => {
const { blue, red, gold, cyan, green, grey } = colors;
const greyColors = {
0: grey[0],
@ -30,7 +30,7 @@ const Theme = (colors) => {
main: purple[400],
},
secondary: {
main: deepPurple[100]
main: darkMode ? deepPurple[800] : deepPurple[100]
},
error: {
lighter: red[0],

View file

@ -0,0 +1,39 @@
import Folder from '../assets/images/icons/folder(1).svg';
export const sanitizeRoute = (route) => {
if (!route.UseHost) {
route.Host = "";
}
if (!route.UsePathPrefix) {
route.PathPrefix = "";
}
return route;
}
const addProtocol = (url) => {
if (url.indexOf("http://") === 0 || url.indexOf("https://") === 0) {
return url;
}
return "https://" + url;
}
export const getOrigin = (route) => {
return (route.UseHost ? route.Host : '') + (route.UsePathPrefix ? route.PathPrefix : '');
}
export const getFullOrigin = (route) => {
return addProtocol(getOrigin(route));
}
export const getFaviconURL = (route) => {
const addRemote = (url) => {
return '/cosmos/api/favicon?q=' + encodeURIComponent(url)
}
if(route.Mode == "SERVAP") {
return addRemote(addProtocol(route.Target))
} else if (route.Mode == "STATIC") {
return Folder;
} else {
return addRemote(addProtocol(getOrigin(route)));
}
}

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

5
go.mod
View file

@ -17,8 +17,10 @@ require (
github.com/Azure/go-autorest/tracing v0.6.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
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.869 // indirect
github.com/andybalholm/cascadia v1.1.0 // indirect
github.com/aws/aws-sdk-go v1.36.29 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
@ -38,6 +40,7 @@ require (
github.com/foomo/simplecert v1.8.4 // indirect
github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/friendsofgo/errors v0.9.2 // indirect
github.com/go-acme/lego/v4 v4.1.3 // indirect
github.com/go-chi/chi v4.0.2+incompatible // indirect
github.com/go-chi/httprate v0.7.1 // indirect
@ -115,6 +118,7 @@ require (
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.deanishe.net/favicon v0.1.0 // indirect
go.mongodb.org/mongo-driver v1.11.3 // indirect
go.opencensus.io v0.22.5 // indirect
go.uber.org/ratelimit v0.1.0 // indirect
@ -127,6 +131,7 @@ require (
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/api v0.36.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210119180700-e258113e47cc // indirect

72
go.sum
View file

@ -78,6 +78,8 @@ github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2y
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=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
github.com/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArdnx94=
github.com/PuerkitoBio/goquery v1.6.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.18/go.mod h1:L+HB2uBoDgi3+r1pJEJcbGwyyHhd2QXaGsKLbDwtm8Q=
@ -88,8 +90,12 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/aliyun/alibaba-cloud-sdk-go v1.61.458/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.869 h1:UPhKTR08iX1hNGYP5bLAF1qsHFlZNl10yZXsK+nQXoc=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.869/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/aws/aws-sdk-go v1.36.29 h1:lM1G3AF1+7vzFm0n7hfH8r2+750BTo+6Lo6FtPB7kzk=
github.com/aws/aws-sdk-go v1.36.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -159,6 +165,8 @@ github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9 h1:RPOsDNbnDUFaSt/
github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9/go.mod h1:OdiGKKgTAfMv7x9Hh9qYFueue77tr09LUAxwy2+M8wY=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
github.com/getkin/kin-openapi v0.26.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
@ -181,8 +189,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
github.com/go-openapi/strfmt v0.19.8/go.mod h1:qBBipho+3EoIqn6YDI+4RnQEtj6jT/IdKm+PAlXxSUc=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@ -195,6 +203,30 @@ github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r
github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -304,16 +336,19 @@ github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo=
github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
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/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
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=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@ -325,7 +360,10 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
@ -353,6 +391,8 @@ github.com/linode/linodego v0.24.2 h1:wj7DO4/8zGEJm6HtGp03L1HLATzmffkwEoifiKIFi0
github.com/linode/linodego v0.24.2/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE=
github.com/liquidweb/liquidweb-go v1.6.1 h1:O51RbJo3ZEWFkZFfP32zIF6MCoZzwuuybuXsvZvVEEI=
github.com/liquidweb/liquidweb-go v1.6.1/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -362,6 +402,7 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@ -370,6 +411,7 @@ github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@ -415,6 +457,7 @@ github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -449,6 +492,8 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/roberthodgen/spa-server v0.0.0-20171007154335-bb87b4ff3253 h1:GDTuxylfsSbc8CHl6kNC9an/tUoe1EklYP5Uc6qoxCA=
github.com/roberthodgen/spa-server v0.0.0-20171007154335-bb87b4ff3253/go.mod h1:OrGqvII1PHEVwjTXyjnk4d2wz65qSRH928rsuvaevXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
@ -460,6 +505,8 @@ github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZ
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@ -467,8 +514,11 @@ github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:s
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
@ -506,6 +556,8 @@ github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
@ -515,8 +567,9 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.deanishe.net/favicon v0.1.0 h1:Afy941gjRik+DjUUcYHUxcztFEeFse2ITBkMMOlgefM=
go.deanishe.net/favicon v0.1.0/go.mod h1:vIKVI+lUh8k3UAzaN4gjC+cpyatLQWmx0hVX4vLE8jU=
go.mongodb.org/mongo-driver v1.4.2/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y=
go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@ -536,7 +589,9 @@ golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -582,6 +637,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -638,6 +694,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -659,9 +716,11 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -727,9 +786,13 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@ -772,6 +835,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=

1449
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.1.18-unstable4",
"version": "0.2.0-unstable",
"description": "",
"main": "test-server.js",
"bugs": {
@ -22,7 +22,6 @@
"@testing-library/user-event": "^14.4.3",
"@vitejs/plugin-react": "^3.1.0",
"apexcharts": "^3.35.5",
"better-commit": "^3.1.1",
"formik": "^2.2.9",
"framer-motion": "^7.3.6",
"history": "^5.3.0",

View file

@ -21,6 +21,8 @@ Cosmos is a self-hosted platform for running server applications securely and wi
Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution to secure them all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box.
* **Easy to use** 🚀👍 to install and use, with a simple web UI to manage your applications
* **User-friendly** 🧑‍🎨 For both new and experienced users: easily integrates into your existing home server (even with NGinx, Traefik, Portainer, etc...), the already existing applications you have, and the new ones you want to install
* **Secure Authentication** 👦👩 Connect to all your applications with the same account, including **strong security** and **multi-factor authentication**
* **Latest Encryption Methods** 🔒🔑 To encrypt your data and protect your privacy. Security by design, and not as an afterthought
* **Reverse Proxy** 🔄🔗 Reverse Proxy included, with a UI to easily manage your applications and their settings

View file

@ -1,49 +1,16 @@
package main
import (
"os"
"regexp"
"encoding/json"
"github.com/azukaar/cosmos-server/src/utils"
)
func LoadConfig() utils.Config {
configFile := utils.GetConfigFileName()
utils.Log("Using config file: " + configFile)
if utils.CreateDefaultConfigFileIfNecessary() {
utils.LoadBaseMainConfig(utils.DefaultConfig)
return utils.DefaultConfig
}
file, err := os.Open(configFile)
if err != nil {
utils.Fatal("Opening Config File: ", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
config := utils.Config{}
err = decoder.Decode(&config)
// check file is not empty
if err != nil {
// check error is not empty
if err.Error() == "EOF" {
utils.Fatal("Reading Config File: File is empty.", err)
}
// get error string
errString := err.Error()
// replace string in error
m1 := regexp.MustCompile(`json: cannot unmarshal ([A-Za-z\.]+) into Go struct field ([A-Za-z\.]+) of type ([A-Za-z\.]+)`)
errString = m1.ReplaceAllString(errString, "Invalid JSON in config file.\n > Field $2 is wrong.\n > Type is $1 Should be $3")
utils.Fatal("Reading Config File: " + errString, err)
}
config := utils.ReadConfigFromFile()
// check if config is valid
utils.Log("Validating config file...")
err = utils.Validate.Struct(config)
err := utils.Validate.Struct(config)
if err != nil {
utils.Fatal("Reading Config File: " + err.Error(), err)
}

View file

@ -12,7 +12,7 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
}
if(req.Method == "GET") {
config := utils.GetBaseMainConfig()
config := utils.ReadConfigFromFile()
// delete AuthPrivateKey and TLSKey
config.HTTPConfig.AuthPrivateKey = ""

View file

@ -30,12 +30,13 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
}
// restore AuthPrivateKey and TLSKey
config := utils.GetBaseMainConfig()
config := utils.ReadConfigFromFile()
request.HTTPConfig.AuthPrivateKey = config.HTTPConfig.AuthPrivateKey
request.HTTPConfig.TLSKey = config.HTTPConfig.TLSKey
request.NewInstall = config.NewInstall
utils.SaveConfigTofile(request)
utils.NeedsRestart = true
// if err != nil {
// utils.Error("SettingsUpdate: Error saving config to file", err)

View file

@ -139,8 +139,15 @@ func EditContainer(containerID string, newConfig types.ContainerJSON) (string, e
newName,
)
// is force secure
isForceSecure := newConfig.Config.Labels["cosmos-force-network-secured"] == "true"
// re-connect to networks
for networkName, _ := range oldContainer.NetworkSettings.Networks {
if(isForceSecure && networkName == "bridge") {
utils.Log("EditContainer - Skipping network " + networkName + " (cosmos-force-network-secured is true)")
continue
}
errNet := ConnectToNetworkSync(networkName, createResponse.ID)
if errNet != nil {
utils.Error("EditContainer - Failed to connect to network " + networkName, errNet)

View file

@ -180,6 +180,8 @@ func StartServer() {
srapi := router.PathPrefix("/cosmos").Subrouter()
srapi.HandleFunc("/api/status", StatusRoute)
srapi.HandleFunc("/api/favicon", GetFavicon)
srapi.HandleFunc("/api/ping", PingURL)
srapi.HandleFunc("/api/newInstall", NewInstallRoute)
srapi.HandleFunc("/api/login", user.UserLogin)
srapi.HandleFunc("/api/logout", user.UserLogout)

200
src/icons.go Normal file
View file

@ -0,0 +1,200 @@
package main
import (
"net/http"
"net/url"
// "fmt"
"os"
"io/ioutil"
"encoding/json"
"strconv"
"github.com/azukaar/cosmos-server/src/utils"
"go.deanishe.net/favicon"
)
type CachedImage struct {
ContentType string
ETag string
Body []byte
}
var cache = make(map[string]CachedImage)
func sendImage(w http.ResponseWriter, image CachedImage) {
// Copy the response to the output
w.Header().Set("Content-Type", image.ContentType)
w.Header().Set("ETag", image.ETag)
w.Header().Set("Cache-Control", "max-age=86400")
w.WriteHeader(http.StatusOK)
w.Write(image.Body)
}
func sendFallback(w http.ResponseWriter) {
// Send the fallback image
pwd,_ := os.Getwd()
imgsrc := "cosmos_gray.png"
fallback, err := ioutil.ReadFile(pwd + "/" + imgsrc)
if err != nil {
utils.Error("Favicon: fallback", err)
utils.HTTPError(w, "Favicon", http.StatusInternalServerError, "FA003")
return
}
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "max-age=5")
w.WriteHeader(http.StatusOK)
w.Write(fallback)
}
func GetFavicon(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
return
}
// get url from query string
escsiteurl := req.URL.Query().Get("q")
// URL decode
siteurl, err := url.QueryUnescape(escsiteurl)
if err != nil {
utils.Error("Favicon: URL decode", err)
utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "FA002")
return
}
if(req.Method == "GET") {
utils.Log("Fetch favicon for " + siteurl)
// Check if we have the favicon in cache
if _, ok := cache[siteurl]; ok {
utils.Debug("Favicon in cache")
resp := cache[siteurl]
sendImage(w, resp)
return
}
icons, err := favicon.Find(siteurl)
utils.Debug("Found Favicon: " + strconv.Itoa(len(icons)))
if err != nil {
utils.Error("FaviconFetch", err)
sendFallback(w)
return
}
if len(icons) == 0 {
utils.Error("FaviconFetch", err)
sendFallback(w)
return
}
iconIndex := len(icons)-1
iconChanged := false
for i, icon := range icons {
utils.Debug("Favicon Width: " + icon.URL + " " + strconv.Itoa(icon.Width))
if icon.Width <= 256 {
iconIndex = i
iconChanged = true
break
}
}
if !iconChanged {
iconIndex = 0
}
icon := icons[iconIndex]
utils.Log("Favicon: " + icon.URL)
// Fetch the favicon
resp, err := http.Get(icon.URL)
if err != nil {
utils.Error("FaviconFetch", err)
sendFallback(w)
return
}
// save the body to a file
// out, err := os.Create("favicon.ico")
// if err != nil {
// utils.Error("FaviconFetch", err)
// utils.HTTPError(w, "Favicon Fetch", http.StatusInternalServerError, "FA001")
// return
// }
// defer out.Close()
// _, err = io.Copy(out, resp.Body)
// if err != nil {
// utils.Error("FaviconFetch", err)
// utils.HTTPError(w, "Favicon Fetch", http.StatusInternalServerError, "FA001")
// return
// }
// Cache the response
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
utils.Error("FaviconFetch", err)
sendFallback(w)
return
}
finalImage := CachedImage{
ContentType: resp.Header.Get("Content-Type"),
ETag: resp.Header.Get("ETag"),
Body: body,
}
cache[siteurl] = finalImage
sendImage(w, cache[siteurl])
} else {
utils.Error("Favicon: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func PingURL(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
return
}
// get url from query string
escsiteurl := req.URL.Query().Get("q")
// URL decode
siteurl, err := url.QueryUnescape(escsiteurl)
if err != nil {
utils.Error("Ping: URL decode", err)
utils.HTTPError(w, "Ping URL decode", http.StatusInternalServerError, "FA002")
return
}
if(req.Method == "GET") {
utils.Log("Ping for " + siteurl)
resp, err := http.Get(siteurl)
if err != nil {
utils.Error("Ping", err)
utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "PI0001")
return
}
if resp.StatusCode >= 500 {
utils.Error("Ping", err)
utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "PI0002")
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{
},
})
} else {
utils.Error("Favicon: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -43,6 +43,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
"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,
"needsRestart": utils.NeedsRestart,
},
})
} else {

View file

@ -137,11 +137,11 @@ func logOutUser(w http.ResponseWriter) {
}
func redirectToReLogin(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/ui/login?invalid=1&redirect=" + req.URL.Path, http.StatusFound)
http.Redirect(w, req, "/ui/login?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
}
func SendUserToken(w http.ResponseWriter, user utils.User) {
expiration := time.Now().Add(2 * 24 * time.Hour)
expiration := time.Now().Add(3 * 24 * time.Hour)
token := jwt.New(jwt.SigningMethodEdDSA)
claims := token.Claims.(jwt.MapClaims)

View file

@ -61,6 +61,7 @@ func SetSecurityHeaders(next http.Handler) http.Handler {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"math/rand"
"regexp"
"net/http"
"os"
"strconv"
@ -16,6 +17,8 @@ var BaseMainConfig Config
var MainConfig Config
var IsHTTPS = false
var NeedsRestart = false
var DefaultConfig = Config{
LoggingLevel: "INFO",
NewInstall: true,
@ -83,7 +86,44 @@ func SetBaseMainConfig(config Config) {
SaveConfigTofile(config)
}
func LoadBaseMainConfig(config Config) {
func ReadConfigFromFile() Config {
configFile := GetConfigFileName()
Log("Using config file: " + configFile)
if CreateDefaultConfigFileIfNecessary() {
LoadBaseMainConfig(DefaultConfig)
return DefaultConfig
}
file, err := os.Open(configFile)
if err != nil {
Fatal("Opening Config File: ", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
config := Config{}
err = decoder.Decode(&config)
// check file is not empty
if err != nil {
// check error is not empty
if err.Error() == "EOF" {
Fatal("Reading Config File: File is empty.", err)
}
// get error string
errString := err.Error()
// replace string in error
m1 := regexp.MustCompile(`json: cannot unmarshal ([A-Za-z\.]+) into Go struct field ([A-Za-z\.]+) of type ([A-Za-z\.]+)`)
errString = m1.ReplaceAllString(errString, "Invalid JSON in config file.\n > Field $2 is wrong.\n > Type is $1 Should be $3")
Fatal("Reading Config File: " + errString, err)
}
return config
}
func LoadBaseMainConfig(config Config){
BaseMainConfig = config
MainConfig = config