mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 08:17:22 +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,
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user