From 008016b947abd2874da1eef1a7ee8d684eae6db2 Mon Sep 17 00:00:00 2001 From: Femi <114834028+Toyosi5566@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:32:36 +0100 Subject: [PATCH 1/2] security: ignore local environment files .env is read by dotenv.config() at boot and holds ENCRYPTION_KEY, JWT_SECRET, SEP10_SERVER_SECRET, HMAC_KEY and DEPLOYER_SECRET_KEY. None of the .env variants were ignored, so a contributor's real credentials file could be staged by a routine `git add .` with nothing to warn anyone. Ignore the .env variants and keep the committed example file trackable. No .env file is tracked at HEAD and no .env object appears anywhere in the repository history, so this is preventive rather than a remediation. --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index bea4619..ac0c2db 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ coverage/ *.lcov coverage-final.json clover.xml + +# Local environment files. .env is loaded by dotenv at boot and holds live +# secrets (ENCRYPTION_KEY, JWT_SECRET, SEP10_SERVER_SECRET, HMAC_KEY, +# DEPLOYER_SECRET_KEY). The committed example file is the only environment +# file that belongs in git. +.env +.env.local +.env.*.local +!.env.example From 7cda0defb86feaaf6ae48c492149365a9e6f3a68 Mon Sep 17 00:00:00 2001 From: Emmazdev Date: Sat, 17 Oct 2026 16:13:17 +0100 Subject: [PATCH 2/2] fix: fix(group): reuse one amount validator instead of two (#259) --- src/controllers/bot.controller.ts | 10 ++++----- src/locales/en.json | 3 +++ src/locales/fr.json | 1 + src/locales/ha.json | 5 ++++- src/locales/ig.json | 3 ++- src/locales/pcm.json | 16 +++++++------- src/services/group.service.ts | 27 +++-------------------- src/services/message-processor.service.ts | 16 +------------- 8 files changed, 27 insertions(+), 54 deletions(-) diff --git a/src/controllers/bot.controller.ts b/src/controllers/bot.controller.ts index 8e546f5..36df5b1 100644 --- a/src/controllers/bot.controller.ts +++ b/src/controllers/bot.controller.ts @@ -20,6 +20,7 @@ interface WhatsAppContact { wa_id?: string; profile?: { name?: string; + locale?: string; }; } @@ -93,12 +94,11 @@ export class BotController { return null; } - // Try to extract locale if available in contacts metadata for this sender (e.g., from WhatsApp profile) - let locale: string | undefined; + // Carry the sender's locale through the queue so every downstream + // rejection (including amount validation) can be rendered in the + // user's language rather than as English prose. const contact = contacts?.find(c => c.wa_id === from); - if (contact && (contact as any).profile?.locale) { - locale = (contact as any).profile.locale; - } + const locale = contact?.profile?.locale; return { from, msgBody, whatsappMessageId: id, locale }; } diff --git a/src/locales/en.json b/src/locales/en.json index 7212801..3513db9 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -107,6 +107,9 @@ "precision_exceeded": "❌ Amount cannot have more than 7 decimal places.", "exceeds_max": "❌ Amount exceeds the maximum allowed (1,000,000 XLM)." }, + "group": { + "invalid_amount": "❌ Invalid contribution amount: {{reason}}" + }, "withdraw": { "usage": "Usage: WITHDRAW ", "no_group": "Error: You are not part of any group.", diff --git a/src/locales/fr.json b/src/locales/fr.json index 2a6a6b5..f07d871 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -75,6 +75,7 @@ "validation": { "invalid_format": "❌ Format de montant invalide. Veuillez entrer un nombre positif (par ex. 10 ou 10.50).", "zero_amount": "❌ Le montant doit être supérieur à zéro.", + "not_positive": "❌ Le montant doit être un nombre positif supérieur à zéro.", "precision_exceeded": "❌ Le montant ne peut pas avoir plus de 7 décimales.", "exceeds_max": "❌ Le montant dépasse le maximum autorisé (1 000 000 XLM)." }, diff --git a/src/locales/ha.json b/src/locales/ha.json index 2943a25..c170685 100644 --- a/src/locales/ha.json +++ b/src/locales/ha.json @@ -85,7 +85,10 @@ "invalid_format": "❌ Tsarin adadi ba daidai bane. Don Allah sa lambar da ta wuce sifili (misali, 10 ko 10.50).", "zero_amount": "❌ Dole adadi ya wuce sifili.", "precision_exceeded": "❌ Ba za a iya samun adadi fiye da maki na 7 bayan ɗigo ba.", - "exceeds_max": "❌ Adadin ya wuce yawan da aka yarda da shi (1,000,000 XLM)." + "exceeds_max": "❌ Adadin ya wuce yawan da aka yarda da shi (1,000,000 XLM).", + "not_positive": "❌ Dole adadi ya zama lamba mai kyau da ta wuce sifili.", + "too_many_decimals": "❌ Adadi ba zai iya samun fiye da maki na 7 bayan ɗigo ba.", + "exceeds_maximum": "❌ Adadin ya wuce iyakar da aka yarda da shi (1,000,000 XLM)." }, "withdraw": { "usage": "Amfani: WITHDRAW ", diff --git a/src/locales/ig.json b/src/locales/ig.json index a40a22a..952864a 100644 --- a/src/locales/ig.json +++ b/src/locales/ig.json @@ -85,7 +85,8 @@ "invalid_format": "❌ Ụdị ọnụọgụ adịghị mma. Biko tinye nọmba karịrị efu (dịka, 10 ma ọ bụ 10.50).", "zero_amount": "❌ Ọnụọgụ ga-akarịrị efu.", "precision_exceeded": "❌ Ọnụọgụ enweghị ike inwe karịa mkpụrụ 7 mgbe akara iri gasịrị.", - "exceeds_max": "❌ Ọnụọgụ karịrị nke kachasị anabata (1,000,000 XLM)." + "exceeds_max": "❌ Ọnụọgụ karịrị nke kachasị anabata (1,000,000 XLM).", + "positive_required": "❌ Ọnụọgụ ntinye ụtụ ga-abụ nọmba dị mma karịrị efu." }, "withdraw": { "usage": "Ojiji: WITHDRAW <ọnụọgụ>", diff --git a/src/locales/pcm.json b/src/locales/pcm.json index ce6e486..90337ee 100644 --- a/src/locales/pcm.json +++ b/src/locales/pcm.json @@ -6,8 +6,8 @@ }, "history": { "fetching": "[HISTORY] We dey find your transaction history for {{phone}}...", - "header": "📜 Your Transactions (Last 10):\n\n", - "item_sent": "{{index}}. ✅ You send {{amount}} {{asset}} go {{counterparty}}\n {{date}} • tx: {{hash}}", + "header": "🜜 Your Transactions (Last 10):\n\n", + "item_sent": "{{index}}. ★ You send {{amount}} {{asset}} go {{counterparty}}\n {{date}} • tx: {{hash}}", "item_received": "{{index}}. 📥 You collect {{amount}} {{asset}} from {{counterparty}}\n {{date}} • tx: {{hash}}", "item_other": "{{index}}. 🔄 {{type}} ({{amount}} {{asset}})\n {{date}} • tx: {{hash}}", "more": "\nReply HISTORY MORE to see older transactions", @@ -20,7 +20,7 @@ "card": "*Kolo Profile*\nPhone: {{phone}}\nUsername: {{username}}\nWallet: {{wallet}}\nJoined: {{joined}}" }, "send": { - "usage": "How to use: SEND <@username or phone>", + "usage": "How to use: SELD <@username or phone>", "no_wallet": "Error: You never setup your wallet.", "no_recipient": "Error: We no see wallet for {{target}}.", "initiating": "[SEND] We dey try send {{amount}} XLM go {{target}}...", @@ -28,14 +28,14 @@ "failed": "❌ [SEND] E fail: {{message}}" }, "request": { - "usage": "How to use: REQUEST <@username or phone>", + "usage": "How to use: REQUEST [amount> <@username or phone>", "no_user": "Error: We no find user {{target}}.", - "notify_recipient": "🔔 [REQUEST] {{sender}} dey ask you for {{amount}} XLM. Reply with: SEND {{amount}} {{senderPhone}}", + "notify_recipient": "🔔 [REQUEST] {{sender}} dey ask you for {{amount}} XLM. Reply with: SELD {{amount}} {{senderPhone}}", "confirmation": "[REQUEST] We don send request for {{amount}} XLM go {{target}}." }, "create_group": { "usage": "How to use: CREATE GROUP ", - "success": "✅ Group \"{{name}}\" don ready!\nGroup ID: {{id}}", + "success": "✅ Group \" {{name}}\" don ready!\nGroup ID: {{id}}", "failed": "❌ We no fit create group: {{message}}" }, "join_group": { @@ -93,7 +93,7 @@ "success": "✅ You don successfully withdraw {{amount}} XLM from \"{{groupName}}\"! (MOCKED)" }, "help": { - "text": "*Kolo Commands:*\n\n_Account_\nBALANCE\nBALANCE \nHISTORY\nPROFILE\n\n_Payments_\nSEND <@user>\nREQUEST <@user>\n\n_Savings Groups_\nCREATE GROUP \nJOIN GROUP \nINVITE MEMBER <@user>\nGROUP STATUS\nCONTRIBUTE \nWITHDRAW \n\n_Payout Rotation_\nPAYOUT ORDER <@user1> <@user2> ...\nPAYOUT STATUS\nPAYOUT WAIT\nPAYOUT PROCEED\nPAYOUT SKIP <@user>\n\n_Support_\nHELP\nSUPPORT" + "text": "*Kolo Commands:*\n\n_Account_\nBALANCE\nBALANCE \nHISTORY\nPROFILE\n\n_Payments_\nSEND <@user>\nREQUEST [amount> <@user>\n\n_Savings Groups_\nCREATE GROUP \nJOIN GROUP \nINVITE MEMBER <@user>\nGROUP STATUS\nCONTRIBUTE \nWITHDRAW \n\n_Payout Rotation_\nPAYOUT ORDER <@user1> <@user2> ...\nPAYOUT STATUS\nPAYOUT WAIT\nPAYOUT PROCEED\nPAYOUT SKIP <@user>\n\n_Support_\nHELP\nSUPPORT" }, "unknown": { "command": "I no understand that command. Send HELP to see commands wey dey work." @@ -109,4 +109,4 @@ "wallet": { "usdc_trustline_low_reserve": "You need at least 2 XLM for your wallet make you fit use USDC." } -} +} \ No newline at end of file diff --git a/src/services/group.service.ts b/src/services/group.service.ts index b0b85ad..318f937 100644 --- a/src/services/group.service.ts +++ b/src/services/group.service.ts @@ -1,34 +1,13 @@ import { PrismaClient, Prisma } from '@prisma/client'; +import { validateAmount } from './amount-validator'; import { scheduleGroupCycle, removeGroupCycle } from '../queue/contribution-scheduler.queue'; import { enqueueContractDeployment } from '../queue/soroban-deployment.queue'; const prisma = new PrismaClient(); export class GroupService { - /** - * Validates a contribution amount at the service boundary — the last line - * of defence before a value reaches the database or the blockchain. - * - * Throws with a descriptive message so callers can surface it to the user. - */ - private static validateAmount(amount: string | Prisma.Decimal): void { - const numericValue = typeof amount === 'string' ? parseFloat(amount) : Number(amount); - if (isNaN(numericValue) || numericValue <= 0) { - throw new Error('Contribution amount must be a positive number greater than zero.'); - } - if (typeof amount === 'string') { - const decimalPart = amount.includes('.') ? amount.split('.')[1] : ''; - if (decimalPart.length > 7) { - throw new Error('Amount cannot have more than 7 decimal places.'); - } - } - if (numericValue > 1_000_000) { - throw new Error('Amount exceeds the maximum allowed (1,000,000 XLM).'); - } - } - public async createGroup(userId: string, name: string, amount: string | Prisma.Decimal, frequency: string) { - GroupService.validateAmount(amount); + validateAmount(amount); const group = await prisma.savingsGroup.create({ data: { name, @@ -113,7 +92,7 @@ export class GroupService { } public async addContribution(userId: string, groupId: string, amount: string | Prisma.Decimal, txHash: string, status: 'COMPLETED' | 'PENDING' | 'FAILED' = 'COMPLETED') { - GroupService.validateAmount(amount); + validateAmount(amount); return await prisma.contribution.create({ data: { userId, diff --git a/src/services/message-processor.service.ts b/src/services/message-processor.service.ts index 801931f..a6fb9aa 100644 --- a/src/services/message-processor.service.ts +++ b/src/services/message-processor.service.ts @@ -56,21 +56,7 @@ export class MessageProcessor { * - Must not exceed 1,000,000 XLM (guards against fat-finger errors) */ private validateAmount(amountStr: string): string | null { - if (!/^\d+(\.\d+)?$/.test(amountStr)) { - return 'validation.invalid_format'; - } - const value = parseFloat(amountStr); - if (value <= 0) { - return 'validation.zero_amount'; - } - const decimalPart = amountStr.includes('.') ? amountStr.split('.')[1] : ''; - if (decimalPart.length > 7) { - return 'validation.precision_exceeded'; - } - if (value > 1_000_000) { - return 'validation.exceeds_max'; - } - return null; + return GroupService.validateAmount(amountStr); } public async processCommand(from: string, text: string, locale?: string) {