fix(security): add input validation for POST/PATCH endpoints
- Install express-validator package - Add validation to beneficiaries.js: - POST /: name (string 1-200), phone (optional), address (optional) - PATCH /🆔 name (string 1-200), phone, address, customName (max 100) - Add validation to stripe.js: - create-checkout-session: userId, beneficiaryName, beneficiaryAddress, email - create-portal-session: customerId (string) - create-payment-sheet: email (valid email), amount (positive int) - create-subscription: beneficiaryId (int), paymentMethodId (string) - cancel-subscription: beneficiaryId (int) - reactivate-subscription: beneficiaryId (int) - create-subscription-payment-sheet: beneficiaryId (int) - confirm-subscription-payment: subscriptionId (string) - Add validation to invitations.js: - POST /: beneficiaryId (int), role (enum: caretaker/guardian), email (valid) - POST /accept: code (string) - POST /accept-public: code (string) - PATCH /🆔 role (enum: caretaker/guardian) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a055e1b6f8
commit
4a4fc5c077
29
backend/package-lock.json
generated
29
backend/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"express-validator": "^7.3.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
@ -2283,6 +2284,19 @@
|
|||||||
"express": ">= 4.11"
|
"express": ">= 4.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-validator": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"validator": "~13.15.23"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-xml-parser": {
|
"node_modules/fast-xml-parser": {
|
||||||
"version": "5.2.5",
|
"version": "5.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
||||||
@ -2703,6 +2717,12 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@ -3603,6 +3623,15 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/validator": {
|
||||||
|
"version": "13.15.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
||||||
|
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"express-validator": "^7.3.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const Stripe = require('stripe');
|
const Stripe = require('stripe');
|
||||||
|
const { body, validationResult } = require('express-validator');
|
||||||
const { supabase } = require('../config/supabase');
|
const { supabase } = require('../config/supabase');
|
||||||
const storage = require('../services/storage');
|
const storage = require('../services/storage');
|
||||||
const legacyAPI = require('../services/legacyAPI');
|
const legacyAPI = require('../services/legacyAPI');
|
||||||
@ -362,15 +363,32 @@ router.get('/:id', async (req, res) => {
|
|||||||
* Now uses the proper beneficiaries table (not users)
|
* Now uses the proper beneficiaries table (not users)
|
||||||
* AUTO-CREATES FIRST DEPLOYMENT
|
* AUTO-CREATES FIRST DEPLOYMENT
|
||||||
*/
|
*/
|
||||||
router.post('/', async (req, res) => {
|
router.post('/',
|
||||||
|
[
|
||||||
|
body('name')
|
||||||
|
.isString().withMessage('name must be a string')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 200 }).withMessage('name must be between 1 and 200 characters'),
|
||||||
|
body('phone')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('phone must be a string')
|
||||||
|
.trim(),
|
||||||
|
body('address')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('address must be a string')
|
||||||
|
.trim()
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
const { name, phone, address } = req.body;
|
const { name, phone, address } = req.body;
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
return res.status(400).json({ error: 'name is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[BENEFICIARY] Creating beneficiary:', { userId, name });
|
console.log('[BENEFICIARY] Creating beneficiary:', { userId, name });
|
||||||
|
|
||||||
// Create beneficiary in the proper beneficiaries table (not users!)
|
// Create beneficiary in the proper beneficiaries table (not users!)
|
||||||
@ -563,8 +581,35 @@ router.post('/', async (req, res) => {
|
|||||||
* - name, phone, address: beneficiary data (custodian only)
|
* - name, phone, address: beneficiary data (custodian only)
|
||||||
* - customName: user's personal alias for this beneficiary (any role)
|
* - customName: user's personal alias for this beneficiary (any role)
|
||||||
*/
|
*/
|
||||||
router.patch('/:id', async (req, res) => {
|
router.patch('/:id',
|
||||||
|
[
|
||||||
|
body('name')
|
||||||
|
.optional()
|
||||||
|
.isString().withMessage('name must be a string')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 200 }).withMessage('name must be between 1 and 200 characters'),
|
||||||
|
body('phone')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('phone must be a string')
|
||||||
|
.trim(),
|
||||||
|
body('address')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('address must be a string')
|
||||||
|
.trim(),
|
||||||
|
body('customName')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('customName must be a string')
|
||||||
|
.trim()
|
||||||
|
.isLength({ max: 100 }).withMessage('customName must be 100 characters or less')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
const beneficiaryId = parseInt(req.params.id, 10);
|
const beneficiaryId = parseInt(req.params.id, 10);
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const { body, validationResult } = require('express-validator');
|
||||||
const { supabase } = require('../config/supabase');
|
const { supabase } = require('../config/supabase');
|
||||||
const { sendInvitationEmail } = require('../services/email');
|
const { sendInvitationEmail } = require('../services/email');
|
||||||
|
|
||||||
@ -78,14 +79,23 @@ router.get('/info/:code', async (req, res) => {
|
|||||||
* POST /api/invitations/accept-public
|
* POST /api/invitations/accept-public
|
||||||
* Used from web page - no login required
|
* Used from web page - no login required
|
||||||
*/
|
*/
|
||||||
router.post('/accept-public', async (req, res) => {
|
router.post('/accept-public',
|
||||||
|
[
|
||||||
|
body('code')
|
||||||
|
.notEmpty().withMessage('code is required')
|
||||||
|
.isString().withMessage('code must be a string')
|
||||||
|
.trim()
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { code } = req.body;
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
if (!code) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(400).json({ error: 'Code is required' });
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { code } = req.body;
|
||||||
|
|
||||||
console.log('[INVITE] Public accept:', { code });
|
console.log('[INVITE] Public accept:', { code });
|
||||||
|
|
||||||
// Find invitation by code
|
// Find invitation by code
|
||||||
@ -232,21 +242,37 @@ function generateInviteToken() {
|
|||||||
* POST /api/invitations
|
* POST /api/invitations
|
||||||
* Creates an invitation for someone to access a beneficiary
|
* Creates an invitation for someone to access a beneficiary
|
||||||
*/
|
*/
|
||||||
router.post('/', async (req, res) => {
|
router.post('/',
|
||||||
|
[
|
||||||
|
body('beneficiaryId')
|
||||||
|
.notEmpty().withMessage('beneficiaryId is required')
|
||||||
|
.isInt({ min: 1 }).withMessage('beneficiaryId must be a positive integer'),
|
||||||
|
body('role')
|
||||||
|
.notEmpty().withMessage('role is required')
|
||||||
|
.isIn(['caretaker', 'guardian']).withMessage('role must be caretaker or guardian'),
|
||||||
|
body('email')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isEmail().withMessage('email must be a valid email address')
|
||||||
|
.normalizeEmail(),
|
||||||
|
body('label')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('label must be a string')
|
||||||
|
.trim()
|
||||||
|
.isLength({ max: 100 }).withMessage('label must be 100 characters or less')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
const { beneficiaryId, role, email, label } = req.body;
|
const { beneficiaryId, role, email, label } = req.body;
|
||||||
|
|
||||||
console.log('[INVITE] Creating invitation:', { userId, beneficiaryId, role, email });
|
console.log('[INVITE] Creating invitation:', { userId, beneficiaryId, role, email });
|
||||||
|
|
||||||
if (!beneficiaryId || !role) {
|
|
||||||
return res.status(400).json({ error: 'beneficiaryId and role are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['caretaker', 'guardian'].includes(role)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid role. Must be: caretaker or guardian' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current user's email to check self-invite
|
// Get current user's email to check self-invite
|
||||||
const { data: currentUser, error: userError } = await supabase
|
const { data: currentUser, error: userError } = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
@ -439,15 +465,24 @@ router.get('/beneficiary/:beneficiaryId', async (req, res) => {
|
|||||||
* POST /api/invitations/accept
|
* POST /api/invitations/accept
|
||||||
* Accepts an invitation code
|
* Accepts an invitation code
|
||||||
*/
|
*/
|
||||||
router.post('/accept', async (req, res) => {
|
router.post('/accept',
|
||||||
|
[
|
||||||
|
body('code')
|
||||||
|
.notEmpty().withMessage('code is required')
|
||||||
|
.isString().withMessage('code must be a string')
|
||||||
|
.trim()
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
const { code } = req.body;
|
const { code } = req.body;
|
||||||
|
|
||||||
if (!code) {
|
|
||||||
return res.status(400).json({ error: 'Code is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find valid invitation (no expiration check - invitations are permanent)
|
// Find valid invitation (no expiration check - invitations are permanent)
|
||||||
const { data: invitation, error: findError } = await supabase
|
const { data: invitation, error: findError } = await supabase
|
||||||
.from('invitations')
|
.from('invitations')
|
||||||
@ -606,18 +641,26 @@ router.get('/', async (req, res) => {
|
|||||||
* PATCH /api/invitations/:id
|
* PATCH /api/invitations/:id
|
||||||
* Updates an invitation's role (before it's accepted)
|
* Updates an invitation's role (before it's accepted)
|
||||||
*/
|
*/
|
||||||
router.patch('/:id', async (req, res) => {
|
router.patch('/:id',
|
||||||
|
[
|
||||||
|
body('role')
|
||||||
|
.notEmpty().withMessage('role is required')
|
||||||
|
.isIn(['caretaker', 'guardian']).withMessage('role must be caretaker or guardian')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
const invitationId = parseInt(req.params.id, 10);
|
const invitationId = parseInt(req.params.id, 10);
|
||||||
const { role } = req.body;
|
const { role } = req.body;
|
||||||
|
|
||||||
console.log('[INVITE] Update invitation:', { userId, invitationId, role });
|
console.log('[INVITE] Update invitation:', { userId, invitationId, role });
|
||||||
|
|
||||||
if (!role || !['caretaker', 'guardian'].includes(role)) {
|
|
||||||
return res.status(400).json({ error: 'Valid role is required (caretaker or guardian)' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check invitation belongs to user
|
// Check invitation belongs to user
|
||||||
const { data: invitation, error: findError } = await supabase
|
const { data: invitation, error: findError } = await supabase
|
||||||
.from('invitations')
|
.from('invitations')
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const Stripe = require('stripe');
|
const Stripe = require('stripe');
|
||||||
|
const { body, validationResult } = require('express-validator');
|
||||||
const { supabase } = require('../config/supabase');
|
const { supabase } = require('../config/supabase');
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
||||||
@ -9,8 +10,42 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|||||||
* POST /api/stripe/create-checkout-session
|
* POST /api/stripe/create-checkout-session
|
||||||
* Creates a Stripe Checkout session for purchasing Starter Kit + optional Premium subscription
|
* Creates a Stripe Checkout session for purchasing Starter Kit + optional Premium subscription
|
||||||
*/
|
*/
|
||||||
router.post('/create-checkout-session', async (req, res) => {
|
router.post('/create-checkout-session',
|
||||||
|
[
|
||||||
|
body('userId')
|
||||||
|
.notEmpty().withMessage('userId is required')
|
||||||
|
.isString().withMessage('userId must be a string'),
|
||||||
|
body('beneficiaryName')
|
||||||
|
.notEmpty().withMessage('beneficiaryName is required')
|
||||||
|
.isString().withMessage('beneficiaryName must be a string')
|
||||||
|
.trim(),
|
||||||
|
body('beneficiaryAddress')
|
||||||
|
.notEmpty().withMessage('beneficiaryAddress is required')
|
||||||
|
.isString().withMessage('beneficiaryAddress must be a string')
|
||||||
|
.trim(),
|
||||||
|
body('beneficiaryPhone')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('beneficiaryPhone must be a string')
|
||||||
|
.trim(),
|
||||||
|
body('beneficiaryNotes')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString().withMessage('beneficiaryNotes must be a string')
|
||||||
|
.trim(),
|
||||||
|
body('includePremium')
|
||||||
|
.optional()
|
||||||
|
.isBoolean().withMessage('includePremium must be a boolean'),
|
||||||
|
body('email')
|
||||||
|
.optional()
|
||||||
|
.isEmail().withMessage('email must be a valid email address')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userId,
|
userId,
|
||||||
beneficiaryName,
|
beneficiaryName,
|
||||||
@ -21,14 +56,6 @@ router.post('/create-checkout-session', async (req, res) => {
|
|||||||
includePremium = true
|
includePremium = true
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: 'userId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!beneficiaryName || !beneficiaryAddress) {
|
|
||||||
return res.status(400).json({ error: 'Beneficiary name and address are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build line items
|
// Build line items
|
||||||
const lineItems = [
|
const lineItems = [
|
||||||
{
|
{
|
||||||
@ -111,14 +138,23 @@ router.post('/create-checkout-session', async (req, res) => {
|
|||||||
* POST /api/stripe/create-portal-session
|
* POST /api/stripe/create-portal-session
|
||||||
* Creates a Stripe Customer Portal session for managing subscriptions
|
* Creates a Stripe Customer Portal session for managing subscriptions
|
||||||
*/
|
*/
|
||||||
router.post('/create-portal-session', async (req, res) => {
|
router.post('/create-portal-session',
|
||||||
|
[
|
||||||
|
body('customerId')
|
||||||
|
.notEmpty().withMessage('customerId is required')
|
||||||
|
.isString().withMessage('customerId must be a string')
|
||||||
|
.trim()
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { customerId } = req.body;
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
if (!customerId) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(400).json({ error: 'customerId is required' });
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { customerId } = req.body;
|
||||||
|
|
||||||
const session = await stripe.billingPortal.sessions.create({
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
customer: customerId,
|
customer: customerId,
|
||||||
return_url: `${process.env.FRONTEND_URL}/settings`,
|
return_url: `${process.env.FRONTEND_URL}/settings`,
|
||||||
@ -136,8 +172,23 @@ router.post('/create-portal-session', async (req, res) => {
|
|||||||
* POST /api/stripe/create-payment-sheet
|
* POST /api/stripe/create-payment-sheet
|
||||||
* Creates PaymentIntent for in-app Payment Sheet (React Native)
|
* Creates PaymentIntent for in-app Payment Sheet (React Native)
|
||||||
*/
|
*/
|
||||||
router.post('/create-payment-sheet', async (req, res) => {
|
router.post('/create-payment-sheet',
|
||||||
|
[
|
||||||
|
body('email')
|
||||||
|
.optional()
|
||||||
|
.isEmail().withMessage('email must be a valid email address'),
|
||||||
|
body('amount')
|
||||||
|
.optional()
|
||||||
|
.isInt({ min: 1 }).withMessage('amount must be a positive integer')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const { email, amount = 24900 } = req.body; // $249.00 default (Starter Kit)
|
const { email, amount = 24900 } = req.body; // $249.00 default (Starter Kit)
|
||||||
|
|
||||||
// Create or retrieve customer
|
// Create or retrieve customer
|
||||||
@ -249,14 +300,26 @@ async function getOrCreateStripeCustomer(beneficiaryId) {
|
|||||||
* Creates a Stripe Subscription for a beneficiary
|
* Creates a Stripe Subscription for a beneficiary
|
||||||
* Uses Stripe as the source of truth - no local subscription table needed!
|
* Uses Stripe as the source of truth - no local subscription table needed!
|
||||||
*/
|
*/
|
||||||
router.post('/create-subscription', async (req, res) => {
|
router.post('/create-subscription',
|
||||||
|
[
|
||||||
|
body('beneficiaryId')
|
||||||
|
.notEmpty().withMessage('beneficiaryId is required')
|
||||||
|
.isInt({ min: 1 }).withMessage('beneficiaryId must be a positive integer'),
|
||||||
|
body('paymentMethodId')
|
||||||
|
.optional()
|
||||||
|
.isString().withMessage('paymentMethodId must be a string')
|
||||||
|
.trim()
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { beneficiaryId, paymentMethodId } = req.body;
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
if (!beneficiaryId) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(400).json({ error: 'beneficiaryId is required' });
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { beneficiaryId, paymentMethodId } = req.body;
|
||||||
|
|
||||||
// Get or create Stripe customer for this beneficiary
|
// Get or create Stripe customer for this beneficiary
|
||||||
const customerId = await getOrCreateStripeCustomer(beneficiaryId);
|
const customerId = await getOrCreateStripeCustomer(beneficiaryId);
|
||||||
|
|
||||||
@ -421,15 +484,23 @@ router.get('/subscription-status/:beneficiaryId', async (req, res) => {
|
|||||||
* POST /api/stripe/cancel-subscription
|
* POST /api/stripe/cancel-subscription
|
||||||
* Cancels subscription at period end
|
* Cancels subscription at period end
|
||||||
*/
|
*/
|
||||||
router.post('/cancel-subscription', async (req, res) => {
|
router.post('/cancel-subscription',
|
||||||
|
[
|
||||||
|
body('beneficiaryId')
|
||||||
|
.notEmpty().withMessage('beneficiaryId is required')
|
||||||
|
.isInt({ min: 1 }).withMessage('beneficiaryId must be a positive integer')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
|
}
|
||||||
|
|
||||||
const { beneficiaryId } = req.body;
|
const { beneficiaryId } = req.body;
|
||||||
console.log('[CANCEL] Request received for beneficiaryId:', beneficiaryId);
|
console.log('[CANCEL] Request received for beneficiaryId:', beneficiaryId);
|
||||||
|
|
||||||
if (!beneficiaryId) {
|
|
||||||
return res.status(400).json({ error: 'beneficiaryId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get beneficiary's stripe_customer_id
|
// Get beneficiary's stripe_customer_id
|
||||||
const { data: beneficiary, error: dbError } = await supabase
|
const { data: beneficiary, error: dbError } = await supabase
|
||||||
.from('beneficiaries')
|
.from('beneficiaries')
|
||||||
@ -482,14 +553,22 @@ router.post('/cancel-subscription', async (req, res) => {
|
|||||||
* POST /api/stripe/reactivate-subscription
|
* POST /api/stripe/reactivate-subscription
|
||||||
* Reactivates a subscription that was set to cancel
|
* Reactivates a subscription that was set to cancel
|
||||||
*/
|
*/
|
||||||
router.post('/reactivate-subscription', async (req, res) => {
|
router.post('/reactivate-subscription',
|
||||||
|
[
|
||||||
|
body('beneficiaryId')
|
||||||
|
.notEmpty().withMessage('beneficiaryId is required')
|
||||||
|
.isInt({ min: 1 }).withMessage('beneficiaryId must be a positive integer')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { beneficiaryId } = req.body;
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
if (!beneficiaryId) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(400).json({ error: 'beneficiaryId is required' });
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { beneficiaryId } = req.body;
|
||||||
|
|
||||||
const { data: beneficiary } = await supabase
|
const { data: beneficiary } = await supabase
|
||||||
.from('beneficiaries')
|
.from('beneficiaries')
|
||||||
.select('stripe_customer_id')
|
.select('stripe_customer_id')
|
||||||
@ -532,14 +611,22 @@ router.post('/reactivate-subscription', async (req, res) => {
|
|||||||
* Creates a SetupIntent for collecting payment method in React Native app
|
* Creates a SetupIntent for collecting payment method in React Native app
|
||||||
* Then creates subscription with that payment method
|
* Then creates subscription with that payment method
|
||||||
*/
|
*/
|
||||||
router.post('/create-subscription-payment-sheet', async (req, res) => {
|
router.post('/create-subscription-payment-sheet',
|
||||||
|
[
|
||||||
|
body('beneficiaryId')
|
||||||
|
.notEmpty().withMessage('beneficiaryId is required')
|
||||||
|
.isInt({ min: 1 }).withMessage('beneficiaryId must be a positive integer')
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { beneficiaryId } = req.body;
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
if (!beneficiaryId) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(400).json({ error: 'beneficiaryId is required' });
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { beneficiaryId } = req.body;
|
||||||
|
|
||||||
// Get or create Stripe customer for this beneficiary
|
// Get or create Stripe customer for this beneficiary
|
||||||
const customerId = await getOrCreateStripeCustomer(beneficiaryId);
|
const customerId = await getOrCreateStripeCustomer(beneficiaryId);
|
||||||
|
|
||||||
@ -644,14 +731,23 @@ router.post('/create-subscription-payment-sheet', async (req, res) => {
|
|||||||
* POST /api/stripe/confirm-subscription-payment
|
* POST /api/stripe/confirm-subscription-payment
|
||||||
* Confirms the latest invoice PaymentIntent for a subscription if needed
|
* Confirms the latest invoice PaymentIntent for a subscription if needed
|
||||||
*/
|
*/
|
||||||
router.post('/confirm-subscription-payment', async (req, res) => {
|
router.post('/confirm-subscription-payment',
|
||||||
|
[
|
||||||
|
body('subscriptionId')
|
||||||
|
.notEmpty().withMessage('subscriptionId is required')
|
||||||
|
.isString().withMessage('subscriptionId must be a string')
|
||||||
|
.trim()
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { subscriptionId } = req.body;
|
// Check validation errors
|
||||||
|
const errors = validationResult(req);
|
||||||
if (!subscriptionId) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(400).json({ error: 'subscriptionId is required' });
|
return res.status(400).json({ error: errors.array()[0].msg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { subscriptionId } = req.body;
|
||||||
|
|
||||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
|
||||||
expand: ['latest_invoice.payment_intent']
|
expand: ['latest_invoice.payment_intent']
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user