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:
parent
3db5ec25c2
commit
f6ed4f1e2e
8 changed files with 79 additions and 34 deletions
|
|
@ -33,8 +33,8 @@ var corsExposedResponseHeaders = []string{
|
|||
var corsExposedResponseHeadersJoined = strings.Join(corsExposedResponseHeaders, ", ")
|
||||
|
||||
const (
|
||||
exampleAPIKeyManagementPath = "/management.html"
|
||||
exampleAPIKeyManagementURL = "/management.html?safe-mode=configure"
|
||||
exampleAPIKeyManagementPath = "/admin"
|
||||
exampleAPIKeyManagementURL = "/admin?safe-mode=configure"
|
||||
)
|
||||
|
||||
func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc {
|
||||
|
|
@ -45,7 +45,11 @@ func (s *Server) homeHeartbeatMiddleware() gin.HandlerFunc {
|
|||
}
|
||||
if c != nil && c.Request != nil {
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
|
@ -87,7 +91,7 @@ func (s *Server) exampleAPIKeySafeModeMiddleware() gin.HandlerFunc {
|
|||
c.Header("X-CPA-SAFE-MODE", "example-api-key")
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"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.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import (
|
|||
"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() {
|
||||
healthzHandler := func(c *gin.Context) {
|
||||
if c.Request.Method == http.MethodHead {
|
||||
|
|
@ -21,8 +19,10 @@ func (s *Server) setupRoutes() {
|
|||
s.engine.GET("/healthz", healthzHandler)
|
||||
s.engine.HEAD("/healthz", healthzHandler)
|
||||
|
||||
s.engine.GET("/management.html", s.serveManagementControlPanel)
|
||||
s.engine.HEAD("/management.html", s.serveManagementControlPanel)
|
||||
s.engine.GET("/admin", 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.HEAD("/management-assets/*filepath", s.serveManagementAsset)
|
||||
|
||||
|
|
@ -59,7 +59,6 @@ func (s *Server) setupRoutes() {
|
|||
if state != "" {
|
||||
_, _ = managementHandlers.WriteOAuthCallbackFileForPendingSession(s.cfg.AuthDir, "codex", state, code, errStr)
|
||||
}
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, oauthCallbackSuccessHTML)
|
||||
c.Redirect(http.StatusSeeOther, "/admin/oauth-success")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1529,7 +1529,7 @@ func TestHomeEnabledHidesManagementEndpointsAndControlPanel(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()
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
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())
|
||||
}
|
||||
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) {
|
||||
t.Fatalf("warning page missing %q: %s", want, body)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("management html defaults to warning page", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/management.html", nil)
|
||||
t.Run("admin defaults to warning page", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
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") {
|
||||
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) {
|
||||
req := httptest.NewRequest(http.MethodHead, "/management.html", nil)
|
||||
t.Run("admin head stops at warning page", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodHead, "/admin", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
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) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/management.html?safe-mode=configure", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin?safe-mode=configure", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
|
|
@ -1643,7 +1643,7 @@ func TestExampleAPIKeySafeModeShowsWarningAndKeepsManagement(t *testing.T) {
|
|||
if strings.Contains(rr.Body.String(), "management_url") {
|
||||
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())
|
||||
}
|
||||
if got := rr.Header().Get(internallogging.CPATraceIDHeader); got != "" {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"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",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7"
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ function Header({ loggedIn, onLogout }: { loggedIn: boolean; onLogout: () => voi
|
|||
size="sm"
|
||||
className="mr-1 shrink-0 px-[13px]! text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
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]!" />}
|
||||
</Button>
|
||||
|
|
@ -180,7 +180,7 @@ function Header({ loggedIn, onLogout }: { loggedIn: boolean; onLogout: () => voi
|
|||
size="sm"
|
||||
className="h-9 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
nativeButton={false}
|
||||
render={<a href="/management.html" />}
|
||||
render={<a href="/admin" />}
|
||||
>
|
||||
Vibe Proxy
|
||||
</Button>
|
||||
|
|
@ -307,6 +307,7 @@ function AccountCard({
|
|||
};
|
||||
|
||||
const [emailHidden, setEmailHidden] = useState(true);
|
||||
const { themePolarity } = useTheme();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -341,7 +342,7 @@ function AccountCard({
|
|||
{statusProblem ? (
|
||||
account.status_message || <AlertTriangle color="orange" />
|
||||
) : (
|
||||
<Check color="lightGreen" />
|
||||
<Check color={themePolarity === 'dark' ? 'lightGreen' : 'green'} />
|
||||
)}
|
||||
</Badge>
|
||||
</CardAction>
|
||||
|
|
@ -358,7 +359,7 @@ function AccountCard({
|
|||
</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>
|
||||
<div className="font-medium">Quota</div>
|
||||
|
|
@ -396,7 +397,7 @@ function AccountCard({
|
|||
)}
|
||||
|
||||
{cachedQuota && windows.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
{windows.map((window) => (
|
||||
<div key={window.key}>
|
||||
<Progress value={100 - window.usedPercent} max={100}>
|
||||
|
|
@ -420,7 +421,7 @@ function AccountCard({
|
|||
)}
|
||||
</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)}>
|
||||
<Trash2 />
|
||||
Delete account
|
||||
|
|
|
|||
40
frontend/src/OAuthSuccess.tsx
Normal file
40
frontend/src/OAuthSuccess.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { StrictMode } from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { App } from './App';
|
||||
import { OAuthSuccess } from './OAuthSuccess';
|
||||
|
||||
const methaniumTheme = BUILT_IN_THEMES.filter((theme) => theme.id === 'methanium');
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ createRoot(document.getElementById('root')!).render(
|
|||
parentThemeStorageKey={null}
|
||||
designStorageKey={null}
|
||||
>
|
||||
<App />
|
||||
{window.location.pathname === '/admin/oauth-success' ? <OAuthSuccess /> : <App />}
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
|||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
|
|
@ -18,8 +18,8 @@ importers:
|
|||
frontend:
|
||||
dependencies:
|
||||
'@methanium/ui':
|
||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/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)
|
||||
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.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:
|
||||
specifier: ^0.542.0
|
||||
version: 0.542.0(react@19.2.8)
|
||||
|
|
@ -398,9 +398,9 @@ packages:
|
|||
'@marijn/find-cluster-break@1.0.4':
|
||||
resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==}
|
||||
|
||||
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.29/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}
|
||||
version: 0.0.29
|
||||
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.32/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.32
|
||||
peerDependencies:
|
||||
react: ^19.2.7
|
||||
react-dom: ^19.2.7
|
||||
|
|
@ -3911,7 +3911,7 @@ snapshots:
|
|||
|
||||
'@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:
|
||||
'@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
|
||||
|
|
@ -4200,7 +4200,7 @@ snapshots:
|
|||
immer: 11.1.18
|
||||
redux: 5.0.1
|
||||
redux-thunk: 3.1.0(redux@5.0.1)
|
||||
reselect: 5.2.0
|
||||
reselect: 5.3.0
|
||||
optionalDependencies:
|
||||
react: 19.2.8
|
||||
react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue