source: Klonkt/src/routes/admin-circle.js@ d41f4be

main
Last change on this file since d41f4be was 2520dcc, checked in by roboburr <roboburr@…>, 3 months ago

feat(cirkel): add site without typing https://

A bare domain (artiest.klonkt.com) is automatically prefixed with https://; a
typed http:// is upgraded to https:// (federation is intentionally https-only).
Field is now type=text (type=url blocked a bare domain before submission) with
inputmode=url + placeholder without a scheme.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 6.1 KB
RevLine 
[0091cb7]1/**
2 * Admin: Cirkel-beheer (god-only).
3 * GET /admin/circle -> lijst van cirkel-links + status
4 * POST /admin/circle/add -> Klonkt-URL toevoegen
5 * POST /admin/circle/:id/remove
6 * POST /admin/circle/:id/sync -> nu verversen (pull + verifieer)
7 * POST /admin/circle/allow -> toggle "mag in cirkels van anderen verschijnen"
8 *
9 * Zie docs/cirkels-v1-spec.md §5d.
10 */
11
12import express from 'express';
13import crypto from 'crypto';
14import { renderPage } from '../middleware/render.js';
15import { requireGod } from '../middleware/auth.js';
16import db from '../config/database.js';
17import { getTenancy } from '../services/SettingsService.js';
[62b899d]18import { syncOne, sync } from '../services/CircleService.js';
[0091cb7]19
20const router = express.Router();
21
22function primarySite() {
23 return db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get();
24}
25
26router.get('/', requireGod, (req, res) => {
27 const site = primarySite();
28 const links = site
29 ? db.prepare('SELECT * FROM circle_links WHERE local_site_id = ? ORDER BY added_at DESC').all(site.id)
30 : [];
31 const counts = {};
32 for (const l of links) {
33 counts[l.id] = l.remote_actor_id
34 ? db.prepare('SELECT COUNT(*) AS n FROM remote_posts WHERE actor_id = ?').get(l.remote_actor_id).n
35 : 0;
36 }
37 renderPage(req, res, 'pages/admin-circle', {
38 pageTitle: 'Cirkel',
39 bodyClass: 'on-admin',
40 tenancy: getTenancy(),
41 site,
42 links,
43 counts,
44 allowCircle: site ? site.allow_circle !== 0 : true,
45 success: req.query.success || null,
46 error: req.query.error || null,
47 });
48});
49
[dfc5379]50router.post('/add', requireGod, async (req, res) => {
[0091cb7]51 const site = primarySite();
52 if (!site) return res.redirect('/admin/circle?error=' + encodeURIComponent('Geen site gevonden'));
[2520dcc]53 // Schema automatisch aanvullen: een kale domeinnaam → https://, een getypte
54 // http:// → https:// (federatie is bewust https-only, getekende feeds). Zo hoeft
55 // de gebruiker nooit zelf http(s):// te typen.
56 let url = (req.body.remote_url || '').toString().trim().replace(/\/+$/, '');
57 if (url && !/^[a-z]+:\/\//i.test(url)) url = 'https://' + url;
58 url = url.replace(/^http:\/\//i, 'https://');
[0091cb7]59 if (!/^https:\/\/[^\s/]+(\/[^\s]*)?$/i.test(url)) {
[2520dcc]60 return res.redirect('/admin/circle?error=' + encodeURIComponent('Voer een geldig site-adres in (bv. voorbeeld.nl)'));
[0091cb7]61 }
62 const label = (req.body.label || '').toString().slice(0, 80).trim() || null;
[dfc5379]63 const id = crypto.randomUUID();
[0091cb7]64 try {
65 db.prepare("INSERT INTO circle_links (id, local_site_id, remote_url, label, status) VALUES (?, ?, ?, ?, 'active')")
[dfc5379]66 .run(id, site.id, url, label);
[0091cb7]67 } catch (e) {
68 return res.redirect('/admin/circle?error=' + encodeURIComponent('Deze site staat al in je cirkel'));
69 }
[dfc5379]70 // Meteen ophalen i.p.v. wachten op de 15-min-loop.
71 try {
72 const link = db.prepare('SELECT * FROM circle_links WHERE id = ?').get(id);
73 await syncOne(link);
74 return res.redirect('/admin/circle?success=' + encodeURIComponent('Toegevoegd en gesynchroniseerd ✓'));
75 } catch (e) {
[2fc9b7f]76 const msg = String((e && e.message) || e);
77 // 404 = geen cirkel-endpoint. Hubs federeren bewust NIET (hun /.klonkt/actor.json
78 // geeft 404), net als losse niet-Klonkt-sites. Niet toevoegen: rol de insert terug
79 // zodat er geen dode "fout"-rij in de cirkel blijft staan.
80 if (/\b404\b/.test(msg)) {
81 db.prepare('DELETE FROM circle_links WHERE id = ?').run(id);
82 return res.redirect('/admin/circle?error=' + encodeURIComponent('Niet toegevoegd: deze site doet niet mee aan cirkels. Een hub kan geen cirkel-partner zijn (en losse/niet-Klonkt-sites ook niet).'));
83 }
84 // Andere (mogelijk tijdelijke) fout → link blijft staan; later "Verversen".
[dfc5379]85 return res.redirect('/admin/circle?success=' + encodeURIComponent('Toegevoegd — synchroniseren mislukte (klik "Verversen" om opnieuw te proberen)'));
86 }
[0091cb7]87});
88
89router.post('/:id/remove', requireGod, (req, res) => {
90 const link = db.prepare('SELECT * FROM circle_links WHERE id = ?').get(req.params.id);
91 if (link) {
92 db.prepare('DELETE FROM circle_links WHERE id = ?').run(link.id);
93 // Gecachte content opruimen als geen andere link nog naar deze actor wijst.
94 if (link.remote_actor_id) {
95 const other = db.prepare('SELECT 1 FROM circle_links WHERE remote_actor_id = ? LIMIT 1').get(link.remote_actor_id);
96 if (!other) {
97 db.prepare('DELETE FROM remote_posts WHERE actor_id = ?').run(link.remote_actor_id);
98 db.prepare('DELETE FROM remote_actors WHERE id = ?').run(link.remote_actor_id);
99 }
100 }
101 }
102 res.redirect('/admin/circle?success=' + encodeURIComponent('Verwijderd'));
103});
104
105router.post('/:id/sync', requireGod, async (req, res) => {
106 const link = db.prepare('SELECT * FROM circle_links WHERE id = ?').get(req.params.id);
107 if (!link) return res.redirect('/admin/circle?error=' + encodeURIComponent('Niet gevonden'));
108 try {
109 const r = await syncOne(link);
110 res.redirect('/admin/circle?success=' + encodeURIComponent(`Bijgewerkt — ${r.items} posts opgehaald`));
111 } catch (e) {
112 const msg = String((e && e.message) || e).slice(0, 300);
113 db.prepare("UPDATE circle_links SET status='error', last_error=?, last_synced=CURRENT_TIMESTAMP WHERE id=?")
114 .run(msg, link.id);
115 res.redirect('/admin/circle?error=' + encodeURIComponent(msg));
116 }
117});
118
[62b899d]119// Alles in één keer verversen (handig "voor de zekerheid").
120router.post('/sync-all', requireGod, async (req, res) => {
121 try {
122 const r = await sync();
123 const n = (r && r.results) ? r.results.filter((x) => x && x.ok).length : 0;
124 res.redirect('/admin/circle?success=' + encodeURIComponent(`Cirkel gesynchroniseerd (${n} site(s) bijgewerkt)`));
125 } catch (e) {
126 res.redirect('/admin/circle?error=' + encodeURIComponent(String((e && e.message) || e).slice(0, 200)));
127 }
128});
129
[0091cb7]130router.post('/allow', requireGod, (req, res) => {
131 const site = primarySite();
132 if (site) {
133 const v = (req.body.allow_circle === 'on' || req.body.allow_circle === '1') ? 1 : 0;
134 db.prepare('UPDATE sites SET allow_circle = ? WHERE id = ?').run(v, site.id);
135 }
136 res.redirect('/admin/circle?success=' + encodeURIComponent('Opgeslagen'));
137});
138
139export default router;
Note: See TracBrowser for help on using the repository browser.