diff --git a/src/common/validation/stellar-address.validator.ts b/src/common/validation/stellar-address.validator.ts new file mode 100644 index 0000000..a4c72f0 --- /dev/null +++ b/src/common/validation/stellar-address.validator.ts @@ -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' } + ); \ No newline at end of file diff --git a/src/db.ts b/src/db.ts index a148246..d3871df 100644 --- a/src/db.ts +++ b/src/db.ts @@ -26,3 +26,23 @@ export async function setNgoVerified(id: string, verified: boolean): Promise` + 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), + }; + } +} \ No newline at end of file diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts new file mode 100644 index 0000000..89224ef --- /dev/null +++ b/src/notifications/notifications.service.ts @@ -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 { + 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}`); + } + } +} \ No newline at end of file