Add custom field to billing agreement using paypal node sdk - javascript

Im attempting to add a custom field to a billing agreement so I know what user is starting or stopping their subscription when I recieve he IPN POST call.
const billingAgreementAttributes = {
"name": "TEST Web App",
"description": "TEST WEB APP.",
"start_date": null,
"plan": {
"id": ""
},
"custom": "string"
"payer": {
"payment_method": "paypal",
},
};
api.post('/create', authCheck, (req, res) => {
const body = req.body
const type = body.type // default basic
const user = body.user
let isoDate = new Date();
isoDate.setSeconds(isoDate.getSeconds() + 120);
isoDate.toISOString().slice(0, 19) + 'Z';
let agreement = billingAgreementAttributes
agreement.plan.id = type ? basic : unlimited
agreement.start_date = isoDate
// Use activated billing plan to create agreement
paypal.billingAgreement.create(agreement, function (error, billingAgreement) {
if (error) {
console.error(JSON.stringify(error));
res.json(error)
} else {
console.log("Create Billing Agreement Response");
//console.log(billingAgreement);
for (var index = 0; index < billingAgreement.links.length; index++) {
if (billingAgreement.links[index].rel === 'approval_url') {
var approval_url = billingAgreement.links[index].href;
console.log("For approving subscription via Paypal, first redirect user to");
console.log(approval_url);
res.json({ approval_url })
// See billing_agreements/execute.js to see example for executing agreement
// after you have payment token
}
}
}
});
})
When I add to the billing agreement I get malformed json error.
api.post('/execute', authCheck, (req, res) => {
const token = req.body
console.log(token)
paypal.billingAgreement.execute(token.token, {"custom": "foobar"}, function (error, billingAgreement) {
if (error) {
console.error(JSON.stringify(error));
res.json(error)
} else {
res.json(billingAgreement)
console.log(JSON.stringify(billingAgreement));
console.log('Billing Agreement Created Successfully');
}
});
})
If I try to add it in the data parameter when executing the agreement it is never returned anywhere. So currently if a user were to start or stop a subscription I wont know what user did it.

I am not sure what version of the Subscriptions API and Billing Agreements you are using, but the SDKs do not support the latest version, and a custom parameter is not supported.
You must associate Subscription/billing agreement IDs with a user at creation time, when a user creates one via your site/application. This user-associated Subscription/billing agreement id should be persisted in your own database. Then, any later events on that id can be looked up and matched with the user (although really you want to think of users as having active Subscription/BA ids on file as an object)

Related

Implement security logic to limit access to data based on configuration

I'm trying to implement logic to restrict download access:
const userPermittedFutures = {};
app.get('/futures', checkAuthenticated, async (req, res) => {
const query = {
name: 'fetch-futures-portfolios',
text: 'SELECT ' +
'future.instrument.symbol ' +
.......
'WHERE future.exchange.name = $1',
values: ['Futures'],
};
const futuresPortfolios = await pgClient.query(query).catch((err) => {
console.log("Failed to select futures portfolios: " + err.message);
res.status(503).send({
status: 'fail',
message: err.message
});
return;
});
let allowedFuturesAssetsForUser = futuresPortfolios.rows;
let currentUser = initializePassport.getCurrentUser();
let traderId = traderIdMnemonicMapping[currentUser]
if (!getPermittedFuturesAssets[traderId]) {
console.log("Couldn't find Futures contracts for " + currentUser)
return;
} else {
console.log("Specific Futures contracts are set for user " + currentUser);
allowedFuturesAssetsForUser = Object.keys(getPermittedFuturesAssets[traderId]);
}
res.status(200).send(allowedFuturesAssetsForUser);
});
const getPermittedFuturesAssets = (traderId) => {
if (!pgClient) {
setTimeout(function () {
getPermittedFuturesAssets()
}, 1000);
return;
}
const query = {
name: 'fetch-futures-assets-permissions',
text: 'SELECT trader_id, pair, pair_limit ' +
'FROM common.accounts_pairs_permits app ' +
'WHERE exists(SELECT * FROM future.instrument ap WHERE ap.symbol = app.pair) AND trader_id = $1',
values: [traderId],
};
pgClient.query(query, (err, res) => {
if (err) {
console.error("Can't get account permitted futures permits. Retrying in a second...");
setTimeout(function () {
getPermittedFuturesAssets();
}, 1000);
return;
}
for (let row of res.rows) {
addUserFuturesConstraints(row.trader_id, row.pair, row.pair_limit);
}
});
}
function addUserFuturesConstraints(trading_id, futuresAsset, limit) {
if (!userPermittedFutures[trading_id]) {
let user_permitted_limits = {}
user_permitted_limits[futuresAsset] = createUserFuturesAssetsConstraints(limit);
userPermittedFutures[trading_id] = user_permitted_limits;
} else {
userPermittedFutures[trading_id][futuresAsset] = createUserFuturesAssetsConstraints(limit);
}
}
function isUserAbleToTradeFutures(req, res, next) {
if (adminUsers[req.user.username]) { // admin users can trade with all pairs without limits
return next();
}
if (!isFuturesAssetAllowedForUser(req.user.username, req.body.selectedPair)) {
return res.status(503).send({status: "Failed to execute because Futures Asset is not allowed for user."});
}
if (isUserFuturesLimitReached(req.user.username, req.body.amount, req.body.selectedPair)) {
return res.status(503).send({status: "Failed to execute because the order exceeds the user's trading limit for Futures Asset."});
}
return next();
}
function isFuturesAssetAllowedForUser(username, selectedFutureAsset) {
let traderId = traderIdMnemonicMapping[username];
if (!userPermittedFutures[traderId]) {
return false;
}
for (let futureAsset of Object.entries(userPermittedFutures[traderId])) {
if (futureAsset[0] === selectedFutureAsset) {
return true;
}
}
return false;
}
function isUserFuturesLimitReached(username, amount, selectedFuturesAsset) {
let traderId = traderIdMnemonicMapping[username]
if (!userPermittedFutures[traderId][selectedFuturesAsset]) { // if limit is undefined then it is unlimited.
return false;
}
return parseFloat(amount) > userPermittedFutures[traderId][selectedFuturesAsset].pairQuantityLimit;
}
function createUserFuturesAssetsConstraints(pairQuantityLimit) {
return {
pairQuantityLimit: parseFloat(pairQuantityLimit),
splitConstraints: [],
}
}
But I'm stuck how to implement createUserFuturesAssetsConstraints.
When user tries to access endpoint and make request see should be limited to an asset for example asset1 based on the username.
How I can implement the logic so that user can can access only a defined list of items based on his username?
I would implore you to isolate logic in clear containers; for instance by creating repository modules or classes that are responsible for fetching data, service modules for calling the repository and executing business rules related to the authorization etc.
The endpoint /futures appears to imply that you want to return a list of futures (which I assume refers to a list of financial products). The middleware in your source code is defined as checkAuthenticated; however, authentication and authorization are two different things. Whereas authentication refers to the verification of whether a person is who he says he is, while authorization refers to the verification of necessary user rights to access the resource.
Therefore I would suggest using different layers (read: chained middleware functions); i.e. separate authentication and authorization as separate layers, returning an adequate http status response code for each (isAuthenticated: 401 authentication, isAuthorized: 403 authorization for error codes).
Authorizations are usually stored in the database according to an agreed upon RBAC (role based access control). This allows roles to be assigned according to domain specific parameters, and can later on be managed dynamically by admins.
The middleware you apply for verifying authorizations is therefore simple and based on the role the user was assigned.
In your source code you hint to certain constraints, which might imply you want to create a constraints table and link those constraints to a role.
Without understanding the data model you're working with, it is difficult to assess, but with the information provided you should be able to come up with a model that works for you.
If there is any misunderstanding on the question, don't hesitate to comment.

Advice converting an onCreate firebase cloud function trigger with FCM Messaging to support async/await and database reads

I initially had a simple firebase cloud function that sent out a push notification to a topic when a new message child was created in my real-time database. But I wanted to add message filtering where notifications for messages from some filtered users would be sent only to admin users. For this, I have created user groups in my real-time database of the format {userName: FIRToken}, which gets written to from my iOS App every time it launches and I get a FIRToken. So now I will have to load 2 lists 1) Admin Users, 2) Filtered Users before I can actually decide where to send the notification.
So I looked into ways to do this and async/await seemed better than doing a promise inside a promise for loading my 2 user lists. I then saw a firestore video tutorial where a similar usecase function was converted to use async/await instead of promises in promises. Following that, I refactored my code to await on the 2 snapshots for admin and filtered users, before going on to decide where to send the notification and return a promise. My refactoring seems correct. But unfortunately, my old iPhone is stuck on <DeviceName> is busy: Copying cache files from device. Hence I can't physically login from 2 different devices and test if the notifications are going only to my admin user account. Which is why I am posting my function here to see if I have refactored my code correctly or missed something. Please let me know if I will get the intended results or I should fix something in the code.
Edit: Updated code to fix these issues:
Also, the methods to send messages are very confusing. send needs topic name to be defined in the payload but does not support apns. sendToTopic needs a topic name as an argument with the payload. sendMulticast fails to send messages to users whereas sendToDevice sends properly.
Finally sendToDevice supports sound field in notification field, but send does not.
functions.database
.ref("/discussionMessages/{autoId}/")
.onCreate(async (snapshot, context) => {
// console.log("Snapshot: ", snapshot);
try {
const groupsRef = admin.database().ref("people/groups");
const adminUsersRef = groupsRef.child("admin");
const filteredUsersRef = groupsRef.child("filtered");
const filteredUsersSnapshot = await filteredUsersRef.once("value");
const adminUsersSnapshot = await adminUsersRef.once("value");
var adminUsersFIRTokens = {};
var filteredUsersFIRTokens = {};
if (filteredUsersSnapshot.exists()) {
filteredUsersFIRTokens = filteredUsersSnapshot.val();
}
if (adminUsersSnapshot.exists()) {
adminUsersFIRTokens = adminUsersSnapshot.val();
}
// console.log(
// "Admin and Filtered Users: ",
// adminUsersFIRTokens,
// " ",
// filteredUsersFIRTokens
// );
const topicName = "SpeechDrillDiscussions";
const message = snapshot.val();
// console.log("Received new message: ", message);
const senderName = message.userName;
const senderCountry = message.userCountryEmoji;
const title = senderName + " " + senderCountry;
const messageText = message.message;
const messageTimestamp = message.messageTimestamp.toString();
const messageID = message.hasOwnProperty("messageID")
? message.messageID
: undefined;
const senderEmailId = message.userEmailAddress;
const senderUserName = getUserNameFromEmail(senderEmailId);
const isSenderFiltered = filteredUsersFIRTokens.hasOwnProperty(
senderUserName
);
console.log(
"Will attempt to send notification for message with message id: ",
messageID
);
var payload = {
notification: {
title: title,
body: messageText,
},
data: {
messageID: messageID,
messageTimestamp: messageTimestamp,
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
};
console.log("Is sender filtered? ", isSenderFiltered);
if (isSenderFiltered) {
adminFIRTokens = Object.values(adminUsersFIRTokens);
console.log("Sending filtered notification with sendMulticast()");
payload.tokens = adminFIRTokens; //Needed for sendMulticast
return admin
.messaging()
.sendMulticast(payload)
.then((response) => {
console.log(
"Sent filtered message (using sendMulticast) notification: ",
JSON.stringify(response)
);
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(adminFIRTokens[idx]);
}
});
console.log(
"List of tokens that caused failures: " + failedTokens
);
}
return true;
});
} else {
console.log("Sending topic message with send()");
payload.topic = topicName;
return admin
.messaging()
.send(payload)
.then((response) => {
console.log(
"Sent topic message (using send) notification: ",
JSON.stringify(response)
);
return true;
});
}
} catch (error) {
console.log("Notification sent failed:", error);
return false;
}
});

How can I delay a POST request from a user form until AFTER a webhook POST from Stripe is received?

I want users to pay a fee before a POST request from a front end form is processed. I have a Stripe webhook that works fine on the backend, but I'm not sure how to delay the front end posting of the form until after the payment confirmation is received.
In the code below, right now, createTour and createTourPay run at the same time. I would like for createTourPay to execute first, and the createTour only triggers after Stripe posts to my application from the webhook. How can I achieve this?
Controller File (webhook):
exports.webhookCheckout = (req, res, next) => {
const signature = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
if (
event.type === 'checkout.session.completed' &&
event.line_items.name === 'New Job Purchase'
) {
res.status(200).json({ recieved: true });
// Somehow, I want this to trigger the execution of the POST request in my front end JS file.
} else {
if (event.type === 'checkout.session.completed')
createBookingCheckout(event.data.object);
res.status(200).json({ recieved: true });
}
};
Front end JS file:
export const createTourPay = async myForm => {
try {
// 1) Get the checkout session from API response
const session = await axios(`/api/v1/tours/tour-pay`);
const complete = 1;
// console.log(session);
// 2) Create checkout form + charge the credit card
await stripe.redirectToCheckout({
sessionId: session.data.session.id
});
} catch (err) {
// console.log(err);
showAlert('error', err);
}
};
export const createTour = async myForm => {
try {
const startLocation = {
type: 'Point',
coordinates: [-10.185942, 95.774772],
address: '123 Main Street',
description: 'Candy Land'
};
const res = await axios({
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${myForm._boundary}`
},
url: '/api/v1/tours',
data: myForm
});
if (res.data.status === 'success') {
showAlert('success', 'NEW TOUR CREATED!');
window.setTimeout(() => {
location.assign('/');
}, 1500);
}
} catch (err) {
showAlert('error', err.response.data.message);
}
};
Broadly: don't do this. Instead, you in fact should create some pending/unpaid version of the "tour" (or any other product/service) in your system, then attach the unique id (eg: tour_123) to the Checkout session when you create it, either using the client_reference_id (doc) or metadata (doc):
const session = await stripe.checkout.sessions.create({
// ... other params
client_reference_id: 'tour_123',
metadata: { tour_id: 'tour_123' },
});
Then you'd use the webhook to inspect those values, and update your own database to indicate the payment has been made and that you can fulfill the order to the customer (ship product, send codes, allow access to service etc).
If you really want to proceed with a more synchronous flow, you can use separate auth and capture to sequence your customer experience and capture the funds later after authorizing and creating your tour entity.
Edit: a note about security
You should never trust client-side logic for restricted operations like creating a "paid" tour. A motivated user could, for example, simply call your /api/v1/tours create endpoint without ever going through your payment flow. Unless you validate a payment and track that state on your server you won't be able to know which of these had actually paid you.

How can I send a notification by using the email inserted by another user who requested the notification using Cloud Functions for Firebase?

I'm a beginner on Cloud Functions for Firebase and I'm developing a web app using it to send notifications to specific users. However, the problem is that I want to let the user decid for who he wants to send the message, and the way I've found out to do this was allowing the user to insert through a form in my site the receiver's email, so that I could save it in my database and then activate a function that would send a previously created notification to the registered user who had the same email inserted by the sender user.
So, I know that I have to trigger the function whenever a user sends the form with the receiver's email, since it's when my database is changed. However, I don't know how to compare the email inserted with the email of all others users and then catch only the right user's token to send the notification. Does anyone know how to do this?
These are my code, which I took some parts of this question as a base, and the JSON from part of my database:
Function
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendPush =
functions.database.ref('/Messages/Receivers/{pushId}').onWrite(event => {
const snapshot = event.data;
const email = snapshot.val().email;
const getAllUsersPromise = admin.database().ref('Users/').once('value');
const payload = {
notification: {
title: 'You have a notification',
body: 'You received a new message'
}
};
return getAllUsersPromise.then(result => {
const userUidSnapShot = result;
const users = Object.keys(userUidSnapShot.val());
var AllUsersFCMPromises = [];
for (var i = 0;i<userUidSnapShot.numChildren(); i++) {
const user=users[i];
console.log('getting promise of user uid=',user);
AllUsersFCMPromises[i]= admin.database().ref(`/Users/${user}/email`).equalTo(email).once('value').then(token => {
var token = admin.database().ref(`/Users/${user}/token`).once('value');
return token;
});
}
return Promise.all(AllUsersFCMPromises).then(results => {
var tokens = [];
for(var i in results){
var usersTokenSnapShot=results[i];
console.log('For user = ',i);
if(usersTokenSnapShot.exists()){
if (usersTokenSnapShot.hasChildren()) {
const t= Object.keys(usersTokenSnapShot.val());
tokens = tokens.concat(t);
console.log('token[s] of user = ',t);
}
else{
}
}
}
console.log('final tokens = ',tokens," notification= ",payload);
return admin.messaging().sendToDevice(tokens, payload).then(response => {
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to uid=', tokens[index], error);
if (error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(usersTokenSnapShot.ref.child(tokens[index]).remove());
}
}
else{
console.log("notification sent",result);
}
});
});
});
});
});
JSON Structure
{
"Messages" : {
"Receivers" : {
"-Ko-Gc8Ch58uYKGIT_Ze" : {
"email" : "phgrespan#gmail.com"
},
}
},
"Users" : {
"1rwdq0O9Iqdo1JUNauwmlC9HXfY2" : {
"apartamento" : "12",
"bloco" : "L",
"celular" : "148162491784",
"email" : "jose#gmail.com",
"nome" : "josé",
"sobrenome" : "josé",
"telefone" : "418947912497",
"token" : "een1HZ0ZzP0:APA91bHaY06dT68W3jtlC3xykcnnC7nS3zaNiKBYOayBq-wuZsbv1DMFL8zE6oghieYkvvSn39bDCkXLtc3pfC82AGd8-uvmkuXCkPoTuzxMk14wVQNOB01AQ6L7bmsQBisycm2-znz7"
},
"CZv1oxUkfsVpbXNBMQsUZNzSvbt1" : {
"apartamento" : "8",
"bloco" : "P",
"celular" : "123456789",
"email" : "phgrespan#gmail.com",
"nome" : "Pedro",
"sobrenome" : "Henrique",
"telefone" : "99876543234",
"token" : "dvE4gBh1fwU:APA91bF9zLC1cOxT4jLsfPRdsxE8q0Z9P4uKuZlp8M5fIoxWd2MOS30u4TLuOQ4G2Sg0mlDqFMuzvjX3_ZSSi9XATyGtTtNse4AxwLYuD-Piw9oFn6Ma68nGfPSTnIEpvDYRwVnRI2e4"
},
}
}
I hope I have been able to make myself understood and thanks since then.
It seems like you're querying all users when you only need to query the user or users who have the selected email. Instead of using getAllUsersPromise, use .orderByChild() and query the children whose email is equal to the selected email.
let selectedUsers = admin.database.ref('Users/').orderByChild('email').equalTo(email).once(snap => {
// Get the token
})
This will give you the snapshot of just the user or users who have that email. You can then iterate through the snapshots and get the token(s).

Creating a Cognito UserPool with lambda's configured as triggers

I am attempting to create a Cognito user pool programmatically from a script using the JavaScript SDK.
I have successfully created the user-pool and defined a pre-signup and post-confirmation trigger by specifying the arn's of the relevant lambdas in my config. (as per the docs)
My script looks like this:
const aws = require('aws-sdk');
const awsConfig = require('../config/config');
aws.config.update({ region: awsConfig.REGION });
const provider = new aws.CognitoIdentityServiceProvider();
// user provided args
const stage = process.argv[2];
if (!stage) {
process.stdout.write('Please provide stage as argument\n');
process.exit(1);
}
// generate arns for pre and post cognito triggers
const getArn = (lambdaName) => {
return `arn:aws:lambda:${awsConfig.REGION}:${awsConfig.AWS_ACCOUNT_ID}` +
`:function:my-project-name-${stage}-${lambdaName}`;
};
const preSignUp = getArn('preSignUp');
const postConfirmation = getArn('postConfirmation');
const userPoolConfig = {
PoolName: `mypool-${stage}`,
AutoVerifiedAttributes: ['email'],
Schema: [
{
"StringAttributeConstraints": {
"MaxLength": "2048",
"MinLength": "0"
},
"Mutable": true,
"Required": true,
"AttributeDataType": "String",
"Name": "email",
"DeveloperOnlyAttribute": false
}
],
LambdaConfig: {
PostConfirmation: postConfirmation,
PreSignUp: preSignUp
}
};
const callback = (err, resp) => {
if (err) {
process.stdout.write(`${err}\n`);
} else {
process.stdout.write(resp.UserPool.Id);
}
};
provider.createUserPool(userPoolConfig, callback);
When I run this script it successfully creates the user pool, an and when I inspect it in the console the triggers are set correctly.
When I try to register a user on my user pool I get the error:
AccessDeniedException { code: 'UnexpectedLambdaException', ... }
If I go into the console and set the trigger manually it works just fine.
This bug has been reported - but I see no confirmation, nor solution:
https://github.com/aws/aws-cli/issues/2256
Desperately unable to fix or find a workaround.
If you need to add the permission in a serverless.yml file then this is what worked for us. Add it to your Resources section:
UserPoolLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:invokeFunction
Principal: cognito-idp.amazonaws.com
FunctionName: <function-name>
SourceArn: arn:aws:cognito-idp:<your region>:<your account>:userpool/*
This gives all the user pools the ability to invoke your particular function.
You can get the function name to use by looking in .serverless/serverless-state.json and there against your lambda you'll see the FunctionName property.
If you have set up your user pool and lambda triggers in cloud formation you will need to add the appropirate permissions for the user pool to invoke the lambda function.
You would have to add something like this to your cloud formation template.
"UserPoolPreSignupLambdaInvokePermission" : {
"Type" : "AWS::Lambda::Permission",
"Properties" : {
"Action" : "lambda:invokeFunction",
"Principal" : "cognito-idp.amazonaws.com",
"FunctionName" :{ "Ref" : "AutoVerifyEmailPreSignupLambdaFunction" },
"SourceArn" : {
"Fn::Sub" : "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}"
}
}
}
I managed to solve this problem. The issue is that the lambda does not have the correct permissions to interact with cognito.
I found this snippet of information hidden away here
So the in the callback function for the create user pool I attached the correct permissions like this:
const callback = (err, resp) => {
if (err) {
process.stdout.write(`${err}\n`);
} else {
const userPoolId = resp.UserPool.Id;
// the lambdas must have a permission attached that allows them to interact
// directly with cognito
const generateLambdaPersmission = (userPoolName, lambdaName) => {
return {
Action: 'lambda:InvokeFunction',
Principal: 'cognito-idp.amazonaws.com',
SourceArn: `arn:aws:cognito-idp:${awsConfig.REGION}:${awsConfig.AWS_ACCOUNT_ID}:userpool/${userPoolId}`,
FunctionName: getArn(lambdaName),
StatementId: `${stage}1`
};
};
lambda.addPermission(generateLambdaPersmission(userPoolId, 'preSignUp'), (err, resp) => {
if (err) {
process.stdout.write(`error attaching permission to lambda: ${err}`);
}
});
lambda.addPermission(generateLambdaPersmission(userPoolId, 'postConfirmation'), (err, resp) => {
if (err) {
process.stdout.write(`error attaching permission to lambda: ${err}`);
}
});
process.stdout.write(userPoolId);
}
};
See the documentation on adding permissions via the JavaScript SDK here

Categories