Ad reporting + featured rotates one link + clear campaign form on submit
- Ad reporting (auto-approved ads need a safety valve): new reports.js store (dual-mode) + ad_reports table + POST /api/report-ad; a "⚠ report" control on served ad slots opens a reason prompt and notifies the admin by email (siteConfig.adminEmail / ADMIN_EMAIL). - Featured links now show ONE link at a time and cycle (true rotation), instead of listing all links at once. - Campaign form now clears EVERY field on submit (target/creative/title/etc.). A leftover target URL was carrying into the next campaign — the same reason a video short was saved with the wrong (massifly) CTA target. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,16 @@ async function bootstrap() {
|
|||||||
// per-account chat settings: availability toggle (default on) + muted-member list (JSON emails)
|
// per-account chat settings: availability toggle (default on) + muted-member list (JSON emails)
|
||||||
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_available TINYINT NOT NULL DEFAULT 1');
|
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_available TINYINT NOT NULL DEFAULT 1');
|
||||||
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_mutes VARCHAR(4000) NULL');
|
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_mutes VARCHAR(4000) NULL');
|
||||||
|
await q(`CREATE TABLE IF NOT EXISTS ad_reports (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
campaign_id INT NOT NULL,
|
||||||
|
reporter VARCHAR(190) NOT NULL DEFAULT '',
|
||||||
|
reason VARCHAR(20) NOT NULL,
|
||||||
|
note VARCHAR(600) NULL,
|
||||||
|
ts BIGINT NOT NULL,
|
||||||
|
resolved TINYINT NOT NULL DEFAULT 0,
|
||||||
|
INDEX (resolved, ts), INDEX (campaign_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||||
await q(`CREATE TABLE IF NOT EXISTS burns (
|
await q(`CREATE TABLE IF NOT EXISTS burns (
|
||||||
id VARCHAR(32) PRIMARY KEY,
|
id VARCHAR(32) PRIMARY KEY,
|
||||||
member_id INT NOT NULL,
|
member_id INT NOT NULL,
|
||||||
|
|||||||
+19
-3
@@ -96,6 +96,21 @@ window.IAP = (function () {
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
// Render one served ad into #<elId>. Silent if no inventory.
|
// Render one served ad into #<elId>. Silent if no inventory.
|
||||||
|
// report an ad (auto-approved ads need a member-facing flag → admin notified)
|
||||||
|
function reportAd(campaignId) {
|
||||||
|
if (!campaignId) return;
|
||||||
|
const reason = (prompt('Report this ad. Reason: broken, inappropriate, spam, scam, or other', 'broken') || '').trim().toLowerCase();
|
||||||
|
if (!reason) return;
|
||||||
|
const note = prompt('Anything to add? (optional)') || '';
|
||||||
|
fetch('/api/report-ad', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaignId, reason, note }) })
|
||||||
|
.then(() => status('Thanks — this ad was reported to the admin for review.', 'ok'))
|
||||||
|
.catch(() => status('Could not send the report. Try again.', 'bad'));
|
||||||
|
}
|
||||||
|
const reportTag = ad => ' <a class="ad-report small muted" href="#" data-cid="' + ad.id + '" style="margin-left:8px">⚠ report</a>';
|
||||||
|
function wireReport(el) {
|
||||||
|
const rl = el.querySelector('.ad-report');
|
||||||
|
if (rl) rl.addEventListener('click', e => { e.preventDefault(); reportAd(Number(rl.dataset.cid)); });
|
||||||
|
}
|
||||||
async function adSlot(type, elId) {
|
async function adSlot(type, elId) {
|
||||||
try {
|
try {
|
||||||
const { ad } = await (await fetch('/api/ads/slot?type=' + type)).json();
|
const { ad } = await (await fetch('/api/ads/slot?type=' + type)).json();
|
||||||
@@ -104,13 +119,14 @@ window.IAP = (function () {
|
|||||||
if (ad.imageUrl) {
|
if (ad.imageUrl) {
|
||||||
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow">'
|
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow">'
|
||||||
+ '<img src="' + ad.imageUrl + '" alt="advertisement" style="max-width:100%;border-radius:8px"></a>'
|
+ '<img src="' + ad.imageUrl + '" alt="advertisement" style="max-width:100%;border-radius:8px"></a>'
|
||||||
+ '<div class="small muted">member ad</div>';
|
+ '<div class="small muted">member ad' + reportTag(ad) + '</div>';
|
||||||
} else {
|
} else {
|
||||||
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><b>' + ad.title + '</b>'
|
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><b>' + ad.title + '</b>'
|
||||||
+ (ad.body ? ' · ' + ad.body : '') + '</a> <span class="small muted">member ad</span>';
|
+ (ad.body ? ' · ' + ad.body : '') + '</a> <span class="small muted">member ad' + reportTag(ad) + '</span>';
|
||||||
}
|
}
|
||||||
|
wireReport(el);
|
||||||
el.hidden = false;
|
el.hidden = false;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, $ };
|
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, $ };
|
||||||
})();
|
})();
|
||||||
|
|||||||
+18
-7
@@ -28,16 +28,24 @@
|
|||||||
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
|
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
|
||||||
}
|
}
|
||||||
// featured rotation strip on the overview
|
// featured rotation strip on the overview
|
||||||
|
let featItems = [], featIdx = 0, featTimer = null;
|
||||||
|
function renderOneFeatured() {
|
||||||
|
if (!featItems.length) return;
|
||||||
|
const i = featItems[featIdx % featItems.length];
|
||||||
|
$('featStrip').innerHTML = '<a href="' + i.url + '" target="_blank" rel="noopener nofollow"><span>' + esc(i.title) + '</span>'
|
||||||
|
+ (i.by ? '<span class="by">' + esc(i.by) + '</span>' : '') + '</a>';
|
||||||
|
}
|
||||||
async function loadFeatured() {
|
async function loadFeatured() {
|
||||||
try {
|
try {
|
||||||
const r = await (await fetch('/api/featured')).json();
|
const r = await (await fetch('/api/featured')).json();
|
||||||
const card = $('featuredCard'); if (!card) return;
|
const card = $('featuredCard'); if (!card) return;
|
||||||
if (!r.items || !r.items.length) { card.hidden = true; return; }
|
if (!r.items || !r.items.length) { card.hidden = true; return; }
|
||||||
card.hidden = false;
|
card.hidden = false;
|
||||||
|
featItems = r.items; featIdx = Math.floor(Math.random() * featItems.length);
|
||||||
$('featSub').textContent = r.items.length + ' link' + (r.items.length === 1 ? '' : 's') + ' in rotation';
|
$('featSub').textContent = r.items.length + ' link' + (r.items.length === 1 ? '' : 's') + ' in rotation';
|
||||||
$('featStrip').innerHTML = r.items.map(i =>
|
renderOneFeatured(); // show ONE at a time (true rotation), cycle if more than one
|
||||||
'<a href="' + i.url + '" target="_blank" rel="noopener nofollow"><span>' + esc(i.title) + '</span>'
|
clearInterval(featTimer);
|
||||||
+ (i.by ? '<span class="by">' + esc(i.by) + '</span>' : '') + '</a>').join('');
|
if (featItems.length > 1) featTimer = setInterval(() => { featIdx++; renderOneFeatured(); }, 6000);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,11 +673,14 @@
|
|||||||
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
|
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
|
||||||
budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) });
|
budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) });
|
||||||
IAP.status('Campaign is live. It starts serving right away.', 'ok');
|
IAP.status('Campaign is live. It starts serving right away.', 'ok');
|
||||||
$('cName').value = ''; $('cBudget').value = '';
|
// clear EVERY field so no target/creative carries into the next campaign
|
||||||
$('cSoloEd').innerHTML = ''; $('cSoloRaw').value = ''; $('cCtaLabel').value = '';
|
['cName', 'cBudget', 'cTarget', 'cImage', 'cTitle', 'cBody', 'cCtaLabel',
|
||||||
$('cVideoUrl').value = ''; $('cVideoTitle').value = ''; $('cVideoCta').value = '';
|
'cVideoUrl', 'cVideoTitle', 'cVideoCta', 'cVisitTitle', 'cVisitCount', 'cFeatTitle']
|
||||||
|
.forEach(id => { if ($(id)) $(id).value = ''; });
|
||||||
|
$('cSoloEd').innerHTML = ''; if ($('cSoloRaw')) $('cSoloRaw').value = '';
|
||||||
$('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = '';
|
$('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = '';
|
||||||
$('edMediaInfo').textContent = '';
|
if ($('edMediaInfo')) $('edMediaInfo').textContent = '';
|
||||||
|
cVidDims = null;
|
||||||
await loadCampaigns();
|
await loadCampaigns();
|
||||||
}));
|
}));
|
||||||
// defers the busy() lookup to click time (busy is declared below)
|
// defers the busy() lookup to click time (busy is declared below)
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260906m"></script>
|
<script src="/assets/common.js?v=20260907m"></script>
|
||||||
<script src="/assets/contract.js?v=20260906m"></script>
|
<script src="/assets/contract.js?v=20260906m"></script>
|
||||||
<script src="/assets/chat.js?v=20260906m"></script>
|
<script src="/assets/chat.js?v=20260906m"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+1
-1
@@ -437,7 +437,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/assets/common.js?v=20260906m"></script>
|
<script src="/assets/common.js?v=20260907m"></script>
|
||||||
<script src="/assets/wallet.js?v=20260907f"></script>
|
<script src="/assets/wallet.js?v=20260907f"></script>
|
||||||
<script src="/assets/home.js?v=20260906m"></script>
|
<script src="/assets/home.js?v=20260906m"></script>
|
||||||
<script src="/assets/chat.js?v=20260906m"></script>
|
<script src="/assets/chat.js?v=20260906m"></script>
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
|
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260906m"></script>
|
<script src="/assets/common.js?v=20260907m"></script>
|
||||||
<script src="/assets/ledger.js?v=20260906m"></script>
|
<script src="/assets/ledger.js?v=20260906m"></script>
|
||||||
<script src="/assets/chat.js?v=20260906m"></script>
|
<script src="/assets/chat.js?v=20260906m"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Member area | InstantAdPay</title>
|
<title>Member area | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260907l">
|
<link rel="stylesheet" href="/assets/site.css?v=20260907m">
|
||||||
</head>
|
</head>
|
||||||
<body class="bo-body">
|
<body class="bo-body">
|
||||||
|
|
||||||
@@ -641,9 +641,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260907l"></script>
|
<script src="/assets/common.js?v=20260907m"></script>
|
||||||
<script src="/assets/wallet.js?v=20260907f"></script>
|
<script src="/assets/wallet.js?v=20260907f"></script>
|
||||||
<script src="/assets/my.js?v=20260907l"></script>
|
<script src="/assets/my.js?v=20260907m"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@
|
|||||||
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
|
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script src="/assets/common.js?v=20260906m"></script>
|
<script src="/assets/common.js?v=20260907m"></script>
|
||||||
<script src="/assets/tx.js?v=20260906m"></script>
|
<script src="/assets/tx.js?v=20260906m"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script src="/assets/common.js?v=20260906m"></script>
|
<script src="/assets/common.js?v=20260907m"></script>
|
||||||
<script src="/assets/wall.js?v=20260906m"></script>
|
<script src="/assets/wall.js?v=20260906m"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
// Ad reports: members flag a broken or inappropriate ad (ads are auto-approved,
|
||||||
|
// so this is the safety valve). Stored dual-mode (MySQL when DATABASE_URL, else
|
||||||
|
// a JSON file in the volume) and surfaced to the admin.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const db = require('./db');
|
||||||
|
|
||||||
|
let DATA_DIR = null;
|
||||||
|
const J = {
|
||||||
|
db: null,
|
||||||
|
FILE: () => path.join(DATA_DIR, 'ad-reports.json'),
|
||||||
|
load() { try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) { this.db = { nextId: 1, items: [] }; } },
|
||||||
|
save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} }
|
||||||
|
};
|
||||||
|
function init(opts) { DATA_DIR = opts.dataDir; }
|
||||||
|
|
||||||
|
const REASONS = ['broken', 'inappropriate', 'spam', 'scam', 'other'];
|
||||||
|
|
||||||
|
async function add(campaignId, reporterEmail, reason, note) {
|
||||||
|
const now = Date.now();
|
||||||
|
const r = REASONS.includes(String(reason)) ? String(reason) : 'other';
|
||||||
|
const n = String(note || '').slice(0, 500);
|
||||||
|
const cid = Number(campaignId) || 0;
|
||||||
|
const who = String(reporterEmail || '').toLowerCase();
|
||||||
|
if (db.enabled()) {
|
||||||
|
const ins = await db.q('INSERT INTO ad_reports (campaign_id,reporter,reason,note,ts) VALUES (?,?,?,?,?)',
|
||||||
|
[cid, who, r, n, now]);
|
||||||
|
return { id: ins.insertId, campaignId: cid, reason: r };
|
||||||
|
}
|
||||||
|
if (!J.db) J.load();
|
||||||
|
const id = J.db.nextId++;
|
||||||
|
J.db.items.push({ id, campaignId: cid, reporter: who, reason: r, note: n, ts: now, resolved: false });
|
||||||
|
J.save();
|
||||||
|
return { id, campaignId: cid, reason: r };
|
||||||
|
}
|
||||||
|
// recent reports for the admin view (newest first)
|
||||||
|
async function list(limit = 200) {
|
||||||
|
if (db.enabled()) {
|
||||||
|
const rows = await db.q('SELECT id,campaign_id,reporter,reason,note,ts,resolved FROM ad_reports ORDER BY ts DESC LIMIT ?', [limit]);
|
||||||
|
return rows.map(x => ({ id: x.id, campaignId: x.campaign_id, reporter: x.reporter, reason: x.reason, note: x.note, ts: Number(x.ts), resolved: !!x.resolved }));
|
||||||
|
}
|
||||||
|
if (!J.db) J.load();
|
||||||
|
return J.db.items.slice().sort((a, b) => b.ts - a.ts).slice(0, limit);
|
||||||
|
}
|
||||||
|
async function resolve(id) {
|
||||||
|
if (db.enabled()) { await db.q('UPDATE ad_reports SET resolved=1 WHERE id=?', [Number(id)]); return { ok: true }; }
|
||||||
|
if (!J.db) J.load();
|
||||||
|
const it = J.db.items.find(x => x.id === Number(id)); if (it) { it.resolved = true; J.save(); }
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
// how many unresolved (for an admin badge)
|
||||||
|
async function openCount() {
|
||||||
|
if (db.enabled()) { const r = await db.q('SELECT COUNT(*) n FROM ad_reports WHERE resolved=0'); return r[0].n; }
|
||||||
|
if (!J.db) J.load();
|
||||||
|
return J.db.items.filter(x => !x.resolved).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, add, list, resolve, openCount, REASONS };
|
||||||
@@ -17,6 +17,7 @@ const accounts = require('./accounts');
|
|||||||
const ads = require('./ads');
|
const ads = require('./ads');
|
||||||
const mailer = require('./mailer');
|
const mailer = require('./mailer');
|
||||||
const messages = require('./messages');
|
const messages = require('./messages');
|
||||||
|
const reports = require('./reports');
|
||||||
const spaces = require('./spaces'); // DO Spaces video storage (inert unless DO_SPACES_* set)
|
const spaces = require('./spaces'); // DO Spaces video storage (inert unless DO_SPACES_* set)
|
||||||
let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ }
|
let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ }
|
||||||
const chatbot = require('./chatbot');
|
const chatbot = require('./chatbot');
|
||||||
@@ -126,6 +127,7 @@ async function boot() {
|
|||||||
ads.init({ dataDir: DATA_DIR, chain });
|
ads.init({ dataDir: DATA_DIR, chain });
|
||||||
mailer.init({ dataDir: DATA_DIR });
|
mailer.init({ dataDir: DATA_DIR });
|
||||||
messages.init({ dataDir: DATA_DIR });
|
messages.init({ dataDir: DATA_DIR });
|
||||||
|
reports.init({ dataDir: DATA_DIR });
|
||||||
chatbot.init({ dataDir: DATA_DIR, chain });
|
chatbot.init({ dataDir: DATA_DIR, chain });
|
||||||
setTimeout(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 1000);
|
setTimeout(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 1000);
|
||||||
setInterval(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 60 * 1000);
|
setInterval(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 60 * 1000);
|
||||||
@@ -705,6 +707,24 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return json(res, 200, { ok: true, balanceWei: BigInt(nb || '0x0').toString() });
|
return json(res, 200, { ok: true, balanceWei: BigInt(nb || '0x0').toString() });
|
||||||
} catch (e) { return json(res, 502, { error: 'Faucet is unavailable right now.' }); }
|
} catch (e) { return json(res, 502, { error: 'Faucet is unavailable right now.' }); }
|
||||||
}
|
}
|
||||||
|
// -- report an ad (auto-approved ads need a safety valve): store + notify admin
|
||||||
|
if (p === '/api/report-ad' && req.method === 'POST') {
|
||||||
|
const b = await readBody(req);
|
||||||
|
if (!Number(b.campaignId)) return json(res, 400, { error: 'Which ad?' });
|
||||||
|
const s = await auth.fromRequest(req);
|
||||||
|
const who = (s && s.email) || '';
|
||||||
|
const rec = await reports.add(b.campaignId, who, b.reason, b.note);
|
||||||
|
try {
|
||||||
|
const adminEmail = siteConfig().adminEmail || process.env.ADMIN_EMAIL || '';
|
||||||
|
if (adminEmail && mailer.hasKey()) {
|
||||||
|
mailer.send(adminEmail, 'Ad reported on InstantAdPay (campaign #' + rec.campaignId + ')',
|
||||||
|
'A member flagged an ad.\n\nCampaign: #' + rec.campaignId + '\nReason: ' + rec.reason
|
||||||
|
+ '\nReported by: ' + (who || 'anonymous') + '\nNote: ' + (String(b.note || '').slice(0, 500) || '(none)')
|
||||||
|
+ '\n\nPause or review it from the admin.').catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return json(res, 200, { ok: true });
|
||||||
|
}
|
||||||
if (p === '/api/my/chat/send' && req.method === 'POST') {
|
if (p === '/api/my/chat/send' && req.method === 'POST') {
|
||||||
const s = await auth.fromRequest(req);
|
const s = await auth.fromRequest(req);
|
||||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||||
|
|||||||
Reference in New Issue
Block a user