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
16 changes: 16 additions & 0 deletions src/common/validation/stellar-address.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { StrKey } from '@stellar/stellar-sdk';

export const stellarAddressSchema = z
.string()
.min(1, { message: 'Stellar address is required' })
.refine(
(val) => {
try {
return StrKey.isValidEd25519PublicKey(val);
} catch {
return false;
}
},
{ message: 'Invalid Stellar public key or bad checksum' }
);
20 changes: 20 additions & 0 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,23 @@ export async function setNgoVerified(id: string, verified: boolean): Promise<voi
data: { verified },
});
}


const DEFAULT_LOCAL_DATABASE_URL = 'postgresql://streamgive:streamgive@localhost:5432/streamgive';

export function getDatabaseUrl(): string {
const databaseUrl = process.env.DATABASE_URL;
const nodeEnv = process.env.NODE_ENV;

if (!databaseUrl) {
if (nodeEnv === 'production') {
throw new Error(
'FATAL: DATABASE_URL environment variable is required in production but was not provided.'
);
}
// Explicit fallback for local development / testing only
return DEFAULT_LOCAL_DATABASE_URL;
}

return databaseUrl;
}
45 changes: 45 additions & 0 deletions src/impacts/impact.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; // adjust path as needed

@Injectable()
export class ImpactService {
constructor(private readonly prisma: PrismaService) {}

async getPlatformImpact() {
// Use SQL aggregation instead of loading all streams into memory
const result = await this.prisma.$queryRaw<{ totalAmount: number; totalStreams: number }[]>`
SELECT
COALESCE(SUM(amount), 0) as "totalAmount",
COUNT(id) as "totalStreams"
FROM "Stream";
`;

return {
totalAmount: Number(result[0]?.totalAmount || 0),
totalStreams: Number(result[0]?.totalStreams || 0),
};
}

async getNgoImpact(ngoId: string) {
// Aggregate platform total and NGO-specific total in SQL
const [platformResult, ngoResult] = await Promise.all([
this.prisma.$queryRaw<{ totalAmount: number }[]>`
SELECT COALESCE(SUM(amount), 0) as "totalAmount" FROM "Stream";
`,
this.prisma.$queryRaw<{ totalAmount: number; totalStreams: number }[]>`
SELECT
COALESCE(SUM(amount), 0) as "totalAmount",
COUNT(id) as "totalStreams"
FROM "Stream"
WHERE "ngoId" = ${ngoId};
`,
]);

return {
ngoId,
ngoTotalAmount: Number(ngoResult[0]?.totalAmount || 0),
ngoTotalStreams: Number(ngoResult[0]?.totalStreams || 0),
platformTotalAmount: Number(platformResult[0]?.totalAmount || 0),
};
}
}
37 changes: 37 additions & 0 deletions src/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Logger } from '@nestjs/common';

export class NotificationsService {
private readonly logger = new Logger(NotificationsService.name);

async notifyWebhook(webhookUrl: string, payload: any): Promise<void> {
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

if (!response.ok) {
// Redact URL path to prevent leaking Slack/Discord webhook secrets
let redactedOrigin = 'unknown-origin';
try {
const parsedUrl = new URL(webhookUrl);
redactedOrigin = parsedUrl.origin;
} catch {
redactedOrigin = '[invalid-url]';
}

this.logger.error(
`webhook notification failed with status ${response.status} for ${redactedOrigin}`
);
}
} catch (error: any) {
let redactedOrigin = 'unknown-origin';
try {
redactedOrigin = new URL(webhookUrl).origin;
} catch {}

this.logger.error(`webhook notification error for ${redactedOrigin}: ${error.message}`);
}
}
}
Loading