feat: Add invite token-based user registration and an admin interface for token management.

This commit is contained in:
x
2026-01-24 16:01:40 +01:00
parent 1b1867332b
commit 16da3ac9d0
8 changed files with 298 additions and 5 deletions

View File

@@ -16,6 +16,7 @@
<!-- <li><a href="/admin/log">Logs</a></li> -->
<li><a href="/admin/approve">Approval Queue</a></li>
<li><a href="/admin/sessions">Sessions</a></li>
<li><a href="/admin/tokens">Invite Tokens</a></li>
</ul>
</div>
</div>

89
views/admin/tokens.html Normal file
View File

@@ -0,0 +1,89 @@
@include(snippets/header)
<div class="container" style="padding-top: 20px;">
<h2>Invite Tokens</h2>
<div style="margin-bottom: 20px; text-align: right;">
<button id="generate-token" class="btn-upload" style="width: auto; padding: 10px 20px;">Generate New
Token</button>
</div>
<div class="upload-form" style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; color: var(--white);">
<thead>
<tr style="border-bottom: 1px solid var(--nav-border-color); text-align: left;">
<th style="padding: 10px;">Token</th>
<th style="padding: 10px;">Status</th>
<th style="padding: 10px;">Used By</th>
<th style="padding: 10px;">Created</th>
<th style="padding: 10px;">Actions</th>
</tr>
</thead>
<tbody id="token-list">
<!-- Populated by JS -->
</tbody>
</table>
</div>
</div>
<script>
const loadTokens = async () => {
try {
console.log('Loading tokens...');
const res = await fetch('/api/v2/admin/tokens');
const data = await res.json();
console.log('Tokens data:', data);
if (data.success) {
const tbody = document.getElementById('token-list');
tbody.innerHTML = data.tokens.map(t =>
'<tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">' +
'<td style="padding: 10px; font-family: monospace; font-size: 1.1em; color: var(--accent);">' + t.token + '</td>' +
'<td style="padding: 10px;">' +
(t.is_used ? '<span style="color: #ff6b6b">Used</span>' : '<span style="color: #51cf66">Available</span>') +
'</td>' +
'<td style="padding: 10px;">' + (t.used_by_name || '-') + '</td>' +
'<td style="padding: 10px;">' + new Date(parseInt(t.created_at) * 1000).toLocaleString() + '</td>' +
'<td style="padding: 10px;">' +
(!t.is_used ? '<button onclick="deleteToken(' + t.id + ')" class="btn-remove" style="padding: 5px 10px; font-size: 0.8em;">Delete</button>' : '') +
'</td>' +
'</tr>'
).join('');
}
} catch (e) { console.error(e); }
};
const generateToken = async () => {
console.log('Generating...');
try {
const res = await fetch('/api/v2/admin/tokens/create', { method: 'POST' });
const data = await res.json();
console.log('Gen result:', data);
if (data.success) {
loadTokens();
} else {
alert('Failed: ' + data.msg);
}
} catch (e) {
console.error(e);
alert('Error: ' + e.message);
}
};
const deleteToken = async (id) => {
if (!confirm('Delete this token?')) return;
const res = await fetch('/api/v2/admin/tokens/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const data = await res.json();
if (data.success) {
loadTokens();
}
};
document.getElementById('generate-token').addEventListener('click', generateToken);
loadTokens();
</script>
@include(snippets/footer)

28
views/register.html Normal file
View File

@@ -0,0 +1,28 @@
<!doctype f0ck>
<html theme="amoled">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>register</title>
<link href="/s/css/f0ck.css" rel="stylesheet" />
</head>
<body type="login">
<form class="login-form" method="post" action="/register">
<h2 style="text-align: center; margin-bottom: 20px;">Register</h2>
@if(typeof error !== 'undefined')
<div style="color: #ff6b6b; margin-bottom: 10px; text-align: center;">{{ error }}</div>
@endif
<input type="text" name="username" placeholder="username" autocomplete="off" required />
<input type="password" name="password" placeholder="password" autocomplete="off" required />
<input type="password" name="password_confirm" placeholder="confirm password" autocomplete="off" required />
<input type="text" name="token" placeholder="invite token" autocomplete="off" required />
<button type="submit">Create Account</button>
<div style="margin-top: 15px; text-align: center;">
<a href="/login" style="color: var(--accent); text-decoration: none;">Back to Login</a>
</div>
</form>
</body>
</html>

View File

@@ -56,10 +56,11 @@
<div class="nav-left-group">
<div class="nav-user-dropdown">
<button class="nav-user-btn" id="nav-visitor-toggle">
Not logged in
guest
</button>
<div class="nav-user-menu" id="nav-visitor-menu">
<a href="#" id="nav-login-btn">Login</a>
<a href="#" id="nav-register-btn">Register</a>
<div class="nav-user-divider"></div>
<a href="/about">about</a>
</div>
@@ -109,4 +110,19 @@
<button type="submit">Login</button>
</form>
</div>
</div>
<!-- Register Modal -->
<div id="register-modal" style="display: none;">
<div class="login-modal-content">
<button id="register-modal-close">&times;</button>
<form class="login-form" method="post" action="/register">
<h2 style="text-align: center; margin-bottom: 20px;">Register</h2>
<input type="text" name="username" placeholder="username" autocomplete="off" required />
<input type="password" name="password" placeholder="password" autocomplete="off" required />
<input type="password" name="password_confirm" placeholder="confirm password" autocomplete="off" required />
<input type="text" name="token" placeholder="invite token" autocomplete="off" required />
<button type="submit">Create Account</button>
</form>
</div>
</div>