Sync dev changes into release-1.0

This commit is contained in:
2026-01-24 18:51:15 +13:00
parent 52e3d680f7
commit d2ff2b3e41
17 changed files with 2227 additions and 5 deletions

View File

@@ -0,0 +1,194 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { authFetch, getApiBase, setToken } from '../lib/auth'
import BrandingLogo from '../ui/BrandingLogo'
type SignupConfig = {
invites_enabled: boolean
captcha_provider: string
hcaptcha_site_key?: string | null
recaptcha_site_key?: string | null
turnstile_site_key?: string | null
}
export default function RegisterPage() {
const router = useRouter()
const [config, setConfig] = useState<SignupConfig | null>(null)
const [inviteCode, setInviteCode] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [email, setEmail] = useState('')
const [discord, setDiscord] = useState('')
const [telegram, setTelegram] = useState('')
const [matrix, setMatrix] = useState('')
const [captchaToken, setCaptchaToken] = useState('')
const [status, setStatus] = useState<string | null>(null)
useEffect(() => {
const load = async () => {
try {
const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/auth/signup/config`)
if (!response.ok) {
throw new Error('Signup unavailable')
}
const data = await response.json()
setConfig(data)
} catch (err) {
console.error(err)
setStatus('Sign-up is not available right now.')
}
}
void load()
}, [])
useEffect(() => {
if (!config?.captcha_provider || config.captcha_provider === 'none') {
return
}
const provider = config.captcha_provider
const script = document.createElement('script')
if (provider === 'hcaptcha') {
script.src = 'https://js.hcaptcha.com/1/api.js'
} else if (provider === 'recaptcha') {
script.src = 'https://www.google.com/recaptcha/api.js'
} else if (provider === 'turnstile') {
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
}
script.async = true
script.defer = true
document.body.appendChild(script)
;(window as any).magentCaptchaCallback = (token: string) => {
setCaptchaToken(token)
}
return () => {
document.body.removeChild(script)
}
}, [config?.captcha_provider])
const submit = async (event: React.FormEvent) => {
event.preventDefault()
setStatus(null)
try {
const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
invite_code: inviteCode,
username,
password,
contact: { email, discord, telegram, matrix },
captcha_token: captchaToken,
}),
})
if (!response.ok) {
const text = await response.text()
throw new Error(text || 'Registration failed')
}
const data = await response.json()
if (data?.access_token) {
setToken(data.access_token)
}
router.push('/')
} catch (err) {
console.error(err)
const message =
err instanceof Error && err.message
? err.message.replace(/^\\{\"detail\":\"|\"\\}$/g, '')
: 'Could not complete sign-up.'
setStatus(message)
}
}
const captchaProvider = config?.captcha_provider || 'none'
const captchaKey =
captchaProvider === 'hcaptcha'
? config?.hcaptcha_site_key
: captchaProvider === 'recaptcha'
? config?.recaptcha_site_key
: config?.turnstile_site_key
return (
<main className="card auth-card">
<BrandingLogo className="brand-logo brand-logo--login" />
<h1>Create your account</h1>
{!config?.invites_enabled ? (
<div className="status-banner">Sign-ups are currently closed.</div>
) : (
<form onSubmit={submit} className="auth-form">
<label>
Invite code
<input
type="text"
value={inviteCode}
onChange={(event) => setInviteCode(event.target.value)}
required
/>
</label>
<label>
Username
<input
type="text"
value={username}
onChange={(event) => setUsername(event.target.value)}
required
/>
</label>
<label>
Password
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
required
/>
</label>
<label>
Email address (optional)
<input type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
</label>
<label>
Discord handle (optional)
<input
type="text"
value={discord}
onChange={(event) => setDiscord(event.target.value)}
/>
</label>
<label>
Telegram ID (optional)
<input
type="text"
value={telegram}
onChange={(event) => setTelegram(event.target.value)}
/>
</label>
<label>
Matrix ID (optional)
<input type="text" value={matrix} onChange={(event) => setMatrix(event.target.value)} />
</label>
{captchaProvider !== 'none' && captchaKey ? (
<div className="captcha-wrap">
{captchaProvider === 'hcaptcha' && (
<div className="h-captcha" data-sitekey={captchaKey} data-callback="magentCaptchaCallback" />
)}
{captchaProvider === 'recaptcha' && (
<div className="g-recaptcha" data-sitekey={captchaKey} data-callback="magentCaptchaCallback" />
)}
{captchaProvider === 'turnstile' && (
<div className="cf-turnstile" data-sitekey={captchaKey} data-callback="magentCaptchaCallback" />
)}
</div>
) : null}
{status && <div className="status-banner">{status}</div>}
<div className="auth-actions">
<button type="submit">Create account</button>
</div>
</form>
)}
</main>
)
}