fix: prewiev Bot expires links
This commit is contained in:
parent
ca52b2115b
commit
cbf41d5022
2 changed files with 41 additions and 1 deletions
|
|
@ -4,7 +4,7 @@ import { pipeline } from 'node:stream/promises';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { queryOne, execute } from '../db.js';
|
import { queryOne, execute } from '../db.js';
|
||||||
import { rawPath, gzPath } from '../storage.js';
|
import { rawPath, gzPath } from '../storage.js';
|
||||||
import { gonePage } from '../views.js';
|
import { gonePage, downloadLandingPage } from '../views.js';
|
||||||
import { ah } from '../util.js';
|
import { ah } from '../util.js';
|
||||||
import log from '../log.js';
|
import log from '../log.js';
|
||||||
|
|
||||||
|
|
@ -15,8 +15,35 @@ function contentDisposition(name) {
|
||||||
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(name)}`;
|
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(name)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET shows a landing page and does NOT consume the link. This is deliberate: chat
|
||||||
|
// apps and email scanners (WhatsApp, Slack, iMessage, ...) fetch the URL to build a
|
||||||
|
// preview, and if GET burned the token the link would be "used" before the human
|
||||||
|
// clicks. The one-time consumption happens on the POST below.
|
||||||
router.get('/d/:token', ah(async (req, res) => {
|
router.get('/d/:token', ah(async (req, res) => {
|
||||||
const token = req.params.token || '';
|
const token = req.params.token || '';
|
||||||
|
const row = await queryOne(
|
||||||
|
`SELECT f.original_name, f.size_bytes, f.is_bundle, dl.used, dl.expires_at > NOW() AS valid
|
||||||
|
FROM download_links dl JOIN files f ON f.id = dl.file_id
|
||||||
|
WHERE dl.token = ?`,
|
||||||
|
[token]
|
||||||
|
);
|
||||||
|
if (!row || row.used || !row.valid) {
|
||||||
|
return res.status(410).send(gonePage());
|
||||||
|
}
|
||||||
|
res.send(
|
||||||
|
downloadLandingPage({
|
||||||
|
token,
|
||||||
|
name: row.original_name,
|
||||||
|
size: row.size_bytes,
|
||||||
|
isBundle: row.is_bundle,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// POST performs the actual one-time download (triggered by the button on the landing
|
||||||
|
// page). Preview bots issue GET, never POST, so the link survives until a real click.
|
||||||
|
router.post('/d/:token', ah(async (req, res) => {
|
||||||
|
const token = req.params.token || '';
|
||||||
|
|
||||||
// Atomically reserve the one-time link: succeeds only if unused AND unexpired.
|
// Atomically reserve the one-time link: succeeds only if unused AND unexpired.
|
||||||
const result = await execute(
|
const result = await execute(
|
||||||
|
|
|
||||||
13
src/views.js
13
src/views.js
|
|
@ -203,6 +203,19 @@ export function dashboardPage({ user, files, disk, flash, newLink }) {
|
||||||
return layout({ title: 'My files', body, user, flash, active: 'dash' });
|
return layout({ title: 'My files', body, user, flash, active: 'dash' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function downloadLandingPage({ token, name, size, isBundle }) {
|
||||||
|
const body = `<section class="card narrow">
|
||||||
|
<h1>You've received a file</h1>
|
||||||
|
<p class="fname">${esc(name)}</p>
|
||||||
|
<p class="sub">${humanBytes(size)}${isBundle ? ' · zip bundle' : ''}</p>
|
||||||
|
<form method="post" action="${u(`/d/${token}`)}">
|
||||||
|
<button class="btn" type="submit">Download</button>
|
||||||
|
</form>
|
||||||
|
<p class="muted" style="margin-top:14px">This is a one-time link — it works once, then expires.</p>
|
||||||
|
</section>`;
|
||||||
|
return layout({ title: 'Download', body, user: null });
|
||||||
|
}
|
||||||
|
|
||||||
export function gonePage() {
|
export function gonePage() {
|
||||||
return messagePage({
|
return messagePage({
|
||||||
title: 'Link unavailable',
|
title: 'Link unavailable',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue