Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 2 additions & 74 deletions backend/package-lock.json

Large diffs are not rendered by default.

186 changes: 56 additions & 130 deletions backend/src/auth/services/authorization.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Dispute } from "../../entities/dispute.entity";
import { Participant } from "../../entities/participant.entity";
import { Split } from "../../entities/split.entity";
import { Group } from "../../group/entities/group.entity";
import { Receipt } from "../../receipts/entities/receipt.entity";
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Dispute } from '../../entities/dispute.entity';
import { Participant } from '../../entities/participant.entity';
import { Split } from '../../entities/split.entity';
import { Group } from '../../group/entities/group.entity';
import { Receipt } from '../../receipts/entities/receipt.entity';

@Injectable()
export class AuthorizationService {
Expand All @@ -22,50 +22,32 @@ export class AuthorizationService {
private groupRepository: Repository<Group>,
) {}

// Split authorization methods
async canAccessSplit(userId: string, splitId: string): Promise<boolean> {
const split = await this.splitRepository.findOne({
where: { id: splitId },
relations: ["participants"],
relations: ['participants'],
});

if (!split) {
return false;
}

// Check if user is a participant or creator
if (!split) return false;
return (
split.participants.some((p: Participant) => p.userId === userId) ||
split.creatorWalletAddress === userId
);
}

async canCreatePayment(userId: string, splitId: string): Promise<boolean> {
// Users can create payments for splits they participate in
return this.canAccessSplit(userId, splitId);
}

async canAddParticipant(userId: string, splitId: string): Promise<boolean> {
// Only split creator can add participants (or participants can invite others)
const split = await this.splitRepository.findOne({
where: { id: splitId },
});

const split = await this.splitRepository.findOne({ where: { id: splitId } });
return (
split?.creatorWalletAddress === userId ||
(await this.canAccessSplit(userId, splitId))
);
}

async canRemoveParticipant(
userId: string,
splitId: string,
): Promise<boolean> {
// Only split creator can remove participants
const split = await this.splitRepository.findOne({
where: { id: splitId },
});

async canRemoveParticipant(userId: string, splitId: string): Promise<boolean> {
const split = await this.splitRepository.findOne({ where: { id: splitId } });
return split?.creatorWalletAddress === userId;
}

Expand All @@ -74,142 +56,100 @@ export class AuthorizationService {
splitId: string,
participantId: string,
): Promise<boolean> {
// Users can only create payments for themselves or if they're the split creator
if (!(await this.canAccessSplit(userId, splitId))) {
return false;
}

if (!(await this.canAccessSplit(userId, splitId))) return false;
const participant = await this.participantRepository.findOne({
where: { id: participantId, splitId },
});

if (!participant) {
return false;
}

// User can create payment for themselves or if they're the creator
if (!participant) return false;
return (
participant.userId === userId ||
(await this.isSplitCreator(userId, splitId))
);
}

async canAccessParticipantPayments(
userId: string,
participantId: string,
): Promise<boolean> {
async canAccessParticipantPayments(userId: string, participantId: string): Promise<boolean> {
const participant = await this.participantRepository.findOne({
where: { id: participantId },
relations: ["split"],
relations: ['split'],
});

if (!participant) {
return false;
}

// User can access their own payments or if they can access the split
if (!participant) return false;
return (
participant.userId === userId ||
(await this.canAccessSplit(userId, participant.splitId))
);
}

// Receipt authorization methods
async canAccessReceipt(userId: string, receiptId: string): Promise<boolean> {
const receipt = await this.receiptRepository.findOne({
where: { id: receiptId },
relations: ["split"],
relations: ['split'],
});

if (!receipt) {
return false;
}

if (!receipt) return false;
return this.canAccessSplit(userId, receipt.splitId);
}

// Dispute authorization methods
async canAccessDispute(userId: string, disputeId: string): Promise<boolean> {
const dispute = await this.disputeRepository.findOne({
where: { id: disputeId },
});

if (!dispute) {
return false;
}

// Users can access disputes for splits they participate in
const dispute = await this.disputeRepository.findOne({ where: { id: disputeId } });
if (!dispute) return false;
return this.canAccessSplit(userId, dispute.splitId);
}

async isAdmin(userId: string): Promise<boolean> {
// TODO: Implement admin check based on user roles
// For now, return false - no admin functionality
return false;
}

// Group authorization methods
async canAccessGroup(userId: string, groupId: string): Promise<boolean> {
const group = await this.groupRepository.findOne({
where: { id: groupId },
});

if (!group) {
return false;
}

// Check if user is creator or member
const group = await this.groupRepository.findOne({ where: { id: groupId } });
if (!group) return false;
return (
group.creatorId === userId ||
group.members.some((member: any) => member.wallet === userId)
);
}

async canManageGroupMembers(
userId: string,
groupId: string,
): Promise<boolean> {
const group = await this.groupRepository.findOne({
where: { id: groupId },
});

if (!group) {
return false;
}

// Only creator and admins can manage members
async canManageGroupMembers(userId: string, groupId: string): Promise<boolean> {
const group = await this.groupRepository.findOne({ where: { id: groupId } });
if (!group) return false;
return (
group.creatorId === userId ||
group.members.some(
(member: any) => member.wallet === userId && member.role === "admin",
)
group.members.some((member: any) => member.wallet === userId && member.role === 'admin')
);
}

async canCreateGroupSplit(userId: string, groupId: string): Promise<boolean> {
// Any group member can create splits
return this.canAccessGroup(userId, groupId);
}

// Helper methods
private async isSplitCreator(
userId: string,
splitId: string,
): Promise<boolean> {
const split = await this.splitRepository.findOne({
where: { id: splitId },
// ? NEW - Can user generate a short link for this split?
async canGenerateShortLink(userId: string, splitId: string): Promise<boolean> {
return this.canAccessSplit(userId, splitId);
}

// ? NEW - Can user delete a short link?
async canDeleteShortLink(userId: string, splitId: string): Promise<boolean> {
return this.isSplitCreator(userId, splitId);
}

// ? NEW - Can user view analytics for a short link?
async canViewShortLinkAnalytics(userId: string, splitId: string): Promise<boolean> {
return this.isSplitCreator(userId, splitId);
}

// ? NEW - Is participant actually part of this split?
async isParticipantInSplit(participantId: string, splitId: string): Promise<boolean> {
const participant = await this.participantRepository.findOne({
where: { id: participantId, splitId },
});
return !!participant;
}

private async isSplitCreator(userId: string, splitId: string): Promise<boolean> {
const split = await this.splitRepository.findOne({ where: { id: splitId } });
return split?.creatorWalletAddress === userId;
}

// Batch authorization for multiple resources
async filterAccessibleSplits(
userId: string,
splitIds: string[],
): Promise<string[]> {
async filterAccessibleSplits(userId: string, splitIds: string[]): Promise<string[]> {
const splits = await this.splitRepository.findByIds(splitIds);

return splits
.filter(
(split: Split) =>
Expand All @@ -219,39 +159,25 @@ export class AuthorizationService {
.map((split: Split) => split.id);
}

async filterAccessibleReceipts(
userId: string,
receiptIds: string[],
): Promise<string[]> {
async filterAccessibleReceipts(userId: string, receiptIds: string[]): Promise<string[]> {
const receipts = await this.receiptRepository.findByIds(receiptIds);

const accessibleSplitIds = await this.filterAccessibleSplits(
userId,
receipts.map((r) => r.splitId),
);

return receipts
.filter((receipt: Receipt) =>
accessibleSplitIds.includes(receipt.splitId),
)
.filter((receipt: Receipt) => accessibleSplitIds.includes(receipt.splitId))
.map((receipt: Receipt) => receipt.id);
}

async filterAccessibleDisputes(
userId: string,
disputeIds: string[],
): Promise<string[]> {
async filterAccessibleDisputes(userId: string, disputeIds: string[]): Promise<string[]> {
const disputes = await this.disputeRepository.findByIds(disputeIds);

const accessibleSplitIds = await this.filterAccessibleSplits(
userId,
disputes.map((d) => d.splitId),
);

return disputes
.filter((dispute: Dispute) =>
accessibleSplitIds.includes(dispute.splitId),
)
.filter((dispute: Dispute) => accessibleSplitIds.includes(dispute.splitId))
.map((dispute: Dispute) => dispute.id);
}
}
10 changes: 5 additions & 5 deletions backend/src/short-links/entities/link-access-log.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import {
Column,
ManyToOne,
CreateDateColumn,
} from "typeorm";
import { SplitShortLink } from "./split-short-link.entity";
} from 'typeorm';
import { SplitShortLink } from './split-short-link.entity';

@Entity("link_access_logs")
@Entity('link_access_logs')
export class LinkAccessLog {
@PrimaryGeneratedColumn("uuid")
@PrimaryGeneratedColumn('uuid')
id!: string;

@ManyToOne(() => SplitShortLink, { onDelete: "CASCADE" })
@ManyToOne(() => SplitShortLink, { onDelete: 'CASCADE' })
shortLink!: SplitShortLink;

@CreateDateColumn()
Expand Down
Loading
Loading