| "use client"; |
|
|
| import { useState, useEffect } from "react"; |
| import PropTypes from "prop-types"; |
| import { Modal, Button, Input } from "@/shared/components"; |
|
|
| |
| |
| |
| |
| export default function CursorAuthModal({ isOpen, onSuccess, onClose }) { |
| const [accessToken, setAccessToken] = useState(""); |
| const [machineId, setMachineId] = useState(""); |
| const [error, setError] = useState(null); |
| const [importing, setImporting] = useState(false); |
| const [autoDetecting, setAutoDetecting] = useState(false); |
| const [autoDetected, setAutoDetected] = useState(false); |
| const [windowsManual, setWindowsManual] = useState(false); |
|
|
| const runAutoDetect = async () => { |
| setAutoDetecting(true); |
| setError(null); |
| setAutoDetected(false); |
| setWindowsManual(false); |
|
|
| try { |
| const res = await fetch("/api/oauth/cursor/auto-import"); |
| const data = await res.json(); |
|
|
| if (data.found) { |
| setAccessToken(data.accessToken); |
| setMachineId(data.machineId); |
| setAutoDetected(true); |
| } else if (data.windowsManual) { |
| setWindowsManual(true); |
| } else { |
| setError(data.error || "Could not auto-detect tokens"); |
| } |
| } catch (err) { |
| setError("Failed to auto-detect tokens"); |
| } finally { |
| setAutoDetecting(false); |
| } |
| }; |
|
|
| |
| useEffect(() => { |
| if (!isOpen) return; |
| runAutoDetect(); |
| }, [isOpen]); |
|
|
| const handleImportToken = async () => { |
| if (!accessToken.trim()) { |
| setError("Please enter an access token"); |
| return; |
| } |
|
|
| if (!machineId.trim()) { |
| setError("Please enter a machine ID"); |
| return; |
| } |
|
|
| setImporting(true); |
| setError(null); |
|
|
| try { |
| const res = await fetch("/api/oauth/cursor/import", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| accessToken: accessToken.trim(), |
| machineId: machineId.trim(), |
| }), |
| }); |
|
|
| const data = await res.json(); |
|
|
| if (!res.ok) { |
| throw new Error(data.error || "Import failed"); |
| } |
|
|
| onSuccess?.(); |
| onClose(); |
| } catch (err) { |
| setError(err.message); |
| } finally { |
| setImporting(false); |
| } |
| }; |
|
|
| return ( |
| <Modal isOpen={isOpen} title="Connect Cursor IDE" onClose={onClose}> |
| <div className="flex flex-col gap-4"> |
| {/* Auto-detecting state */} |
| {autoDetecting && ( |
| <div className="text-center py-6"> |
| <div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"> |
| <span className="material-symbols-outlined text-3xl text-primary animate-spin"> |
| progress_activity |
| </span> |
| </div> |
| <h3 className="text-lg font-semibold mb-2">Auto-detecting tokens...</h3> |
| <p className="text-sm text-text-muted"> |
| Reading from Cursor IDE database |
| </p> |
| </div> |
| )} |
| |
| {/* Form (shown after auto-detect completes) */} |
| {!autoDetecting && ( |
| <> |
| {/* Success message if auto-detected */} |
| {autoDetected && ( |
| <div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg border border-green-200 dark:border-green-800"> |
| <div className="flex gap-2"> |
| <span className="material-symbols-outlined text-green-600 dark:text-green-400">check_circle</span> |
| <p className="text-sm text-green-800 dark:text-green-200"> |
| Tokens auto-detected from Cursor IDE successfully! |
| </p> |
| </div> |
| </div> |
| )} |
| |
| {/* Windows manual instructions */} |
| {windowsManual && ( |
| <div className="bg-amber-50 dark:bg-amber-900/20 p-3 rounded-lg border border-amber-200 dark:border-amber-800 flex flex-col gap-2"> |
| <div className="flex gap-2 items-center"> |
| <span className="material-symbols-outlined text-amber-600 dark:text-amber-400">info</span> |
| <p className="text-sm font-medium text-amber-800 dark:text-amber-200"> |
| Could not read Cursor database automatically. |
| </p> |
| </div> |
| <p className="text-xs text-amber-700 dark:text-amber-300"> |
| Make sure Cursor IDE has been opened at least once, then click <strong>Retry</strong>. If the problem persists, paste your tokens manually below. |
| </p> |
| <Button onClick={runAutoDetect} variant="outline" fullWidth> |
| Retry |
| </Button> |
| </div> |
| )} |
| |
| {/* Info message if not auto-detected */} |
| {!autoDetected && !windowsManual && !error && ( |
| <div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800"> |
| <div className="flex gap-2"> |
| <span className="material-symbols-outlined text-blue-600 dark:text-blue-400">info</span> |
| <p className="text-sm text-blue-800 dark:text-blue-200"> |
| Cursor IDE not detected. Please paste your tokens manually. |
| </p> |
| </div> |
| </div> |
| )} |
| |
| {/* Access Token Input */} |
| <div> |
| <label className="block text-sm font-medium mb-2"> |
| Access Token <span className="text-red-500">*</span> |
| </label> |
| <textarea |
| value={accessToken} |
| onChange={(e) => setAccessToken(e.target.value)} |
| placeholder="Access token will be auto-filled..." |
| rows={3} |
| className="w-full px-3 py-2 text-sm font-mono border border-border rounded-lg bg-background focus:outline-none focus:border-primary resize-none" |
| /> |
| </div> |
| |
| {/* Machine ID Input */} |
| <div> |
| <label className="block text-sm font-medium mb-2"> |
| Machine ID <span className="text-red-500">*</span> |
| </label> |
| <Input |
| value={machineId} |
| onChange={(e) => setMachineId(e.target.value)} |
| placeholder="Machine ID will be auto-filled..." |
| className="font-mono text-sm" |
| /> |
| </div> |
| |
| {/* Error Display */} |
| {error && ( |
| <div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-lg border border-red-200 dark:border-red-800"> |
| <p className="text-sm text-red-600 dark:text-red-400">{error}</p> |
| </div> |
| )} |
| |
| {/* Action Buttons */} |
| <div className="flex gap-2"> |
| <Button |
| onClick={handleImportToken} |
| fullWidth |
| disabled={importing || !accessToken.trim() || !machineId.trim()} |
| > |
| {importing ? "Importing..." : "Import Token"} |
| </Button> |
| <Button onClick={onClose} variant="ghost" fullWidth> |
| Cancel |
| </Button> |
| </div> |
| </> |
| )} |
| </div> |
| </Modal> |
| ); |
| } |
|
|
| CursorAuthModal.propTypes = { |
| isOpen: PropTypes.bool.isRequired, |
| onSuccess: PropTypes.func, |
| onClose: PropTypes.func.isRequired, |
| }; |
|
|