Fixed users not being able to view their files

This commit is contained in:
2026-06-19 09:50:42 +03:30
parent 0c5756d325
commit 3c863bb90c
12 changed files with 745 additions and 222 deletions

View 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);
});
});

View 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;
}

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -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

View 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 })),
};
}