feat(names): pre-registration claim system for soft launch
Adds claim/listClaims endpoints so visitors can reserve .lthn names before chain registration is fully automated. Claims are stored with email for notification when approved. Admin endpoint lists all claims. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b4b6d5264
commit
1f31444171
3 changed files with 110 additions and 8 deletions
|
|
@ -535,6 +535,78 @@ class NamesController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/names/claim {"name": "mysite", "email": "user@example.com"}
|
||||
*
|
||||
* Pre-register a name claim. Queued for manual approval during soft launch.
|
||||
*/
|
||||
public function claim(Request $request): JsonResponse
|
||||
{
|
||||
$name = strtolower(trim((string) $request->input('name')));
|
||||
$email = trim((string) $request->input('email'));
|
||||
|
||||
if (! $this->isValidName($name)) {
|
||||
return response()->json(['error' => 'Invalid name. Use 6+ lowercase alphanumeric characters.'], 422);
|
||||
}
|
||||
|
||||
if (strlen($name) < 6) {
|
||||
return response()->json(['error' => 'Name must be at least 6 characters.'], 422);
|
||||
}
|
||||
|
||||
if (empty($email) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return response()->json(['error' => 'Valid email required for claim notification.'], 422);
|
||||
}
|
||||
|
||||
// Check not already registered
|
||||
$alias = $this->rpc->getAliasByName($name);
|
||||
if ($alias !== null) {
|
||||
return response()->json(['error' => 'Name already registered.', 'name' => $name], 409);
|
||||
}
|
||||
|
||||
// Check not already claimed
|
||||
$claims = Cache::get('name_claims', []);
|
||||
foreach ($claims as $claim) {
|
||||
if ($claim['name'] === $name) {
|
||||
return response()->json(['error' => 'Name already claimed. Awaiting approval.', 'name' => $name], 409);
|
||||
}
|
||||
}
|
||||
|
||||
// Store claim
|
||||
$claimId = bin2hex(random_bytes(6));
|
||||
$claims[] = [
|
||||
'id' => $claimId,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'status' => 'pending',
|
||||
'created_at' => now()->toIso8601String(),
|
||||
];
|
||||
Cache::put('name_claims', $claims, 86400 * 30); // 30 day retention
|
||||
|
||||
return response()->json([
|
||||
'claim_id' => $claimId,
|
||||
'name' => $name,
|
||||
'fqdn' => "{$name}.lthn",
|
||||
'status' => 'pending',
|
||||
'message' => 'Your claim has been submitted. We will notify you at ' . $email . ' when approved.',
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/names/claims
|
||||
*
|
||||
* List all pending claims (admin only).
|
||||
*/
|
||||
public function listClaims(): JsonResponse
|
||||
{
|
||||
$claims = Cache::get('name_claims', []);
|
||||
|
||||
return response()->json([
|
||||
'claims' => $claims,
|
||||
'total' => count($claims),
|
||||
'pending' => count(array_filter($claims, fn ($c) => $c['status'] === 'pending')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches daemon's validate_alias_name: a-z, 0-9, dash, dot. Max 255 chars.
|
||||
* We additionally require at least 1 char (daemon allows empty but we don't).
|
||||
|
|
|
|||
|
|
@ -18,3 +18,7 @@ Route::get('/health', [NamesController::class, 'health']);
|
|||
// Sunrise domain verification
|
||||
Route::get('/sunrise/verify/{name}', [NamesController::class, 'sunriseVerify']);
|
||||
Route::get('/sunrise/check/{name}', [NamesController::class, 'sunriseCheck']);
|
||||
|
||||
// Pre-registration claims (soft launch)
|
||||
Route::post('/claim', [NamesController::class, 'claim'])->middleware('throttle:10,1');
|
||||
Route::get('/claims', [NamesController::class, 'listClaims'])->middleware('auth.api');
|
||||
|
|
|
|||
|
|
@ -76,14 +76,40 @@
|
|||
if (data.available && !data.reserved) {
|
||||
var wrap = el('div', {});
|
||||
wrap.appendChild(el('p', {color: '#34d399', fontWeight: '600', fontSize: '1.1rem', marginBottom: '0.75rem'}, name + '.lthn is available!'));
|
||||
var link = el('a', {
|
||||
display: 'inline-block', padding: '0.75rem 2rem',
|
||||
background: '#34d399', color: '#fff', borderRadius: '0.5rem',
|
||||
textDecoration: 'none', fontWeight: '600', fontSize: '1rem'
|
||||
}, 'Register ' + name + '.lthn now');
|
||||
link.href = 'https://order.lthn.ai/order/main/packages/domains/?group_id=1';
|
||||
link.target = '_blank';
|
||||
wrap.appendChild(link);
|
||||
|
||||
var emailInput = el('input', {
|
||||
padding: '0.5rem 1rem', background: '#0a0e17', border: '1px solid #1f2937',
|
||||
borderRadius: '0.5rem', color: '#e5e7eb', fontSize: '0.9rem', width: '100%', marginBottom: '0.5rem'
|
||||
});
|
||||
emailInput.type = 'email';
|
||||
emailInput.placeholder = 'Your email address';
|
||||
emailInput.id = 'claim-email';
|
||||
wrap.appendChild(emailInput);
|
||||
|
||||
var claimBtn = el('button', {
|
||||
padding: '0.75rem 2rem', background: '#34d399', color: '#fff',
|
||||
borderRadius: '0.5rem', border: 'none', fontWeight: '600', fontSize: '1rem', cursor: 'pointer', width: '100%'
|
||||
}, 'Claim ' + name + '.lthn');
|
||||
claimBtn.addEventListener('click', function() {
|
||||
var email = document.getElementById('claim-email').value;
|
||||
if (!email) { return; }
|
||||
claimBtn.disabled = true;
|
||||
claimBtn.textContent = 'Submitting...';
|
||||
fetch('/v1/names/claim', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
|
||||
body: JSON.stringify({name: name, email: email})
|
||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
result.textContent = '';
|
||||
if (d.claim_id) {
|
||||
result.appendChild(el('p', {color: '#34d399', fontWeight: '600', fontSize: '1rem'}, 'Claim submitted!'));
|
||||
result.appendChild(el('p', {color: '#9ca3af', fontSize: '0.85rem'}, 'Claim ID: ' + d.claim_id + '. We will email ' + email + ' when approved.'));
|
||||
} else {
|
||||
result.appendChild(el('p', {color: '#fbbf24', fontSize: '0.9rem'}, d.error || 'Something went wrong.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
wrap.appendChild(claimBtn);
|
||||
result.appendChild(wrap);
|
||||
} else if (data.reserved) {
|
||||
var p = el('p', {color: '#fbbf24', fontSize: '0.9rem'}, name + '.lthn is reserved during the sunrise period. ');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue