feat(admin-panel): update url

feat(admin-panel): update some of the ui
feat(oauth-screen): make oauth success screen match the rest of the app
This commit is contained in:
Alois 2026-08-30 12:55:20 +02:00
commit f6ed4f1e2e
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
8 changed files with 79 additions and 34 deletions

View file

@ -33,8 +33,8 @@ var corsExposedResponseHeaders = []string{
var corsExposedResponseHeadersJoined = strings.Join(corsExposedResponseHeaders, ", ") var corsExposedResponseHeadersJoined = strings.Join(corsExposedResponseHeaders, ", ")
const ( const (
exampleAPIKeyManagementPath = "/management.html" exampleAPIKeyManagementPath = "/admin"
exampleAPIKeyManagementURL = "/management.html?safe-mode=configure" exampleAPIKeyManagementURL = "/admin?safe-mode=configure"
) )
func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc { func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc {
@ -45,7 +45,11 @@ func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc {
} }
if c != nil && c.Request != nil { if c != nil && c.Request != nil {
path := c.Request.URL.Path path := c.Request.URL.Path
if strings.HasPrefix(path, "/v0/management/") || path == "/v0/management" || strings.HasPrefix(path, "/v0/resource/plugins/") || path == "/management.html" { if strings.HasPrefix(path, "/v0/management/") ||
path == "/v0/management" ||
strings.HasPrefix(path, "/v0/resource/plugins/") ||
path == exampleAPIKeyManagementPath ||
strings.HasPrefix(path, exampleAPIKeyManagementPath+"/") {
c.Next() c.Next()
return return
} }
@ -87,7 +91,7 @@ func (s *Server) exampleAPIKeySafeModeMiddleware() gin.HandlerFunc {
c.Header("X-CPA-SAFE-MODE", "example-api-key") c.Header("X-CPA-SAFE-MODE", "example-api-key")
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "unsafe_example_api_key", "error": "unsafe_example_api_key",
"message": "Proxy API endpoints are disabled because api-keys contains template values. Open /management.html?safe-mode=configure, update api-keys in Management, then retry.", "message": "Proxy API endpoints are disabled because api-keys contains template values. Open /admin?safe-mode=configure, update api-keys in Management, then retry.",
}) })
} }
} }

View file

@ -8,8 +8,6 @@ import (
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai" "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers/openai"
) )
const oauthCallbackSuccessHTML = `<html><head><meta charset="utf-8"><title>Authentication successful</title><script>setTimeout(function(){window.close();},5000);</script></head><body><h1>Authentication successful!</h1><p>You can close this window.</p><p>This window will close automatically in 5 seconds.</p></body></html>`
func (s *Server) setupRoutes() { func (s *Server) setupRoutes() {
healthzHandler := func(c *gin.Context) { healthzHandler := func(c *gin.Context) {
if c.Request.Method == http.MethodHead { if c.Request.Method == http.MethodHead {
@ -21,8 +19,10 @@ func (s *Server) setupRoutes() {
s.engine.GET("/healthz", healthzHandler) s.engine.GET("/healthz", healthzHandler)
s.engine.HEAD("/healthz", healthzHandler) s.engine.HEAD("/healthz", healthzHandler)
s.engine.GET("/management.html", s.serveManagementControlPanel) s.engine.GET("/admin", s.serveManagementControlPanel)
s.engine.HEAD("/management.html", s.serveManagementControlPanel) s.engine.HEAD("/admin", s.serveManagementControlPanel)
s.engine.GET("/admin/oauth-success", s.serveManagementControlPanel)
s.engine.HEAD("/admin/oauth-success", s.serveManagementControlPanel)
s.engine.GET("/management-assets/*filepath", s.serveManagementAsset) s.engine.GET("/management-assets/*filepath", s.serveManagementAsset)
s.engine.HEAD("/management-assets/*filepath", s.serveManagementAsset) s.engine.HEAD("/management-assets/*filepath", s.serveManagementAsset)
@ -59,7 +59,6 @@ func (s *Server) setupRoutes() {
if state != "" { if state != "" {
_, _ = managementHandlers.WriteOAuthCallbackFileForPendingSession(s.cfg.AuthDir, "codex", state, code, errStr) _, _ = managementHandlers.WriteOAuthCallbackFileForPendingSession(s.cfg.AuthDir, "codex", state, code, errStr)
} }
c.Header("Content-Type", "text/html; charset=utf-8") c.Redirect(http.StatusSeeOther, "/admin/oauth-success")
c.String(http.StatusOK, oauthCallbackSuccessHTML)
}) })
} }

View file

@ -1529,7 +1529,7 @@ func TestHomeEnabledHidesManagementEndpointsAndControlPanel(t *testing.T) {
}) })
t.Run("management control panel returns 404", func(t *testing.T) { t.Run("management control panel returns 404", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/management.html", nil) req := httptest.NewRequest(http.MethodGet, "/admin", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
server.engine.ServeHTTP(rr, req) server.engine.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound { if rr.Code != http.StatusNotFound {
@ -1566,27 +1566,27 @@ func TestExampleAPIKeySafeModeShowsWarningAndKeepsManagement(t *testing.T) {
t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusOK, rr.Body.String()) t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusOK, rr.Body.String())
} }
body := rr.Body.String() body := rr.Body.String()
for _, want := range []string{"Example API key detected", "Open Management", `href="/management.html?safe-mode=configure"`} { for _, want := range []string{"Example API key detected", "Open Management", `href="/admin?safe-mode=configure"`} {
if !strings.Contains(body, want) { if !strings.Contains(body, want) {
t.Fatalf("warning page missing %q: %s", want, body) t.Fatalf("warning page missing %q: %s", want, body)
} }
} }
}) })
t.Run("management html defaults to warning page", func(t *testing.T) { t.Run("admin defaults to warning page", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/management.html", nil) req := httptest.NewRequest(http.MethodGet, "/admin", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
server.engine.ServeHTTP(rr, req) server.engine.ServeHTTP(rr, req)
if rr.Code != http.StatusOK { if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusOK, rr.Body.String()) t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusOK, rr.Body.String())
} }
if !strings.Contains(rr.Body.String(), "Example API key detected") { if !strings.Contains(rr.Body.String(), "Example API key detected") {
t.Fatalf("management.html did not show warning page: %s", rr.Body.String()) t.Fatalf("admin page did not show warning page: %s", rr.Body.String())
} }
}) })
t.Run("management html head stops at warning page", func(t *testing.T) { t.Run("admin head stops at warning page", func(t *testing.T) {
req := httptest.NewRequest(http.MethodHead, "/management.html", nil) req := httptest.NewRequest(http.MethodHead, "/admin", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
server.engine.ServeHTTP(rr, req) server.engine.ServeHTTP(rr, req)
if rr.Code != http.StatusOK { if rr.Code != http.StatusOK {
@ -1601,7 +1601,7 @@ func TestExampleAPIKeySafeModeShowsWarningAndKeepsManagement(t *testing.T) {
}) })
t.Run("management button query opens control panel", func(t *testing.T) { t.Run("management button query opens control panel", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/management.html?safe-mode=configure", nil) req := httptest.NewRequest(http.MethodGet, "/admin?safe-mode=configure", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
server.engine.ServeHTTP(rr, req) server.engine.ServeHTTP(rr, req)
if rr.Code != http.StatusOK { if rr.Code != http.StatusOK {
@ -1643,7 +1643,7 @@ func TestExampleAPIKeySafeModeShowsWarningAndKeepsManagement(t *testing.T) {
if strings.Contains(rr.Body.String(), "management_url") { if strings.Contains(rr.Body.String(), "management_url") {
t.Fatalf("body should not include management_url field: %s", rr.Body.String()) t.Fatalf("body should not include management_url field: %s", rr.Body.String())
} }
if !strings.Contains(rr.Body.String(), "/management.html?safe-mode=configure") { if !strings.Contains(rr.Body.String(), "/admin?safe-mode=configure") {
t.Fatalf("body missing management link in message: %s", rr.Body.String()) t.Fatalf("body missing management link in message: %s", rr.Body.String())
} }
if got := rr.Header().Get(internallogging.CPATraceIDHeader); got != "" { if got := rr.Header().Get(internallogging.CPATraceIDHeader); got != "" {

View file

@ -15,7 +15,7 @@
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz", "@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.32/methanium-ui.tgz",
"lucide-react": "^0.542.0", "lucide-react": "^0.542.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7" "react-dom": "^19.2.7"

View file

@ -170,7 +170,7 @@ function Header({ loggedIn, onLogout }: { loggedIn: boolean; onLogout: () => voi
size="sm" size="sm"
className="mr-1 shrink-0 px-[13px]! text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" className="mr-1 shrink-0 px-[13px]! text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
nativeButton={false} nativeButton={false}
render={<a href="/management.html" aria-label="Vibe Proxy management" />} render={<a href="/admin" aria-label="Vibe Proxy management" />}
> >
{methaniumLogo && <img src={methaniumLogo} alt="" className="w-[20.59px]!" />} {methaniumLogo && <img src={methaniumLogo} alt="" className="w-[20.59px]!" />}
</Button> </Button>
@ -180,7 +180,7 @@ function Header({ loggedIn, onLogout }: { loggedIn: boolean; onLogout: () => voi
size="sm" size="sm"
className="h-9 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" className="h-9 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
nativeButton={false} nativeButton={false}
render={<a href="/management.html" />} render={<a href="/admin" />}
> >
Vibe Proxy Vibe Proxy
</Button> </Button>
@ -307,6 +307,7 @@ function AccountCard({
}; };
const [emailHidden, setEmailHidden] = useState(true); const [emailHidden, setEmailHidden] = useState(true);
const { themePolarity } = useTheme();
return ( return (
<Card> <Card>
@ -341,7 +342,7 @@ function AccountCard({
{statusProblem ? ( {statusProblem ? (
account.status_message || <AlertTriangle color="orange" /> account.status_message || <AlertTriangle color="orange" />
) : ( ) : (
<Check color="lightGreen" /> <Check color={themePolarity === 'dark' ? 'lightGreen' : 'green'} />
)} )}
</Badge> </Badge>
</CardAction> </CardAction>
@ -358,7 +359,7 @@ function AccountCard({
</div> </div>
</div> </div>
<div className="border-t border-border pt-4 flex flex-col gap-4"> <div className="pt-4 flex flex-col gap-4">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div> <div>
<div className="font-medium">Quota</div> <div className="font-medium">Quota</div>
@ -396,7 +397,7 @@ function AccountCard({
)} )}
{cachedQuota && windows.length > 0 && ( {cachedQuota && windows.length > 0 && (
<div className="space-y-4"> <div className="flex flex-col gap-4">
{windows.map((window) => ( {windows.map((window) => (
<div key={window.key}> <div key={window.key}>
<Progress value={100 - window.usedPercent} max={100}> <Progress value={100 - window.usedPercent} max={100}>
@ -420,7 +421,7 @@ function AccountCard({
)} )}
</div> </div>
<div className="flex justify-end border-t border-border pt-4"> <div className="flex justify-end pt-4">
<Button variant="destructive" size="sm" onClick={() => setDeleteOpen(true)}> <Button variant="destructive" size="sm" onClick={() => setDeleteOpen(true)}>
<Trash2 /> <Trash2 />
Delete account Delete account

View file

@ -0,0 +1,40 @@
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@methanium/ui';
import { Check } from 'lucide-react';
import { useEffect, useState } from 'react';
export function OAuthSuccess() {
const [secondsRemaining, setSecondsRemaining] = useState(5);
useEffect(() => {
document.title = 'Authentication successful | Vibe Proxy';
const closeTimer = window.setTimeout(() => window.close(), 5_000);
const countdownTimer = window.setInterval(() => {
setSecondsRemaining((current) => Math.max(0, current - 1));
}, 1_000);
return () => {
window.clearTimeout(closeTimer);
window.clearInterval(countdownTimer);
};
}, []);
return (
<main className="flex min-h-screen items-center justify-center bg-background p-4 text-foreground">
<Card className="text-center p-6! py-8!">
<CardHeader className="items-center">
<CardTitle>Authentication successful</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4">
<p className="text-sm text-muted-foreground">
This window will close automatically in {secondsRemaining}{' '}
{secondsRemaining === 1 ? 'second' : 'seconds'}.
</p>
<Button type="button" variant="outline" onClick={() => window.close()}>
Close window
</Button>
</CardContent>
</Card>
</main>
);
}

View file

@ -5,6 +5,7 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from './App'; import { App } from './App';
import { OAuthSuccess } from './OAuthSuccess';
const methaniumTheme = BUILT_IN_THEMES.filter((theme) => theme.id === 'methanium'); const methaniumTheme = BUILT_IN_THEMES.filter((theme) => theme.id === 'methanium');
@ -23,7 +24,7 @@ createRoot(document.getElementById('root')!).render(
parentThemeStorageKey={null} parentThemeStorageKey={null}
designStorageKey={null} designStorageKey={null}
> >
<App /> {window.location.pathname === '/admin/oauth-success' ? <OAuthSuccess /> : <App />}
</ThemeProvider> </ThemeProvider>
</StrictMode>, </StrictMode>,
); );

14
pnpm-lock.yaml generated
View file

@ -18,8 +18,8 @@ importers:
frontend: frontend:
dependencies: dependencies:
'@methanium/ui': '@methanium/ui':
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.32/methanium-ui.tgz
version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@8.1.1)(typescript@6.0.3) version: https://git.methanium.net/methanium/ui/releases/download/0.0.32/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@8.1.1)(typescript@6.0.3)
lucide-react: lucide-react:
specifier: ^0.542.0 specifier: ^0.542.0
version: 0.542.0(react@19.2.8) version: 0.542.0(react@19.2.8)
@ -398,9 +398,9 @@ packages:
'@marijn/find-cluster-break@1.0.4': '@marijn/find-cluster-break@1.0.4':
resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==}
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz': '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.32/methanium-ui.tgz':
resolution: {integrity: sha512-ij9zo/PdP5l/F/70t7a1o+OFZbMFRNNseNc1dlqddUFqIkAy+71VL6ptrXNpeVtzKFq8LuycwzXGsGihuViVig==, tarball: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz} resolution: {integrity: sha512-MLQ3xCLz4cBbrpioT5VMC7yEDvGTaIB3Mk14woe7BEsI2AJY0CFy2GXf4xrcZdfzZucuTboGe5M0aZwSA9TC3Q==, tarball: https://git.methanium.net/methanium/ui/releases/download/0.0.32/methanium-ui.tgz}
version: 0.0.29 version: 0.0.32
peerDependencies: peerDependencies:
react: ^19.2.7 react: ^19.2.7
react-dom: ^19.2.7 react-dom: ^19.2.7
@ -3911,7 +3911,7 @@ snapshots:
'@marijn/find-cluster-break@1.0.4': {} '@marijn/find-cluster-break@1.0.4': {}
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@8.1.1)(typescript@6.0.3)': '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.32/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@8.1.1)(typescript@6.0.3)':
dependencies: dependencies:
'@base-ui/react': 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@base-ui/react': 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@codemirror/autocomplete': 6.20.3 '@codemirror/autocomplete': 6.20.3
@ -4200,7 +4200,7 @@ snapshots:
immer: 11.1.18 immer: 11.1.18
redux: 5.0.1 redux: 5.0.1
redux-thunk: 3.1.0(redux@5.0.1) redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.2.0 reselect: 5.3.0
optionalDependencies: optionalDependencies:
react: 19.2.8 react: 19.2.8
react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1)