Files
Lauri Ojansivu 7c0ae509ac Fix #2769 #1992 #2292 #2494 #1853 #1554 #2306 #1437.
- #2769: refresh drag geometry when the DOM changes mid-drag - cards no
  longer drop into the wrong swimlane while someone adds a card.
- #1992: board import restores all five card date fields (and creator)
  with fallbacks; receivedAt/endAt were never imported at all.
- #2292: archiving a swimlane archives its cards (mirroring lists);
  restore brings back exactly the cards archived with it.
- #2494: client noCache card lookup returned a Promise since the
  Meteor 3 port - multi-select board moves wrote sort: NaN everywhere.
- #1853: subtask "View it" falls back to the subtask's boardId when the
  deposit board is not loaded, instead of silently doing nothing.
- #1554: restore the reactive dependency lost in 7673c77c5 so cards
  added after board render accept sidebar label/member drops.
- #2306: "filter to selection" and every bulk action are board-scoped;
  foreign-board cards can no longer be silently archived.
- #1437: new POST /users/logout API endpoint revokes the presented
  resume token, or all tokens with {"all": true}.

All with positive+negative regression tests (see tests/*.test.cjs).

Closes #1389: already fixed - the standalone pencil edit button
(0cc63b810) plus the wrapped viewer (bf2b3b40c) guarantee an edit target
even when a full-width link fills the description.

Thanks to hever, Cactusbone, Vermeille, Vanclief, Miffe, IcedQuinn,
ppouliot, GhassenRjab and xet7.
2026-07-17 03:34:23 +03:00

85 lines
2.8 KiB
JavaScript

'use strict';
// Pure helper for the POST /users/logout REST endpoint (issue #1437: "Old
// tokens are not replaced or set invalid"). Extracted so the request
// validation / Mongo-update planning is unit-testable in plain Node without
// Meteor (mirrors models/lib/restLabel.js and models/lib/boardSortReorder.js).
//
// #1437: the REST API has always had POST /users/login (which mints a new
// resume login token on every call) but never a logout, so tokens acquired
// via the API could not be invalidated — they simply piled up in
// services.resume.loginTokens until their ~90 day expiry ("I have probably
// created 50+ valid tokens recently"). A leaked token therefore stayed valid
// with no server-side way to revoke it short of editing the database.
//
// buildLogoutPlan validates the (already-authenticated) request and yields a
// definite outcome for the route handler:
// - { ok: true, all, selector, modifier } -> caller runs
// Meteor.users.updateAsync(selector, modifier) and reports success.
// - { ok: false, status, error, reason } -> caller sends that JSON error.
//
// Two modes:
// - default: revoke ONLY the token presented in the Authorization header
// ($pull of its hashed form) — other sessions (e.g. the browser GUI)
// stay logged in;
// - { "all": true } in the request body: revoke EVERY resume token of the
// user ("logout everywhere"), cleaning up any accumulated/leaked tokens.
// Truthy spellings accepted for the `all` flag. Body parsing may deliver a
// boolean (JSON body) or a string (urlencoded body), so accept both.
function wantsAllSessions(rawAll) {
if (rawAll === true) return true;
if (typeof rawAll === 'string') {
const s = rawAll.trim().toLowerCase();
return s === 'true' || s === '1' || s === 'yes';
}
if (rawAll === 1) return true;
return false;
}
function buildLogoutPlan(request) {
const { userId, hashedToken, all } =
request && typeof request === 'object' ? request : {};
if (typeof userId !== 'string' || userId.length === 0) {
return {
ok: false,
status: 401,
error: 'unauthorized',
reason:
'You must be logged in to log out. Send your token as an Authorization: Bearer header.',
};
}
const everywhere = wantsAllSessions(all);
if (everywhere) {
return {
ok: true,
all: true,
selector: { _id: userId },
modifier: { $set: { 'services.resume.loginTokens': [] } },
};
}
if (typeof hashedToken !== 'string' || hashedToken.length === 0) {
return {
ok: false,
status: 401,
error: 'unauthorized',
reason: 'No login token found on the request.',
};
}
return {
ok: true,
all: false,
selector: { _id: userId },
modifier: { $pull: { 'services.resume.loginTokens': { hashedToken } } },
};
}
module.exports = {
buildLogoutPlan,
wantsAllSessions,
};