Changeset 91094a4 in Klonkt


Ignore:
Timestamp:
06/19/2026 01:09:15 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
37edecd
Parents:
2e247e4
Message:

Download-for-email (premium feature #2)

Fan leaves their email -> receives the file; address is added to the mailing list
(source 'download', single opt-in so the download is not behind a confirm barrier).
Reuses SubscriberService (#1).

  • DB: audio_tracks.downloadable (default 0).
  • admin-audio: per-track no-JS toggle (POST /admin/audio/:id/downloadable) + ⬇ button in the admin list (premium-gated); downloadable also in the /api/:id whitelist.
  • routes/download.js (premium-gated): GET /downloads (list), GET /download/:id (capture page), POST /download/:id (save email + session grant), GET /download/:id/bestand (serves attachment from storage/audio, 15-min session window, path-traversal guards).
  • views: pages/downloads.ejs + pages/download.ejs (form/ready, auto-start download).
  • admin dashboard: ⬇ Downloads link (premium, non-hub).

node --check passed. Nothing else reuses this; #8 notify reuses subscribers.

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

Location:
src
Files:
3 added
5 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r2e247e4 r91094a4  
    8787  ensureColumn('posts', 'view_count', 'INTEGER DEFAULT 0');         // weergaven per post
    8888  ensureColumn('audio_tracks', 'play_count', 'INTEGER DEFAULT 0');  // plays per track
     89  ensureColumn('audio_tracks', 'downloadable', 'INTEGER DEFAULT 0'); // download-voor-email (premium #2)
    8990
    9091  // Playlists (v9 feature) — first-class entity. CREATE IF NOT EXISTS is
  • src/routes/admin-audio.js

    r2e247e4 r91094a4  
    7878  const rows = db.prepare(`
    7979    SELECT t.id, t.title, t.artist, t.album, t.duration, t.cover_url,
    80            t.position, t.created_at, m.filename, m.size, m.mime_type
     80           t.position, t.created_at, t.downloadable, m.filename, m.size, m.mime_type
    8181    FROM audio_tracks t
    8282    LEFT JOIN media m ON m.id = t.media_id
     
    241241    });
    242242  });
     243});
     244
     245// Download-voor-email per track aan/uit (premium #2). Zonder-JS toggle vanaf de
     246// audio-beheerlijst → flip + terug.
     247router.post('/:id/downloadable', requireGod, (req, res) => {
     248  const site = res.locals.site;
     249  if (!site) return res.status(404).send('Site required');
     250  const row = db.prepare('SELECT downloadable FROM audio_tracks WHERE id = ? AND site_id = ?').get(req.params.id, site.id);
     251  if (row) {
     252    db.prepare('UPDATE audio_tracks SET downloadable = ? WHERE id = ? AND site_id = ?')
     253      .run(row.downloadable ? 0 : 1, req.params.id, site.id);
     254  }
     255  res.redirect('/admin/audio');
    243256});
    244257
     
    413426  }
    414427
     428  if (Object.prototype.hasOwnProperty.call(body, 'downloadable')) {
     429    fields.push('downloadable = ?'); values.push(body.downloadable ? 1 : 0);
     430  }
     431
    415432  if (fields.length === 0) {
    416433    return res.status(400).json({ error: 'Niks om te updaten' });
  • src/server.js

    r2e247e4 r91094a4  
    5555import newsletterRoutes from './routes/newsletter.js';
    5656import adminNewsletterRoutes from './routes/admin-newsletter.js';
     57import downloadRoutes from './routes/download.js';
    5758
    5859if (!process.env.SESSION_SECRET) {
     
    271272app.use('/', epkRoutes); // /pers perskit (premium; niet-premium: next() -> 404)
    272273app.use('/', newsletterRoutes); // /nieuwsbrief in/uitschrijven (premium; niet-premium: next())
     274app.use('/', downloadRoutes); // /downloads + /download/:id download-voor-email (premium)
    273275app.use('/', postsRoutes);
    274276
  • src/views/pages/admin-audio.ejs

    r2e247e4 r91094a4  
    101101              <% } %>
    102102              <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="Bewerken" title="Bewerken">✎</button>
     103              <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
     104                <form action="/admin/audio/<%= t.id %>/downloadable" method="post" class="ax-track-delete" style="display:inline">
     105                  <button type="submit" class="ax-icon-btn" aria-label="Download-voor-email" title="<%= t.downloadable ? 'Download-voor-email staat AAN — klik om uit te zetten' : 'Download-voor-email staat uit — klik om aan te zetten' %>" style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button>
     106                </form>
     107              <% } %>
    103108              <form action="/admin/audio/<%= t.id %>/delete" method="post" onsubmit="return confirm('Track verwijderen?')" class="ax-track-delete">
    104109                <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="Verwijderen" title="Verwijderen">🗑</button>
  • src/views/pages/admin.ejs

    r2e247e4 r91094a4  
    3535      <a href="/admin/newsletter" class="btn">✉️ Nieuwsbrief</a>
    3636      <% if (tenancy !== 'hub' && primarySite) { %><a href="/pers" class="btn" target="_blank">📰 Perskit</a><% } %>
     37      <% if (tenancy !== 'hub' && primarySite) { %><a href="/downloads" class="btn" target="_blank">⬇ Downloads</a><% } %>
    3738    <% } %>
    3839    <a href="/admin/updates" class="btn">🔄 Updates</a>
Note: See TracChangeset for help on using the changeset viewer.