How can I edit the backend of firebase cloud function extension? - node.js

I am coding a project similar to patreon.com and my goal is to let multiple members create their own subscription plans and sell it.
I came across a firebase extension for stripe payments - https://firebase.google.com/products/extensions/stripe-firestore-stripe-payments
The problem with this extension is that I can only create a 1 premium membership that adds custom claims to the auth object and I can validate it like this:
export default async function isUserPremium(): Promise<boolean> {
await auth.currentUser?.getIdToken(true);
const decodedToken = await auth.currentUser?.getIdTokenResult();
return decodedToken?.claims?.stripeRole ? true : false;
}
That means that even if I have 100 different subscriptions, I can only attach single boolean value, which is useless.
I went to the source code and I found this snippet:
// Update their custom claims
if (role) {
try {
// Get existing claims for the user
const { customClaims } = await admin.auth().getUser(uid);
// Set new role in custom claims as long as the subs status allows
if (['trialing', 'active'].includes(subscription.status)) {
logs.userCustomClaimSet(uid, 'stripeRole', role);
await admin
.auth()
.setCustomUserClaims(uid, { ...customClaims, stripeRole: role });
} else {
logs.userCustomClaimSet(uid, 'stripeRole', 'null');
await admin
.auth()
.setCustomUserClaims(uid, { ...customClaims, stripeRole: null });
}
} catch (error) {
// User has been deleted, simply return.
return;
}
}
I don't fully understand this code, but I think this is where the boolean value is assigned.
Would it be possible to somehow edit this source code, so that instead of boolean value, I could store subscription plan ids in Array , so that in the front end I could validate and allow customer to access users content only if he has active plan in that array
?

Related

is it okay if I intentionally make my Google Cloud Functions has multiple errors?

I have collection and sub-collection like this
users/{userID}/followers/{followerID}
everytime a follower document is deleted in followers sub-collection, then it will trigger this firestore trigger below to decrease the numberOfFollowers field in user document. this is triggered when a user click unfollow button
exports.onDeleteFollower = functions
.firestore.document("users/{userID}/followers/{followerID}")
.onDelete((snapshot, context) => {
// normally triggered after a user push unfollow button
// then update the user document
const userID = context.params.userID;
const updatedData = {
numberOfFollowers: admin.firestore.FieldValue.increment(-1),
};
return db.doc(`users/${userID}`).update(updatedData);
});
now I have a case like this ....
if a user deletes their account, then I will delete the user document ( users/{userID} ), but if I delete a user document, it will not automatically delete all documents inside its sub-collection, right
so after I delete the user document, I have another function to delete all documents inside the followers sub-collection.
but the problem is, the onDeleteFollower triggers function above will be executed multiple times, and it will throw error multiple times, because the user document has been deleted ( the function above will be used to a update a field in deleted user doc)
I will have this error in functions emulator
⚠ functions: Error: 5 NOT_FOUND: no entity to update: app: "myApp"
path <
Element {
type: "users"
name: "u1-HuWQ5hoCQnOAwh0zRQM0nOe96K03"
}
>
⚠ Your function was killed because it raised an unhandled error.
I actually can write a logic to check if a user document still exist or not. if exist then update numberOfFollowers field
but deleting a user document is very rare if compared to a user click the unfollow button, I think it is not very efficient.
I have a plan like this, I will intentionally let the errors happened. say a user has 1000 followers, then it will trigger the onDeleteFollower function above, then I will have 1000 function errors
my question is .....
is it okay if I have multiple errors in a short time like that? will Google Cloud Function terminates my function, or .... I don't know, I am worried something bad will happen that I don't know
as far as I know, cloud functions will automatically run the function again after it is killed, will my function always ready again after an error like that?
I can't let the follower update the organizer (user) document directly from the client app, because it is not safe. creating security rules to facilitate this is complicated and it seems error prone
Have you considered instead of setting/removing users/{userID}/followers/{followerID} directly, that you create a "follow request" system?
"users/{userID}/followRequests/{requestID}": { // requestID would be auto-generated
user: "{followerID}",
follow: true // true = add user as follower, false = remove user as follower
}
This then allows you to use a single onCreate trigger to update your followers list eliminating the need for your current onCreate and onDelete triggers on users/{userID}/followers/{followerID}. From this function you can implement restrictions on following other users like follow limits or denying follow requests for blocked users.
export const newFollowRequest = functions.firestore
.document('users/{userId}/followRequests/{requestId}')
.onCreate(async (snap, context) => {
const request = snap.data();
const followingUserId = request.user;
const followedUserId = context.params.userId;
const db = admin.firestore();
const userDocRef = db.doc(`users/${followedUserId}`);
const followerDocRef = userDocRef.child(`followers/${followingUserId}`);
// /users/${followingUserId}/following/${followedUserId} ?
try {
if (request.follow) {
// Example restriction: Is the user who is attempting to follow
// blocked by followedUserId?
// await assertUserIDHasNotBlockedUserID(followedUserId, followingUserId);
// following
db.update(userDocRef, {
numberOfFollowers: admin.firestore.FieldValue.increment(1),
});
db.set(followerDocRef, {
/* ... */
});
} else {
// unfollowing
db.update(userDocRef, {
numberOfFollowers: admin.firestore.FieldValue.increment(-1),
});
db.delete(followerDocRef);
}
// delete this request when successful
db.delete(snap.ref);
// commit database changes
await db.commit();
console.log(`#${followingUserId} ${request.follow ? "followed" : "unfollowed"} #${followedUserId} successfully`);
} catch (err) {
// something went wrong, update this document with a failure reason (to show on the client);
let failureReason = undefined;
switch (err.message) {
case "other user is blocked":
failureReason = "You are blocked by #otherUser";
break;
case "user is blocked":
failureReason = "You have blocked #otherUser";
break;
}
return db.ref(snap.ref)
.update({
failureReason: failureReason || "Unknown server error";
})
.then(() => {
if (failureReason) {
console.log("REQUEST REJECTED: " + failureReason);
} else {
console.error("UNEXPECTED ERROR:", err)
}
},
(err) => {
console.error("UNEXPECTED FIRESTORE ERROR:", err);
});
}
});

How can I add user info to conv.user.storage?

I'm using Actions Builder to create my chatbot and after user logins using Google I want to save his ID to storage variable.
This storage variable doesn't exist on conv.user.
So I do this:
if (conv.user.verificationStatus === 'VERIFIED') {
conv.user.storage = {};
conv.user.storage.id = str.rows[0].id;
console.log("STORAGE");
console.log(conv.user.storage.id);
}
But on Google Assistant it returns the error message and on my Webhook it's all good (no errors shown):
Google Assistant Error
What can I do to save/persist at least my user ID for future referings?
Since user has the Google Sign In process done once, every time he enters in your action you have his info on the request (payload). It´s automatically added to user storage.
You should store it on conv.user.params and refer to it in your code.
You may have a get and set method to help you with:
getUserId(conv) {
return conv.user.params.userId;
}
setUserId(conv, userId) {
try {
conv.user.params.userId = userId;
} catch (e) {
throw new error("Error setting USERID");
}
return userId;
}

How to listen for new users when using Firebase's listUsers()

The Firebase admin SDK for Node.js provides us with a way of retrieving every user in our project, as seen in the documentation here.
I have implemented this in my own code as follows:
const listAllUsers = (nextPageToken) => {
return new Promise((resolve, reject) => {
// List batch of users, 1000 at a time.
const customerUIDs = []
admin.auth().listUsers(1000, nextPageToken)
.then((listUsersResult) => {
listUsersResult.users.forEach((userRecord) => {
// check for customers by their claims
if (userRecord.toJSON().customClaims.customer) {
customerUIDs.push(userRecord.toJSON().uid)
}
})
if (listUsersResult.pageToken) {
// List next batch of users.
listAllUsers(listUsersResult.pageToken)
}
resolve(customerUIDs)
})
.catch((error) => {
reject(error)
})
})
}
...
// some
// more
// code
NB: When a user is created, we assign the custom claims of customer: true to specify that the user is a customer, not an admin. That happens in my Cloud functions, so no need to paste it here again.
My question is this:
This function above is a one-time operation. How do I listen for new users, and add them to the customerUIDs array?
The Admin SDK doesn't provide any way to listen for newly created users.
If you want to write some code that runs when a new user account is created, you should look into using an Authentication trigger provided by Cloud Functions.

Auth0 access control

I am using Auth0 to manage a large set of users across several different applications with some being web based and others desktop and mobile. Under the meta data for each user I have an array of applications each user can access, I wondered how I might check this when authenticating so that access would be refused if not within that list.
I can do this very easily on the applications, however it would be great to do it on Auth0.
Using a Rule defined as follows has provided me with the functionality I was looking for:
function (user, context, callback) {
// ACL object
var acl = {
"someAppName": [ 'user1#mail.com', 'user2#mail.com' ],
"otherApp": ['user2#mail.com']
}
// if App is not in the ACL, skip
if(!acl.hasOwnProperty(context.clientName)){
return callback(null, user, context);
}
// check if user has access to app
var userHasAccess = acl[context.clientName].some(
function (email) {
return email === user.email;
}
);
if (!userHasAccess) {
return callback(new UnauthorizedError('Access denied.'));
}
callback(null, user, context);
}

OpenID OWIN auth and lack of user permissions

I may be handling this totally incorrect, but I am using OpenID with MS Azure to authentication my users, then I check to make sure the user has a user account in the notifications of the OpenID middleware, if the user is not found, I am throwing a security exception. How do I return a You do not have access to this applicaiton type page. Am I just missing the hook?
Here is the example:
https://gist.github.com/phillipsj/3200ddda158eddac74ca
You can use try...catch inside the notifications, something along these lines:
SecurityTokenValidated = (context) =>
{
try
{
// retriever caller data from the incoming principal
var username = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value.Split('#')[0];
var database = DependencyResolver.Current.GetService(typeof (IDatabase)) as IDatabase;
var employee = database.Query(new GetEmployeeByUsername(username));
if (employee == null)
{
throw new SecurityTokenValidationException();
}
// I add my custom claims here
context.AuthenticationTicket.Identity.AddClaims(claims);
return Task.FromResult(0);
}
catch (SecurityTokenValidationException ex)
{
context.HandleResponse(); // This will skip executing rest of the code in the middleware
context.Response.Redirect(....);
return Task.FromResult(0);
}
}

Resources