likwid/frontend/src/pages/admin/community-creators.astro

272 lines
8 KiB
Text
Raw Normal View History

backend, frontend: add 8 files, rename 4 files, modify 7 files Verified changes: - add backend/.sqlx/query-6c6f48705ccafb301ec03c8a0b719c0f702cb6530e056fd1504227f3aa1fd930.json - rename backend/.sqlx/query-0620f314de8df0c7990ef63fda55f2ff646d5159c59d8288e4ffdfdb07dc159f.json -> backend/.sqlx/query-709e697a9c2e689a114c9bf3f1877bd5ad8b39f1803a0f86a421d467368015bd.json - add backend/.sqlx/query-71eef9a9c46cd41a8272aeaa9d86b032d0607f699a477487a6e4924bd75cef41.json - rename backend/.sqlx/query-593dc329afc129680dd505221df649aa8cb544841fa78a5fe740adfbb4439502.json -> backend/.sqlx/query-85b30d0c2440ee40790c496173ebd1f93f6514df19c9b83969f0d58900f461ec.json - add backend/.sqlx/query-8d5731d3f05af6b7069068752bd7ad6184392718f1da98b793a8ba2dfaf84c2b.json - rename backend/.sqlx/query-786d22de6313d554e95069d020362b4880f40187cb022d94126df9ced0f3f162.json -> backend/.sqlx/query-8f7e57bc074a8a9c80e20ae51d497582ed79f8dc5fd135ea7117bfc3d1d33c11.json - rename backend/.sqlx/query-8dd178663df95d64d72c776e3b8bda63851d2ad0e13a6d80b327610078ecbaeb.json -> backend/.sqlx/query-a1e179a81e94c91a7ad6201cea880a8e133766adfa97bad4bb22936317ddcd6c.json - add backend/.sqlx/query-cf573b897b379059a8a14132fdedb844e5210ef3ad0205ae2515ab7e4589240c.json - add backend/migrations/20260221160000_instance_type_and_community_creator_role.sql - add backend/migrations/20260221163000_user_roles_unique_platform.sql - modify backend/src/api/roles.rs - modify backend/src/api/settings.rs - modify backend/src/api/users.rs - modify frontend/src/components/AdminNav.astro - modify frontend/src/layouts/Layout.astro - add frontend/src/pages/admin/community-creators.astro - add frontend/src/pages/admin/index.astro - modify frontend/src/pages/communities/new.astro - modify frontend/src/pages/setup.astro Diffstat: - 19 files changed, 828 insertions(+), 45 deletions(-)
2026-02-21 15:52:50 +00:00
---
export const prerender = false;
import Layout from '../../layouts/Layout.astro';
import AdminNav from '../../components/AdminNav.astro';
import { API_BASE as apiBase } from '../../lib/api';
---
<Layout title="Admin - Community Creators">
<div class="admin-container">
<AdminNav currentPage="/admin/community-creators" />
<main class="admin-content">
<header class="ui-page-header">
<div class="ui-page-title">
<h1>Community Creators</h1>
<p class="ui-subtitle">Grant who can create communities when the platform is restricted</p>
</div>
</header>
<div id="loading" class="loading">Loading...</div>
<div id="error" class="error-box" style="display: none;"></div>
<div id="success" class="success-box" style="display: none;"></div>
<section class="ui-card ui-card-soft ui-card-pad-md" style="margin-bottom: 1.25rem;">
<h2 style="margin-top: 0;">Assign creator role</h2>
<form id="assign-form" class="ui-form" style="--ui-form-control-max-width: 420px; --ui-form-group-mb: 0.75rem;">
<div class="form-group">
<label for="user_id">User</label>
<select id="user_id" name="user_id" required></select>
<small class="help-text">Select a user to grant the Community Creator platform role.</small>
</div>
<div class="form-actions">
<button class="ui-btn ui-btn-primary" type="submit">Grant role</button>
</div>
</form>
</section>
<section class="ui-card ui-card-soft ui-card-pad-md">
<h2 style="margin-top: 0;">Current community creators</h2>
<div id="creators" class="list">
<p class="loading-small">Loading...</p>
</div>
</section>
</main>
</div>
</Layout>
<style>
.admin-container {
display: flex;
min-height: calc(100vh - 60px);
}
.admin-content {
flex: 1;
padding: 2rem;
max-width: 1000px;
}
.loading {
color: var(--color-text-muted);
padding: 2rem;
text-align: center;
}
.error-box {
background: color-mix(in srgb, var(--color-error) 18%, transparent);
color: var(--color-error);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.success-box {
background: color-mix(in srgb, var(--color-success) 18%, transparent);
color: var(--color-success);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.list {
display: grid;
gap: 0.75rem;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.meta {
color: var(--color-text-muted);
font-size: 0.875rem;
}
</style>
<script define:vars={{ apiBase }}>
(function() {
const token = localStorage.getItem('token');
if (!token) {
window.location.href = '/login?redirect=/admin/community-creators';
return;
}
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error');
const successEl = document.getElementById('success');
const creatorsEl = document.getElementById('creators');
const assignForm = document.getElementById('assign-form');
const userSelect = document.getElementById('user_id');
let creatorRoleId = null;
function showError(msg) {
if (!errorEl) return;
errorEl.textContent = msg;
errorEl.style.display = 'block';
}
function showSuccess(msg) {
if (!successEl) return;
successEl.textContent = msg;
successEl.style.display = 'block';
setTimeout(() => {
successEl.style.display = 'none';
successEl.textContent = '';
}, 2500);
}
function clearError() {
if (!errorEl) return;
errorEl.style.display = 'none';
errorEl.textContent = '';
}
function escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, function(ch) {
switch (ch) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case "'": return '&#39;';
default: return ch;
}
});
}
async function fetchJson(url, init) {
const res = await fetch(url, init);
if (res.status === 401) {
window.location.href = '/login?redirect=/admin/community-creators';
return null;
}
const text = await res.text();
let json = null;
try { json = text ? JSON.parse(text) : null; } catch (e) {}
if (!res.ok) {
throw new Error((json && json.error) || text || 'Request failed');
}
return json;
}
async function loadRoles() {
const roles = await fetchJson(`${apiBase}/api/roles`, {
headers: { 'Authorization': `Bearer ${token}` },
});
const found = (roles || []).find(r => r && r.name === 'community_creator');
if (!found) {
throw new Error('community_creator role not found (migration not applied yet?)');
}
creatorRoleId = found.id;
}
async function loadUsers() {
const users = await fetchJson(`${apiBase}/api/users`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!userSelect) return;
userSelect.innerHTML = (users || []).map(u => {
const label = (u.display_name ? `${u.display_name} (@${u.username})` : `@${u.username}`);
return `<option value="${escapeHtml(u.id)}">${escapeHtml(label)}</option>`;
}).join('');
}
async function loadCreators() {
if (!creatorRoleId) return;
const list = await fetchJson(`${apiBase}/api/roles/${creatorRoleId}/users`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!creatorsEl) return;
if (!list || list.length === 0) {
creatorsEl.innerHTML = '<p class="meta">No community creators assigned yet.</p>';
return;
}
creatorsEl.innerHTML = list.map(u => {
const label = (u.display_name ? `${u.display_name} (@${u.username})` : `@${u.username}`);
return `
<div class="row ui-card ui-card-pad-md ui-card-soft">
<div>
<div><strong>${escapeHtml(label)}</strong></div>
<div class="meta">User id: ${escapeHtml(u.id)}</div>
</div>
<button class="ui-btn ui-btn-danger js-remove" type="button" data-user-id="${escapeHtml(u.id)}">Remove</button>
</div>
`;
}).join('');
creatorsEl.querySelectorAll('.js-remove').forEach(btn => {
btn.addEventListener('click', async () => {
const userId = btn.getAttribute('data-user-id');
if (!userId) return;
try {
clearError();
await fetchJson(`${apiBase}/api/roles/${creatorRoleId}/users/${userId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` },
});
showSuccess('Role removed.');
await loadCreators();
} catch (e) {
showError(e && e.message ? e.message : String(e));
}
});
});
}
assignForm?.addEventListener('submit', async (e) => {
e.preventDefault();
if (!creatorRoleId) return;
if (!(userSelect instanceof HTMLSelectElement)) return;
try {
clearError();
await fetchJson(`${apiBase}/api/roles/${creatorRoleId}/assign`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ user_id: userSelect.value, expires_at: null }),
});
showSuccess('Role granted.');
await loadCreators();
} catch (e) {
showError(e && e.message ? e.message : String(e));
}
});
(async function init() {
try {
await loadRoles();
await loadUsers();
await loadCreators();
if (loadingEl) loadingEl.style.display = 'none';
} catch (e) {
showError(e && e.message ? e.message : String(e));
if (loadingEl) loadingEl.style.display = 'none';
}
})();
})();
</script>