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() {
- Federated social network infrastructure + {nodeInfo.name && nodeInfo.name !== 'Synapsis' && ( +
+ {nodeInfo.description}
- 3-20 characters, letters, numbers, and underscores only -
+