Req.params and req.user missing after hitting stripe checkout flow - javascript

I'm trying to integrate Stripe subscriptions billing in my app with a tiered pricing model and based on my understanding, I need to do two things:
Allow my new users to also create a stripe customer account (via the integration)
Monitor Stripe webhook 'events' to provision access while customer subscription payments are active
My userflow is as follows:
create profile in my app (saved to database) -> redirected to stripe checkout portal for billing info (saved to stripe database) -> attempt to save stripe customerId to my database so I can monitor subscription status
However, I can't figure out how to save the customerId info in my app because req.user and req.params are empty as the users are sent back to my app from the stripe billing portal
Controller function
module.exports.stripeWebhook = async (req, res) => {
let data;
const webhookSecret = stripeWebhookSecret;
if (webhookSecret) {
let event;
let signature = req.headers["stripe-signature"];
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
webhookSecret
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed.`);
return res.sendStatus(400);
}
data = event.data;
eventType = event.type;
} else {
// retrieve the event data directly from the request body.
data = req.body.data;
eventType = req.body.type;
}
switch (eventType) {
case 'payment_intent.succeeded': {
console.log('PaymentIntent was successful!');
break;
}
case 'checkout.session.completed':
// Payment is successful and the subscription is created.
// You should provision the subscription and save the customer ID to your database.
console.log(data.object.customer); <- works
const user = await User.findById(req.user.id); <- comes back empty so my next two lines of code don't work
user.stripeId.push(data.object.customer);
await user.save();
break;
default:
}
res.sendStatus(200);
};
App.js
app.use(bodyParser.raw({type: "application/json"}));
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
I included the app.js code because the bodyparser.raw has an impact on how the body comes through in my controller function.
I was counting on the req.user or req.params to find the user in my database but it's not working. How do I save the customerId to my database like the stripe comments suggest?

You should create Stripe customer account before creating checkout session for the customer.
Check if customer already have stripe_customer account (Stripe customer account). If yes, use that one. If not, create one for him and save it in database.
Set stripe_customer for the checkout session, so customer will be automatically authenticated in the Stripe checkout
You can optionally put user's _id in the metadata of the Stripe checkout session, so you access that data later in the webhook.
Note: You should create one stripe_customer account for each currency. So one user can have multiple stripe_customer accounts, one for each currency.
router.post('/create-checkout-session', authenticate.verifyUser, async (req, res) => {
const { currency } = req.body;
...
// If user does not have stripe customer for order currency, create a new one.
if (!(req.user.stripe_customer && req.user.stripe_customer[currency])) {
const new_stripe_customer = await stripe.customers.create({
email: req.user.email,
metadata: {
user_id: req.user._id.toString(),
},
});
let update = {};
update[`stripe_customer.${currency}`] = new_stripe_customer.id;
await Users.findByIdAndUpdate(req.user._id, update);
if (!req.user.stripe_customer) {
req.user.stripe_customer = {};
req.user.stripe_customer[currency] = new_stripe_customer.id;
} else {
req.user.stripe_customer[currency] = new_stripe_customer.id;
}
}
...
// Set `stripe_customer` for checkout session.
const session = await stripe.checkout.sessions.create({
customer: req.user.stripe_customer[currency],
...
});
...
}

Related

Google API to list users in directory

I am developing a user page to allow managers to create users by selecting a new user from a drop down list. I will like to populate the drop down with company users from Google directory. So this user page will only be accessible after login.
As I read, Google Admin SDK accesses private user data and needs access token to work. I will like to use this Google directory API users.list method to retrieve users from Google directory. I have a look at the example from quick start for javascript. The issue is this script requires the user to login again, and this would confuse the admin users.
On the login page, I use the google the HTML sign-in button to render Login page, returning the JWT token to our webapp's login endpoint. The returned token credential contains login user's email profile but no access token.
Could someone please advise how to modify below on so that I can retrieve users on the user page.
async function initializeGapiClient() { await gapi.client.init({
apiKey: API_KEY,
discoveryDocs: [DISCOVERY_DOC],
});
await listUsers();
}
async function listUsers() {
let response; try {
const request = {
'customer': 'my_customer',
'maxResults': 10,
'orderBy': 'email',
};
response = await gapi.client.directory.users.list(request);
} catch (err) {
document.getElementById('content').innerText = err.message;
return;
}
const users = response.result.users;
}
The main issue you are having is that you are using JavaScript. Client side JavaScript does not have the ability to store login information for future use. Which means that every time your users return they will need to login and authorize your application again.
I would suggest that you switch to a server sided language which will allow your users to login and for you to store a refresh token for them enabling you to request a new access token when ever you need to. node.js for example would work as well.
Remember though only users who have admin access on your workspace account are going to be able to do this the user will need to have access
Node.js Quickstart
The following is a quick example for node.js, its for an installed app but will show you how things need to be put together for refreshing your access token.
// npm install googleapis#105 #google-cloud/local-auth#2.1.0 --save
// npm install googleapis
const fs = require('fs');
const path = require('path');
const process = require('process');
const {authenticate} = require('#google-cloud/local-auth');
const {google} = require('googleapis');
// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/admin.directory.user.readonly'];
// Token File Name
const TOKEN_FILE = 'token.json'
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = path.join(process.cwd(), TOKEN_FILE);
const CREDENTIALS_PATH = 'C:\\Development\\FreeLance\\GoogleSamples\\Credentials\\Workspace-Installed-TestEverything.json';
// the workspace customer id found in admin on Google Workspace
const WORKSPACE_CUSTOMER_ID = '[REDACTED]'
/**
* Reads previously authorized credentials from the save file.
*
* #return {Promise<OAuth2Client|null>}
*/
async function loadSavedCredentialsIfExist() {
try {
const content = fs.readFileSync(TOKEN_PATH,{encoding:'utf8', flag:'r'});
const credentials = JSON.parse(content);
return google.auth.fromJSON(credentials);
} catch (err) {
return null;
}
}
/**
* Serializes credentials to a file compatible with GoogleAUth.fromJSON.
*
* #param {OAuth2Client} client
* #return {Promise<void>}
*/
async function saveCredentials(client) {
const content = fs.readFileSync(CREDENTIALS_PATH, {encoding:'utf8', flag:'r'});
const keys = JSON.parse(content);
const key = keys.installed || keys.web;
const payload = JSON.stringify({
type: 'authorized_user',
client_id: key.client_id,
client_secret: key.client_secret,
refresh_token: client.credentials.refresh_token,
});
await fs.writeFileSync(TOKEN_PATH, payload);
}
/**
* Load or request or authorization to call APIs.
*
*/
async function authorize() {
let client = await loadSavedCredentialsIfExist();
if (client) {
return client;
}
client = await authenticate({
scopes: SCOPES,
keyfilePath: CREDENTIALS_PATH,
});
if (client.credentials) {
await saveCredentials(client);
}
return client;
}
/**
* Lists the names and IDs of up to ten users on Google Workspace.
* #param {OAuth2Client} authClient An authorized OAuth2 client.
*/
async function listUsers(authClient) {
const service = google.admin({version: 'directory_v1', auth: authClient});
const res = await service.users.list({
customer: WORKSPACE_CUSTOMER_ID,
pageSize: 10,
fields: 'nextPageToken, users(id, name)',
});
const users = res.data.users;
if (users.length === 0) {
console.log('No users found.');
return;
}
console.log('users:');
users.map((user) => {
console.log(`${user.name.fullName} (${user.id})`);
});
}
authorize().then(listUsers).catch(console.error);
Google workspace
In google workspace under the user, You can check if they have admin access.

How do I pass additional data when calling socket.connect() in socket.io?

I am trying to find a way of passing additional data such as username, avatar_url when connecting to socket when user clicks login. Example:
const logIn = ({ username, avatar }) => {
socket.auth = { username };
socket.connect({ query: { username: username, avatar: avatar } });
}
On the server side:
io.use((socket, next) => {
console.log("===> Data:", socket.request._query);
});
But I do not get any key-value for username and avatar field. I tried searching the documentation but could not find any information for socket.connect() relating to passing data.
How can I pass data in socket.connect()?
Use JWT and pass that data as part of the object.
So the flow would be that when you 'auth' have it send back the JWT with the avitar etc.
When you send it to the server and decode the token you can set that as part of the socket object
As an example ( we currently use the jsonwebtoken package )
// verify token
jwt.verify(token, '[password]', (err, decoded) => {
try{
/* connecting */
socket.avitar = decoded.avitar;
socket.nickname = "nick name";
if(err) return next(err);
//('success!')
Then you can just access it when emitting, because the socket ID is unique to the user it should hold the values, if they disconnect / reconnect the token will still contain the values.
In cluster mode, use redis socket.io and pass this in as an hset to store the data with the socketID

Stripe succesfully creating customer without card details

Complete web/Stripe newbie here. I built an iOS app, but the method of taking payments that I wanted to use isn't allowed on iOS, so I had to set up a website for it.
The website uses HTML/CSS, with a node backend hosted on Heroku. The website is a simple site that takes the name and card details of the user, but there's currently an issue with my implementation.
In app.get(), I create a customer and a setupIntent, and then this gets filled out when the user clicks a button on the site (just an event listener in js on the client-side).
My issue is, when I create a customer it creates an empty customer every time the page is loaded. If I remove this customer, there is no extra customer being added on load, and the correct customer is created, but there is no card attached to the customer's account!
I'm sure this is a basic error on my part, as I rushed through learning web dev in order to get the app to accept payments (we got an unexpected rejection from the App Review team, basically saying our app will never be acccepted as long as it takes card details on the app).
Thanks in advance for any/all help.
Cheers!
Josh
Server-side:
const express = require("express");
const bodyParser = require("body-parser");
const ejs = require("ejs");
require('dotenv').config()
const app = express();
app.set('view engine', 'ejs');
app.use(bodyParser.urlencoded({extended: true}));
app.use(express.static("public"));
const Stripe = require('stripe');
const stripe = Stripe(process.env.SECRET_KEY);
app.get('/', async (req, res) => {
var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
const customer = await stripe.customers.create({
email: fullUrl.split('=')[1] //This gets the email sent in the URL from the app
});
const intent = await stripe.setupIntents.create({
customer: customer.id,
payment_method_types: ['card'],
});
console.log(fullUrl)
console.log(fullUrl.split('=')[1])
res.render('index', { client_secret: intent.client_secret });
})
app.listen(process.env.PORT || 3000);
Client-side:
var stripe = Stripe('livePublicKeyIsHere');
// const firebase = require("firebase");
// require("firebase/firestore");
var elements = stripe.elements();
var cardElement = elements.create('card');
cardElement.mount('#card-element');
var db = firebase.firestore();
var cardholderName = document.getElementById('cardholder-name');
var setupForm = document.getElementById('setup-form');
var clientSecret = setupForm.dataset.secret;
const queryString = window.location.search;
const email = queryString.split('=')[1];
setupForm.addEventListener('submit', function(ev) {
ev.preventDefault();
stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: cardholderName.value
},
},
}
).then(function(result) {
if (result.error) {
console.log("Error!!!" + result.error.message);
window.alert("There's an error: " + result.error.message);
} else {
console.log("Success!!!");
window.alert("Account created! Download and log into the app in order to continue.");
addUserToFirestore(email)
}
});
});
function addUserToFirestore(email) {
createUserOnFirestore(email);
db.collection("Users").doc(email).collection("Settings").doc("info").set({
cardDetailsAdded: true
})
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
}
function createUserOnFirestore(email) {
db.collection("Users").doc(email).set({
exists: true
})
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
}
The reason is because you're using get instead of post. When your user clicks the button, it should fire a POST request to your server to generate a SetupIntent object which you have already done. You also should store a relation mapping between your user and the Customer that is created, so you don't always create a new Customer when user adds a new card, instead you add the new card to existing Customer object.
While using a customer is recommended, ultimately it is optional to provide a customer (API ref). You can also attach a payment method to a customer separately, as long as you do so before using it for a payment.
Note that unless attached to a customer, a payment method is one-time-use only.

How can I email links work on the front end?

I have built some functionality into my API that sends a verification email to a user and then when a get request is made to the route the user then becomes active and can be used to login. In order to make this work on the front end I have used a res.redirect which takes me to the login page.
The problem, however, is that this means that when I want to replicate this journey in postman I then receive a load of html rather than a formatted json response. So my question is, is there a way that we can handle this request so that, depending on where it is called, a different response is sent back? As I do not think an event listener will work in the case of an email.
For context, my application uses nodejs, mongodb and pug templates.
`exports.signup = catchAsync(async (req, res, next) => {
const { token } = req.params;
const { email } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findOne({ email });
if (!user || !((await user.signUpId) === token)) {
return next(new AppError('Invalid link', 401));
}
user.ready = true;
user.singUpId = undefined;
await user.save({ validateBeforeSave: false });
const url = `${req.protocol}://${req.get('host')}/me`;
await new Email(user, url).sendWelcome();
await res.redirect('http://localhost:5000/login');
});`

Problem maintaining session data with Vue JS and Sails JS

Session information is not being maintained when APIs invoked via the VueJs.
In the sails backend, login action set the user id to the session. Whereas in the sessionAuth policy I am simply checking for the user id in the req session.
module.exports = async function (req, res, next) {
// User is allowed, proceed to the next policy,
// or if this is the last policy, the controller
const id = req.session.userId;
if (id) {
const user = await User.findOne({ id });
req.user = user;
if (user) {
return next();
}
}
// User is not allowed
// (default res.forbidden() behavior can be overridden in `config/403.js`)
return res.forbidden('You are not permitted to perform this action.');
};
The above policy works perfectly when requests are being made from Postmen. I invoke login action, any action invoked after that does have user id set in the session. But if the same sequence is followed in a web application user id is missing from the session even after the successful login.
Login component
axios.post(`${this.$config.baseUrl}/login`, this.user)
.then(res => {
this.$router.push({ path: "/home" });
})
Component accessing secured data
const response = await axios.get(`${baseUrl}/course`
return { courseList: response.data };
}

Categories