mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-14 08:51:49 +02:00
feat(hub): add OAUTH_DISABLE_POPUP env var (#1900)
Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
42
internal/hub/server.go
Normal file
42
internal/hub/server.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/hub/utils"
|
||||
)
|
||||
|
||||
// PublicAppInfo defines the structure of the public app information that will be injected into the HTML
|
||||
type PublicAppInfo struct {
|
||||
BASE_PATH string
|
||||
HUB_VERSION string
|
||||
HUB_URL string
|
||||
OAUTH_DISABLE_POPUP bool `json:"OAUTH_DISABLE_POPUP,omitempty"`
|
||||
}
|
||||
|
||||
// modifyIndexHTML injects the public app information into the index.html content
|
||||
func modifyIndexHTML(hub *Hub, html []byte) string {
|
||||
info := getPublicAppInfo(hub)
|
||||
content, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return string(html)
|
||||
}
|
||||
htmlContent := strings.ReplaceAll(string(html), "./", info.BASE_PATH)
|
||||
return strings.Replace(htmlContent, "\"{info}\"", string(content), 1)
|
||||
}
|
||||
|
||||
func getPublicAppInfo(hub *Hub) PublicAppInfo {
|
||||
parsedURL, _ := url.Parse(hub.appURL)
|
||||
info := PublicAppInfo{
|
||||
BASE_PATH: strings.TrimSuffix(parsedURL.Path, "/") + "/",
|
||||
HUB_VERSION: beszel.Version,
|
||||
HUB_URL: hub.appURL,
|
||||
}
|
||||
if val, _ := utils.GetEnv("OAUTH_DISABLE_POPUP"); val == "true" {
|
||||
info.OAUTH_DISABLE_POPUP = true
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||
)
|
||||
@@ -38,7 +36,7 @@ func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
resp.Body.Close()
|
||||
// Create a new response with the modified body
|
||||
modifiedBody := rm.modifyHTML(string(body))
|
||||
modifiedBody := modifyIndexHTML(rm.hub, body)
|
||||
resp.Body = io.NopCloser(strings.NewReader(modifiedBody))
|
||||
resp.ContentLength = int64(len(modifiedBody))
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody)))
|
||||
@@ -46,19 +44,6 @@ func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (rm *responseModifier) modifyHTML(html string) string {
|
||||
parsedURL, err := url.Parse(rm.hub.appURL)
|
||||
if err != nil {
|
||||
return html
|
||||
}
|
||||
// fix base paths in html if using subpath
|
||||
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
||||
html = strings.ReplaceAll(html, "./", basePath)
|
||||
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
|
||||
html = strings.Replace(html, "{{HUB_URL}}", rm.hub.appURL, 1)
|
||||
return html
|
||||
}
|
||||
|
||||
// startServer sets up the development server for Beszel
|
||||
func (h *Hub) startServer(se *core.ServeEvent) error {
|
||||
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
|
||||
@@ -5,10 +5,8 @@ package hub
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/hub/utils"
|
||||
"github.com/henrygd/beszel/internal/site"
|
||||
|
||||
@@ -18,17 +16,8 @@ import (
|
||||
|
||||
// startServer sets up the production server for Beszel
|
||||
func (h *Hub) startServer(se *core.ServeEvent) error {
|
||||
// parse app url
|
||||
parsedURL, err := url.Parse(h.appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fix base paths in html if using subpath
|
||||
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
||||
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
|
||||
html := strings.ReplaceAll(string(indexFile), "./", basePath)
|
||||
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
|
||||
html = strings.Replace(html, "{{HUB_URL}}", h.appURL, 1)
|
||||
html := modifyIndexHTML(h, indexFile)
|
||||
// set up static asset serving
|
||||
staticPaths := [2]string{"/static/", "/assets/"}
|
||||
serveStatic := apis.Static(site.DistDirFS, false)
|
||||
|
||||
@@ -22,11 +22,7 @@
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
globalThis.BESZEL = {
|
||||
BASE_PATH: "%BASE_URL%",
|
||||
HUB_VERSION: "{{V}}",
|
||||
HUB_URL: "{{HUB_URL}}"
|
||||
}
|
||||
globalThis.BESZEL = "{info}"
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label"
|
||||
import { pb } from "@/lib/api"
|
||||
import { $authenticated } from "@/lib/stores"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { $router, Link, prependBasePath } from "../router"
|
||||
import { $router, Link, basePath, prependBasePath } from "../router"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { OtpInputForm } from "./otp-forms"
|
||||
|
||||
@@ -37,8 +37,7 @@ const RegisterSchema = v.looseObject({
|
||||
passwordConfirm: passwordSchema,
|
||||
})
|
||||
|
||||
export const showLoginFaliedToast = (description?: string) => {
|
||||
description ||= t`Please check your credentials and try again`
|
||||
export const showLoginFaliedToast = (description = t`Please check your credentials and try again`) => {
|
||||
toast({
|
||||
title: t`Login attempt failed`,
|
||||
description,
|
||||
@@ -130,10 +129,6 @@ export function UserAuthForm({
|
||||
[isFirstRun]
|
||||
)
|
||||
|
||||
if (!authMethods) {
|
||||
return null
|
||||
}
|
||||
|
||||
const authProviders = authMethods.oauth2.providers ?? []
|
||||
const oauthEnabled = authMethods.oauth2.enabled && authProviders.length > 0
|
||||
const passwordEnabled = authMethods.password.enabled
|
||||
@@ -142,6 +137,12 @@ export function UserAuthForm({
|
||||
|
||||
function loginWithOauth(provider: AuthProviderInfo, forcePopup = false) {
|
||||
setIsOauthLoading(true)
|
||||
|
||||
if (globalThis.BESZEL.OAUTH_DISABLE_POPUP) {
|
||||
redirectToOauthProvider(provider)
|
||||
return
|
||||
}
|
||||
|
||||
const oAuthOpts: OAuth2AuthConfig = {
|
||||
provider: provider.name,
|
||||
}
|
||||
@@ -150,10 +151,7 @@ export function UserAuthForm({
|
||||
const authWindow = window.open()
|
||||
if (!authWindow) {
|
||||
setIsOauthLoading(false)
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: t`Please enable pop-ups for this site`,
|
||||
})
|
||||
showLoginFaliedToast(t`Please enable pop-ups for this site`)
|
||||
return
|
||||
}
|
||||
oAuthOpts.urlCallback = (url) => {
|
||||
@@ -171,16 +169,57 @@ export function UserAuthForm({
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user to the OAuth provider's authentication page in the same window.
|
||||
* Requires the app's base URL to be registered as a redirect URI with the OAuth provider.
|
||||
*/
|
||||
function redirectToOauthProvider(provider: AuthProviderInfo) {
|
||||
const url = new URL(provider.authURL)
|
||||
// url.searchParams.set("redirect_uri", `${window.location.origin}${basePath}`)
|
||||
sessionStorage.setItem("provider", JSON.stringify(provider))
|
||||
window.location.href = url.toString()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// auto login if password disabled and only one auth provider
|
||||
if (!passwordEnabled && authProviders.length === 1 && !sessionStorage.getItem("lo")) {
|
||||
// Add a small timeout to ensure browser is ready to handle popups
|
||||
setTimeout(() => {
|
||||
loginWithOauth(authProviders[0], true)
|
||||
}, 300)
|
||||
// handle redirect-based OAuth callback if we have a code
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const code = params.get("code")
|
||||
if (code) {
|
||||
const state = params.get("state")
|
||||
const provider: AuthProviderInfo = JSON.parse(sessionStorage.getItem("provider") ?? "{}")
|
||||
if (!state || provider.state !== state) {
|
||||
showLoginFaliedToast()
|
||||
} else {
|
||||
setIsOauthLoading(true)
|
||||
window.history.replaceState({}, "", window.location.pathname)
|
||||
pb.collection("users")
|
||||
.authWithOAuth2Code(provider.name, code, provider.codeVerifier, `${window.location.origin}${basePath}`)
|
||||
.then(() => $authenticated.set(pb.authStore.isValid))
|
||||
.catch((e: unknown) => showLoginFaliedToast((e as Error).message))
|
||||
.finally(() => setIsOauthLoading(false))
|
||||
}
|
||||
}
|
||||
|
||||
// auto login if password disabled and only one auth provider
|
||||
if (!code && !passwordEnabled && authProviders.length === 1 && !sessionStorage.getItem("lo")) {
|
||||
// Add a small timeout to ensure browser is ready to handle popups
|
||||
setTimeout(() => loginWithOauth(authProviders[0], false), 300)
|
||||
return
|
||||
}
|
||||
|
||||
// refresh auth if not in above states (required for trusted auth header)
|
||||
pb.collection("users")
|
||||
.authRefresh()
|
||||
.then((res) => {
|
||||
pb.authStore.save(res.token, res.record)
|
||||
$authenticated.set(!!pb.authStore.isValid)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!authMethods) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (otpId && mfaId) {
|
||||
return <OtpInputForm otpId={otpId} mfaId={mfaId} />
|
||||
}
|
||||
@@ -248,7 +287,7 @@ export function UserAuthForm({
|
||||
)}
|
||||
<div className="sr-only">
|
||||
{/* honeypot */}
|
||||
<label htmlFor="website"></label>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="text"
|
||||
|
||||
@@ -94,18 +94,6 @@ const Layout = () => {
|
||||
document.documentElement.dir = direction
|
||||
}, [direction])
|
||||
|
||||
useEffect(() => {
|
||||
// refresh auth if not authenticated (required for trusted auth header)
|
||||
if (!authenticated) {
|
||||
pb.collection("users")
|
||||
.authRefresh()
|
||||
.then((res) => {
|
||||
pb.authStore.save(res.token, res.record)
|
||||
$authenticated.set(!!pb.authStore.isValid)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DirectionProvider dir={direction}>
|
||||
{!authenticated ? (
|
||||
|
||||
1
internal/site/src/types.d.ts
vendored
1
internal/site/src/types.d.ts
vendored
@@ -7,6 +7,7 @@ declare global {
|
||||
BASE_PATH: string
|
||||
HUB_VERSION: string
|
||||
HUB_URL: string
|
||||
OAUTH_DISABLE_POPUP: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user