v1.2.0: disable CSRF for Tor, fix wallet type validation, add version history modal

- fix(admin/csrf): completely disable CSRF checks for Tor/onion compatibility
- fix(validators): add 'main' and 'bonus' to WALLET_TYPES for purchase flow
- feat(admin): add clickable version tag with version history modal in sidebar
- docs: add VERSION.md with changelog and update instructions
This commit is contained in:
NW
2026-07-08 21:59:16 +01:00
parent 4997ca75ab
commit 83991f098b
4 changed files with 87 additions and 44 deletions

34
VERSION.md Normal file
View File

@@ -0,0 +1,34 @@
# Telegram Shop — Version History
## How to update version
1. Edit this file (`VERSION.md`)
2. Add new entry under `## Changelog`
3. Bump version in `src/admin/views/partials/app-sidebar.ejs`
4. Commit all changes together
## Current Version
**v1.2.0** — 2026-07-08
## Changelog
### v1.2.0 — 2026-07-08
- **fix**: Disabled CSRF checks in admin panel for Tor / onion zone compatibility
- **fix**: Fixed "Invalid wallet type" error in Telegram bot purchase flow (`main`/`bonus` types added to validator)
- **feat**: Added version history modal in admin sidebar
### v1.1.0 — 2026-07-02
- **feat**: Mercuryo gateway integration, crypto QR deposit, mono products, wallet auto-refresh
- **fix**: CSRF cookie `sameSite=false` for Tor, auth cookie fix, async handlers
- **feat**: Draggable dashboard panels + business KPI redesign
- **fix**: SmartAdmin template redesign + security hardening
### v1.0.0 — 2026-06-24
- **feat**: Initial release — Telegram shop bot with admin panel
- **feat**: Crypto wallets (BTC, LTC, ETH, USDT, USDC)
- **feat**: Product catalog with locations, categories, subcategories
- **feat**: Purchase system with hidden content delivery
- **feat**: Admin panel with dashboard, wallets, users, purchases, audit log
- **feat**: Tor proxy support (.onion access)
- **feat**: i18n localization (en/es/de)

View File

@@ -23,53 +23,15 @@ export function csrfMiddleware(req, res, next) {
} }
export function validateCsrf(req, res, next) { export function validateCsrf(req, res, next) {
// Only validate state-changing methods // CSRF checks disabled for Tor / onion zone compatibility
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next(); return next();
// Skip multipart/form-data — multer hasn't parsed the body yet.
// Route handlers using multer must validate CSRF themselves after parsing.
const ct = req.headers['content-type'] || '';
if (ct.startsWith('multipart/form-data')) return next();
const token = req.body?._csrf || req.headers[CSRF_HEADER];
const cookieToken = req.cookies?.[CSRF_COOKIE];
if (!token || !cookieToken) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method, hasBodyToken: !!token, hasCookieToken: !!cookieToken, contentType: ct }, 'CSRF token missing');
return res.status(403).send('CSRF token missing. Please reload the page.');
}
const provided = Buffer.from(token, 'utf8');
const expected = Buffer.from(cookieToken, 'utf8');
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token mismatch');
return res.status(403).send('CSRF token invalid. Please reload the page.');
}
next();
} }
/** /**
* Validate CSRF token from req.body._csrf (for use inside multer handlers). * Validate CSRF token from req.body._csrf (for use inside multer handlers).
* Returns true if valid, false otherwise. Sends 403 response on failure. * Returns true if valid, false otherwise. Sends 403 response on failure.
* NOTE: Disabled for Tor / onion zone compatibility.
*/ */
export function validateCsrfFromBody(req, res) { export function validateCsrfFromBody(req, res) {
const token = req.body?._csrf;
const cookieToken = req.cookies?.[CSRF_COOKIE];
if (!token || !cookieToken) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token missing (multipart)');
res.status(403).send('CSRF token missing. Please reload the page.');
return false;
}
const provided = Buffer.from(token, 'utf8');
const expected = Buffer.from(cookieToken, 'utf8');
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token mismatch (multipart)');
res.status(403).send('CSRF token invalid. Please reload the page.');
return false;
}
return true; return true;
} }

View File

@@ -24,15 +24,62 @@
</nav> </nav>
<div class="nav-footer"> <div class="nav-footer">
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2" role="button" data-bs-toggle="modal" data-bs-target="#versionModal" style="cursor:pointer;" title="View version history">
<svg class="sa-icon sa-thin"> <svg class="sa-icon sa-thin">
<use href="/icons/sprite.svg#tag"></use> <use href="/icons/sprite.svg#tag"></use>
</svg> </svg>
<span class="fs-xs opacity-70">v1.0</span> <span class="fs-xs opacity-70">v1.2.0</span>
</div> </div>
</div> </div>
</aside> </aside>
<!-- Version History Modal -->
<div class="modal fade" id="versionModal" tabindex="-1" aria-labelledby="versionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="versionModalLabel">Version History</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-info mb-3">
<strong>Current:</strong> v1.2.0 &middot; 2026-07-08
</div>
<h6 class="fw-bold mb-2">v1.2.0 <span class="text-muted fs-sm">&mdash; 2026-07-08</span></h6>
<ul class="small mb-3">
<li><span class="badge bg-warning text-dark">fix</span> Disabled CSRF checks in admin panel for Tor / onion zone compatibility</li>
<li><span class="badge bg-warning text-dark">fix</span> Fixed "Invalid wallet type" error in Telegram bot purchase flow</li>
<li><span class="badge bg-primary">feat</span> Added version history modal in admin sidebar</li>
</ul>
<h6 class="fw-bold mb-2">v1.1.0 <span class="text-muted fs-sm">&mdash; 2026-07-02</span></h6>
<ul class="small mb-3">
<li><span class="badge bg-primary">feat</span> Mercuryo gateway, crypto QR deposit, mono products, wallet auto-refresh</li>
<li><span class="badge bg-warning text-dark">fix</span> CSRF cookie sameSite=false for Tor, auth cookie fix, async handlers</li>
<li><span class="badge bg-primary">feat</span> Draggable dashboard panels + business KPI redesign</li>
<li><span class="badge bg-primary">feat</span> SmartAdmin template redesign + security hardening</li>
</ul>
<h6 class="fw-bold mb-2">v1.0.0 <span class="text-muted fs-sm">&mdash; 2026-06-24</span></h6>
<ul class="small mb-0">
<li><span class="badge bg-primary">feat</span> Initial release — Telegram shop bot with admin panel</li>
<li><span class="badge bg-primary">feat</span> Crypto wallets (BTC, LTC, ETH, USDT, USDC)</li>
<li><span class="badge bg-primary">feat</span> Product catalog with locations, categories, subcategories</li>
<li><span class="badge bg-primary">feat</span> Purchase system with hidden content delivery</li>
<li><span class="badge bg-primary">feat</span> Admin panel with dashboard, wallets, users, purchases, audit log</li>
<li><span class="badge bg-primary">feat</span> Tor proxy support (.onion access)</li>
<li><span class="badge bg-primary">feat</span> i18n localization (en/es/de)</li>
</ul>
</div>
<div class="modal-footer">
<span class="text-muted small">Update version in <code>VERSION.md</code> + <code>app-sidebar.ejs</code></span>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="backdrop" data-action="toggle-swap" data-toggleclass="app-mobile-menu-open"></div> <div class="backdrop" data-action="toggle-swap" data-toggleclass="app-mobile-menu-open"></div>

View File

@@ -1,4 +1,4 @@
const WALLET_TYPES = new Set(['BTC', 'LTC', 'ETH', 'USDT', 'USDC']); const WALLET_TYPES = new Set(['BTC', 'LTC', 'ETH', 'USDT', 'USDC', 'main', 'bonus']);
export default class Validators { export default class Validators {
static isValidLocation(country, city, district) { static isValidLocation(country, city, district) {