Files
remotelink-docker/app/(dashboard)/dashboard/connect/page.tsx
monoadmin e16a2fa978 Add Python agent, WebSocket relay, real viewer, enrollment tokens
- WebSocket relay service (FastAPI) bridges agents and viewers
- Python agent with screen capture (mss), input control (pynput),
  script execution, and auto-reconnect
- Windows service wrapper, PyInstaller spec, NSIS installer for
  silent mass deployment (RemoteLink-Setup.exe /S /SERVER= /ENROLL=)
- Enrollment token system: admin generates tokens, agents self-register
- Real WebSocket viewer replaces simulated canvas
- Linux agent binary served from /downloads/remotelink-agent-linux
- DB migration 0002: viewer_token on sessions, enrollment_tokens table
- Sign-up pages cleaned up (invite-only redirect)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:25:10 -07:00

139 lines
4.8 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Link2, ArrowRight, Loader2, AlertCircle, Clock, Shield } from 'lucide-react'
export default function ConnectPage() {
const [sessionCode, setSessionCode] = useState('')
const [isConnecting, setIsConnecting] = useState(false)
const [error, setError] = useState<string | null>(null)
const router = useRouter()
const handleConnect = async (e: React.FormEvent) => {
e.preventDefault()
setIsConnecting(true)
setError(null)
const code = sessionCode.replace(/\s/g, '').toUpperCase()
if (code.length !== 6) {
setError('Please enter a valid 6-character session code')
setIsConnecting(false)
return
}
try {
const res = await fetch('/api/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
})
const data = await res.json()
if (!res.ok) {
setError(data.error || 'Invalid or expired session code')
setIsConnecting(false)
return
}
router.push(`/viewer/${data.sessionId}?token=${data.viewerToken}`)
} catch {
setError('Network error. Please try again.')
setIsConnecting(false)
}
}
const formatCode = (value: string) => {
const cleaned = value.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
if (cleaned.length <= 3) return cleaned
return `${cleaned.slice(0, 3)} ${cleaned.slice(3, 6)}`
}
return (
<div className="max-w-xl mx-auto space-y-6">
<div className="text-center">
<h2 className="text-2xl font-bold">Quick Connect</h2>
<p className="text-muted-foreground">Enter a session code to connect to a remote machine</p>
</div>
<Card className="border-border/50">
<CardHeader className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
<Link2 className="h-8 w-8 text-primary" />
</div>
<CardTitle>Enter Session Code</CardTitle>
<CardDescription className="text-balance">
Ask the person on the remote machine to generate a code from their RemoteLink agent
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleConnect} className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="code" className="sr-only">Session Code</Label>
<Input
id="code"
type="text"
placeholder="ABC 123"
value={sessionCode}
onChange={(e) => setSessionCode(formatCode(e.target.value))}
maxLength={7}
className="text-center text-3xl font-mono tracking-widest h-16 bg-secondary/50"
autoComplete="off"
autoFocus
/>
</div>
{error && (
<div className="flex items-center gap-2 rounded-md bg-destructive/10 border border-destructive/20 p-3">
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
<p className="text-sm text-destructive">{error}</p>
</div>
)}
<Button
type="submit"
className="w-full h-12"
disabled={isConnecting || sessionCode.replace(/\s/g, '').length < 6}
>
{isConnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connecting...
</>
) : (
<>
Connect
<ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</form>
</CardContent>
</Card>
<div className="grid gap-4 sm:grid-cols-2">
<div className="flex items-start gap-3 p-4 rounded-lg bg-card border border-border/50">
<Clock className="h-5 w-5 text-primary mt-0.5" />
<div>
<p className="font-medium text-sm">Codes expire</p>
<p className="text-sm text-muted-foreground">Session codes are valid for 10 minutes</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 rounded-lg bg-card border border-border/50">
<Shield className="h-5 w-5 text-primary mt-0.5" />
<div>
<p className="font-medium text-sm">Secure connection</p>
<p className="text-sm text-muted-foreground">All sessions are end-to-end encrypted</p>
</div>
</div>
</div>
</div>
)
}