forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaccess-scope.service.ts
More file actions
147 lines (129 loc) · 6.1 KB
/
Copy pathaccess-scope.service.ts
File metadata and controls
147 lines (129 loc) · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { Dispute } from '../../entities/dispute.entity';
import { Participant } from '../../entities/participant.entity';
import { Receipt } from '../../receipts/entities/receipt.entity';
import { Split } from '../../entities/split.entity';
import { Group } from '../../group/entities/group.entity';
/**
* Resource-scoped access helpers extracted from AuthorizationService (issue #370).
* Each method encapsulates exactly one resource policy with deterministic queries.
*/
@Injectable()
export class AccessScopeService {
constructor(
@InjectRepository(Split) private readonly splitRepo: Repository<Split>,
@InjectRepository(Participant) private readonly participantRepo: Repository<Participant>,
@InjectRepository(Receipt) private readonly receiptRepo: Repository<Receipt>,
@InjectRepository(Dispute) private readonly disputeRepo: Repository<Dispute>,
@InjectRepository(Group) private readonly groupRepo: Repository<Group>,
) {}
// ── Split scope ────────────────────────────────────────────────────────────
async isSplitCreator(userId: string, splitId: string): Promise<boolean> {
const split = await this.splitRepo.findOne({
where: { id: splitId },
select: ['creatorWalletAddress'],
});
return split?.creatorWalletAddress === userId;
}
async isSplitParticipant(userId: string, splitId: string): Promise<boolean> {
const count = await this.participantRepo.count({ where: { userId, splitId } });
return count > 0;
}
async canAccessSplit(userId: string, splitId: string): Promise<boolean> {
const [isCreator, isParticipant] = await Promise.all([
this.isSplitCreator(userId, splitId),
this.isSplitParticipant(userId, splitId),
]);
return isCreator || isParticipant;
}
async filterAccessibleSplits(userId: string, splitIds: string[]): Promise<string[]> {
if (splitIds.length === 0) return [];
const splits = await this.splitRepo.find({ where: { id: In(splitIds) } });
const participantRows = await this.participantRepo.find({
where: { userId, splitId: In(splitIds) },
select: ['splitId'],
});
const participantSplitIds = new Set(participantRows.map((p) => p.splitId));
return splits
.filter((s) => s.creatorWalletAddress === userId || participantSplitIds.has(s.id))
.map((s) => s.id);
}
// ── Participant scope ──────────────────────────────────────────────────────
async canAccessParticipantPayments(userId: string, participantId: string): Promise<boolean> {
const participant = await this.participantRepo.findOne({
where: { id: participantId },
select: ['userId', 'splitId'],
});
if (!participant) return false;
return participant.userId === userId || this.canAccessSplit(userId, participant.splitId);
}
async isParticipantInSplit(participantId: string, splitId: string): Promise<boolean> {
const count = await this.participantRepo.count({ where: { id: participantId, splitId } });
return count > 0;
}
// ── Receipt scope ──────────────────────────────────────────────────────────
async canAccessReceipt(userId: string, receiptId: string): Promise<boolean> {
const receipt = await this.receiptRepo.findOne({
where: { id: receiptId },
select: ['splitId'],
});
if (!receipt) return false;
return this.canAccessSplit(userId, receipt.splitId);
}
async filterAccessibleReceipts(userId: string, receiptIds: string[]): Promise<string[]> {
if (receiptIds.length === 0) return [];
const receipts = await this.receiptRepo.find({
where: { id: In(receiptIds) },
select: ['id', 'splitId'],
});
const accessibleSplitIds = new Set(
await this.filterAccessibleSplits(userId, receipts.map((r) => r.splitId)),
);
return receipts.filter((r) => accessibleSplitIds.has(r.splitId)).map((r) => r.id);
}
// ── Dispute scope ──────────────────────────────────────────────────────────
async canAccessDispute(userId: string, disputeId: string): Promise<boolean> {
const dispute = await this.disputeRepo.findOne({
where: { id: disputeId },
select: ['splitId'],
});
if (!dispute) return false;
return this.canAccessSplit(userId, dispute.splitId);
}
async filterAccessibleDisputes(userId: string, disputeIds: string[]): Promise<string[]> {
if (disputeIds.length === 0) return [];
const disputes = await this.disputeRepo.find({
where: { id: In(disputeIds) },
select: ['id', 'splitId'],
});
const accessibleSplitIds = new Set(
await this.filterAccessibleSplits(userId, disputes.map((d) => d.splitId)),
);
return disputes.filter((d) => accessibleSplitIds.has(d.splitId)).map((d) => d.id);
}
// ── Group scope ────────────────────────────────────────────────────────────
async isGroupMember(userId: string, groupId: string): Promise<boolean> {
const group = await this.groupRepo.findOne({
where: { id: groupId },
relations: ['members'],
});
if (!group) return false;
return (
group.creatorId === userId ||
group.members.some((m) => m.wallet === userId)
);
}
async isGroupAdmin(userId: string, groupId: string): Promise<boolean> {
const group = await this.groupRepo.findOne({
where: { id: groupId },
relations: ['members'],
});
if (!group) return false;
return (
group.creatorId === userId ||
group.members.some((m) => m.wallet === userId && m.role === 'admin')
);
}
}