Files
reflector-dashboard/auth/signup.php
T
bjorneandClaude Fable 5.1 25b481e2ae Public web-only release: SvxPortal front-end + auth backend, install templates and INSTALL.md
Stripped from the internal repo: per-box deploy trees, Go services (nodelist/astmeta/proxy/svxdr),
deploy/sync scripts and all secrets. Config templates for nginx, PHP-FPM, Redis and tmpfiles
live in install/ with CHANGEME placeholders.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 23:07:05 +02:00

101 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
$j = json_in();
$callsign = base_call(trim((string)($j['callsign'] ?? '')));
$firstname = trim((string)($j['firstname'] ?? ''));
$lastname = trim((string)($j['lastname'] ?? ''));
$email = norm_email((string)($j['email'] ?? ''));
$city = trim((string)($j['city'] ?? ''));
$state = trim((string)($j['state'] ?? ''));
$country = trim((string)($j['country'] ?? ''));
$pass1 = (string)($j['password'] ?? '');
$pass2 = (string)($j['password2'] ?? '');
// Rate limit: 3 registrations per hour per IP
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
rate_limit("register:{$ip}", 3, 3600);
// Validation
if ($callsign === '' || $firstname === '' || $email === '' || $pass1 === '') {
json_out(['ok' => false, 'error' => 'Callsign, first name, email and password are required.'], 400);
}
if (strlen($callsign) < 3 || strlen($callsign) > 10) {
json_out(['ok' => false, 'error' => 'Callsign must be 3-10 characters.'], 400);
}
if (!preg_match('/^[A-Z0-9]+$/', $callsign)) {
json_out(['ok' => false, 'error' => 'Callsign must contain only letters and numbers.'], 400);
}
if ($pass1 !== $pass2) {
json_out(['ok' => false, 'error' => 'Passwords do not match.'], 400);
}
if (strlen($pass1) < 6) {
json_out(['ok' => false, 'error' => 'Password must be at least 6 characters.'], 400);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
json_out(['ok' => false, 'error' => 'Invalid email address.'], 400);
}
// Global callsign ban blocks registration
if (is_call_banned($callsign)) {
json_out(['ok' => false, 'error' => 'This callsign is not permitted to register.'], 403);
}
// Check duplicates
$r = redis();
$existing_cs = $r->get(call_index_key($callsign));
if (is_string($existing_cs) && $existing_cs !== '') {
json_out(['ok' => false, 'error' => 'Callsign or email already registered.'], 409);
}
$existing_em = $r->get(email_index_key($email));
if (is_string($existing_em) && $existing_em !== '') {
json_out(['ok' => false, 'error' => 'Callsign or email already registered.'], 409);
}
// Create account
$uuid = uuidv4();
$now = (string)time();
$r->hMSet(acct_key($uuid), [
'callsign' => $callsign,
'firstname' => $firstname,
'lastname' => $lastname,
'shortname' => $firstname,
'email' => $email,
'city' => $city,
'state' => $state,
'country' => $country,
'password_hash' => password_hash($pass1, PASSWORD_DEFAULT),
'email_verified' => '0',
'acct_flags' => '1',
'created_at' => $now,
]);
// Create indexes
$r->set(call_index_key($callsign), $uuid);
$r->set(email_index_key($email), $uuid);
// Generate email verification token
$tid = token_id();
$secret = token_secret();
$hash = token_hash($secret);
$k = rkey("token:verify:$tid");
$r->hMSet($k, ['uid' => $uuid, 'hash' => $hash]);
$r->expire($k, 60 * 60 * 24);
// Send verification email
$base = rtrim(cfg()['base_url'], '/');
$link = $base . '/auth/verify.php?tid=' . urlencode($tid) . '&token=' . urlencode($secret);
$body = "Hello {$firstname},\n\n";
$body .= "Thank you for registering on SvxPortal with callsign {$callsign}.\n\n";
$body .= "Please verify your email address by clicking the link below:\n\n";
$body .= "{$link}\n\n";
$body .= "This link is valid for 24 hours.\n\n";
$body .= "After email verification, an administrator will review and activate your account.\n\n";
$body .= "73,\nSvxPortal";
$mail_sent = mail_send($email, "Verify your email - SvxPortal", $body);
json_out(['ok' => true, 'callsign' => $callsign, 'email_sent' => $mail_sent]);