diff --git a/src/app/api/auth/check-handle/route.ts b/src/app/api/auth/check-handle/route.ts new file mode 100644 index 0000000..0cbb0b6 --- /dev/null +++ b/src/app/api/auth/check-handle/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db, users } from '@/db'; +import { eq } from 'drizzle-orm'; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const handle = searchParams.get('handle')?.toLowerCase().trim(); + + if (!handle || handle.length < 3) { + return NextResponse.json({ available: false, error: 'Handle too short' }); + } + + if (!/^[a-zA-Z0-9_]+$/.test(handle)) { + return NextResponse.json({ available: false, error: 'Invalid characters' }); + } + + const existingUser = await db.query.users.findFirst({ + where: eq(users.handle, handle), + }); + + return NextResponse.json({ + available: !existingUser, + handle + }); + } catch (error) { + console.error('Check handle error:', error); + return NextResponse.json({ error: 'Failed to check handle' }, { status: 500 }); + } +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index ce6ce30..85b515d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { SynapsisLogo } from '@/components/Icons'; @@ -15,6 +15,46 @@ export default function LoginPage() { const [displayName, setDisplayName] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [nodeInfo, setNodeInfo] = useState({ name: '', description: '' }); + const [handleStatus, setHandleStatus] = useState<'idle' | 'checking' | 'available' | 'taken'>('idle'); + + // Fetch node info + useEffect(() => { + fetch('/api/node') + .then(res => res.json()) + .then(data => { + setNodeInfo({ + name: data.name || '', + description: data.description || 'Federated social network infrastructure' + }); + }) + .catch(() => { }); + }, []); + + // Handle availability check + useEffect(() => { + if (mode !== 'register' || !handle || handle.length < 3) { + setHandleStatus('idle'); + return; + } + + const timer = setTimeout(async () => { + setHandleStatus('checking'); + try { + const res = await fetch(`/api/auth/check-handle?handle=${handle}`); + const data = await res.json(); + if (data.available) { + setHandleStatus('available'); + } else { + setHandleStatus('taken'); + } + } catch { + setHandleStatus('idle'); + } + }, 500); + + return () => clearTimeout(timer); + }, [handle, mode]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -65,12 +105,17 @@ export default function LoginPage() {
{/* Logo */}
-
+
Synapsis
-

- Federated social network infrastructure + {nodeInfo.name && nodeInfo.name !== 'Synapsis' && ( +

+ {nodeInfo.name} +
+ )} +

+ {nodeInfo.description}

@@ -158,9 +203,26 @@ export default function LoginPage() { maxLength={20} />
-

- 3-20 characters, letters, numbers, and underscores only -

+
+ + 3-20 characters, alphanumeric and underscores + + {handleStatus === 'checking' && ( + Checking... + )} + {handleStatus === 'available' && ( + Available + )} + {handleStatus === 'taken' && ( + Taken + )} +
diff --git a/src/app/settings/migration/page.tsx b/src/app/settings/migration/page.tsx index bef0b67..7b511ab 100644 --- a/src/app/settings/migration/page.tsx +++ b/src/app/settings/migration/page.tsx @@ -31,6 +31,32 @@ export default function MigrationPage() { const [isImporting, setIsImporting] = useState(false); const [importError, setImportError] = useState(null); const [importSuccess, setImportSuccess] = useState(null); + const [handleStatus, setHandleStatus] = useState<'idle' | 'checking' | 'available' | 'taken'>('idle'); + + // Handle availability check + useEffect(() => { + if (activeTab !== 'import' || !importHandle || importHandle.length < 3) { + setHandleStatus('idle'); + return; + } + + const timer = setTimeout(async () => { + setHandleStatus('checking'); + try { + const res = await fetch(`/api/auth/check-handle?handle=${importHandle}`); + const data = await res.json(); + if (data.available) { + setHandleStatus('available'); + } else { + setHandleStatus('taken'); + } + } catch { + setHandleStatus('idle'); + } + }, 500); + + return () => clearTimeout(timer); + }, [importHandle, activeTab]); const handleExport = async () => { if (!exportPassword) { @@ -359,15 +385,45 @@ export default function MigrationPage() { - setImportHandle(e.target.value)} - placeholder="e.g., alice" - /> -
- This will be your @handle on this node. Your DID remains the same. +
+ @ + setImportHandle(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, ''))} + style={{ paddingLeft: '28px' }} + placeholder="yourhandle" + required + minLength={3} + maxLength={20} + /> +
+
+ + 3-20 characters, alphanumeric and underscores + + {handleStatus === 'checking' && ( + Checking... + )} + {handleStatus === 'available' && ( + Available + )} + {handleStatus === 'taken' && ( + Taken + )}