-
Notifications
You must be signed in to change notification settings - Fork 4
Dev #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dev #24
Changes from all commits
8f2225c
29c50ab
04f60ad
73b5fea
0abf10e
60f097b
79bd94c
8c86ebd
2ed04b9
c73399f
87cf150
457ab3a
c5fcedd
868e454
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| SUPABASE_DATABASE_URL= | ||
| DIRECT_URL= | ||
| JWT_SECRET= | ||
| JWT_REFRESH_SECRET= | ||
| GOOGLE_CLIENT_ID= | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ node_modules | |
| dist | ||
| .next | ||
| coverage | ||
| test-results | ||
| .agents | ||
| .agent | ||
| .turbo | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| SUPABASE_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/fairshare | ||
| DIRECT_URL=postgresql://postgres:postgres@localhost:5432/fairshare | ||
| JWT_SECRET=fairshare_dev_jwt_secret_change_me | ||
| JWT_REFRESH_SECRET=fairshare_dev_jwt_refresh_secret_change_me | ||
| GOOGLE_CLIENT_ID=fairshare_dev_google_client_id | ||
| GOOGLE_CLIENT_SECRET=fairshare_dev_google_client_secret | ||
| REDIS_URL=redis://127.0.0.1:6380 | ||
| AWS_ACCESS_KEY_ID=fairshare_dev_aws_access_key | ||
| AWS_SECRET_ACCESS_KEY=fairshare_dev_aws_secret | ||
| AWS_REGION=ap-south-1 | ||
| S3_BUCKET=fairshare-dev-receipts | ||
| STRIPE_SECRET_KEY=sk_test_fairshare_dummy_key | ||
| STRIPE_WEBHOOK_SECRET=whsec_fairshare_dummy_webhook_secret | ||
| CORS_ORIGINS=http://localhost:3000,http://localhost:8081,exp://*:* | ||
| SENTRY_DSN= |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -83,6 +83,7 @@ export class GroupsService { | |||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| deletedAt: null, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| orderBy: { createdAt: 'desc' }, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
@@ -235,7 +236,10 @@ export class GroupsService { | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async getUserSummary(userId: string): Promise<{ totalBalanceCents: string }> { | ||||||||||||||||||||||||||||
| const balances = await this.prisma.balance.findMany({ | ||||||||||||||||||||||||||||
| where: { userId }, | ||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||
| group: { deletedAt: null } | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| select: { amountCents: true }, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -254,6 +258,7 @@ export class GroupsService { | |||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| deletedAt: null, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||
| _count: { | ||||||||||||||||||||||||||||
|
|
@@ -670,6 +675,48 @@ export class GroupsService { | |||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async delete(groupId: string, actorUserId: string): Promise<{ success: true }> { | ||||||||||||||||||||||||||||
| const group = await this.prisma.group.findUnique({ | ||||||||||||||||||||||||||||
| where: { id: groupId }, | ||||||||||||||||||||||||||||
| select: { id: true, deletedAt: true }, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (!group) { | ||||||||||||||||||||||||||||
| throw new NotFoundException('Group not found'); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (group.deletedAt) { | ||||||||||||||||||||||||||||
| throw new NotFoundException('Group has already been deleted'); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const membership = await this.prisma.groupMember.findUnique({ | ||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||
| groupId_userId: { | ||||||||||||||||||||||||||||
| groupId, | ||||||||||||||||||||||||||||
| userId: actorUserId, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (!membership || membership.role !== 'OWNER') { | ||||||||||||||||||||||||||||
| throw new ForbiddenException('Only the group owner can delete the group'); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| await this.prisma.group.update({ | ||||||||||||||||||||||||||||
| where: { id: groupId }, | ||||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||||
| deletedAt: new Date(), | ||||||||||||||||||||||||||||
| shareEnabled: false, | ||||||||||||||||||||||||||||
| shareToken: null | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
Comment on lines
+678
to
+712
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Soft-deleted groups are still accessible through existing member endpoints. This only hides deleted groups from list/dashboard-style queries. Because Suggested direction- private async assertMembership(groupId: string, userId: string): Promise<void> {
- const membership = await this.prisma.groupMember.findUnique({
- where: {
- groupId_userId: {
- groupId,
- userId,
- },
- },
- });
+ private async assertMembership(groupId: string, userId: string): Promise<void> {
+ const membership = await this.prisma.groupMember.findFirst({
+ where: {
+ groupId,
+ userId,
+ group: {
+ deletedAt: null,
+ },
+ },
+ });
if (!membership) {
throw new ForbiddenException('Actor is not a group member');
}
}The same rule needs to be applied to direct group reads/updates in this file, otherwise deleted groups remain operational through stale clients. 🧰 Tools🪛 Biome (2.4.10)[error] 677-677: Expected a semicolon or an implicit semicolon after a statement, but found none (parse) [error] 677-677: expected (parse) [error] 677-677: the target for a delete operator cannot be a single identifier (parse) [error] 677-677: Expected a semicolon or an implicit semicolon after a statement, but found none (parse) [error] 677-677: Expected a statement but instead found '>'. (parse) 🪛 GitHub Actions: CI[error] Command failed with exit code 1: tsc --noEmit (backend@1.0.0 lint). 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| await this.redis.invalidateGroupCache(groupId); | ||||||||||||||||||||||||||||
| await this.redis.invalidateUserDashboardCache(actorUserId); | ||||||||||||||||||||||||||||
|
Comment on lines
+714
to
+715
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other group members' dashboard caches are not invalidated. Only the actor's dashboard cache is invalidated. Other group members will continue seeing the deleted group in their cached dashboards until the cache TTL (120s) expires. Consider invalidating dashboard caches for all group members. 🛠️ Suggested approach+ // Fetch all member IDs before invalidation
+ const members = await this.prisma.groupMember.findMany({
+ where: { groupId },
+ select: { userId: true },
+ });
await this.redis.invalidateGroupCache(groupId);
- await this.redis.invalidateUserDashboardCache(actorUserId);
+ // Invalidate dashboard cache for all members
+ await Promise.all(
+ members.map((m) => this.redis.invalidateUserDashboardCache(m.userId))
+ );
return { success: true };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async resolvePendingInvites(userId: string, email: string): Promise<void> { | ||||||||||||||||||||||||||||
| const invites = await this.prisma.groupInvite.findMany({ | ||||||||||||||||||||||||||||
| where: { email: email.toLowerCase() }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Arun-kushwaha007/FairShare
Length of output: 3109
🏁 Script executed:
Repository: Arun-kushwaha007/FairShare
Length of output: 2835
Enforce soft-delete filters on all user-facing group read operations.
Current group queries bypass soft-delete checks in several critical paths:
getGroupById()(line 114): NodeletedAtfilter; returns deleted groupstoggleShare()(line 380): NodeletedAtfilter on lookup; allows modifications to deleted groupsgetGroupByShareToken()(line 418): NodeletedAtfilter; public share links to deleted groups remain accessiblebalances.service.ts(line 50): NodeletedAtfilter; deleted group data can be exportedAll group reads must include
where: { ..., deletedAt: null }or equivalent guard to prevent access to deleted groups.🧰 Tools
🪛 GitHub Actions: CI
[error] Command failed with exit code 1: tsc --noEmit (backend@1.0.0 lint).
🤖 Prompt for AI Agents