I have created a simple invoice application using the MERN stack. The application is great at handling data for the logged in user as long as one user is logged in, but if another user logs in then the invoices for the user that first logged in is shown.
I am currently using app.set() and app.get() to pass data between endpoints and send to my frontend client. Auth0 handles the authentication layer, but would express-session solve this issue? And if it is how would I go about implementing this? Or is there a better solution?
Below is the code that sends the invoices currently to the frontend:
var express = require('express');
var app = express();
var userInvoices = express.Router();
const axios = require('axios');
const InvoiceModel = require('../models/Invoice');
const UserModel = require('../models/User');
//Functions//
async function clientCall() {
const url = `${process.env.REACT_APP_SAVE_USER}`;
const axiosConfig = {
method: 'get',
url
};
await axios(axiosConfig).catch((e) => {console.log(e)});
};
async function fetchUsersFromDB() {
const usersFromDB = await UserModel.find().catch((e) => {console.log(e)});
return usersFromDB;
};
async function saveUser(User) {
const condition = {email: User.email};
const query = {
nickname: User.nickname,
name: User.name,
picture: User.picture,
email: User.email,
email_verified: User.email_verified,
sub: User.sub,
};
const options = { upsert: true };
const update = await UserModel.updateMany(condition, query, options).catch((e) => {console.log(e)});
// Log the amount of documents that where updated in the DB.
if(update.nModified > 0 ) {
console.log('Number of Users added or updated to DB:', update.nModified)
}
};
function findCommonUser(dbUser, User) {
if(dbUser.length <= 0) {
UserModel.create(User, () => {console.log('Users saved to database')});
console.log('An Error has occured with Database')
} else {
dbUser.forEach((DBElement) => {
if(User.email !== DBElement.email) {
saveUser(User);
}
})
}
console.log(' Users match')
};
function matchUserAndInvoice(dbInvoices, loggedInUser) {
let newArray = [];
dbInvoices.forEach(async (DBElement) => {
if(DBElement.customer_email === loggedInUser.email){
newArray.push(DBElement);
app.set('userInvoices', newArray);
}
})
}
// prevents user from having to refresh to get data.
clientCall();
userInvoices.post('/saveUser', async (req, res) => {
try {
const User = req.body;
const usersFromDB = await fetchUsersFromDB().catch((e) => {console.log(e)});
findCommonUser(usersFromDB, User);
app.set('Users', User)
} catch (error) {
console.log(error)
}
})
userInvoices.get('/fetchUserInvoices', async (req,res) => {
try {
const invoices = await InvoiceModel.find().catch((e) => {console.log(e)});
const user = await app.get('Users');
await matchUserAndInvoice(invoices,user);
const userInvoices = await app.get('userInvoices')
res.json(userInvoices);
} catch (error) {
console.log(error)
}
});
;
module.exports = userInvoices;
app.get() is essentially global to your server instance so putting data there to use between requests for individual users will (as you have discovered) get data confused between different users as all users are trying to store data in the same place.
The usual way to solve a problem like this is to use express-session. This cookies the end-user's connection the first time they connect to your server and then creates a server-side object that is automatically associated with that cookie. Then, inside of any request handler, you can read or set data in req.session and that data will uniquely belong to just that user.
If the user changes devices or clears their cookies, then the association with the session object for that user will be lost (creating a new session object for them upon their next connection). But, if you have a persistent data store and you use some sort of login that allows you to populate the session from the user's persistent store, you can even make the session persistent across devices for the same user (though often times this is not required).
In the specific application you describe in your question (tracking invoices), it seems like your invoice data should be tagged with a specific user when it is stored in your database and then any future requests to display invoices should query based on the particular user that is making the request. This requires a login and login cookie so that you can uniquely identify each user and thus show them only the invoices that pertain to them.
The above-described session object should only be used for temporal session state, not for persistent storage such as invoices. If your server implementation is truly RESTFUL, you may not even need any data stored in the session object other than user's logged in userID.
Related
I want to return some default data along with the authenticated data. So basically I have a table called rooms with two columns user and owner_id with relation to another table. I have a few data which does not have any owner_id associated with it. Those are my default data and I want to always send them and then from within my app I can create custom room which will have owner_id associated with it and for those I want to return only the authenticated users data along with the default one. But my problem is either I can send only the authenticated data or all of the data from default to all authenticated users data. Giving the code and screenshots please help.
async find(ctx) {
const owner_id = ctx.state.user.id;
const dname = await strapi.services.rooms.find();
const name = await strapi.services.rooms.find({owner_id})
return sanitizeEntity(name, { model: strapi.models.rooms });},
Assuming you are using PostGreSQL with strapi, you can easily fetch the rooms which have owner linked and the default by using strapi.query instead of strapi.services
async find(ctx) {
const resp = await strapi
.query('rooms')
.model.query(function (qb) {
if (ctx.state.user) qb.where('owner_id', ctx.state.user.id);
qb.orWhere('owner_id', null);
})
.fetchAll();
const data = resp.toJSON();
return data
}
I am building a user auth system with Nextjs
I am trying to create a document within firestore for each user in my firebase authentication system. I was easily able to do this in previous projects when creating an account with email and password but with the 'sign in with google' feature I can't seem to figure out how.
I don't want to create a new document every time the user logs in..
My only idea is this:
When user signs in, loop through all firestore documents and see if the users e-mail matches any firestore doc email. If not, create document, else return.
I feel like there is another way though..
Simplest way would be to make a custom hook that can be used anywhere across the application.
First in the _app file inside useeffect hook simply try to get the data from doc if data exist well it means user document is already there and if data does not exists, we need to create a document for that, quite simple. Let's see the code now
Make sure you read comments written inside the code to better understand
In _app.js,
useEffect(async () => {
// now this checks if user is logged in or not
const unsubscribe = auth.onAuthStateChanged(async (userAuth) => {
if (userAuth) {
// if logged in it simply passes the userAuth object to handle user profile
// which is a custom hook to check if document for this user pre-exist or not!
// if there wont be any document it will go and create a document and return
// that document.
// If already there is a document created it will simply return that.
const userRef = await handleUserProfile(userAuth);
userRef.onSnapshot((snapshot) => {
// later you can save currentUsr value in any of the state to use it later
const currentUsr = {
id: snapshot.id,
...snapshot.data(),
};
}
});
}
}
});
return () => unsubscribe();
}, []);
Now the custom hook to check if document is already there or not, here comes the tricky part.
export const handleUserProfile = async (userAuth) => {
// as a second check it check if falsy values are returned
if (!userAuth) return;
const { uid } = userAuth;
// first it tries to get data from that uid
const userRef = firestore.doc(`users/${uid}`);
const snapshot = await userRef.get();
// checks if snapshot exist
if (!snapshot.exists) {
// if snapshot does not exist, it will simply create a document with document
// name as this 'uid'
const { displayName, email } = userAuth;
const timeStamp = new Date();
try {
// making use of same userRef that we created above to create
await userRef.set({
displayName,
email,
createdAt: timeStamp,
});
} catch (error) {}
}
// if snapshot exist it will simply return the userRef which contains the
// document only.
return userRef;
};
Voila! :)
There is no reason why you should not use the onAuthStateChanged event on auth. A write would cost you the same as a read to check if the data is already there. But with a read you would sometimes need also a write. In total only writes every time come less expensive in read/write actions.
Just listen to auth state changes and update your firestore data each time it changes:
firebase.auth().onAuthStateChanged(async (user) => {
if (user) {
await firebase.firestore()
.collection("users")
.doc(user.uid)
.set(data, {merge:true});
// User is signed in.
}
});
Make sure to use set with merge turned on. That will ensure that the data will be created if it doens't exist and update only the field you want to update.
Also make sure to store the data under the user uid. With that you ensure that each user has an unique idenfier. It is a bad practice to store users under the email. One of the reasons for that is that emails could have chars that are not supported as keys so would need to remove those when saving and add them again when reading the keys.
Firestore won't create duplicate docs if created when signing in with Google.. so this works:
const signInWithGoogle = () => {
fire
.auth()
.signInWithPopup(google_provider)
.then((result) => {
/** #type {firebase.auth.OAuthCredential} */
var credential = result.credential;
// This gives you a Google Access Token. You can use it to access the Google API.
var token = credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
.catch((error) => {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
})
// CREATE USER DATA IN FIRESTORE
.then(async () => {
const data = {
//ADD DATA HERE
};
await fire
.firestore()
.collection("users")
.doc(fire.auth().currentUser.email)
.set(data);
});
};
I am receiving an undefined error when attempting to set a session for the user upon validation of credentials on login. I am trying to use express-session to create the session for the user but do not have it directly imported into the file (I was guided to not do so) but am unsure how to resolve this error given. Any help and insight would be much appreciated!
End point:
router.post("/login", async (req, res, next) => {
try {
const { username, password } = req.body
// * checks for record existence in db, assigns record to var for access
const user = await users_access.findByFilter({ username })
if (!user) {
return res.status(401).json({ message: 'invalid crededentials' });
}
// * compares the entered password to the hash on record
const passwordValid = await secure.compare(password, user.password)
// * handling invalid responses + creating session
if (!passwordValid) {
return res.status(401).json({ message: 'invalid credentials' });
}
req.session.user = user
res.json({ message: `welcome, ${user.username}!`})
} catch(error) {
next(error)
}
});
application model:
// * add users to the datbase
// * inserts argument into user table in db access
// * returns a user found by id
const add = async (user) => {
const [id] = await database_access("users")
.insert(user)
return findById(id)
}
// * find user record with username and password
const find = () => {
return database_access("users").select("id", "username")
}
// * find user by filter
const findByFilter = (filter) => {
return database_access("users")
.select("id", "username", "password")
.where(filter)
.first()
}
// * find user with id
const findById = (id) => {
return database_access("users")
.select("id", "username")
.where({ id }) // desctructuring id from the request
.first()
}
module.exports = {
add,
find,
findByFilter,
findById
}
if you need to see any additional code to assess I am happy to provide but believe this is the source of issue per the error response. Thank you in advanced!
so I guess you are using the express-session module in your entry file for the server, app.js, server.js, index.js however you call it.
this login handler require to be used in a context where the session is available.
I think what you want is not a unit test for this particular router, but an integration test, to test this router in the context of your app.
This is all I can see from the information you provided. If this was not helpful enough, maybe you can show us your servers main file. and how this router is used.
So i working with firebase auth and database in order to set new user to data base, if set successful i want to set claims for that user.
So it means i have a promise within a promise:
function setUser(user){
// no need for the database code before this, but userRef is set properly
return userRef.set(user)
.then(succ => {
return firebase.firebase.auth().setCustomUserClaims(user.key, {admin: true})
.then(() => {
console.log("setting claims")
return true;
});
})
.catch(err => {
return err
})
}
calling function:
app.post("/register_user",jsonParser,async (req, res) => {
var user = req.body.user;
let result = await fireBase.setUser(user);
res.send(result);
})
What happens is that i get the set on the database but claims are not set nor i can i see the log. I know its a js question and not firebase one. I tried many different ways (with await) but non worked.
firebase.firebase does not seem correct. You need to be using the admin object which can be initialised using const admin = require('firebase-admin'); This is not part of the firebase db sdk, but the admin one. You can also use the userRef.uid as that gives you the id of the document of the user, if that is what you want, else use your user.key
return admin.auth().setCustomUserClaims(userRef.uid, {
admin: true
}).then(() => {
//on success
});
In my app, I am sending push notifications to the followers of a user when that user creates a new post. As you can see in the code below, I have some additional settings that I need to query from each follower's profile to get their push token and check for some additional notification settings. I am afraid that this query of each user's profile might become a bottleneck if a user has a large number of followers i.e. 1000.
What is the best way to approach this?
// The cloud function to trigger when a post is created
exports.newPost = functions.database.ref('/posts/{postId}').onCreate(event => {
const postId = event.params.postId;
const post = event.data.val();
const userId = post.author;
let tokens = [];
let promises = [];
return admin.database().ref(`/followers/${userId}`).once('value', (followers) => {
followers.forEach((f) => {
let follower = f.key;
promises.push(
admin.database().ref(`users/${follower}`).once('value')
);
});
})
.then(() => {
return Promise.all(promises).then((users) => {
users.forEach((user) => {
const userDetails = user.val();
if (userDetails.post_notifications) {
if(userDetails.push_id != null) {
tokens.push(userDetails.push_id);
}
}
})
})
})
.then(() => {
if (tokens.length > 0) {
const payload = {
notification: {
title: 'New Post!',
body: 'A new post has been created'
}
};
// Send notifications to all tokens.
return admin.messaging().sendToDevice(tokens, payload);
}
});
})
EDIT:
We have thought about using topics. But we are not sure how we can still have our customized notifications settings working with topics. Here's our dilemma.
We have multiple actions that can create notifications and we provide individual switches for each type of notification in the app so a user can select which type of notifications they want to switch off.
Let's say when User A follows User B. We can subscribe User A to "User B's topic" so whenever User B performs an action which sends out notifications to his/her followers, I can send send out notifications to users subscribed to "User B topic".
Because we have multiple notification switches in the app and when user A changes his/her settings that they do not want notifications for new posts but still want other types of notifications from the users he/she follows, we have not been able to figure out how we can use topics in this case.
Instead of using tokens, you can use topics for this. So lets say a user started following someone, then he will register to that topic.
Lets say he followed someone called "Peter", then you can execute this:
FirebaseMessaging.getInstance().subscribeToTopic("Peter");
Now if you have this database:
posts
postid
postdetails: detailshere
author: Peter
then using onCreate():
exports.newPost = functions.database.ref('/posts/{postId}').onCreate(event => {
const postId = event.params.postId;
const post = event.data.val();
const authorname = post.author;
const details=post.postdetails;
const payload = {
data: {
title:userId,
body: details,
sound: "default"
},
};
const options = {
priority: "high",
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToTopic(authorname, payload, options);
});
You can use this, everytime the author creates a new post, onCreate() is triggered then you can add the details of the post and the author name in the notification (if you want) and sendToTopic() will send it to all users subscribed to the topic that is the authorname (ex: Peter)
After your edit, I think you want the user to be unsubscribed from a topic, but stay subscribed to other topics, then you have to use the admin sdk for this:
https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions
Using the admin sdk you can unsubscribe the user also from a topic, a simple example:
// These registration tokens come from the client FCM SDKs.
var registrationTokens = [
'YOUR_REGISTRATION_TOKEN_1',
// ...
'YOUR_REGISTRATION_TOKEN_n'
];
// Unsubscribe the devices corresponding to the registration tokens from
// the topic.
admin.messaging().unsubscribeFromTopic(registrationTokens, topic)
.then(function(response) {
// See the MessagingTopicManagementResponse reference documentation
// for the contents of response.
console.log('Successfully unsubscribed from topic:', response);
})
.catch(function(error) {
console.log('Error unsubscribing from topic:', error);
});