@7365admin1/core 3.136.2 → 3.137.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.136.2",
4
+ "version": "3.137.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -573,6 +573,9 @@ function buildApp(nsu, core) {
573
573
  wrongJoinPreview,
574
574
  wrongJoinRepair,
575
575
  wrongJoinUndo,
576
+ finishSetupPreview,
577
+ finishSetupApply,
578
+ finishSetupUndo,
576
579
  linkSite,
577
580
  received,
578
581
  getOne,
@@ -594,6 +597,9 @@ function buildApp(nsu, core) {
594
597
  invitations.get("/:id/wrong-join", requireAuth, wrongJoinPreview);
595
598
  invitations.put("/:id/wrong-join/repair", requireAuth, wrongJoinRepair);
596
599
  invitations.put("/:id/wrong-join/undo", requireAuth, wrongJoinUndo);
600
+ invitations.get("/:id/finish-setup", requireAuth, finishSetupPreview);
601
+ invitations.put("/:id/finish-setup", requireAuth, finishSetupApply);
602
+ invitations.put("/:id/finish-setup/undo", requireAuth, finishSetupUndo);
597
603
  invitations.put("/:id/link-site", requireAuth, linkSite);
598
604
  }
599
605
 
@@ -384,6 +384,145 @@ describe("SP invitation: email status, Super Admin alert, new company typed by s
384
384
  "the account now opens its own company",
385
385
  );
386
386
  });
387
+
388
+ // ------------------------- D. sign-in keeps landing in the sender (2026-10-01)
389
+
390
+ // The production state of latitude.sec@ after the 30 Sep Repair: the two
391
+ // memberships in JLL are inactive, but the account's default organisation
392
+ // still points at JLL - which is what every later sign-in opened.
393
+ async function repairedAccount(email) {
394
+ const site = new ObjectId();
395
+ await h.db.collection("sites").insertOne({ _id: site, name: "Latitude", orgId: id.pmOrg, status: "active" });
396
+ const inviteId = await invite(email, "security_agency", site);
397
+ await approve(inviteId);
398
+ const sid = await newAccount(email);
399
+ const user = await h.db.collection("users").findOne({ email });
400
+ const later = new Date(Date.now() + 1000);
401
+ await h.db.collection("members").insertMany([
402
+ { user: user._id, org: id.pmOrg, role: new ObjectId(), type: "organization", siteId: site, siteName: "Latitude", status: "inactive", createdAt: later },
403
+ { user: user._id, org: id.pmOrg, role: new ObjectId(), type: "security_agency", siteId: site, siteName: "Latitude", status: "inactive", createdAt: later },
404
+ ]);
405
+ await h.db.collection("users").updateOne({ _id: user._id }, { $set: { defaultOrg: id.pmOrg.toString() } });
406
+ return { site, inviteId, sid, user };
407
+ }
408
+
409
+ it("D1. Repair also clears a default organisation that points at the sender; Undo restores it", async () => {
410
+ const email = "repair-default@e2e.example.com";
411
+ const { inviteId, user } = await repairedAccount(email);
412
+ // memberships are already inactive: only the default org is left to fix
413
+ const preview = await h.api(`/service-provider-invitations/${inviteId}/wrong-join`, { sid: adminSid });
414
+ assert.equal(preview.body.memberships.length, 0);
415
+ assert.equal(preview.body.defaultOrgIsSender, true);
416
+
417
+ const repaired = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/repair`, { method: "PUT", sid: adminSid });
418
+ assert.equal(repaired.status, 200, JSON.stringify(repaired.body));
419
+ assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg ?? ""), "");
420
+
421
+ const undone = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/undo`, { method: "PUT", sid: adminSid });
422
+ assert.equal(undone.status, 200, JSON.stringify(undone.body));
423
+ assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), id.pmOrg.toString());
424
+ });
425
+
426
+ it("D2. Finish setup for invitee: Super Admin only; preview writes nothing; apply creates the typed company, links the site, accepts; undo reverses; apply again", async () => {
427
+ const email = "latitude-finish@e2e.example.com";
428
+ const { site, inviteId, sid, user } = await repairedAccount(email);
429
+ const NAME = "Henderson Security Services Pte Ltd";
430
+
431
+ for (const who of [pmSid, sid]) {
432
+ const refused = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { sid: who });
433
+ assert.ok([401, 403].includes(refused.status), JSON.stringify(refused.body));
434
+ const refusedPut = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: who, body: { companyName: NAME } });
435
+ assert.ok([401, 403].includes(refusedPut.status), JSON.stringify(refusedPut.body));
436
+ }
437
+
438
+ const orgsBefore = await h.db.collection("organizations").countDocuments();
439
+ const preview = await h.api(`/service-provider-invitations/${inviteId}/finish-setup?companyName=${encodeURIComponent(NAME)}`, { sid: adminSid });
440
+ assert.equal(preview.status, 200, JSON.stringify(preview.body));
441
+ assert.equal(preview.body.canApply, true, preview.body.blocked);
442
+ assert.equal(preview.body.companyName, NAME);
443
+ assert.equal(preview.body.service, "security_agency");
444
+ assert.equal(preview.body.usesExistingCompany, false);
445
+ assert.equal(preview.body.currentDefaultOrgName, "JLL Singapore");
446
+ assert.equal(await h.db.collection("organizations").countDocuments(), orgsBefore, "preview writes nothing");
447
+
448
+ const applied = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: adminSid, body: { companyName: ` ${NAME} ` } });
449
+ assert.equal(applied.status, 200, JSON.stringify(applied.body));
450
+ assert.equal(applied.body.message, `${NAME} is set up and linked to Latitude.`);
451
+ const org = await h.db.collection("organizations").findOne({ _id: new ObjectId(applied.body.orgId) });
452
+ assert.equal(org.name, NAME);
453
+ assert.equal(org.nature, "security_agency");
454
+ assert.equal(String(org.createdBy), user._id.toString(), "the invitee's company, not the Super Admin's");
455
+ const owners = await h.db.collection("members").find({ user: user._id, org: org._id, status: "active" }).toArray();
456
+ assert.deepEqual(owners.map((m) => m.type).sort(), ["organization", "security_agency"]);
457
+ assert.ok(await h.db.collection("customer.sites").findOne({ org: org._id, site, status: "active" }), "site linked");
458
+ assert.equal((await h.db.collection("verifications").findOne({ _id: new ObjectId(inviteId) })).status, "complete");
459
+ assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), org._id.toString());
460
+ // and the invitee's own sign-in now finds its Security company
461
+ const mine = await h.api(`/members/users/${user._id}`, { sid });
462
+ assert.ok(mine.body.items.some((m) => String(m.org) === org._id.toString() && m.type === "security_agency" && m.status === "active"));
463
+
464
+ // a second apply is refused (no longer waiting), nothing duplicated
465
+ const twice = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: adminSid, body: { companyName: NAME } });
466
+ assert.equal(twice.status, 400, JSON.stringify(twice.body));
467
+
468
+ const undone = await h.api(`/service-provider-invitations/${inviteId}/finish-setup/undo`, { method: "PUT", sid: adminSid });
469
+ assert.equal(undone.status, 200, JSON.stringify(undone.body));
470
+ assert.equal((await h.db.collection("verifications").findOne({ _id: new ObjectId(inviteId) })).status, "pending");
471
+ assert.equal(await h.db.collection("customer.sites").countDocuments({ org: org._id, site, status: "active" }), 0);
472
+ assert.equal(await h.db.collection("members").countDocuments({ org: org._id, status: "active" }), 0);
473
+ assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), id.pmOrg.toString());
474
+
475
+ const again = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: adminSid, body: { companyName: NAME } });
476
+ assert.equal(again.status, 200, JSON.stringify(again.body));
477
+ assert.notEqual(again.body.orgId, org._id.toString(), "a fresh company, not the undone one");
478
+ });
479
+
480
+ it("D3. existing account signs in to accept: the typed name (sanitised) + invited type + site link; plain sign-in can find the open invitation", async () => {
481
+ const email = "latitude-signin@e2e.example.com";
482
+ const { site, inviteId, sid, user } = await repairedAccount(email);
483
+
484
+ // plain sign-in (no invitation in the link): the account has no company,
485
+ // and the open invitation is found from its own session
486
+ const received = await h.api(`/service-provider-invitations/received`, { sid });
487
+ assert.equal(received.status, 200, JSON.stringify(received.body));
488
+ const row = received.body.items.find((r) => String(r._id) === inviteId);
489
+ assert.ok(row, "the pending invitation is listed for the signed-in invitee");
490
+ assert.equal(row.service, "security_agency");
491
+
492
+ const res = await h.api(`/service-provider-invitations/${inviteId}/accept-new-company`, {
493
+ method: "PUT",
494
+ sid,
495
+ body: { companyName: " <b>Henderson</b> Security Services Pte Ltd " },
496
+ });
497
+ assert.equal(res.status, 200, JSON.stringify(res.body));
498
+ assert.equal(res.body.companyName, "b Henderson /b Security Services Pte Ltd");
499
+ assert.equal(res.body.siteName, "Latitude");
500
+ const org = await h.db.collection("organizations").findOne({ _id: new ObjectId(res.body.orgId) });
501
+ assert.equal(org.nature, "security_agency");
502
+ assert.ok(await h.db.collection("customer.sites").findOne({ org: org._id, site, status: "active" }));
503
+ assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), org._id.toString());
504
+ });
505
+
506
+ it("D4. a new account creates: the same result", async () => {
507
+ const site = new ObjectId();
508
+ await h.db.collection("sites").insertOne({ _id: site, name: "Latitude", orgId: id.pmOrg, status: "active" });
509
+ const email = "brand-new-guard@e2e.example.com";
510
+ const inviteId = await invite(email, "security_agency", site);
511
+ await approve(inviteId);
512
+ const sid = await newAccount(email);
513
+ const res = await h.api(`/service-provider-invitations/${inviteId}/accept-new-company`, {
514
+ method: "PUT",
515
+ sid,
516
+ body: { companyName: "Henderson Security Services Pte Ltd", country: "Singapore" },
517
+ });
518
+ assert.equal(res.status, 200, JSON.stringify(res.body));
519
+ assert.equal(res.body.companyName, "Henderson Security Services Pte Ltd");
520
+ assert.equal(res.body.nature, "security_agency");
521
+ const org = await h.db.collection("organizations").findOne({ _id: new ObjectId(res.body.orgId) });
522
+ assert.equal(org.name, "Henderson Security Services Pte Ltd");
523
+ assert.ok(await h.db.collection("customer.sites").findOne({ org: org._id, site, status: "active" }));
524
+ assert.equal((await h.db.collection("verifications").findOne({ _id: new ObjectId(inviteId) })).status, "complete");
525
+ });
387
526
  });
388
527
 
389
528
  async function seed(h) {
@@ -13,6 +13,7 @@ import {
13
13
  isServiceProviderInviteExpired,
14
14
  serviceProviderInviteViewStatus,
15
15
  } from "./.build/utils/service-provider-invite.util.mjs";
16
+ import { sanitizeCompanyName } from "./.build/utils/service-provider-invite.util.mjs";
16
17
 
17
18
  /**
18
19
  * Inviting a service provider to a site.
@@ -391,3 +392,11 @@ test("accept is bound to a company the caller belongs to, and records which comp
391
392
  assert.match(cs, /"metadata\.providerOrg": providerOrgId/);
392
393
  assert.match(cs, /set: accepted,\s*session,/);
393
394
  });
395
+
396
+ test("sanitizeCompanyName: keeps the typed name, drops markup and control characters", () => {
397
+ assert.equal(sanitizeCompanyName(" Henderson Security Services Pte Ltd "), "Henderson Security Services Pte Ltd");
398
+ assert.equal(sanitizeCompanyName("<b>Acme</b>\u0000 Guards"), "b Acme /b Guards");
399
+ assert.equal(sanitizeCompanyName("x".repeat(300)).length, 120);
400
+ assert.equal(sanitizeCompanyName(null), "");
401
+ assert.equal(sanitizeCompanyName(" \n\t "), "");
402
+ });