forked from Yara724/api
Fixed users not being able to view their files
This commit is contained in:
58
src/helpers/blame-damaged-party.spec.ts
Normal file
58
src/helpers/blame-damaged-party.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Types } from "mongoose";
|
||||
import { partyPersonMatchesUser } from "./iran-mobile";
|
||||
import {
|
||||
blameDamagedPartyMatchesUser,
|
||||
resolveDamagedPartyPerson,
|
||||
resolveGuiltyPartyUserId,
|
||||
} from "./blame-damaged-party";
|
||||
|
||||
describe("blame damaged party helpers", () => {
|
||||
const guiltyId = new Types.ObjectId();
|
||||
const damagedId = new Types.ObjectId();
|
||||
|
||||
const thirdPartyBlame = {
|
||||
type: "THIRD_PARTY",
|
||||
expert: { decision: { guiltyPartyId: guiltyId } },
|
||||
parties: [
|
||||
{ role: "FIRST", person: { userId: guiltyId, phoneNumber: "09111111111" } },
|
||||
{ role: "SECOND", person: { userId: damagedId, phoneNumber: "09912356917" } },
|
||||
],
|
||||
};
|
||||
|
||||
it("resolves guilty party id from expert decision", () => {
|
||||
expect(resolveGuiltyPartyUserId(thirdPartyBlame)).toBe(String(guiltyId));
|
||||
});
|
||||
|
||||
it("resolves damaged party person as non-guilty", () => {
|
||||
expect(resolveDamagedPartyPerson(thirdPartyBlame)).toEqual(
|
||||
thirdPartyBlame.parties[1].person,
|
||||
);
|
||||
});
|
||||
|
||||
it("matches damaged party by userId even when claim.owner is guilty", () => {
|
||||
expect(
|
||||
blameDamagedPartyMatchesUser(
|
||||
thirdPartyBlame,
|
||||
{ sub: String(damagedId), username: "09912356917" },
|
||||
[],
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
blameDamagedPartyMatchesUser(
|
||||
thirdPartyBlame,
|
||||
{ sub: String(guiltyId), username: "09111111111" },
|
||||
[],
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches damaged party by phone when userId differs (duplicate accounts)", () => {
|
||||
expect(
|
||||
partyPersonMatchesUser(
|
||||
{ userId: damagedId, phoneNumber: "9123456789" },
|
||||
{ sub: "other-user-id", username: "09123456789" },
|
||||
[],
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
121
src/helpers/blame-damaged-party.ts
Normal file
121
src/helpers/blame-damaged-party.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { partyPersonMatchesUser } from "./iran-mobile";
|
||||
|
||||
/** Resolve guilty party user id from blame case (expert decision → statement → AGREED first party). */
|
||||
export function resolveGuiltyPartyUserId(blame: {
|
||||
parties?: Array<{
|
||||
role?: string;
|
||||
person?: { userId?: unknown };
|
||||
statement?: { admitsGuilt?: boolean };
|
||||
}>;
|
||||
expert?: { decision?: { guiltyPartyId?: unknown } };
|
||||
blameStatus?: string;
|
||||
}): string | null {
|
||||
const parties = blame?.parties ?? [];
|
||||
const decisionId = blame?.expert?.decision?.guiltyPartyId;
|
||||
if (decisionId != null && String(decisionId)) {
|
||||
return String(decisionId);
|
||||
}
|
||||
|
||||
const admitsGuilt = parties.find((p) => p.statement?.admitsGuilt === true);
|
||||
if (admitsGuilt?.person?.userId != null) {
|
||||
return String(admitsGuilt.person.userId);
|
||||
}
|
||||
|
||||
if (blame?.blameStatus === "AGREED") {
|
||||
const first = parties.find((p) => p.role === "FIRST");
|
||||
if (first?.person?.userId != null) {
|
||||
return String(first.person.userId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Person row for the damaged party on a blame case (claim beneficiary / user-panel actor). */
|
||||
export function resolveDamagedPartyPerson(blame: {
|
||||
type?: string;
|
||||
parties?: Array<{
|
||||
role?: string;
|
||||
person?: { userId?: unknown; phoneNumber?: string; clientId?: unknown };
|
||||
statement?: { admitsGuilt?: boolean; claimsDamage?: boolean };
|
||||
}>;
|
||||
expert?: { decision?: { guiltyPartyId?: unknown } };
|
||||
blameStatus?: string;
|
||||
}): { userId?: unknown; phoneNumber?: string; clientId?: unknown } | null {
|
||||
const parties = blame?.parties ?? [];
|
||||
if (!parties.length) return null;
|
||||
|
||||
const isCarBody =
|
||||
blame?.type === BlameRequestType.CAR_BODY || blame?.type === "CAR_BODY";
|
||||
|
||||
if (isCarBody) {
|
||||
const first = parties.find((p) => p.role === "FIRST");
|
||||
return first?.person ?? null;
|
||||
}
|
||||
|
||||
const guiltyId = resolveGuiltyPartyUserId(blame);
|
||||
if (!guiltyId) {
|
||||
const claimsDamage = parties.find((p) => p.statement?.claimsDamage === true);
|
||||
return claimsDamage?.person ?? null;
|
||||
}
|
||||
|
||||
const damagedParty = parties.find(
|
||||
(p) =>
|
||||
p.person?.userId != null && String(p.person.userId) !== String(guiltyId),
|
||||
);
|
||||
if (damagedParty?.person) return damagedParty.person;
|
||||
|
||||
const nonGuilty = parties.find(
|
||||
(p) => p.person && String(p.person.userId ?? "") !== String(guiltyId),
|
||||
);
|
||||
return nonGuilty?.person ?? null;
|
||||
}
|
||||
|
||||
export function blameDamagedPartyMatchesUser(
|
||||
blame: Parameters<typeof resolveDamagedPartyPerson>[0],
|
||||
user: { sub?: string; username?: string },
|
||||
linkedUserIdStrings: string[] = [],
|
||||
): boolean {
|
||||
const person = resolveDamagedPartyPerson(blame);
|
||||
if (!person) return false;
|
||||
return partyPersonMatchesUser(person, user, linkedUserIdStrings);
|
||||
}
|
||||
|
||||
/** Blame party that owns the claim row (`owner.userId` = guilty for THIRD_PARTY, first party for CAR_BODY). */
|
||||
export function resolveClaimOwnerParty(blame: {
|
||||
type?: string;
|
||||
parties?: Array<{
|
||||
role?: string;
|
||||
person?: { userId?: unknown; phoneNumber?: string; clientId?: unknown };
|
||||
statement?: { admitsGuilt?: boolean };
|
||||
}>;
|
||||
expert?: { decision?: { guiltyPartyId?: unknown } };
|
||||
blameStatus?: string;
|
||||
}): {
|
||||
role?: string;
|
||||
person?: { userId?: unknown; phoneNumber?: string; clientId?: unknown };
|
||||
} | null {
|
||||
const parties = blame?.parties ?? [];
|
||||
const isCarBody =
|
||||
blame?.type === BlameRequestType.CAR_BODY || blame?.type === "CAR_BODY";
|
||||
|
||||
if (isCarBody) {
|
||||
return parties.find((p) => p.role === "FIRST") ?? null;
|
||||
}
|
||||
|
||||
const guiltyId = resolveGuiltyPartyUserId(blame);
|
||||
if (!guiltyId) return null;
|
||||
return (
|
||||
parties.find(
|
||||
(p) => p.person?.userId != null && String(p.person.userId) === guiltyId,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDamagedPartyUserId(
|
||||
blame: Parameters<typeof resolveDamagedPartyPerson>[0],
|
||||
): string | null {
|
||||
const person = resolveDamagedPartyPerson(blame);
|
||||
return person?.userId != null ? String(person.userId) : null;
|
||||
}
|
||||
@@ -52,15 +52,19 @@ export function partyPersonMatchesUser(
|
||||
| null
|
||||
| undefined,
|
||||
user: { sub?: string; username?: string } | null | undefined,
|
||||
linkedUserIdStrings: string[] = [],
|
||||
): boolean {
|
||||
if (!person || !user) return false;
|
||||
if (
|
||||
person.userId != null &&
|
||||
user.sub &&
|
||||
String(person.userId) === String(user.sub)
|
||||
) {
|
||||
|
||||
const userIds = new Set<string>();
|
||||
if (user.sub) userIds.add(String(user.sub));
|
||||
for (const id of linkedUserIdStrings) {
|
||||
if (id) userIds.add(String(id));
|
||||
}
|
||||
if (person.userId != null && userIds.has(String(person.userId))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!person.phoneNumber || !user.username) return false;
|
||||
const stored =
|
||||
normalizeIranMobile(person.phoneNumber) ?? person.phoneNumber;
|
||||
|
||||
@@ -37,6 +37,22 @@ describe("party access queries", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("includes linked duplicate-account user ids", () => {
|
||||
const linked = new Types.ObjectId();
|
||||
const or = buildBlamePartyAccessOrConditions(
|
||||
{ sub: String(userId), username: "09123456789" },
|
||||
[String(linked)],
|
||||
);
|
||||
expect(or[0]).toEqual({
|
||||
$or: expect.arrayContaining([
|
||||
{ "parties.person.userId": String(userId) },
|
||||
{ "parties.person.userId": userId },
|
||||
{ "parties.person.userId": String(linked) },
|
||||
{ "parties.person.userId": linked },
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("partyPersonMatchesUser", () => {
|
||||
@@ -57,5 +73,16 @@ describe("party access queries", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches linked duplicate-account user ids", () => {
|
||||
const linked = new Types.ObjectId();
|
||||
expect(
|
||||
partyPersonMatchesUser(
|
||||
{ userId: linked, phoneNumber: "09111111111" },
|
||||
{ sub: String(userId), username: "09999999999" },
|
||||
[String(linked)],
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,36 @@ import { Types } from "mongoose";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { iranMobileLookupVariants } from "./iran-mobile";
|
||||
|
||||
/** ObjectId + string variants for one or more user ids (legacy duplicate accounts). */
|
||||
export function collectUserIdVariants(
|
||||
...ids: (string | Types.ObjectId | undefined | null)[]
|
||||
): unknown[] {
|
||||
const seen = new Set<string>();
|
||||
const out: unknown[] = [];
|
||||
for (const id of ids) {
|
||||
if (id == null || id === "") continue;
|
||||
const s = String(id);
|
||||
if (seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
out.push(s);
|
||||
if (Types.ObjectId.isValid(s)) {
|
||||
out.push(new Types.ObjectId(s));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildUserIdFieldOrFilter(
|
||||
fieldPath: string,
|
||||
idVariants: unknown[],
|
||||
): Record<string, unknown> | null {
|
||||
if (!idVariants.length) return null;
|
||||
if (idVariants.length === 1) {
|
||||
return { [fieldPath]: idVariants[0] };
|
||||
}
|
||||
return { $or: idVariants.map((id) => ({ [fieldPath]: id })) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mongo filter matching a user id field stored as ObjectId or legacy string.
|
||||
*/
|
||||
@@ -9,26 +39,23 @@ 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 })) };
|
||||
return buildUserIdFieldOrFilter(fieldPath, collectUserIdVariants(userSub));
|
||||
}
|
||||
|
||||
/** `$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>[] {
|
||||
export function buildBlamePartyAccessOrConditions(
|
||||
user: {
|
||||
sub?: string;
|
||||
username?: string;
|
||||
},
|
||||
linkedUserIdStrings: string[] = [],
|
||||
): Record<string, unknown>[] {
|
||||
const or: Record<string, unknown>[] = [];
|
||||
const userIdCond = user?.sub
|
||||
? mongoUserIdEqualityFilter("parties.person.userId", user.sub)
|
||||
: null;
|
||||
const idVariants = collectUserIdVariants(user.sub, ...linkedUserIdStrings);
|
||||
const userIdCond = buildUserIdFieldOrFilter(
|
||||
"parties.person.userId",
|
||||
idVariants,
|
||||
);
|
||||
if (userIdCond) or.push(userIdCond);
|
||||
|
||||
const phoneVariants = user?.username
|
||||
|
||||
52
src/helpers/user-access-resolver.ts
Normal file
52
src/helpers/user-access-resolver.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Types } from "mongoose";
|
||||
import { buildUserLookupByPhone } from "./iran-mobile";
|
||||
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
||||
import { collectUserIdVariants } from "./party-access-queries";
|
||||
|
||||
/** All Mongo user ids that may represent the same person (JWT sub + phone aliases). */
|
||||
export async function resolveLinkedUserIdStrings(
|
||||
userDbService: UserDbService,
|
||||
actor: { sub?: string; username?: string },
|
||||
): Promise<string[]> {
|
||||
const ids = new Set<string>();
|
||||
if (actor?.sub) ids.add(String(actor.sub));
|
||||
|
||||
if (actor?.username?.trim()) {
|
||||
const rows = await userDbService.findAllByPhone(actor.username);
|
||||
for (const row of rows) {
|
||||
const id = (row as { _id?: unknown })._id;
|
||||
if (id != null) ids.add(String(id));
|
||||
}
|
||||
}
|
||||
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
export async function resolveLinkedUserIdVariants(
|
||||
userDbService: UserDbService,
|
||||
actor: { sub?: string; username?: string },
|
||||
): Promise<unknown[]> {
|
||||
const strings = await resolveLinkedUserIdStrings(userDbService, actor);
|
||||
return collectUserIdVariants(...strings);
|
||||
}
|
||||
|
||||
export function ownerUserIdMatchesActor(
|
||||
ownerUserId: unknown,
|
||||
linkedUserIdStrings: string[],
|
||||
): boolean {
|
||||
if (ownerUserId == null || linkedUserIdStrings.length === 0) return false;
|
||||
const owner = String(ownerUserId);
|
||||
return linkedUserIdStrings.some((id) => id === owner);
|
||||
}
|
||||
|
||||
export function buildOwnerUserIdAccessFilter(
|
||||
linkedUserIdVariants: unknown[],
|
||||
): Record<string, unknown> | null {
|
||||
if (!linkedUserIdVariants.length) return null;
|
||||
if (linkedUserIdVariants.length === 1) {
|
||||
return { "owner.userId": linkedUserIdVariants[0] };
|
||||
}
|
||||
return {
|
||||
$or: linkedUserIdVariants.map((id) => ({ "owner.userId": id })),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user