mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add configurable server URL for standalone web hosting
Adds support for hosting the web UI separately from the hapi server on static hosts (GitHub Pages, Cloudflare Pages). Users can now set a custom server origin via a dialog on the login screen, with the ability to return to same-origin behavior. Changes include: - New useServerUrl hook for managing server URL configuration and storage - Updated API client to support baseUrl parameter for all requests - Enhanced login UI with server picker dialog (top-right button) - Auth system now keys tokens per baseUrl to support multiple servers - SSE connection updated to use configured baseUrl - Documentation updates for standalone hosting setup
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ApiClient } from '@/api/client'
|
||||
import { Spinner } from '@/components/Spinner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import type { ServerUrlResult } from '@/hooks/useServerUrl'
|
||||
|
||||
type LoginPromptProps = {
|
||||
onLogin: (token: string) => void
|
||||
baseUrl: string
|
||||
serverUrl: string | null
|
||||
setServerUrl: (input: string) => ServerUrlResult
|
||||
clearServerUrl: () => void
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
@@ -11,6 +18,9 @@ export function LoginPrompt(props: LoginPromptProps) {
|
||||
const [accessToken, setAccessToken] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isServerDialogOpen, setIsServerDialogOpen] = useState(false)
|
||||
const [serverInput, setServerInput] = useState(props.serverUrl ?? '')
|
||||
const [serverError, setServerError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -26,7 +36,7 @@ export function LoginPrompt(props: LoginPromptProps) {
|
||||
|
||||
try {
|
||||
// Validate the token by attempting to authenticate
|
||||
const client = new ApiClient('')
|
||||
const client = new ApiClient('', { baseUrl: props.baseUrl })
|
||||
await client.authenticate({ accessToken: trimmedToken })
|
||||
// If successful, pass the token to parent
|
||||
props.onLogin(trimmedToken)
|
||||
@@ -37,10 +47,96 @@ export function LoginPrompt(props: LoginPromptProps) {
|
||||
}
|
||||
}, [accessToken, props])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isServerDialogOpen) {
|
||||
return
|
||||
}
|
||||
setServerInput(props.serverUrl ?? '')
|
||||
setServerError(null)
|
||||
}, [isServerDialogOpen, props.serverUrl])
|
||||
|
||||
const handleSaveServer = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const result = props.setServerUrl(serverInput)
|
||||
if (!result.ok) {
|
||||
setServerError(result.error)
|
||||
return
|
||||
}
|
||||
setServerError(null)
|
||||
setServerInput(result.value)
|
||||
setIsServerDialogOpen(false)
|
||||
}, [props, serverInput])
|
||||
|
||||
const handleClearServer = useCallback(() => {
|
||||
props.clearServerUrl()
|
||||
setServerInput('')
|
||||
setServerError(null)
|
||||
setIsServerDialogOpen(false)
|
||||
}, [props])
|
||||
|
||||
const displayError = error || props.error
|
||||
const serverSummary = props.serverUrl ?? `${props.baseUrl} (same origin)`
|
||||
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<div className="relative h-full flex items-center justify-center p-4">
|
||||
<div className="absolute right-4 top-4 z-10">
|
||||
<Dialog open={isServerDialogOpen} onOpenChange={setIsServerDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
Server
|
||||
<span className="text-[10px] uppercase tracking-wide text-[var(--app-hint)]">
|
||||
{props.serverUrl ? 'Custom' : 'Default'}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Server URL</DialogTitle>
|
||||
<DialogDescription>
|
||||
Set the hapi server origin for API and live updates.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSaveServer} className="space-y-4">
|
||||
<div className="text-xs text-[var(--app-hint)]">
|
||||
Current: {serverSummary}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium">Server origin</label>
|
||||
<input
|
||||
type="url"
|
||||
value={serverInput}
|
||||
onChange={(e) => {
|
||||
setServerInput(e.target.value)
|
||||
setServerError(null)
|
||||
}}
|
||||
placeholder="https://hapi.example.com"
|
||||
className="w-full px-3 py-2.5 rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] text-[var(--app-fg)] placeholder:text-[var(--app-hint)] focus:outline-none focus:ring-2 focus:ring-[var(--app-button)] focus:border-transparent"
|
||||
/>
|
||||
<div className="text-[11px] text-[var(--app-hint)]">
|
||||
Use http(s) only. Any path is ignored.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{serverError && (
|
||||
<div className="text-sm text-red-500">
|
||||
{serverError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{props.serverUrl && (
|
||||
<Button type="button" variant="outline" onClick={handleClearServer}>
|
||||
Use same origin
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit">
|
||||
Save server
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="w-full max-w-sm space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user