@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/CHANGELOG.md +6 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.js +236 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -119
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +6 -0
- package/test/e2e/service-provider-invite-email-new-company.e2e.test.mjs +139 -0
- package/test/service-provider-invite.test.mjs +9 -0
package/package.json
CHANGED
package/test/e2e/harness.mjs
CHANGED
|
@@ -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
|
+
});
|