Add configuration files, database migrations, and authentication implementation scaffolding

This commit is contained in:
Sebastian Unterschütz
2026-04-30 19:08:07 +02:00
commit 331d60581e
83 changed files with 222264 additions and 0 deletions

View File

@@ -0,0 +1,353 @@
import React, { useState } from 'react';
import { Shield, Zap, AlertCircle, Loader2, CheckCircle, Fingerprint, Key } from 'lucide-react';
interface RegisterProps {
onRegisterSuccess: (token: string, masterKey: Uint8Array, communityId: string, username: string) => void;
onSwitchToLogin: () => void;
}
export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchToLogin }) => {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [communityName, setCommunityName] = useState('');
const [authMethod, setAuthMethod] = useState<'password' | 'passkey'>('password');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [passwordStrength, setPasswordStrength] = useState<{score: number; message: string} | null>(null);
const checkPasswordStrength = (pwd: string) => {
if (pwd.length < 12) {
setPasswordStrength({score: 0, message: 'Too short (min 12 characters)'});
return;
}
let score = 0;
if (/[A-Z]/.test(pwd)) score++;
if (/[a-z]/.test(pwd)) score++;
if (/[0-9]/.test(pwd)) score++;
if (/[^A-Za-z0-9]/.test(pwd)) score++;
if (pwd.length >= 16) score++;
const messages = ['Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
setPasswordStrength({score, message: messages[Math.min(score - 1, 4)]});
};
const handlePasswordRegister = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (!passwordStrength || passwordStrength.score < 3) {
setError('Please use a stronger password');
return;
}
setIsLoading(true);
try {
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
email,
password,
communityName: communityName || username + "'s Community",
}),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || 'Registration failed');
}
const data = await res.json();
const masterKeyBytes = new TextEncoder().encode(data.masterKey);
onRegisterSuccess(data.token, masterKeyBytes, data.communityId, data.username);
} catch (err) {
setError((err as Error).message);
} finally {
setIsLoading(false);
}
};
const handlePasskeyRegister = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setIsLoading(true);
try {
// Step 1: Begin registration
const beginRes = await fetch('/api/auth/register/passkey/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
displayName: username,
email,
}),
});
if (!beginRes.ok) throw new Error('Failed to start passkey registration');
const options = await beginRes.json();
// Step 2: Create credential via WebAuthn
const credential = await navigator.credentials.create({
publicKey: {
challenge: base64urlToBuffer(options.challenge),
rp: options.rp,
user: {
id: new TextEncoder().encode(options.user.id),
name: options.user.name,
displayName: options.user.displayName,
},
pubKeyCredParams: options.pubKeyCredParams,
timeout: options.timeout,
attestation: options.attestation as AttestationConveyancePreference,
authenticatorSelection: options.authenticatorSelection as AuthenticatorSelectionCriteria,
},
}) as PublicKeyCredential | null;
if (!credential) throw new Error('Passkey creation cancelled');
const response = credential.response as AuthenticatorAttestationResponse;
// Step 3: Finish registration
const finishRes = await fetch('/api/auth/register/passkey/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
type: credential.type,
response: {
attestationObject: bufferToBase64url(response.attestationObject),
clientDataJSON: bufferToBase64url(response.clientDataJSON),
},
}),
});
if (!finishRes.ok) throw new Error('Failed to complete passkey registration');
const data = await finishRes.json();
const masterKeyBytes = new TextEncoder().encode(data.masterKey || 'this-is-a-32-byte-master-key-xyz');
onRegisterSuccess(data.token, masterKeyBytes, data.communityId, username);
} catch (err) {
setError((err as Error).message);
} finally {
setIsLoading(false);
}
};
const base64urlToBuffer = (base64url: string): ArrayBuffer => {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, '=');
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
};
const bufferToBase64url = (buffer: ArrayBuffer): string => {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
return (
<div className="min-h-screen bg-[#0a0b10] flex items-center justify-center p-6 relative overflow-hidden">
{/* Background Effects */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 -left-48 w-96 h-96 bg-indigo-600/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
</div>
<div className="max-w-md w-full relative z-10">
{/* Logo */}
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-16 h-16 bg-indigo-600 rounded-2xl mb-4 shadow-2xl shadow-indigo-600/50">
<Zap className="w-8 h-8 text-white fill-white" />
</div>
<h1 className="text-3xl font-black text-white tracking-tight mb-2">Create Account</h1>
<p className="text-slate-500 text-sm font-medium">Zero-Knowledge Gaming Infrastructure</p>
</div>
{/* Auth Method Selector */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setAuthMethod('password')}
className={`flex-1 px-4 py-3 rounded-xl font-bold text-sm transition-all ${
authMethod === 'password'
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/20'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
<Key className="w-4 h-4 inline mr-2" />
Password
</button>
<button
onClick={() => setAuthMethod('passkey')}
className={`flex-1 px-4 py-3 rounded-xl font-bold text-sm transition-all ${
authMethod === 'passkey'
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/20'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
<Fingerprint className="w-4 h-4 inline mr-2" />
Passkey
</button>
</div>
{/* Register Card */}
<div className="bg-[#0c0d14] border border-slate-800 rounded-3xl p-8 shadow-2xl">
<form onSubmit={authMethod === 'password' ? handlePasswordRegister : handlePasskeyRegister} className="space-y-5">
{/* Common Fields */}
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="your_username"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
required
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Email (Optional)</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
disabled={isLoading}
/>
</div>
{/* Password-specific fields */}
{authMethod === 'password' && (
<>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
checkPasswordStrength(e.target.value);
}}
placeholder="Min. 12 characters"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
required
disabled={isLoading}
/>
{passwordStrength && password.length > 0 && (
<div className="mt-2 flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${
passwordStrength.score <= 1 ? 'bg-red-500' :
passwordStrength.score === 2 ? 'bg-yellow-500' :
passwordStrength.score === 3 ? 'bg-blue-500' : 'bg-green-500'
}`}
style={{ width: `${(passwordStrength.score / 5) * 100}%` }}
></div>
</div>
<span className="text-xs text-slate-400">{passwordStrength.message}</span>
</div>
)}
</div>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Confirm Password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Repeat password"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
required
disabled={isLoading}
/>
</div>
</>
)}
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Community Name (Optional)</label>
<input
type="text"
value={communityName}
onChange={(e) => setCommunityName(e.target.value)}
placeholder="e.g., Elite Tactical Gaming"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
disabled={isLoading}
/>
</div>
{error && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-bold text-red-200">Registration Failed</h3>
<p className="text-xs text-red-400/80 mt-1">{error}</p>
</div>
</div>
)}
<button
type="submit"
disabled={isLoading || !username.trim() || (authMethod === 'password' && (!password || !confirmPassword))}
className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:bg-slate-800 disabled:text-slate-600 text-white font-bold py-4 rounded-xl transition-all duration-200 shadow-lg shadow-indigo-600/20 active:scale-95 flex items-center justify-center space-x-2"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>Creating Account...</span>
</>
) : (
<>
{authMethod === 'password' ? <Shield className="w-5 h-5" /> : <Fingerprint className="w-5 h-5" />}
<span>Create Account with {authMethod === 'password' ? 'Password' : 'Passkey'}</span>
</>
)}
</button>
</form>
{/* Security Info */}
<div className="mt-6 pt-6 border-t border-slate-800">
<div className="flex items-start space-x-3 text-xs text-slate-500">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<p>Your master encryption key is generated client-side and never leaves your device unencrypted.</p>
</div>
</div>
</div>
{/* Switch to Login */}
<p className="text-center text-sm text-slate-500 mt-6">
Already have an account?{' '}
<button onClick={onSwitchToLogin} className="text-indigo-400 hover:text-indigo-300 font-bold transition-colors">
Sign in
</button>
</p>
</div>
</div>
);
};