Files
wekan/models/lib/invitationCodeEmail.js
Lauri Ojansivu 9ddbc777dd Startup schema upgrade, self-healing attachments, migration speed; fix
#5695 #4897 #4654 #5174 #4825 #5659 #4730 #4419 #4158 #4560 #4036
#1959 #1971.

- Startup schema upgrade (server/lib/schemaUpgradeSteps.js): on start,
  check that data from EVERY old WeKan version is migrated to the newest
  structure and migrate only what is missing - all platforms, MongoDB and
  FerretDB. Version-gated (marker stores WeKan version + datetime of the
  last re-check; one findOne per boot until a new release), non-blocking,
  live dashboard at /schema-upgrade-status showing the product name, fast
  on big databases (distinct() set-joins, updateMany batches). Steps:
  archived-flag backfill, swimlane structure + visibility rescue
  (#1959 #1971: cards under deleted/archived/foreign swimlanes reassigned
  to a visible swimlane; card-insert hook validates supplied swimlaneIds),
  merge-per-swimlane-lists (v7.98/v8.0x list copies merged back into
  shared lists: no duplicate columns in Lists view, everything visible in
  Swimlanes view, cards keep their swimlane), embedded checklist items,
  customFields boardIds, allows* backfills, members isActive, permission
  lowercase, filesystem path healing.
- Migration speed (5-hour reports): insertMany batches in the text phase,
  preloaded target-record maps in the file phases.
- Snap attachment-repair + FILES_ONLY importer mode (see prior commit
  wiring) extended with CFS FileSystem-store coverage
  (ATTACHMENTS_STORE_PATH era), missing-binary reporting, and
  migrate-gridfs-to-fs.mjs bucket fixes.
- OAuth2/OIDC: OAUTH2_LOGIN_STYLE=redirect honored (#5695); concurrent
  logins no longer contaminate each other's user data (#4897); account
  merge no longer wipes avatars/templates (#4560).
- LDAP: background sync fixed (#4654), password login rejected after LDAP
  migration (#4419), LDAP_ENCRYPTION=true actually encrypts (#4158),
  group filter accepts multiple groups and admits admin-sync groups
  (#4036).
- All Boards tiles: per-list card counts + member avatars restored
  non-reactively (#5174 #4825). Uniform default list widths (#5659).
  Declined invitations no longer resurrect membership (#4730).
- Tests with negative cases for every fix (tests/*.test.cjs; suite green
  except the pre-existing environment-only htmljs failure).

Closes #5600: OAUTH2_CA_CERT already works - the fetch migration
(49a272d6c) applies the CA to all OIDC calls via https.Agent.
Closes #3828: board members seeing all board cards is by design (no
per-card visibility model), and organizations/teams now exist.

Thanks to mueschel, ArturRuta, gerardo-junior, fabianrbz, Miuler,
butteredCat-2021, Griiimm, preciousamorc, farwayer, LeoLu-eng, hnb1,
rangersdo and xet7.
2026-07-17 03:07:59 +03:00

89 lines
4.0 KiB
JavaScript

'use strict';
// Pure helpers for the "invite by email" flow (Meteor method `sendInvitation`
// in server/models/settings.js). Extracted so the rules that guarantee "an
// invitation email always carries a code that will validate at sign-up" are
// unit-testable without Meteor.
//
// #4043 ("Sometimes user get board invitation by mail without invitation
// code"): recipients received an invitation email whose code the sign-up form
// then rejected with "The invitation code doesn't exist". Sign-up only accepts
// a code whose stored document matches { code, email, valid: true }
// (server/models/users.js onCreateUser), so the SENDING side must guarantee:
// 1. the stored email is lowercase — the sign-up form lowercases the typed
// address and MongoDB string matching is case sensitive, so a mixed-case
// invitation document can never match (normalizeInviteEmail);
// 2. a RE-INVITE never re-sends a stale code: when the stored invitation is
// no longer valid (e.g. consumed and left with valid:false) or has no
// usable code, a fresh code must overwrite the old one with `valid`
// restored to true (buildReinviteModifier) — this is the fix proposed in
// the #4043 thread ("send a new invitation code that overwrites the last
// one");
// 3. an email is only sent for an invitation document that exists, has a
// non-empty string code, and is still valid (isInvitationCodeSendable) —
// otherwise fail loudly instead of mailing a dead code;
// 4. a failed SMTP send may only roll back (delete) an invitation code that
// was created by this very invite; deleting a PRE-EXISTING document would
// silently invalidate the code already delivered in an earlier email
// (shouldRemoveInvitationOnEmailFailure).
// Lowercase + trim an invitee address so the stored invitation email always
// matches the (lowercased) address the user signs up with. Non-strings
// normalize to '' (callers skip empty results).
function normalizeInviteEmail(email) {
if (typeof email !== 'string') {
return '';
}
return email.trim().toLowerCase();
}
// True when `code` is a non-empty string — the only shape the sign-up lookup
// { code: <string from the form>, valid: true } can ever match. Legacy or
// hand-inserted documents may carry a Number code, which MongoDB will never
// match against the form's string.
function hasUsableCode(invitation) {
return Boolean(
invitation &&
typeof invitation.code === 'string' &&
invitation.code.length > 0,
);
}
// An invitation email may only be sent for a document that exists, carries a
// usable code, and has not been invalidated (valid === false). A missing
// `valid` field (legacy documents predating the field's defaultValue) counts
// as valid.
function isInvitationCodeSendable(invitation) {
return hasUsableCode(invitation) && invitation.valid !== false;
}
// Build the Mongo modifier for re-inviting an email that already has an
// invitation document. Always refreshes boardsToBeInvited; additionally
// overwrites the code (and restores valid:true) when the stored one could
// never validate at sign-up — stale (valid:false), missing, empty, or not a
// string. `generateCode` supplies the new code (coerced to String so it
// matches the form input type regardless of the generator).
function buildReinviteModifier(invitation, boards, generateCode) {
const $set = { boardsToBeInvited: boards };
if (!invitation || invitation.valid === false || !hasUsableCode(invitation)) {
$set.code = String(generateCode());
$set.valid = true;
}
return { $set };
}
// After an SMTP failure, only an invitation created by this very invite call
// may be deleted (nothing was ever delivered for it). A pre-existing document
// must survive, because its code was already delivered in an earlier email.
function shouldRemoveInvitationOnEmailFailure({ isNewInvitation } = {}) {
return isNewInvitation === true;
}
module.exports = {
normalizeInviteEmail,
hasUsableCode,
isInvitationCodeSendable,
buildReinviteModifier,
shouldRemoveInvitationOnEmailFailure,
};