forked from Yara724/api
Fixed GET users
This commit is contained in:
61
src/helpers/party-access-queries.spec.ts
Normal file
61
src/helpers/party-access-queries.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Types } from "mongoose";
|
||||
import { partyPersonMatchesUser } from "./iran-mobile";
|
||||
import {
|
||||
buildBlamePartyAccessOrConditions,
|
||||
mongoUserIdEqualityFilter,
|
||||
} from "./party-access-queries";
|
||||
|
||||
describe("party access queries", () => {
|
||||
const userId = new Types.ObjectId();
|
||||
|
||||
describe("mongoUserIdEqualityFilter", () => {
|
||||
it("includes both ObjectId and string forms", () => {
|
||||
const filter = mongoUserIdEqualityFilter(
|
||||
"parties.person.userId",
|
||||
String(userId),
|
||||
);
|
||||
expect(filter).toEqual({
|
||||
$or: [
|
||||
{ "parties.person.userId": String(userId) },
|
||||
{ "parties.person.userId": userId },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildBlamePartyAccessOrConditions", () => {
|
||||
it("builds userId and phone branches", () => {
|
||||
const or = buildBlamePartyAccessOrConditions({
|
||||
sub: String(userId),
|
||||
username: "09123456789",
|
||||
});
|
||||
expect(or).toHaveLength(2);
|
||||
expect(or[0]).toHaveProperty("$or");
|
||||
expect(or[1]).toEqual({
|
||||
"parties.person.phoneNumber": {
|
||||
$in: expect.arrayContaining(["09123456789", "9123456789"]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("partyPersonMatchesUser", () => {
|
||||
it("matches party userId stored as ObjectId against string JWT sub", () => {
|
||||
expect(
|
||||
partyPersonMatchesUser(
|
||||
{ userId, phoneNumber: "09111111111" },
|
||||
{ sub: String(userId), username: "09999999999" },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches phone variants when userId is not set yet", () => {
|
||||
expect(
|
||||
partyPersonMatchesUser(
|
||||
{ phoneNumber: "9123456789" },
|
||||
{ sub: "other", username: "09123456789" },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
87
src/helpers/party-access-queries.ts
Normal file
87
src/helpers/party-access-queries.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Types } from "mongoose";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { iranMobileLookupVariants } from "./iran-mobile";
|
||||
|
||||
/**
|
||||
* Mongo filter matching a user id field stored as ObjectId or legacy string.
|
||||
*/
|
||||
export function mongoUserIdEqualityFilter(
|
||||
fieldPath: string,
|
||||
userSub: string,
|
||||
): Record<string, unknown> | null {
|
||||
if (!userSub) return null;
|
||||
const variants: unknown[] = [userSub];
|
||||
if (Types.ObjectId.isValid(userSub)) {
|
||||
variants.push(new Types.ObjectId(userSub));
|
||||
}
|
||||
if (variants.length === 1) {
|
||||
return { [fieldPath]: variants[0] };
|
||||
}
|
||||
return { $or: variants.map((v) => ({ [fieldPath]: v })) };
|
||||
}
|
||||
|
||||
/** `$or` branches: user is a party on a blame case (by userId and/or phone). */
|
||||
export function buildBlamePartyAccessOrConditions(user: {
|
||||
sub?: string;
|
||||
username?: string;
|
||||
}): Record<string, unknown>[] {
|
||||
const or: Record<string, unknown>[] = [];
|
||||
const userIdCond = user?.sub
|
||||
? mongoUserIdEqualityFilter("parties.person.userId", user.sub)
|
||||
: null;
|
||||
if (userIdCond) or.push(userIdCond);
|
||||
|
||||
const phoneVariants = user?.username
|
||||
? iranMobileLookupVariants(user.username)
|
||||
: [];
|
||||
if (phoneVariants.length > 0) {
|
||||
or.push({ "parties.person.phoneNumber": { $in: phoneVariants } });
|
||||
}
|
||||
return or;
|
||||
}
|
||||
|
||||
/** `$or` branches: user owns a claim or is tied via blame party membership. */
|
||||
export function buildClaimOwnerAccessOrConditions(
|
||||
userSub: string,
|
||||
username?: string,
|
||||
): Record<string, unknown>[] {
|
||||
const or: Record<string, unknown>[] = [];
|
||||
const ownerIdCond = mongoUserIdEqualityFilter("owner.userId", userSub);
|
||||
if (ownerIdCond) or.push(ownerIdCond);
|
||||
return or;
|
||||
}
|
||||
|
||||
export function buildFieldExpertInitiatedBlameFilter(
|
||||
expertSub: string,
|
||||
): Record<string, unknown> {
|
||||
const expertOid = Types.ObjectId.isValid(expertSub)
|
||||
? new Types.ObjectId(expertSub)
|
||||
: expertSub;
|
||||
return {
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertOid,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFieldExpertClaimAccessOrConditions(
|
||||
expertSub: string,
|
||||
expertBlameIds: unknown[],
|
||||
): Record<string, unknown>[] {
|
||||
const expertOid = Types.ObjectId.isValid(expertSub)
|
||||
? new Types.ObjectId(expertSub)
|
||||
: expertSub;
|
||||
const or: Record<string, unknown>[] = [
|
||||
{ initiatedByFieldExpertId: expertOid },
|
||||
];
|
||||
if (expertBlameIds.length > 0) {
|
||||
or.push({ blameRequestId: { $in: expertBlameIds } });
|
||||
}
|
||||
return or;
|
||||
}
|
||||
|
||||
/** True when actor is the field expert who initiated this blame (any JWT shape). */
|
||||
export function actorIsFieldExpertInitiator(
|
||||
actor: { sub?: string; role?: string } | null | undefined,
|
||||
): boolean {
|
||||
return actor?.role === RoleEnum.FIELD_EXPERT && !!actor?.sub;
|
||||
}
|
||||
86
src/helpers/tenant-scope.spec.ts
Normal file
86
src/helpers/tenant-scope.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
blameCaseInitiatedByFieldExpert,
|
||||
claimCaseInitiatedByFieldExpert,
|
||||
} from "./tenant-scope";
|
||||
|
||||
describe("field expert ownership helpers", () => {
|
||||
const expertId = new Types.ObjectId();
|
||||
const otherExpertId = new Types.ObjectId();
|
||||
const blameId = new Types.ObjectId();
|
||||
|
||||
describe("blameCaseInitiatedByFieldExpert", () => {
|
||||
it("matches when initiatedByFieldExpertId equals actor.sub (string)", () => {
|
||||
expect(
|
||||
blameCaseInitiatedByFieldExpert(
|
||||
{
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertId,
|
||||
},
|
||||
{ sub: String(expertId) },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not require actor.role on JWT", () => {
|
||||
expect(
|
||||
blameCaseInitiatedByFieldExpert(
|
||||
{
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertId,
|
||||
},
|
||||
{ sub: String(expertId) },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects another expert", () => {
|
||||
expect(
|
||||
blameCaseInitiatedByFieldExpert(
|
||||
{
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertId,
|
||||
},
|
||||
{ sub: String(otherExpertId) },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("claimCaseInitiatedByFieldExpert", () => {
|
||||
it("matches claim.initiatedByFieldExpertId", () => {
|
||||
expect(
|
||||
claimCaseInitiatedByFieldExpert(
|
||||
{ initiatedByFieldExpertId: expertId },
|
||||
{ sub: String(expertId) },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches via linked expert-initiated blame when claim field is missing", () => {
|
||||
expect(
|
||||
claimCaseInitiatedByFieldExpert(
|
||||
{ blameRequestId: blameId },
|
||||
{ sub: String(expertId) },
|
||||
{
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertId,
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects when neither claim nor blame links to actor", () => {
|
||||
expect(
|
||||
claimCaseInitiatedByFieldExpert(
|
||||
{ blameRequestId: blameId },
|
||||
{ sub: String(expertId) },
|
||||
{
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: otherExpertId,
|
||||
},
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BadRequestException, ForbiddenException } from "@nestjs/common";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
|
||||
/** Insurance company tenant id on the actor JWT (Mongo ObjectId string). */
|
||||
export function requireActorClientKey(actor: { clientKey?: string }): string {
|
||||
@@ -65,13 +64,12 @@ export function blameCaseAccessibleToExpert(
|
||||
return blameCaseTouchesClient(doc, ck);
|
||||
}
|
||||
|
||||
/** True when the acting field expert created this expert-initiated blame file. */
|
||||
/** True when `actor` is the field expert who created this expert-initiated blame file. */
|
||||
export function blameCaseInitiatedByFieldExpert(
|
||||
doc: any,
|
||||
actor: { sub: string; role?: string },
|
||||
actor: { sub: string },
|
||||
): boolean {
|
||||
return (
|
||||
actor.role === RoleEnum.FIELD_EXPERT &&
|
||||
!!doc?.expertInitiated &&
|
||||
!!doc?.initiatedByFieldExpertId &&
|
||||
String(doc.initiatedByFieldExpertId) === String(actor.sub)
|
||||
@@ -102,28 +100,40 @@ export function assertClaimCaseForTenant(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a field expert (no clientKey) initiated this claim via an
|
||||
* expert-initiated IN_PERSON blame file.
|
||||
* True when the field expert initiated this claim directly or via the linked
|
||||
* expert-initiated blame file (`blameRequestId` → blame.initiatedByFieldExpertId).
|
||||
*/
|
||||
export function claimCaseInitiatedByFieldExpert(
|
||||
doc: any,
|
||||
actor: { sub: string },
|
||||
blame?: { expertInitiated?: boolean; initiatedByFieldExpertId?: unknown } | null,
|
||||
): boolean {
|
||||
return (
|
||||
!!doc.initiatedByFieldExpertId &&
|
||||
if (
|
||||
doc?.initiatedByFieldExpertId &&
|
||||
String(doc.initiatedByFieldExpertId) === String(actor.sub)
|
||||
);
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
blame?.expertInitiated &&
|
||||
blame?.initiatedByFieldExpertId &&
|
||||
String(blame.initiatedByFieldExpertId) === String(actor.sub)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization check that works for both damage experts (clientKey tenant
|
||||
* scope) and field experts (initiatedByFieldExpertId ownership).
|
||||
* scope) and field experts (initiatedByFieldExpertId / linked blame ownership).
|
||||
*/
|
||||
export function assertClaimCaseForExpertActor(
|
||||
doc: any,
|
||||
actor: { sub: string; clientKey?: string },
|
||||
blame?: { expertInitiated?: boolean; initiatedByFieldExpertId?: unknown } | null,
|
||||
): void {
|
||||
if (claimCaseInitiatedByFieldExpert(doc, actor)) return;
|
||||
if (claimCaseInitiatedByFieldExpert(doc, actor, blame)) return;
|
||||
assertClaimCaseForTenant(doc, actor);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user