Skip to content

fix: public payment PII leak, internal-wallet auth, broken groups & admin guards - #268

Merged
martinzhames merged 1 commit into
dupdab:mainfrom
dev-fani:fix/public-payment-pii-internal-guards-user-id-admin-guard
Sep 1, 2026
Merged

fix: public payment PII leak, internal-wallet auth, broken groups & admin guards#268
martinzhames merged 1 commit into
dupdab:mainfrom
dev-fani:fix/public-payment-pii-internal-guards-user-id-admin-guard

Conversation

@dev-fani

Copy link
Copy Markdown

Summary

Fixes four issues — two security, two correctness — across payments, blockchain-wallet, groups and auth.

Issue Category Fix
#200 security Public GET /pay/:reference no longer returns the raw Payment entity
#201 security InternalWalletController is now actually internal-only, not internal-by-convention
#202 bug Groups write path no longer passes undefined as the user id
#203 bug AdminGuard now recognises real admins instead of 403-ing everyone

#200 — Public GET /pay/:reference exposed full payment records incl. PII

The public (deliberately unauthenticated) payment page called findByReference() and serialized the raw Payment entity, leaking customerEmail, customerWalletAddress, merchant-supplied metadata, and internal fields (merchantId, settlementId, fee/settlement figures, tx hashes) to anyone holding a shareable payment reference.

  • New PublicPaymentViewDto (src/payments/dto/public-payment-view.dto.ts) exposing only what a payment page needs to render: reference, amounts, currency, network, status, deposit address/memo, description, QR, expiry.
  • PublicPaymentController.getByReference() now maps through PublicPaymentViewDto.from(payment) instead of returning the entity.

#201InternalWalletController had zero auth guards

POST /internal/wallet/provision had no @UseGuards(...); its "internal only" status was a comment, not enforcement. Anyone reaching the ingress could provision/probe Stellar wallets for arbitrary userIds.

  • New InternalServiceGuard (src/auth/guards/internal-service.guard.ts): requires an x-internal-service-secret header matching INTERNAL_SERVICE_SECRET, compared in constant time (SHA-256 + timingSafeEqual). Fails closed — if the secret is not configured the endpoint returns 401 rather than being open.
  • InternalWalletController is annotated with @UseGuards(InternalServiceGuard) and documents the required header in Swagger.
  • INTERNAL_SERVICE_SECRET added to .env.example and to the Joi config schema (min(16), required).

#202JwtStrategy never sets .id, but GroupsController read req.user.id

JwtStrategy.validate() and the API-key branch of JwtAuthGuard produce { merchantId, email, role } — never id. Every mutating handler in GroupsController read req.user.id, so undefined flowed into GroupMember.userId (a non-nullable uuid column) and every create/join/update/gate request failed at the DB layer.

  • All req.user.id references in GroupsController changed to req.user.merchantId.
  • New src/groups/groups.controller.spec.ts asserts each mutating handler (create, update, remove, regenerateInviteCode, join, setGate, removeGate) forwards the real merchantId and never undefined.

#203AdminGuard checked user.isAdmin, a field that is never set

The Merchant entity only has role: MerchantRole (ADMIN/MERCHANT/SUPERADMIN); nothing ever sets isAdmin on the request user. Every AdminGuard-protected route (incl. AdminMerchantsController bulk suspend/activate) was a permanent 403.

  • AdminGuard now allows user.role === MerchantRole.ADMIN || user.role === MerchantRole.SUPERADMIN against the fields JwtStrategy actually populates.
  • New src/auth/guards/admin.guard.spec.ts asserts admin & superadmin pass, plain merchant / missing user / legacy isAdmin flag are rejected.

Testing

New unit/controller specs added for #201, #202 and #203; existing test/blockchain-wallet.e2e-spec.ts updated to stub the new InternalServiceGuard. Note: node_modules is not installed in this environment, so the suite was not executed locally — CI should run it.

closes #200
closes #201
closes #202
closes #203

…dmin guard

- dupdab#200: add PublicPaymentViewDto and return it from GET /pay/:reference so the
  unauthenticated public payment page no longer exposes customerEmail,
  customerWalletAddress, merchant metadata, merchantId or settlementId.
- dupdab#201: protect InternalWalletController with a new InternalServiceGuard that
  requires a constant-time-checked x-internal-service-secret shared secret and
  fails closed when INTERNAL_SERVICE_SECRET is unset.
- dupdab#202: GroupsController now reads req.user.merchantId (the shape JwtStrategy
  actually produces) instead of the always-undefined req.user.id, plus a
  controller spec covering every mutating handler.
- dupdab#203: AdminGuard now checks user.role against MerchantRole.ADMIN/SUPERADMIN
  instead of the never-populated user.isAdmin flag, plus a guard spec.

closes dupdab#200
closes dupdab#201
closes dupdab#202
closes dupdab#203
@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@dev-fani Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@martinzhames
martinzhames merged commit 784a947 into dupdab:main Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants