diff --git a/pkg/rest/router.go b/pkg/rest/router.go index f10e858..07bbc5c 100644 --- a/pkg/rest/router.go +++ b/pkg/rest/router.go @@ -53,7 +53,8 @@ func (c *Context) getRouter(assets fs.FS, indexHtml []byte) *http.ServeMux { mux.Handle("/api/oidc", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.oidcProviderHandler))))) mux.Handle("/api/oidc-renew-tokens", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.oidcRenewTokensHandler))))) mux.Handle("/api/oidc/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.oidcProviderElementHandler))))) - mux.Handle("/api/setup", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.setupHandler))))) + mux.Handle("/api/setup/general", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.setupHandler))))) + mux.Handle("/api/setup/vpn", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.vpnSetupHandler))))) mux.Handle("/api/scim-setup", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.scimSetupHandler))))) mux.Handle("/api/saml-setup", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.samlSetupHandler))))) mux.Handle("/api/saml-setup/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.samlSetupElementHandler))))) diff --git a/pkg/rest/setup.go b/pkg/rest/setup.go index 1be5cde..06fc9eb 100644 --- a/pkg/rest/setup.go +++ b/pkg/rest/setup.go @@ -100,21 +100,14 @@ func (c *Context) contextHandler(w http.ResponseWriter, r *http.Request) { } func (c *Context) setupHandler(w http.ResponseWriter, r *http.Request) { - vpnConfig, err := wireguard.GetVPNConfig(c.Storage.Client) - if err != nil { - c.returnError(w, fmt.Errorf("could not get vpn config: %s", err), http.StatusBadRequest) - return - } switch r.Method { case http.MethodGet: - setupRequest := SetupRequest{ + setupRequest := GeneralSetupRequest{ Hostname: c.Hostname, EnableTLS: c.EnableTLS, RedirectToHttps: c.RedirectToHttps, DisableLocalAuth: c.LocalAuthDisabled, EnableOIDCTokenRenewal: c.EnableOIDCTokenRenewal, - Routes: strings.Join(vpnConfig.ClientRoutes, ", "), - VPNEndpoint: vpnConfig.Endpoint, } out, err := json.Marshal(setupRequest) if err != nil { @@ -123,7 +116,7 @@ func (c *Context) setupHandler(w http.ResponseWriter, r *http.Request) { } c.write(w, out) case http.MethodPost: - var setupRequest SetupRequest + var setupRequest GeneralSetupRequest decoder := json.NewDecoder(r.Body) decoder.Decode(&setupRequest) if c.Hostname != setupRequest.Hostname { @@ -145,6 +138,50 @@ func (c *Context) setupHandler(w http.ResponseWriter, r *http.Request) { c.EnableOIDCTokenRenewal = setupRequest.EnableOIDCTokenRenewal c.OIDCRenewal.SetEnabled(c.EnableOIDCTokenRenewal) } + err := SaveConfig(c) + if err != nil { + c.returnError(w, fmt.Errorf("could not save config to disk: %s", err), http.StatusBadRequest) + return + } + out, err := json.Marshal(setupRequest) + if err != nil { + c.returnError(w, fmt.Errorf("could not marshal SetupRequest: %s", err), http.StatusBadRequest) + return + } + c.write(w, out) + default: + c.returnError(w, fmt.Errorf("method not supported"), http.StatusBadRequest) + } +} + +func (c *Context) vpnSetupHandler(w http.ResponseWriter, r *http.Request) { + vpnConfig, err := wireguard.GetVPNConfig(c.Storage.Client) + if err != nil { + c.returnError(w, fmt.Errorf("could not get vpn config: %s", err), http.StatusBadRequest) + return + } + switch r.Method { + case http.MethodGet: + setupRequest := VPNSetupRequest{ + Routes: strings.Join(vpnConfig.ClientRoutes, ", "), + VPNEndpoint: vpnConfig.Endpoint, + AddressRange: vpnConfig.AddressRange.String(), + ClientAddressPrefix: vpnConfig.ClientAddressPrefix, + Port: vpnConfig.Port, + ExternalInterface: vpnConfig.ExternalInterface, + Nameservers: strings.Join(vpnConfig.Nameservers, ","), + DisableNAT: vpnConfig.DisableNAT, + } + out, err := json.Marshal(setupRequest) + if err != nil { + c.returnError(w, fmt.Errorf("could not marshal SetupRequest: %s", err), http.StatusBadRequest) + return + } + c.write(w, out) + case http.MethodPost: + var setupRequest VPNSetupRequest + decoder := json.NewDecoder(r.Body) + decoder.Decode(&setupRequest) if strings.Join(vpnConfig.ClientRoutes, ", ") != setupRequest.Routes { networks := strings.Split(setupRequest.Routes, ",") validatedNetworks := []string{} diff --git a/pkg/rest/types.go b/pkg/rest/types.go index 67c3578..1131f79 100644 --- a/pkg/rest/types.go +++ b/pkg/rest/types.go @@ -93,7 +93,7 @@ type UserInfoResponse struct { UserType string `json:"userType"` } -type SetupRequest struct { +type GeneralSetupRequest struct { Hostname string `json:"hostname"` EnableTLS bool `json:"enableTLS"` RedirectToHttps bool `json:"redirectToHttps"` @@ -103,6 +103,17 @@ type SetupRequest struct { VPNEndpoint string `json:"vpnEndpoint"` } +type VPNSetupRequest struct { + Routes string `json:"routes"` + VPNEndpoint string `json:"vpnEndpoint"` + AddressRange string `json:"addressRange"` + ClientAddressPrefix string `json:"clientAddressPrefix"` + Port int `json:"port"` + ExternalInterface string `json:"externalInterface"` + Nameservers string `json:"nameservers"` + DisableNAT bool `json:"disableNAT"` +} + type NewConnectionResponse struct { Name string `json:"name"` } diff --git a/webapp/src/Routes/Setup/GeneralSetup.tsx b/webapp/src/Routes/Setup/GeneralSetup.tsx new file mode 100644 index 0000000..a19963e --- /dev/null +++ b/webapp/src/Routes/Setup/GeneralSetup.tsx @@ -0,0 +1,195 @@ +import { Text, Checkbox, Container, UnstyledButton, Tooltip, Center, rem, TextInput, Space, Button, Alert } from "@mantine/core"; +import classes from './Setup.module.css'; +import { useEffect, useState } from "react"; +import { IconInfoCircle } from "@tabler/icons-react"; +import { AppSettings } from "../../Constants/Constants"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useAuthContext } from "../../Auth/Auth"; +import { useForm } from '@mantine/form'; +import axios, { AxiosError } from "axios"; + +type GeneralSetupRequest = { + hostname: string; + enableTLS: boolean; + redirectToHttps: boolean; + disableLocalAuth: boolean; + enableOIDCTokenRenewal: boolean; +}; +export function GeneralSetup() { + const [saved, setSaved] = useState(false) + const [saveError, setSaveError] = useState("") + const {authInfo} = useAuthContext(); + const queryClient = useQueryClient() + const { isPending, error, data, isSuccess } = useQuery({ + queryKey: ['general-setup'], + queryFn: () => + fetch(AppSettings.url + '/setup/general', { + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + authInfo.token + }, + }).then((res) => { + return res.json() + } + + ), + }) + const form = useForm({ + mode: 'uncontrolled', + initialValues: { + hostname: "", + enableTLS: false, + redirectToHttps: false, + disableLocalAuth: false, + enableOIDCTokenRenewal: false, + }, + }); + const alertIcon = ; + const setupMutation = useMutation({ + mutationFn: (setupRequest: GeneralSetupRequest) => { + return axios.post(AppSettings.url + '/setup', setupRequest, { + headers: { + "Authorization": "Bearer " + authInfo.token + }, + }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + setSaved(true) + }, + onError: (error:AxiosError) => { + setSaveError("Error: "+ error.message) + } + }) + + + useEffect(() => { + if (isSuccess) { + form.setValues({ ...data }); + } + }, [isSuccess]); + + + const hostnameTooltip = ( + + +
+ +
+
+
+ ); + + if(isPending) return "Loading..." + if(error) return 'A backend error has occurred: ' + error.message + + return ( + + {saved ? Settings Saved! : null} + {saveError !== "" ? saveError : null} +
setupMutation.mutate(values))}> + + + form.setFieldValue("enableTLS", !form.getValues().enableTLS )}> + +
+ + Enable TLS (https) + + + Enable TLS (https) using Let's Encrypt (recommended) + +
+
+ + window.location.protocol === "https:" ? form.setFieldValue("redirectToHttps", !form.getValues().redirectToHttps) : null }> + +
+ + Redirect http to https + + + Redirect http requests to https. + Not needed when terminating TLS on an external LoadBalancer. + Can only be enabled once this page is requested through https. + +
+
+ + form.setFieldValue("disableLocalAuth", !form.getValues().disableLocalAuth )}> + +
+ + Disable local auth + + + Once an OIDC Connection is setup, you can disable local authentication. Make sure to have assigned a new admin role. + +
+
+ + form.setFieldValue("enableOIDCTokenRenewal", !form.getValues().enableOIDCTokenRenewal )}> + +
+ + Deactivate a user's VPN connection on OIDC token renewal failure + + + OIDC Tokens can be refreshed when expired. + The OIDC tokens will be renewed, and on renewal failure, the VPN connection of that user will be disabled until the user logs in again. + + Note: Only use this when SCIM provisioning is not possible in your setup. +
+
+ + +
+ + ) +} \ No newline at end of file diff --git a/webapp/src/Routes/Setup/Setup.tsx b/webapp/src/Routes/Setup/Setup.tsx index 2057366..5ff1f70 100644 --- a/webapp/src/Routes/Setup/Setup.tsx +++ b/webapp/src/Routes/Setup/Setup.tsx @@ -1,229 +1,40 @@ -import { Text, Checkbox, Container, Title, UnstyledButton, Tooltip, Center, rem, TextInput, Space, Button, Alert, Divider, InputWrapper } from "@mantine/core"; +import { Container, Tabs, Title, rem } from "@mantine/core"; import classes from './Setup.module.css'; -import { useEffect, useState } from "react"; -import { IconInfoCircle } from "@tabler/icons-react"; -import { AppSettings } from "../../Constants/Constants"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useAuthContext } from "../../Auth/Auth"; -import { useForm } from '@mantine/form'; -import axios, { AxiosError } from "axios"; +import { IconFile, IconNetwork, IconSettings } from "@tabler/icons-react"; +import { GeneralSetup } from "./GeneralSetup"; +import { VPNSetup } from "./VPNSetup"; -type SetupRequest = { - hostname: string; - enableTLS: boolean; - redirectToHttps: boolean; - disableLocalAuth: boolean; - enableOIDCTokenRenewal: boolean; - routes: string; - vpnEndpoint: string; -}; export function Setup() { - const [saved, setSaved] = useState(false) - const [saveError, setSaveError] = useState("") - const {authInfo} = useAuthContext(); - const queryClient = useQueryClient() - const { isPending, error, data, isSuccess } = useQuery({ - queryKey: ['setup'], - queryFn: () => - fetch(AppSettings.url + '/setup', { - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer " + authInfo.token - }, - }).then((res) => { - return res.json() - } - - ), - }) - const form = useForm({ - mode: 'uncontrolled', - initialValues: { - hostname: "", - enableTLS: false, - redirectToHttps: false, - disableLocalAuth: false, - enableOIDCTokenRenewal: false, - routes: "", - vpnEndpoint: "", - }, - }); - const alertIcon = ; - const setupMutation = useMutation({ - mutationFn: (setupRequest: SetupRequest) => { - return axios.post(AppSettings.url + '/setup', setupRequest, { - headers: { - "Authorization": "Bearer " + authInfo.token - }, - }) - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['users'] }) - setSaved(true) - }, - onError: (error:AxiosError) => { - setSaveError("Error: "+ error.message) - } - }) + const iconStyle = { width: rem(12), height: rem(12) }; + return ( + + + VPN Setup + + + + }> + General + + }> + VPN + + }> + Templates + + + + + + + + + + Templates will go here + + + - useEffect(() => { - if (isSuccess) { - form.setValues({ ...data }); - } - }, [isSuccess]); - - - const hostnameTooltip = ( - - -
- -
-
-
- ); - - if(isPending) return "Loading..." - if(error) return 'A backend error has occurred: ' + error.message - - return ( - - - VPN Server Setup - - - {saved ? Settings Saved! : null} - {saveError !== "" ? saveError : null} -
setupMutation.mutate(values))}> - - - form.setFieldValue("enableTLS", !form.getValues().enableTLS )}> - -
- - Enable TLS (https) - - - Enable TLS (https) using Let's Encrypt (recommended) - -
-
- - window.location.protocol === "https:" ? form.setFieldValue("redirectToHttps", !form.getValues().redirectToHttps) : null }> - -
- - Redirect http to https - - - Redirect http requests to https. - Not needed when terminating TLS on an external LoadBalancer. - Can only be enabled once this page is requested through https. - -
-
- - form.setFieldValue("disableLocalAuth", !form.getValues().disableLocalAuth )}> - -
- - Disable local auth - - - Once an OIDC Connection is setup, you can disable local authentication. Make sure to have assigned a new admin role. - -
-
- - form.setFieldValue("enableOIDCTokenRenewal", !form.getValues().enableOIDCTokenRenewal )}> - -
- - Deactivate a user's VPN connection on OIDC token renewal failure - - - OIDC Tokens can be refreshed when expired. - The OIDC tokens will be renewed, and on renewal failure, the VPN connection of that user will be disabled until the user logs in again. - - Note: Only use this when SCIM provisioning is not possible in your setup. -
-
- - - - - - - - - -
- - ) + ) } \ No newline at end of file diff --git a/webapp/src/Routes/Setup/VPNSetup.tsx b/webapp/src/Routes/Setup/VPNSetup.tsx new file mode 100644 index 0000000..da25f67 --- /dev/null +++ b/webapp/src/Routes/Setup/VPNSetup.tsx @@ -0,0 +1,214 @@ + +import { Container, TextInput, Alert, InputWrapper, Button, Space, UnstyledButton, Checkbox, Text } from "@mantine/core"; +import { useEffect, useState } from "react"; +import classes from './Setup.module.css'; +import { IconInfoCircle } from "@tabler/icons-react"; +import { AppSettings } from "../../Constants/Constants"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useAuthContext } from "../../Auth/Auth"; +import { useForm } from '@mantine/form'; +import axios, { AxiosError } from "axios"; + +type VPNSetupRequest = { + routes: string; + vpnEndpoint: string; + addressRange: string, + clientAddressPrefix: string, + port: number, + externalInterface: string, + nameservers: string, + disableNAT: boolean, +}; +export function VPNSetup() { + const [saved, setSaved] = useState(false) + const [saveError, setSaveError] = useState("") + const {authInfo} = useAuthContext(); + const queryClient = useQueryClient() + const { isPending, error, data, isSuccess } = useQuery({ + queryKey: ['vpn-setup'], + queryFn: () => + fetch(AppSettings.url + '/setup/vpn', { + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + authInfo.token + }, + }).then((res) => { + return res.json() + } + + ), + }) + const form = useForm({ + mode: 'uncontrolled', + initialValues: { + routes: "", + vpnEndpoint: "", + addressRange: "", + clientAddressPrefix: "", + port: 0, + externalInterface: "", + nameservers: "", + disableNAT: false, + }, + }); + const setupMutation = useMutation({ + mutationFn: (setupRequest: VPNSetupRequest) => { + return axios.post(AppSettings.url + '/setup', setupRequest, { + headers: { + "Authorization": "Bearer " + authInfo.token + }, + }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + setSaved(true) + }, + onError: (error:AxiosError) => { + setSaveError("Error: "+ error.message) + } + }) + + const alertIcon = ; + + useEffect(() => { + if (isSuccess) { + form.setValues({ ...data }); + } + }, [isSuccess]); + + + if(isPending) return "Loading..." + if(error) return 'A backend error has occurred: ' + error.message + + return ( + + {saved ? Settings Saved! : null} + {saveError !== "" ? saveError : null} + +
setupMutation.mutate(values))}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + form.setFieldValue("disableNAT", !form.getValues().disableNAT )}> + +
+ + Disable NAT + + + Packets will be routed to anywhere on the network, using Network Address Translation (NAT). If the VPN clients only need to access the VPN server and not other devices in the network, you can disable NAT. + +
+
+ + + + +
+ ) +} \ No newline at end of file