feat(cliproxy): add authentication status display to web dashboard

- Add auth status endpoint to show built-in profiles authentication state
- Display auth status in UI with visual indicators for authenticated state
- Show last authentication date and token file count
- Update CLI command to better differentiate between built-in profiles and custom variants
- Improve UI layout to separate built-in profiles from custom variants
This commit is contained in:
kaitranntt
2025-12-07 20:32:00 -05:00
parent bd1f5c610c
commit a283f942a9
5 changed files with 141 additions and 39 deletions
+42 -31
View File
@@ -23,6 +23,7 @@ import {
isCLIProxyInstalled, isCLIProxyInstalled,
getCLIProxyPath, getCLIProxyPath,
} from '../cliproxy'; } from '../cliproxy';
import { getAllAuthStatus, getOAuthConfig } from '../cliproxy/auth-handler';
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector'; import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector'; import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager'; import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
@@ -354,50 +355,60 @@ async function handleCreate(args: string[]): Promise<void> {
async function handleList(): Promise<void> { async function handleList(): Promise<void> {
await initUI(); await initUI();
console.log(header('CLIProxy Variants')); console.log(header('CLIProxy Profiles'));
console.log(''); console.log('');
try { try {
// Show auth status for built-in profiles
console.log(subheader('Built-in Profiles'));
const authStatuses = getAllAuthStatus();
for (const status of authStatuses) {
const oauthConfig = getOAuthConfig(status.provider);
const icon = status.authenticated ? ok('') : warn('');
const authLabel = status.authenticated ? color('authenticated', 'success') : dim('not authenticated');
const lastAuthStr = status.lastAuth
? dim(` (${status.lastAuth.toLocaleDateString()})`)
: '';
console.log(` ${icon} ${color(status.provider, 'command').padEnd(18)} ${oauthConfig.displayName.padEnd(16)} ${authLabel}${lastAuthStr}`);
}
console.log('');
console.log(dim(' To authenticate: ccs <provider> --auth'));
console.log(dim(' To logout: ccs <provider> --logout'));
console.log('');
// Show custom variants if any
const config = loadConfig(); const config = loadConfig();
const variants = config.cliproxy || {}; const variants = config.cliproxy || {};
const variantNames = Object.keys(variants); const variantNames = Object.keys(variants);
if (variantNames.length === 0) { if (variantNames.length > 0) {
console.log(warn('No CLIProxy variants configured')); console.log(subheader('Custom Variants'));
// Build table data
const rows: string[][] = variantNames.map((name) => {
const variant = variants[name] as { provider: string; settings: string };
return [name, variant.provider, variant.settings];
});
// Print table
console.log(
table(rows, {
head: ['Variant', 'Provider', 'Settings'],
colWidths: [15, 12, 35],
})
);
console.log(''); console.log('');
console.log('Built-in CLIProxy profiles:'); console.log(dim(`Total: ${variantNames.length} custom variant(s)`));
CLIPROXY_PROFILES.forEach((p) => console.log(` ${color(p, 'command')}`));
console.log(''); console.log('');
console.log('To create a custom variant:');
console.log(` ${color('ccs cliproxy create', 'command')}`);
console.log('');
return;
} }
// Build table data console.log(dim('To create a custom variant:'));
const rows: string[][] = variantNames.map((name) => { console.log(` ${color('ccs cliproxy create', 'command')}`);
const variant = variants[name] as { provider: string; settings: string };
return [name, variant.provider, variant.settings];
});
// Print table
console.log(
table(rows, {
head: ['Variant', 'Provider', 'Settings'],
colWidths: [15, 12, 35],
})
);
console.log('');
// Show built-in profiles
console.log(subheader('Built-in Profiles'));
console.log(` ${CLIPROXY_PROFILES.join(', ')}`);
console.log('');
console.log(dim(`Total: ${variantNames.length} custom variant(s)`));
console.log(''); console.log('');
} catch (error) { } catch (error) {
console.log(fail(`Failed to list variants: ${(error as Error).message}`)); console.log(fail(`Failed to list profiles: ${(error as Error).message}`));
process.exit(1); process.exit(1);
} }
} }
+21
View File
@@ -11,6 +11,7 @@ import { getCcsDir, getConfigPath, loadConfig, loadSettings } from '../utils/con
import { Config, Settings } from '../types/config'; import { Config, Settings } from '../types/config';
import { expandPath } from '../utils/helpers'; import { expandPath } from '../utils/helpers';
import { runHealthChecks, fixHealthIssue } from './health-service'; import { runHealthChecks, fixHealthIssue } from './health-service';
import { getAllAuthStatus, getOAuthConfig } from '../cliproxy/auth-handler';
export const apiRoutes = Router(); export const apiRoutes = Router();
@@ -293,6 +294,26 @@ apiRoutes.delete('/cliproxy/:name', (req: Request, res: Response): void => {
res.json({ name, deleted: true }); res.json({ name, deleted: true });
}); });
/**
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
*/
apiRoutes.get('/cliproxy/auth', (_req: Request, res: Response) => {
const statuses = getAllAuthStatus();
const authStatus = statuses.map((status) => {
const oauthConfig = getOAuthConfig(status.provider);
return {
provider: status.provider,
displayName: oauthConfig.displayName,
authenticated: status.authenticated,
lastAuth: status.lastAuth?.toISOString() || null,
tokenFiles: status.tokenFiles.length,
};
});
res.json({ authStatus });
});
// ==================== Settings (Phase 05) ==================== // ==================== Settings (Phase 05) ====================
/** /**
+7
View File
@@ -14,6 +14,13 @@ export function useCliproxy() {
}); });
} }
export function useCliproxyAuth() {
return useQuery({
queryKey: ['cliproxy-auth'],
queryFn: () => api.cliproxy.auth(),
});
}
export function useCreateVariant() { export function useCreateVariant() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
+9
View File
@@ -51,6 +51,14 @@ export interface CreateVariant {
model?: string; model?: string;
} }
export interface AuthStatus {
provider: string;
displayName: string;
authenticated: boolean;
lastAuth: string | null;
tokenFiles: number;
}
export interface Account { export interface Account {
name: string; name: string;
type?: string; type?: string;
@@ -76,6 +84,7 @@ export const api = {
}, },
cliproxy: { cliproxy: {
list: () => request<{ variants: Variant[] }>('/cliproxy'), list: () => request<{ variants: Variant[] }>('/cliproxy'),
auth: () => request<{ authStatus: AuthStatus[] }>('/cliproxy/auth'),
create: (data: CreateVariant) => create: (data: CreateVariant) =>
request('/cliproxy', { request('/cliproxy', {
method: 'POST', method: 'POST',
+62 -8
View File
@@ -5,17 +5,18 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react'; import { Plus, Check, X } from 'lucide-react';
import { CliproxyTable } from '@/components/cliproxy-table'; import { CliproxyTable } from '@/components/cliproxy-table';
import { CliproxyDialog } from '@/components/cliproxy-dialog'; import { CliproxyDialog } from '@/components/cliproxy-dialog';
import { useCliproxy } from '@/hooks/use-cliproxy'; import { useCliproxy, useCliproxyAuth } from '@/hooks/use-cliproxy';
export function CliproxyPage() { export function CliproxyPage() {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const { data, isLoading } = useCliproxy(); const { data, isLoading } = useCliproxy();
const { data: authData, isLoading: authLoading } = useCliproxyAuth();
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">CLIProxy</h1> <h1 className="text-2xl font-bold">CLIProxy</h1>
@@ -29,11 +30,64 @@ export function CliproxyPage() {
</Button> </Button>
</div> </div>
{isLoading ? ( {/* Built-in Profiles Auth Status */}
<div className="text-muted-foreground">Loading variants...</div> <div>
) : ( <h2 className="text-lg font-semibold mb-3">Built-in Profiles</h2>
<CliproxyTable data={data?.variants || []} /> {authLoading ? (
)} <div className="text-muted-foreground">Loading auth status...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{authData?.authStatus.map((status) => (
<div
key={status.provider}
className={`p-4 rounded-lg border ${
status.authenticated
? 'border-green-500/30 bg-green-500/5'
: 'border-muted bg-muted/5'
}`}
>
<div className="flex items-center gap-2">
{status.authenticated ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<X className="w-4 h-4 text-muted-foreground" />
)}
<span className="font-medium">{status.displayName}</span>
</div>
<div className="mt-1 text-sm text-muted-foreground">
{status.authenticated ? (
<>
Authenticated
{status.lastAuth && (
<span className="ml-1">
({new Date(status.lastAuth).toLocaleDateString()})
</span>
)}
</>
) : (
<>
Not authenticated
<span className="block text-xs mt-1">
Run: <code className="bg-muted px-1 rounded">ccs {status.provider} --auth</code>
</span>
</>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Custom Variants */}
<div>
<h2 className="text-lg font-semibold mb-3">Custom Variants</h2>
{isLoading ? (
<div className="text-muted-foreground">Loading variants...</div>
) : (
<CliproxyTable data={data?.variants || []} />
)}
</div>
<CliproxyDialog open={dialogOpen} onClose={() => setDialogOpen(false)} /> <CliproxyDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
</div> </div>