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
23 changes: 23 additions & 0 deletions backend/src/graphql/__tests__/resolvers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ describe('GraphQL Resolvers', () => {
});

describe('Vault Resolver', () => {
it('should deny access when admin does not belong to org', async () => {
// Simulate Organization A's admin trying to access Organization B's vault
const mockOrgA = { id: 'orgA', admin_address: '0xadminA' };
const mockOrgB = { id: 'orgB', admin_address: '0xadminB' };
// Mock isAdminOfOrg to return false
jest.mock('../middleware/auth', () => ({
isAdminOfOrg: jest.fn().mockResolvedValue(false)
}));
await expect(vaultResolver.Query.vault(null, { address: '0xvaultB', orgId: mockOrgB.id, adminAddress: mockOrgA.admin_address }))
.rejects.toThrow('Access denied: admin does not belong to organization.');
});

it('should allow access when admin belongs to org', async () => {
const mockOrgA = { id: 'orgA', admin_address: '0xadminA' };
const mockVault = { id: '1', address: '0xvaultA', org_id: 'orgA', beneficiaries: [], subSchedules: [] };
// Mock isAdminOfOrg to return true
jest.mock('../middleware/auth', () => ({
isAdminOfOrg: jest.fn().mockResolvedValue(true)
}));
(models.Vault.findOne as jest.Mock).mockResolvedValue(mockVault);
const result = await vaultResolver.Query.vault(null, { address: '0xvaultA', orgId: mockOrgA.id, adminAddress: mockOrgA.admin_address });
expect(result).toEqual(mockVault);
});
describe('Query.vault', () => {
it('should fetch a vault by address', async () => {
const mockVault = {
Expand Down
14 changes: 14 additions & 0 deletions backend/src/graphql/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Utility to check if admin_address belongs to org_id
export const isAdminOfOrg = async (adminAddress: string, orgId: string) => {
if (!adminAddress || !orgId) return false;
try {
const { models } = require('../../models');
const org = await models.Organization.findOne({
where: { id: orgId, admin_address: adminAddress }
});
return !!org;
} catch (err) {
console.error('Error in isAdminOfOrg:', err);
return false;
}
};
import { GraphQLResolveInfo } from 'graphql';

export interface Context {
Expand Down
39 changes: 15 additions & 24 deletions backend/src/graphql/resolvers/vaultResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ const tvlService = require('../../services/tvlService');

export const vaultResolver = {
Query: {
vault: async (_: any, { address }: { address: string }) => {
vault: async (_: any, { address, orgId, adminAddress }: { address: string, orgId: string, adminAddress: string }) => {
try {
// Enforce org/admin check
const { isAdminOfOrg } = require('../middleware/auth');
const isAdmin = await isAdminOfOrg(adminAddress, orgId);
if (!isAdmin) {
throw new Error('Access denied: admin does not belong to organization.');
}
const vault = await models.Vault.findOne({
where: { address },
where: { address, org_id: orgId },
include: [
{
model: models.Beneficiary,
Expand All @@ -26,24 +32,17 @@ export const vaultResolver = {
}
},

vaults: async (_: any, { ownerAddress, first = 50, after }: { ownerAddress?: string, first?: number, after?: string }) => {
vaults: async (_: any, { orgId, adminAddress, first = 50, after }: { orgId: string, adminAddress: string, first?: number, after?: string }) => {
try {
// Check cache if ownerAddress is provided (user-specific query)
if (ownerAddress) {
const cacheKey = cacheService.getUserVaultsKey(ownerAddress);
const cachedVaults = await cacheService.get(cacheKey);

if (cachedVaults) {
console.log(`Cache hit for user vaults: ${ownerAddress}`);
return cachedVaults;
}
// Enforce org/admin check
const { isAdminOfOrg } = require('../middleware/auth');
const isAdmin = await isAdminOfOrg(adminAddress, orgId);
if (!isAdmin) {
throw new Error('Access denied: admin does not belong to organization.');
}

const whereClause = ownerAddress ? { owner_address: ownerAddress } : {};
const offset = after ? parseInt(after) : 0;

const vaults = await models.Vault.findAll({
where: whereClause,
where: { org_id: orgId },
include: [
{
model: models.Beneficiary,
Expand All @@ -58,14 +57,6 @@ export const vaultResolver = {
offset,
order: [['created_at', 'DESC']]
});

// Cache the result if ownerAddress is provided
if (ownerAddress) {
const cacheKey = cacheService.getUserVaultsKey(ownerAddress);
await cacheService.set(cacheKey, vaults);
console.log(`Cached user vaults for: ${ownerAddress}`);
}

return vaults;
} catch (error) {
console.error('Error fetching vaults:', error);
Expand Down
25 changes: 15 additions & 10 deletions backend/src/services/indexingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,24 @@ class IndexingService {
// Emit internal claim event for WebSocket gateway
claimEventEmitter.emit('claim', claim.toJSON());

// Fire webhook POST for DAOs
// Fire webhook POST for DAOs, but only if admin_address matches organization_id
const { OrganizationWebhook } = require('../models');
const { isAdminOfOrg } = require('../graphql/middleware/auth');
const axios = require('axios');
// Find webhooks for the organization (if vault has organization_id)
if (claim.organization_id) {
const webhooks = await OrganizationWebhook.findAll({ where: { organization_id: claim.organization_id } });
for (const webhook of webhooks) {
try {
await axios.post(webhook.webhook_url, claim.toJSON());
console.log(`Webhook fired: ${webhook.webhook_url}`);
} catch (err) {
console.error(`Webhook failed: ${webhook.webhook_url}`, err);
if (claim.organization_id && claim.admin_address) {
const isAdmin = await isAdminOfOrg(claim.admin_address, claim.organization_id);
if (isAdmin) {
const webhooks = await OrganizationWebhook.findAll({ where: { organization_id: claim.organization_id } });
for (const webhook of webhooks) {
try {
await axios.post(webhook.webhook_url, claim.toJSON());
console.log(`Webhook fired: ${webhook.webhook_url}`);
} catch (err) {
console.error(`Webhook failed: ${webhook.webhook_url}`, err);
}
}
} else {
console.warn('Webhook not fired: admin_address does not match organization_id');
}
}
return claim;
Expand Down