mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
fix: let user add ical
This commit is contained in:
parent
6de6cfacf2
commit
23fc039ac3
6 changed files with 257 additions and 19 deletions
|
|
@ -19,10 +19,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:0.5rem;">
|
<div style="display:flex; gap:0.5rem;">
|
||||||
<button type="button" class="secondary nav-toggle calendar-filters-toggle" id="btn-filters-toggle">Filtres</button>
|
<button type="button" class="secondary nav-toggle calendar-filters-toggle" id="btn-filters-toggle">Filtres</button>
|
||||||
|
<button type="button" class="secondary" id="btn-my-calendars">Mes calendriers</button>
|
||||||
<button type="button" id="btn-refresh">Actualiser les disponibilités</button>
|
<button type="button" id="btn-refresh">Actualiser les disponibilités</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="error"></div>
|
<div id="error"></div>
|
||||||
|
<div class="panel my-feed-banner" id="my-feed-banner" style="display:none">
|
||||||
|
<p class="empty">Vous n'avez pas encore ajouté de calendrier — ajoutez-en un pour apparaître dans les disponibilités.</p>
|
||||||
|
<button type="button" id="btn-my-feed-banner-add">Ajouter mon calendrier</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="calendar-layout">
|
<div class="calendar-layout">
|
||||||
<div class="calendar-filters-backdrop" id="filters-backdrop"></div>
|
<div class="calendar-filters-backdrop" id="filters-backdrop"></div>
|
||||||
|
|
@ -63,6 +68,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="my-calendars-modal-overlay" class="modal-overlay hidden">
|
||||||
|
<div class="modal card modal-wide">
|
||||||
|
<button type="button" class="icon-btn secondary modal-close" id="my-calendars-modal-close">×</button>
|
||||||
|
<h2 style="margin-top:0">Mes calendriers</h2>
|
||||||
|
<p class="note">
|
||||||
|
Ajoutez le lien ICS (iCal) d'un ou plusieurs de vos calendriers pour apparaître dans les disponibilités.
|
||||||
|
L'application ne conserve jamais leur contenu — seul un statut disponible/occupé par créneau est déduit.
|
||||||
|
</p>
|
||||||
|
<div id="my-calendars-list"><p class="empty">Chargement…</p></div>
|
||||||
|
<form id="my-calendars-add-form" class="inline-form">
|
||||||
|
<input name="label" placeholder="Libellé (optionnel)">
|
||||||
|
<input name="icsUrl" type="url" placeholder="URL du calendrier (.ics)" required>
|
||||||
|
<button type="submit" class="secondary icon-btn">+ Ajouter</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
<script src="/js/render.js"></script>
|
<script src="/js/render.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ function renderCalendar() {
|
||||||
|
|
||||||
function buildFilters() {
|
function buildFilters() {
|
||||||
const listEl = document.getElementById('people-list');
|
const listEl = document.getElementById('people-list');
|
||||||
|
listEl.innerHTML = '';
|
||||||
for (const person of state.people) {
|
for (const person of state.people) {
|
||||||
const row = document.createElement('label');
|
const row = document.createElement('label');
|
||||||
row.className = 'person-toggle';
|
row.className = 'person-toggle';
|
||||||
|
|
@ -172,24 +173,6 @@ function buildFilters() {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildLegend();
|
buildLegend();
|
||||||
|
|
||||||
document.getElementById('btn-refresh').addEventListener('click', async () => {
|
|
||||||
const btn = document.getElementById('btn-refresh');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Actualisation…';
|
|
||||||
showToast('Synchronisation des calendriers…');
|
|
||||||
try {
|
|
||||||
await api.post('/api/calendar/refresh', {});
|
|
||||||
showToast('Synchronisation en cours — en attente des résultats…');
|
|
||||||
pollWorkflowStatus(btn);
|
|
||||||
} catch (err) {
|
|
||||||
const message = err.message === 'no_feeds_configured'
|
|
||||||
? "Aucun calendrier n'est configuré — voir la page Administration."
|
|
||||||
: err.message;
|
|
||||||
showToast(message, true);
|
|
||||||
resetRefreshButton(btn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetRefreshButton(btn) {
|
function resetRefreshButton(btn) {
|
||||||
|
|
@ -278,6 +261,7 @@ function closeFilters() {
|
||||||
|
|
||||||
function buildLegend() {
|
function buildLegend() {
|
||||||
const el = document.getElementById('legend');
|
const el = document.getElementById('legend');
|
||||||
|
el.innerHTML = '';
|
||||||
for (const person of state.people) {
|
for (const person of state.people) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'legend-item';
|
item.className = 'legend-item';
|
||||||
|
|
@ -307,6 +291,87 @@ function showError(message) {
|
||||||
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Mes calendriers (self-service ICS feeds) ----------
|
||||||
|
|
||||||
|
let myFeeds = [];
|
||||||
|
|
||||||
|
// Shown for a registered feed instead of its full URL — consistent with the
|
||||||
|
// admin/profile calendar-management panels.
|
||||||
|
function maskIcsUrl(url) {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
return `${u.hostname}/••••`;
|
||||||
|
} catch {
|
||||||
|
return '••••';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function myFeedRowTemplate(feed) {
|
||||||
|
return `
|
||||||
|
<div class="feed-row" data-feed-id="${feed.id}">
|
||||||
|
<span class="feed-label">${feed.label ? `${escapeHtml(feed.label)} — ` : ''}${escapeHtml(maskIcsUrl(feed.icsUrl))}</span>
|
||||||
|
<button type="button" class="secondary icon-btn remove-my-feed-btn" data-feed-id="${feed.id}">Supprimer</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMyFeeds() {
|
||||||
|
myFeeds = await api.get('/api/calendar/my-feeds');
|
||||||
|
renderMyFeedBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMyFeedBanner() {
|
||||||
|
document.getElementById('my-feed-banner').style.display = myFeeds.length ? 'none' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMyCalendarsModal() {
|
||||||
|
const container = document.getElementById('my-calendars-list');
|
||||||
|
container.innerHTML = myFeeds.length
|
||||||
|
? myFeeds.map(myFeedRowTemplate).join('')
|
||||||
|
: '<p class="empty">Aucun calendrier configuré.</p>';
|
||||||
|
container.querySelectorAll('.remove-my-feed-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => onRemoveMyFeed(parseInt(btn.dataset.feedId, 10)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMyCalendarsModal() {
|
||||||
|
renderMyCalendarsModal();
|
||||||
|
document.getElementById('my-calendars-modal-overlay').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMyCalendarsModal() {
|
||||||
|
document.getElementById('my-calendars-modal-overlay').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAddMyFeed(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const label = form.label.value.trim();
|
||||||
|
const icsUrl = form.icsUrl.value.trim();
|
||||||
|
try {
|
||||||
|
await api.post('/api/calendar/my-feeds', { label, icsUrl });
|
||||||
|
form.reset();
|
||||||
|
await loadMyFeeds();
|
||||||
|
renderMyCalendarsModal();
|
||||||
|
await loadPeople();
|
||||||
|
buildFilters();
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemoveMyFeed(feedId) {
|
||||||
|
try {
|
||||||
|
await api.del(`/api/calendar/my-feeds/${feedId}`);
|
||||||
|
await loadMyFeeds();
|
||||||
|
renderMyCalendarsModal();
|
||||||
|
await loadPeople();
|
||||||
|
buildFilters();
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(async function init() {
|
(async function init() {
|
||||||
await initNav('calendar');
|
await initNav('calendar');
|
||||||
|
|
||||||
|
|
@ -314,22 +379,49 @@ function showError(message) {
|
||||||
document.getElementById('modal-overlay').addEventListener('click', (e) => {
|
document.getElementById('modal-overlay').addEventListener('click', (e) => {
|
||||||
if (e.target === e.currentTarget) closeModal();
|
if (e.target === e.currentTarget) closeModal();
|
||||||
});
|
});
|
||||||
|
document.getElementById('my-calendars-modal-close').addEventListener('click', closeMyCalendarsModal);
|
||||||
|
document.getElementById('my-calendars-modal-overlay').addEventListener('click', (e) => {
|
||||||
|
if (e.target === e.currentTarget) closeMyCalendarsModal();
|
||||||
|
});
|
||||||
|
document.getElementById('my-calendars-add-form').addEventListener('submit', onAddMyFeed);
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
closeModal();
|
closeModal();
|
||||||
closeFilters();
|
closeFilters();
|
||||||
|
closeMyCalendarsModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-filters-toggle').addEventListener('click', openFilters);
|
document.getElementById('btn-filters-toggle').addEventListener('click', openFilters);
|
||||||
document.getElementById('btn-filters-close').addEventListener('click', closeFilters);
|
document.getElementById('btn-filters-close').addEventListener('click', closeFilters);
|
||||||
document.getElementById('filters-backdrop').addEventListener('click', closeFilters);
|
document.getElementById('filters-backdrop').addEventListener('click', closeFilters);
|
||||||
|
document.getElementById('btn-my-calendars').addEventListener('click', openMyCalendarsModal);
|
||||||
|
document.getElementById('btn-my-feed-banner-add').addEventListener('click', openMyCalendarsModal);
|
||||||
|
|
||||||
|
document.getElementById('btn-refresh').addEventListener('click', async () => {
|
||||||
|
const btn = document.getElementById('btn-refresh');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Actualisation…';
|
||||||
|
showToast('Synchronisation des calendriers…');
|
||||||
|
try {
|
||||||
|
await api.post('/api/calendar/refresh', {});
|
||||||
|
showToast('Synchronisation en cours — en attente des résultats…');
|
||||||
|
pollWorkflowStatus(btn);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err.message === 'no_feeds_configured'
|
||||||
|
? "Aucun calendrier n'est configuré — voir la page Administration."
|
||||||
|
: err.message;
|
||||||
|
showToast(message, true);
|
||||||
|
resetRefreshButton(btn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadPeople();
|
await loadPeople();
|
||||||
buildFilters();
|
buildFilters();
|
||||||
await loadSlots();
|
await loadSlots();
|
||||||
await loadLastChecked();
|
await loadLastChecked();
|
||||||
|
await loadMyFeeds();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err.message);
|
showError(err.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ async function initNav(activePage) {
|
||||||
<a href="/setlist.html" class="${activePage === 'setlist' ? 'active' : ''}">Prochain concert</a>
|
<a href="/setlist.html" class="${activePage === 'setlist' ? 'active' : ''}">Prochain concert</a>
|
||||||
<a href="/calendar.html" class="${activePage === 'calendar' ? 'active' : ''}">Disponibilités</a>
|
<a href="/calendar.html" class="${activePage === 'calendar' ? 'active' : ''}">Disponibilités</a>
|
||||||
<a href="/history.html" class="${activePage === 'history' ? 'active' : ''}">Historique</a>
|
<a href="/history.html" class="${activePage === 'history' ? 'active' : ''}">Historique</a>
|
||||||
${me.isAdmin ? `<a href="/admin.html" class="${activePage === 'admin' ? 'active' : ''}">Administration</a>` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-user">
|
<div class="nav-user">
|
||||||
<div class="nav-profile" id="nav-profile">
|
<div class="nav-profile" id="nav-profile">
|
||||||
|
|
@ -41,6 +40,7 @@ async function initNav(activePage) {
|
||||||
</button>
|
</button>
|
||||||
<div class="nav-profile-dropdown" id="nav-profile-dropdown">
|
<div class="nav-profile-dropdown" id="nav-profile-dropdown">
|
||||||
<a href="/profile.html">Voir profil</a>
|
<a href="/profile.html">Voir profil</a>
|
||||||
|
${me.isAdmin ? '<a href="/admin.html">Administration</a>' : ''}
|
||||||
<a href="/auth/logout">Se déconnecter</a>
|
<a href="/auth/logout">Se déconnecter</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,62 @@ function statTile(value, label) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shown for a registered feed instead of its full URL, consistent with the
|
||||||
|
// admin calendar-management panel — the URL itself is a secret (whoever has
|
||||||
|
// it can read that person's calendar), so the list view doesn't re-display
|
||||||
|
// the plaintext on every load.
|
||||||
|
function maskIcsUrl(url) {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
return `${u.hostname}/••••`;
|
||||||
|
} catch {
|
||||||
|
return '••••';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function myFeedRowTemplate(feed) {
|
||||||
|
return `
|
||||||
|
<div class="feed-row" data-feed-id="${feed.id}">
|
||||||
|
<span class="feed-label">${feed.label ? `${escapeHtml(feed.label)} — ` : ''}${escapeHtml(maskIcsUrl(feed.icsUrl))}</span>
|
||||||
|
<button type="button" class="secondary icon-btn remove-my-feed-btn" data-feed-id="${feed.id}">Supprimer</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMyFeeds() {
|
||||||
|
const feeds = await api.get('/api/calendar/my-feeds');
|
||||||
|
const container = document.getElementById('my-feeds-list');
|
||||||
|
container.innerHTML = feeds.length
|
||||||
|
? feeds.map(myFeedRowTemplate).join('')
|
||||||
|
: '<p class="empty">Aucun calendrier configuré.</p>';
|
||||||
|
container.querySelectorAll('.remove-my-feed-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => onRemoveMyFeed(parseInt(btn.dataset.feedId, 10)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAddMyFeed(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const label = form.label.value.trim();
|
||||||
|
const icsUrl = form.icsUrl.value.trim();
|
||||||
|
try {
|
||||||
|
await api.post('/api/calendar/my-feeds', { label, icsUrl });
|
||||||
|
form.reset();
|
||||||
|
await loadMyFeeds();
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemoveMyFeed(feedId) {
|
||||||
|
try {
|
||||||
|
await api.del(`/api/calendar/my-feeds/${feedId}`);
|
||||||
|
await loadMyFeeds();
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(async function init() {
|
(async function init() {
|
||||||
await initNav(null);
|
await initNav(null);
|
||||||
try {
|
try {
|
||||||
|
|
@ -45,6 +101,22 @@ function statTile(value, label) {
|
||||||
${statTile(profile.stats.votesCast, 'Votes exprimés')}
|
${statTile(profile.stats.votesCast, 'Votes exprimés')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2>Mes calendriers</h2>
|
||||||
|
<p class="note">
|
||||||
|
Ajoutez le lien ICS (iCal) d'un ou plusieurs de vos calendriers (Google, Outlook, Apple...) pour
|
||||||
|
apparaître dans les disponibilités du groupe sur <a href="/calendar.html">la page Disponibilités</a>.
|
||||||
|
L'application ne conserve jamais le contenu de vos calendriers — seul un statut disponible/occupé
|
||||||
|
par créneau est déduit et enregistré.
|
||||||
|
</p>
|
||||||
|
<div class="panel">
|
||||||
|
<div id="my-feeds-list"><p class="empty">Chargement…</p></div>
|
||||||
|
<form id="add-my-feed-form" class="inline-form">
|
||||||
|
<input name="label" placeholder="Libellé (optionnel)">
|
||||||
|
<input name="icsUrl" type="url" placeholder="URL du calendrier (.ics)" required>
|
||||||
|
<button type="submit" class="secondary icon-btn">+ Ajouter</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<p class="empty">
|
<p class="empty">
|
||||||
Identité gérée par Authentik — pour changer votre nom, email ou mot de passe,
|
Identité gérée par Authentik — pour changer votre nom, email ou mot de passe,
|
||||||
|
|
@ -55,6 +127,8 @@ function statTile(value, label) {
|
||||||
<a href="/auth/logout"><button type="button" class="danger">Se déconnecter</button></a>
|
<a href="/auth/logout"><button type="button" class="danger">Se déconnecter</button></a>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
document.getElementById('add-my-feed-form').addEventListener('submit', onAddMyFeed);
|
||||||
|
await loadMyFeeds();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err.message);
|
showError(err.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,18 @@ async function removeFeed(feedId) {
|
||||||
await pool.query('DELETE FROM calendar_feeds WHERE id = $1', [feedId]);
|
await pool.query('DELETE FROM calendar_feeds WHERE id = $1', [feedId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scoped delete for the self-service "my calendars" endpoints — a user must
|
||||||
|
// only ever be able to delete their own feeds, never guess another user's
|
||||||
|
// feed id. Returns whether a row actually matched (both wrong id and
|
||||||
|
// someone-else's feed look identical from the caller's side: nothing deleted).
|
||||||
|
async function removeFeedForUser(feedId, userId) {
|
||||||
|
const { rowCount } = await pool.query('DELETE FROM calendar_feeds WHERE id = $1 AND user_id = $2', [
|
||||||
|
feedId,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
return rowCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Every app user (not just ones already on the calendar) with their
|
// Every app user (not just ones already on the calendar) with their
|
||||||
// registered feeds attached — lets an admin give someone their first feed,
|
// registered feeds attached — lets an admin give someone their first feed,
|
||||||
// not just manage existing entries. The public getPeople() above deliberately
|
// not just manage existing entries. The public getPeople() above deliberately
|
||||||
|
|
@ -227,6 +239,7 @@ module.exports = {
|
||||||
findFeedsForUser,
|
findFeedsForUser,
|
||||||
addFeed,
|
addFeed,
|
||||||
removeFeed,
|
removeFeed,
|
||||||
|
removeFeedForUser,
|
||||||
getUsersWithFeeds,
|
getUsersWithFeeds,
|
||||||
getSlotSettings,
|
getSlotSettings,
|
||||||
updateSlotSettings,
|
updateSlotSettings,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,43 @@ router.post(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Self-service: any authenticated user manages their own calendar feeds here
|
||||||
|
// (no requireAdmin) — distinct from the /people/:id/feeds admin routes below,
|
||||||
|
// which let an admin manage anyone's feeds on their behalf.
|
||||||
|
router.get(
|
||||||
|
'/my-feeds',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const feeds = await calendarRepo.findFeedsForUser(req.user.id);
|
||||||
|
res.json(feeds.map((f) => ({ id: f.id, label: f.label, icsUrl: f.ics_url })));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/my-feeds',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { label, icsUrl } = req.body || {};
|
||||||
|
if (!icsUrl || !icsUrl.trim()) {
|
||||||
|
return res.status(400).json({ error: 'ics_url_required' });
|
||||||
|
}
|
||||||
|
const feed = await calendarRepo.addFeed(req.user.id, {
|
||||||
|
label: label ? label.trim() : null,
|
||||||
|
icsUrl: icsUrl.trim(),
|
||||||
|
});
|
||||||
|
res.status(201).json({ id: feed.id, label: feed.label, icsUrl: feed.ics_url });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/my-feeds/:feedId',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const deleted = await calendarRepo.removeFeedForUser(parseInt(req.params.feedId, 10), req.user.id);
|
||||||
|
if (!deleted) {
|
||||||
|
return res.status(404).json({ error: 'feed_not_found' });
|
||||||
|
}
|
||||||
|
res.status(204).end();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/people/admin',
|
'/people/admin',
|
||||||
requireAdmin,
|
requireAdmin,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue