mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 16:16:29 +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
|
* Device Code Handler
|
||||||
*
|
*
|
||||||
* Manages device code display prompts during OAuth Device Code flow.
|
* Manages device code display prompts during OAuth Device Code flow.
|
||||||
* Parses CLIProxyAPI stdout for device codes and broadcasts via WebSocket
|
* Broadcasts device code events via WebSocket to both CLI terminal
|
||||||
* to both CLI terminal and Web UI (ccs config).
|
* and Web UI (ccs config).
|
||||||
*
|
*
|
||||||
* Similar pattern to project-selection-handler.ts but for device code flows
|
* Events emitted by oauth-process.ts:
|
||||||
* (GitHub Copilot, Qwen, etc.)
|
* - 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';
|
import { EventEmitter } from 'events';
|
||||||
@@ -22,146 +25,8 @@ export interface DeviceCodePrompt {
|
|||||||
expiresAt: number;
|
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
|
// Global event emitter for device code events
|
||||||
export const deviceCodeEvents = new EventEmitter();
|
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)
|
// Default timeout for device code (15 minutes - GitHub's default)
|
||||||
const DEVICE_CODE_TIMEOUT_MS = 900000;
|
export 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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -52,19 +52,28 @@ export function DeviceCodeDialog({
|
|||||||
const [hasCopied, setHasCopied] = useState(false);
|
const [hasCopied, setHasCopied] = useState(false);
|
||||||
const [timeRemaining, setTimeRemaining] = useState<number | null>(null);
|
const [timeRemaining, setTimeRemaining] = useState<number | null>(null);
|
||||||
|
|
||||||
// Calculate and update remaining time
|
// Calculate and update remaining time, stop at 0
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
|
const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
|
||||||
setTimeRemaining(remaining);
|
setTimeRemaining(remaining);
|
||||||
|
// Stop timer when expired
|
||||||
|
if (remaining === 0 && timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateTime();
|
updateTime();
|
||||||
const timer = setInterval(updateTime, 1000);
|
timer = setInterval(updateTime, 1000);
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
return () => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
};
|
||||||
}, [open, expiresAt]);
|
}, [open, expiresAt]);
|
||||||
|
|
||||||
const handleCopyCode = useCallback(async () => {
|
const handleCopyCode = useCallback(async () => {
|
||||||
@@ -93,12 +102,11 @@ export function DeviceCodeDialog({
|
|||||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Suppress unused variable warning - sessionId used for identification
|
const isExpired = timeRemaining === 0;
|
||||||
void sessionId;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md" data-session-id={sessionId}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<KeyRound className="w-5 h-5" />
|
<KeyRound className="w-5 h-5" />
|
||||||
@@ -111,6 +119,7 @@ export function DeviceCodeDialog({
|
|||||||
(Expires in {formatTime(timeRemaining)})
|
(Expires in {formatTime(timeRemaining)})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{isExpired && <span className="text-destructive ml-1 font-medium">(Code expired)</span>}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -127,6 +136,7 @@ export function DeviceCodeDialog({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="absolute top-2 right-2"
|
className="absolute top-2 right-2"
|
||||||
onClick={handleCopyCode}
|
onClick={handleCopyCode}
|
||||||
|
aria-label={hasCopied ? 'Code copied' : 'Copy verification code'}
|
||||||
>
|
>
|
||||||
{hasCopied ? (
|
{hasCopied ? (
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
<Check className="h-4 w-4 text-green-500" />
|
||||||
|
|||||||
Reference in New Issue
Block a user