mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
refactor(ghcp): remove unused device code session management
- Remove unused functions: registerDeviceCode, completeDeviceCode, failDeviceCode, getActiveDeviceCode, hasActiveDeviceCode, getAllActiveDeviceCodes, ActiveSession interface, activeSessions Map - Reduce device-code-handler.ts from 168 to 33 lines (YAGNI) - Add ARIA labels to copy button for accessibility - Fix timer to stop at 0 seconds instead of continuing negative - Add "(Code expired)" visual indicator with destructive styling - Change void sessionId to data-session-id attribute for debugging Code review findings applied. Gemini OAuth unaffected (uses separate project-selection-handler.ts with Authorization Code Flow).
This commit is contained in:
@@ -2,11 +2,14 @@
|
||||
* Device Code Handler
|
||||
*
|
||||
* Manages device code display prompts during OAuth Device Code flow.
|
||||
* Parses CLIProxyAPI stdout for device codes and broadcasts via WebSocket
|
||||
* to both CLI terminal and Web UI (ccs config).
|
||||
* Broadcasts device code events via WebSocket to both CLI terminal
|
||||
* and Web UI (ccs config).
|
||||
*
|
||||
* Similar pattern to project-selection-handler.ts but for device code flows
|
||||
* (GitHub Copilot, Qwen, etc.)
|
||||
* Events emitted by oauth-process.ts:
|
||||
* - deviceCode:received - When device code is parsed from output
|
||||
* - deviceCode:completed - When auth succeeds
|
||||
* - deviceCode:failed - When auth fails
|
||||
* - deviceCode:expired - When code expires (handled by UI timer)
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
@@ -22,146 +25,8 @@ export interface DeviceCodePrompt {
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active device code session with completion resolver
|
||||
*/
|
||||
interface ActiveSession {
|
||||
prompt: DeviceCodePrompt;
|
||||
resolve: (success: boolean) => void;
|
||||
timeout: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
// Global event emitter for device code events
|
||||
export const deviceCodeEvents = new EventEmitter();
|
||||
|
||||
// Active sessions by session ID
|
||||
const activeSessions = new Map<string, ActiveSession>();
|
||||
|
||||
// Default timeout for device code (15 minutes - GitHub's default)
|
||||
const DEVICE_CODE_TIMEOUT_MS = 900000;
|
||||
|
||||
/**
|
||||
* Parse device/user code from CLIProxy output
|
||||
*
|
||||
* Supports various formats:
|
||||
* - "Enter code: XXXX-YYYY"
|
||||
* - "code XXXX-YYYY"
|
||||
* - "user code: XXXX-YYYY"
|
||||
* - "code \"XXXX-YYYY\""
|
||||
*/
|
||||
export function parseDeviceCode(output: string): string | null {
|
||||
const codeMatch = output.match(
|
||||
/(?:enter\s+)?(?:user\s+)?code[:\s]+["']?([A-Z0-9]{4,8}[-\s]?[A-Z0-9]{4,8})["']?/i
|
||||
);
|
||||
return codeMatch ? codeMatch[1].toUpperCase() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse verification URL from CLIProxy output
|
||||
*/
|
||||
export function parseVerificationUrl(output: string): string | null {
|
||||
const urlMatch = output.match(/(https?:\/\/[^\s]+device[^\s]*)/i);
|
||||
return urlMatch ? urlMatch[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a device code session and emit event for UI broadcast
|
||||
*
|
||||
* @param prompt - Device code prompt data
|
||||
* @returns Promise that resolves when auth completes or times out
|
||||
*/
|
||||
export function registerDeviceCode(prompt: DeviceCodePrompt): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// Set timeout for session expiry
|
||||
const timeout = setTimeout(() => {
|
||||
const session = activeSessions.get(prompt.sessionId);
|
||||
if (session) {
|
||||
activeSessions.delete(prompt.sessionId);
|
||||
deviceCodeEvents.emit('deviceCode:expired', prompt.sessionId);
|
||||
resolve(false);
|
||||
}
|
||||
}, DEVICE_CODE_TIMEOUT_MS);
|
||||
|
||||
// Store active session
|
||||
activeSessions.set(prompt.sessionId, {
|
||||
prompt,
|
||||
resolve,
|
||||
timeout,
|
||||
});
|
||||
|
||||
// Emit event for WebSocket broadcast to UI
|
||||
deviceCodeEvents.emit('deviceCode:received', prompt);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark device code session as completed (auth successful)
|
||||
*/
|
||||
export function completeDeviceCode(sessionId: string): boolean {
|
||||
const session = activeSessions.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearTimeout(session.timeout);
|
||||
activeSessions.delete(sessionId);
|
||||
|
||||
session.resolve(true);
|
||||
deviceCodeEvents.emit('deviceCode:completed', sessionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark device code session as failed
|
||||
*/
|
||||
export function failDeviceCode(sessionId: string, error?: string): boolean {
|
||||
const session = activeSessions.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearTimeout(session.timeout);
|
||||
activeSessions.delete(sessionId);
|
||||
|
||||
session.resolve(false);
|
||||
deviceCodeEvents.emit('deviceCode:failed', { sessionId, error });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active device code prompt
|
||||
*/
|
||||
export function getActiveDeviceCode(sessionId: string): DeviceCodePrompt | null {
|
||||
const session = activeSessions.get(sessionId);
|
||||
return session ? session.prompt : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's an active device code session
|
||||
*/
|
||||
export function hasActiveDeviceCode(sessionId: string): boolean {
|
||||
return activeSessions.has(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active device code sessions (for debugging/status)
|
||||
*/
|
||||
export function getAllActiveDeviceCodes(): DeviceCodePrompt[] {
|
||||
return Array.from(activeSessions.values()).map((s) => s.prompt);
|
||||
}
|
||||
|
||||
export default {
|
||||
parseDeviceCode,
|
||||
parseVerificationUrl,
|
||||
registerDeviceCode,
|
||||
completeDeviceCode,
|
||||
failDeviceCode,
|
||||
getActiveDeviceCode,
|
||||
hasActiveDeviceCode,
|
||||
getAllActiveDeviceCodes,
|
||||
deviceCodeEvents,
|
||||
};
|
||||
export const DEVICE_CODE_TIMEOUT_MS = 900000;
|
||||
|
||||
@@ -52,19 +52,28 @@ export function DeviceCodeDialog({
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
const [timeRemaining, setTimeRemaining] = useState<number | null>(null);
|
||||
|
||||
// Calculate and update remaining time
|
||||
// Calculate and update remaining time, stop at 0
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const updateTime = () => {
|
||||
const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
|
||||
setTimeRemaining(remaining);
|
||||
// Stop timer when expired
|
||||
if (remaining === 0 && timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
updateTime();
|
||||
const timer = setInterval(updateTime, 1000);
|
||||
timer = setInterval(updateTime, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
};
|
||||
}, [open, expiresAt]);
|
||||
|
||||
const handleCopyCode = useCallback(async () => {
|
||||
@@ -93,12 +102,11 @@ export function DeviceCodeDialog({
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// Suppress unused variable warning - sessionId used for identification
|
||||
void sessionId;
|
||||
const isExpired = timeRemaining === 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent className="sm:max-w-md" data-session-id={sessionId}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<KeyRound className="w-5 h-5" />
|
||||
@@ -111,6 +119,7 @@ export function DeviceCodeDialog({
|
||||
(Expires in {formatTime(timeRemaining)})
|
||||
</span>
|
||||
)}
|
||||
{isExpired && <span className="text-destructive ml-1 font-medium">(Code expired)</span>}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -127,6 +136,7 @@ export function DeviceCodeDialog({
|
||||
size="icon"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={handleCopyCode}
|
||||
aria-label={hasCopied ? 'Code copied' : 'Copy verification code'}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
|
||||
Reference in New Issue
Block a user