feat: QR pairing for wallet import (backend + website)

- Backend: nullable wallet_number on wallet_share_requests (migration 0029);
  createPairingRequest + POST /api/patients/wallet/pair; applyShareResponse
  binds the authenticated wallet on QR (pairing) requests
- Frontend: Import dialog gains a "Show QR" mode rendering a temetro-pair QR
  (react-qr-code) the patient scans; requestWalletPairing helper; i18n keys

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-21 21:31:51 +03:00
parent 2d47abcc42
commit 7a359d4911
11 changed files with 4529 additions and 24 deletions
+27
View File
@@ -27,6 +27,33 @@ const requestSchema = z.object({
durationHours: z.number().positive().max(8760).optional(),
});
const pairSchema = z.object({
mode: z.enum(["permanent", "temporary"]).default("permanent"),
durationHours: z.number().positive().max(8760).optional(),
});
// Create a QR pairing request (no wallet number yet). Returns the request id +
// the ephemeral public key the device seals its bundle to; the clinic encodes
// both — plus its own relay URL — into the QR the patient scans.
patientsWalletRouter.post(
"/pair",
requirePermission({ patient: ["write"] }),
async (req, res, next) => {
try {
const input = pairSchema.parse(req.body);
const { view, ephemeralPubKey } = await walletShare.createPairingRequest(
req.organizationId!,
req.user!.id,
input.mode,
input.durationHours,
);
res.status(201).json({ ...view, ephemeralPubKey });
} catch (err) {
next(err);
}
},
);
// Start an import: validate the wallet number, mint a per-request ephemeral key,
// and relay an encrypted-share request to the patient's device. The clinician
// then polls the request until the patient approves on their phone.