mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 06:17:09 +00:00
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:
@@ -23,6 +23,7 @@ import {
|
||||
isCLIProxyInstalled,
|
||||
getCLIProxyPath,
|
||||
} from '../cliproxy';
|
||||
import { getAllAuthStatus, getOAuthConfig } from '../cliproxy/auth-handler';
|
||||
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
||||
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
||||
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
|
||||
@@ -354,50 +355,60 @@ async function handleCreate(args: string[]): Promise<void> {
|
||||
async function handleList(): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.log(header('CLIProxy Variants'));
|
||||
console.log(header('CLIProxy Profiles'));
|
||||
console.log('');
|
||||
|
||||
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 variants = config.cliproxy || {};
|
||||
const variantNames = Object.keys(variants);
|
||||
|
||||
if (variantNames.length === 0) {
|
||||
console.log(warn('No CLIProxy variants configured'));
|
||||
if (variantNames.length > 0) {
|
||||
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('Built-in CLIProxy profiles:');
|
||||
CLIPROXY_PROFILES.forEach((p) => console.log(` ${color(p, 'command')}`));
|
||||
console.log(dim(`Total: ${variantNames.length} custom variant(s)`));
|
||||
console.log('');
|
||||
console.log('To create a custom variant:');
|
||||
console.log(` ${color('ccs cliproxy create', 'command')}`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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('');
|
||||
|
||||
// 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(dim('To create a custom variant:'));
|
||||
console.log(` ${color('ccs cliproxy create', 'command')}`);
|
||||
console.log('');
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getCcsDir, getConfigPath, loadConfig, loadSettings } from '../utils/con
|
||||
import { Config, Settings } from '../types/config';
|
||||
import { expandPath } from '../utils/helpers';
|
||||
import { runHealthChecks, fixHealthIssue } from './health-service';
|
||||
import { getAllAuthStatus, getOAuthConfig } from '../cliproxy/auth-handler';
|
||||
|
||||
export const apiRoutes = Router();
|
||||
|
||||
@@ -293,6 +294,26 @@ apiRoutes.delete('/cliproxy/:name', (req: Request, res: Response): void => {
|
||||
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) ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,13 @@ export function useCliproxy() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useCliproxyAuth() {
|
||||
return useQuery({
|
||||
queryKey: ['cliproxy-auth'],
|
||||
queryFn: () => api.cliproxy.auth(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateVariant() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -51,6 +51,14 @@ export interface CreateVariant {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface AuthStatus {
|
||||
provider: string;
|
||||
displayName: string;
|
||||
authenticated: boolean;
|
||||
lastAuth: string | null;
|
||||
tokenFiles: number;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
name: string;
|
||||
type?: string;
|
||||
@@ -76,6 +84,7 @@ export const api = {
|
||||
},
|
||||
cliproxy: {
|
||||
list: () => request<{ variants: Variant[] }>('/cliproxy'),
|
||||
auth: () => request<{ authStatus: AuthStatus[] }>('/cliproxy/auth'),
|
||||
create: (data: CreateVariant) =>
|
||||
request('/cliproxy', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
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 { CliproxyDialog } from '@/components/cliproxy-dialog';
|
||||
import { useCliproxy } from '@/hooks/use-cliproxy';
|
||||
import { useCliproxy, useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||
|
||||
export function CliproxyPage() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const { data, isLoading } = useCliproxy();
|
||||
const { data: authData, isLoading: authLoading } = useCliproxyAuth();
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">CLIProxy</h1>
|
||||
@@ -29,11 +30,64 @@ export function CliproxyPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground">Loading variants...</div>
|
||||
) : (
|
||||
<CliproxyTable data={data?.variants || []} />
|
||||
)}
|
||||
{/* Built-in Profiles Auth Status */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-3">Built-in Profiles</h2>
|
||||
{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)} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user