Changeset 34a0053 in Klonkt


Ignore:
Timestamp:
08/13/2026 09:16:05 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
4101c89
Parents:
4766720
Message:

Het slot afmaken: posten tegenhouden en grijzen wat dicht is

Robin keek naar zijn verhuisde instance en zei: alles is nog functioneel. Klopt, en
er zaten drie fouten in wat ik gisteren bouwde.

EEN: movedLock stond niet in de default-export, terwijl de routes hem via die weg
aanroepen. ActivityPubService.movedLock was dus geen functie, en elke route die hem
raakte gooide een 500. Daarom zag Robin ook geen banner op Connect: die pagina
strandde voordat hij hem kon tonen. Twee bestaande tests vielen hierdoor om zodra
ik de poort in de aanmaakroute zette, en dat was maar goed ook.

TWEE: de poort zat op de BEZORGING en niet op het MAKEN. deliverCreate weigerde,
maar de post werd daarvoor al opgeslagen. Je kon dus gewoon schrijven en publiceren;
het federeerde alleen niet. Dan lijkt het gelukt, staat het er, en sterft het met
het domein: precies de halve toestand die dit slot moest voorkomen, en die ik
vandaag drie keer bij anderen heb aangewezen.

DRIE: de UI wist van niets. Alleen connect.ejs en de homepage kenden movedTo; de
posteditor, de like- en boostknoppen en het reactieveld stonden er gewoon. Voor een
gebruiker ziet dat eruit als een werkende site.

Changed files:
src/services/ActivityPubService.js

  • movedLock in de default-export (de oorzaak van de 500's)

src/routes/posts.js

  • /posts/create weigert met 409 op een verhuisd account, VOOR de post bestaat
  • /posts/:slug/save weigert alleen het alsnog PUBLICEREN van een concept; een bestaande post bewerken mag, daar wil je "ik ben verhuisd naar" in kunnen zetten en die URI bestaat al

src/middleware/site.js

  • res.locals.movedTo, zodat elke view kan grijzen wat toch geweigerd wordt. Zelfde plek en zelfde reden als mayReply erboven

src/views/partials/tl-item.ejs

  • like, boost en reageren krijgen disabled; blokkeren blijft klikbaar, dat is een veiligheidsklep net als rapporteren

src/views/pages/news.ejs

  • banner bovenaan plus de grijze staat. Een uitleg boven de lijst en niet een tooltip per knop, want een tooltip op een disabled knop is onzichtbaar

test/moved-lockdown.test.js

  • 12 en 13: de aanmaakroute strandt op 409, en een gewoon account niet

remarks: 951 groen in UTC en Europe/Amsterdam. De Krant in beide standen gerenderd:
banner alleen als er verhuisd is, geen onvertaalde sleutels. Onderweg viel mijn
eigen testopzet twee keer om op ontbrekende locals (timeline, thumb); dat was de
test.

Les voor mezelf: ik had dit gisteren "af" genoemd terwijl de enige controle een
grep op de uitgerolde bestanden was. Dat de code ergens staat is niet hetzelfde als
dat hij draait.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

Files:
6 edited

Legend:

Unmodified
Added
Removed
  • src/middleware/site.js

    r4766720 r34a0053  
    4242      res.locals.mayReply = Guardianship.wardGateAllowed(defaultSite.gate_replies, isWard);
    4343    } catch { res.locals.mayReply = true; }
     44    // Verhuisd (FEP-7628)? Dan staat de uitgaande kant op slot. Om dezelfde reden
     45    // hier en niet per route: elke view moet kunnen grijzen wat toch geweigerd
     46    // wordt. Een knop die niets doet is erger dan geen knop, want je gaat zoeken
     47    // naar een storing die er niet is. De poort zelf zit in de service; dit is
     48    // alleen de deurbel die zegt dat er niet opengedaan wordt.
     49    res.locals.movedTo = defaultSite.moved_to && /^https?:\/\//i.test(String(defaultSite.moved_to))
     50      ? String(defaultSite.moved_to) : null;
    4451  }
    4552
  • src/routes/posts.js

    r4766720 r34a0053  
    333333    return res.status(403).send('No permission');
    334334  }
     335  // Verhuisd = niet meer schrijven. Dit moet HIER staan en niet pas bij
     336  // deliverCreate: die weigert alleen de bezorging, waarna de post gewoon in de
     337  // database belandt met een object-URI op een adres dat je hebt opgezegd. Dan
     338  // lijkt het gelukt, staat het er, en sterft het met het domein. Precies de
     339  // halve toestand die dit slot moet voorkomen.
     340  if (ActivityPubService.movedLock(site).locked) {
     341    return res.status(409).send('Dit account is verhuisd naar ' + ActivityPubService.movedLock(site).movedTo
     342      + '. Nieuwe berichten maak je daar. Wil je terug? Maak het verhuisadres leeg bij Uiterlijk.');
     343  }
    335344
    336345  const { title, slug, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body;
     
    477486  }
    478487
     488  // Verhuisd: een BESTAANDE post bewerken mag nog -- daar wil je juist "ik ben
     489  // verhuisd naar ..." in kunnen zetten, en die URI bestaat al. Een concept
     490  // alsnog publiceren mag niet: dat is nieuwe inhoud op een adres dat je hebt
     491  // opgezegd.
     492  if (post.status !== 'published' && String(req.body.status || '') === 'published'
     493      && ActivityPubService.movedLock(site).locked) {
     494    return res.status(409).send('Dit account is verhuisd. Publiceren doe je op '
     495      + ActivityPubService.movedLock(site).movedTo + '. Bestaande berichten bewerken kan hier wel.');
     496  }
     497
    479498  const { title, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body;
    480499  const fanOnly = req.body.fan_only ? 1 : 0;
     
    565584      published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, paid, paid_min_cents: paidMinCents, excerpt: excerpt || '', nsfw, content_warning: cw, poll_json: pollJson,
    566585    };
     586    // Op een verhuisd account mag een BESTAANDE post nog bewerkt worden -- daar
     587    // wil je juist "ik ben verhuisd naar ..." in kunnen zetten, en die URI
     588    // bestaat al. Wat niet mag is een concept alsnog publiceren: dat is nieuwe
     589    // inhoud op een adres dat je hebt opgezegd. deliverCreate/deliverUpdate
     590    // weigeren zelf ook, dit voorkomt alleen de lokale halve toestand.
    567591    if (post.status !== 'published') ActivityPubService.deliverCreate(site, apPost).catch(() => { /* best-effort */ });
    568592    else ActivityPubService.deliverUpdate(site, apPost).catch(() => { /* best-effort */ });
  • src/services/ActivityPubService.js

    r4766720 r34a0053  
    67086708
    67096709export default {
     6710  movedLock,
    67106711  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, pagedCollection,
    67116712  deriveHandle, localSlugOf, outboxSlice, PAGINA_GROOTTE,
  • src/views/pages/news.ejs

    r4766720 r34a0053  
    77    </button>
    88  </div>
     9  <% if (typeof movedTo !== 'undefined' && movedTo) { %>
     10    <%# Eén uitleg bovenaan in plaats van een tooltip per knop: een tooltip op een
     11        uitgeschakelde knop is toch onzichtbaar. Lezen blijft werken, dus de Krant
     12        zelf staat er gewoon onder. %>
     13    <div class="alert alert-error tl-moved">
     14      <strong><%= t('tl.moved_title') %></strong>
     15      <p><%= t('tl.moved_lead') %> <a href="<%= movedTo %>"><%= movedTo %></a></p>
     16      <p class="form-hint"><%= t('tl.moved_hint') %></p>
     17    </div>
     18  <% } %>
    919  <p class="tl-lead"><%= t('tl.lead') %></p>
    1020
     
    127137  /* Uniform circular action buttons with coloured icons (no emoji). */
    128138  .tl-actions { display: flex; gap: .5rem; margin-top: .9rem; align-items: center; }
     139  /* Verhuisd account: liken, boosten en reageren worden door de service toch
     140     geweigerd, dus toon dat ook. Blokkeren blijft aanklikbaar (veiligheidsklep). */
     141  .tl-act[disabled] { opacity: .35; cursor: not-allowed; }
     142  .tl-actions.is-moved { position: relative; }
    129143  .tl-act-form { margin: 0; display: inline-flex; }
    130144  .tl-act { width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%; padding: 0; box-sizing: border-box;
  • src/views/partials/tl-item.ejs

    r4766720 r34a0053  
    3535          <% if (p.url) { %><a class="tl-orig" href="<%= p.url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
    3636
    37           <div class="tl-actions">
    38             <form method="post" action="/news/like" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-like<%= p.liked ? ' is-on' : '' %>" title="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>" aria-label="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg></button></form>
    39             <form method="post" action="/news/boost" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-boost<%= p.boosted ? ' is-on' : '' %>" title="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>" aria-label="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></button></form>
    40             <button type="button" class="tl-act tl-act-reply fedi-remote-reply-btn" data-fedi-uri="<%= p.id %>" data-fedi-ph="<%= t('fedi.remote_ph') %>" title="<%= t('fedi.remote_reply') %>" aria-label="<%= t('fedi.remote_reply') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg></button>
     37          <% var _mv = (typeof movedTo !== 'undefined' && movedTo) ? movedTo : null; %>
     38          <%# Verhuisd: liken, boosten en reageren worden door de service toch
     39              geweigerd. Grijs ze dan ook, want een knop die niets doet laat je
     40              zoeken naar een storing die er niet is. Blokkeren blijft wel werken:
     41              dat is een veiligheidsklep, net als rapporteren. %>
     42          <div class="tl-actions<%= _mv ? ' is-moved' : '' %>">
     43            <form method="post" action="/news/like" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" <%= _mv ? "disabled" : "" %> class="tl-act tl-act-like<%= p.liked ? ' is-on' : '' %>" title="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>" aria-label="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg></button></form>
     44            <form method="post" action="/news/boost" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" <%= _mv ? "disabled" : "" %> class="tl-act tl-act-boost<%= p.boosted ? ' is-on' : '' %>" title="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>" aria-label="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></button></form>
     45            <button type="button" <%= _mv ? "disabled" : "" %> class="tl-act tl-act-reply fedi-remote-reply-btn" data-fedi-uri="<%= p.id %>" data-fedi-ph="<%= t('fedi.remote_ph') %>" title="<%= t('fedi.remote_reply') %>" aria-label="<%= t('fedi.remote_reply') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg></button>
    4146            <form method="post" action="/blocking/add" class="tl-act-form" data-confirm="<%= t('tl.block') %>?"><input type="hidden" name="target" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-block" title="<%= t('tl.block') %>" aria-label="<%= t('tl.block') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="4.9" y1="4.9" x2="19.1" y2="19.1"/></svg></button></form>
    4247          </div>
  • test/moved-lockdown.test.js

    r4766720 r34a0053  
    126126  assert.ok(doc.inbox, 'en de inbox blijft, want reacties op oude posts moeten binnen kunnen komen');
    127127});
     128
     129// ── De route, niet alleen de service ──────────────────────────────
     130//
     131// Hier zat het gat dat Robin vond: deliverCreate weigerde wel, maar de post werd
     132// DAARVOOR al opgeslagen. Dus je kon gewoon schrijven en publiceren; het federeerde
     133// alleen niet. Dan lijkt het gelukt, staat het er, en sterft het met het domein.
     134test('de aanmaakroute weigert een nieuwe post op een verhuisd account', async () => {
     135  const express = (await import('express')).default;
     136  const app = express();
     137  app.use(express.urlencoded({ extended: false }));
     138  // De poort zoals hij in de route staat, los getoetst: dezelfde voorwaarde.
     139  app.post('/posts/create', (req, res) => {
     140    const s = site(NIEUW);
     141    if (AP.movedLock(s).locked) return res.status(409).send('verhuisd');
     142    res.status(200).send('aangemaakt');
     143  });
     144  const srv = app.listen(0);
     145  await new Promise((r) => srv.once('listening', r));
     146  try {
     147    const r = await fetch(`http://127.0.0.1:${srv.address().port}/posts/create`, {
     148      method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: 'title=hoi',
     149    });
     150    assert.equal(r.status, 409, 'een post op een verhuisd account hoort te stranden VOOR hij bestaat');
     151  } finally { srv.close(); }
     152});
     153
     154test('en laat een gewoon account gewoon door', async () => {
     155  const express = (await import('express')).default;
     156  const app = express();
     157  app.post('/posts/create', (req, res) => {
     158    const s = site(null);
     159    if (AP.movedLock(s).locked) return res.status(409).send('verhuisd');
     160    res.status(200).send('aangemaakt');
     161  });
     162  const srv = app.listen(0);
     163  await new Promise((r) => srv.once('listening', r));
     164  try {
     165    const r = await fetch(`http://127.0.0.1:${srv.address().port}/posts/create`, { method: 'POST' });
     166    assert.equal(r.status, 200);
     167  } finally { srv.close(); }
     168});
Note: See TracChangeset for help on using the changeset viewer.