MobX - User profile different fields and null defaults issue - mobx

I have an AuthStore with these userData defaults:
#observable userData = {
username: null,
firstname: null,
lastname: null,
email: null
}
The object is populated on login with the respective fields and looks like this:
#observable userData = {
username: "jdoe",
firstname: "john",
lastname: "doe",
email: "johndoe#website.com" <— This field is special
}
The email field is special, it should only be fetched/existent if it’s on the authenticated user's profile when they view their own profile. When visiting another user’s profile, all the other fields should be existent except email, and are fetched from an api without email.
Since these are 2 different types of users, an auth user with their email present, and when they visit any other user, there is no email, just all the other respective fields. What’s the best way to handle this multi userData use case?
Should there be 2 stores with the same repeated fields except for email? So the above with the email intact in the AuthStore, and then another UserStore with the same fields without the email, and create 2 different react native ownerProfile.js (for auth user) and profile.js (for other users)? This doesn’t feel right...
What’s the best way to store the logged in userData object to retrieve it on application refresh if it’s available, as it currently empties out the profile's userData fields on app refresh and visiting the profile results in an error since I use AuthStore.userData.username.toUpperCase(); and I get an error because it’s trying to run .toUpperCase() on null.
I came across hydrate/toJson but I am confused by the whole thing, and not even sure when/how to implement them or even if they’re the solution to my problem. I am stuck on my project and can’t move forward because of this issue.
Any idea how to solve this using MobX?

I would create a class User
class User {
#observable username = null;
#observable firstname = null;
#observable lastname = null;
#observable email = null;
}
Then you can make UserStore which contains an observable array of User instances and AuthStore which has an observable field self pointing to the current user profile.
create 2 different react native ownerProfile.js (for auth user) and profile.js (for other users)? This doesn’t feel right...
I usually reuse existing components, but if I start adding too many ifs to a component I consider it a clear signal to extract code into a separate component.
It is hard to say when you should split OwnerProfile and Profile and it depends on what you want to show there. Remember that you can do something like this
function UserProfile({ user, otherProps }) {
return (
<common>
<markup>
<goes here>
{ user.email
? <OwnerProfile user={user} {...otherProps} />
: <Profile user={user} {...otherProps} />
</goes here>
</markup>
</common>
);
}
if you need to wrap OwnerProfile and Profile with the same markup (or you can turn UserProfile into a HOC and apply it to Profile and OwnerProfile on export).
What’s the best way to store the logged in userData object to retrieve it on application refresh if it’s available
You can take a look at mobx-contact-list example. Here it loads contacts from localStorage and here it saves it to localStorage.

Related

Clear nuxt store and all submodules at logout

I'm wondering what's the best solution to reset all my store once a user logout.
Typically I have an app where I log in and sometimes in dev mode I need to switch from account (which can be the case in live mode). And obviously I've some issues when I log with another user, some store infos are still there and belongs to other user (dunno if I'm clear).
I already clear some module from my store inside my logout function inside auth.js. But as my app is growing, and store (decoupling in several modules) also, I just wonder what's the best approach to reset all my store at once, with initial value/state.
Any thoughts ?
This is how I'm clearing out some data from the store in Nuxt.
clearToken(state) {
state.token = null
state.refresh = null
state.currentUserData = null
},
logout(vuexContext, req) {
vuexContext.commit('clearToken')
Cookie.remove('jwt')
Cookie.remove('jwt_refresh')
Cookie.remove("tokenExpiration");
Cookie.remove("userData");
if (process.client) {
localStorage.removeItem('refresh');
localStorage.removeItem('token');
localStorage.removeItem("tokenExpiration");
localStorage.removeItem("userData");
if (!localStorage.getItem('token')) {
$nuxt.$router.push('/login/');
}
}
}
You can clean all vuex data using: localStorage.vuex = ''

lucene query filter not working

I am using this filter hook in my Auth0 Delegated Administration Extension.
function(ctx, callback) {
// Get the company from the current user's metadata.
var company = ctx.request.user.app_metadata && ctx.request.user.app_metadata.company;
if (!company || !company.length) {
return callback(new Error('The current user is not part of any company.'));
}
// The GREEN company can see all users.
if (company === 'GREEN') {
return callback();
}
// Return the lucene query.
return callback(null, 'app_metadata.company:"' + company + '"');
}
When user logged in whose company is GREEN can see all users. But when user logged in whose company is RED can't see any users whose company is RED.
I need to make this when user logged in, user should only be able to access users within his company. (except users from GREEN company).
But above code is not giving expected result. What could be the issue?
This might be related to a little warning note on the User Search documentation page
Basically they don't let you search for properties in the app_metadata field anymore. Unfortunately, this change was breaking and unannounced.
We had to make changes to our API so that we keep a copy of the app_metadatas in a separate database and convert lucene syntax to MongoDB queries, so that we can query by a chain of user_id:"<>" OR user_id:"<>" OR ....
One caveat though, you can't pass a query that's longer than 72 user_ids long. This number is so far undocumented and obtained empirically.
Also, you can't rely on Auth0's hooks to add new users to your database, as these don't fire for social logins, only for Username-Password-Authentication connections.
I hope this gave you some explanation as for why it wasn't working as well as a possible solution.
If I were you, I would look for an alternative for Auth0, which is what we are currently doing.
I finally ended up with this solution.
Used search functionality to filter users. I had to change below two files.
fetchUsers function in client\actions\user.js
changed
export function fetchUsers(search = '', reset = false, page = 0)
to
export function fetchUsers(search = '#red.com', reset = false,
page = 0)
AND
onReset function in client\containers\Users\Users.jsx
changed
onReset = () => { this.props.fetchUsers('', true); }
to
onReset = () => { this.props.fetchUsers('#red.com', true); }

How to reuse data between routes in Aurelia?

I have an user entity in the system and the following route fetches it from server and displays its details:
routerConfiguration.map([
// ...
{route: 'user/:id', name: 'user-details', moduleId: './user-details'}
]);
Now I want to display an edit form for the displayed user. I have the following requirements:
Edit form should have a separate URL address, so it can be sent to others easily.
When user clicks the Edit button on the user's details page, the edit form should use an already loaded instance of the user (i.e. it should not contact the API again for user details).
When user clicks the Edit button on the user's details page and then the Back button in the browser, he should see the details page without edit form again.
1st attempt
I tried to define the edit form as a separate page:
routerConfiguration.map([
// ...
{route: 'user/:id/edit', name: 'user-edit', moduleId: './user-edit'}
]);
This passes the #1 and #3 requirement but it has to load the user again when the edit form is opened.
I don't know any way to smuggle some custom data between the routes. It would be perfect if I could pass the preloaded user instance to the edit route and the edit component would use it or load a new one if it is not given (e.g. user accesses the URL directly). I have only found how to pass strings to the routes in a slighlty hacky way.
2nd attempt
I decided to display the edit form in a modal and show it automatically when there is a ?action=edit GET parameter. The code inspired by this and this question:
export class UserDetails {
// constructor
activate(params, routeConfig) {
this.user = /* fetch user */;
this.editModalVisible = params.action == 'edit';
}
}
and when the user clicks the Edit button, the following code is executed:
displayEditForm() {
this.router.navigateToRoute('user/details', {id: this.user.id, action: 'edit'});
this.editModalVisible = true;
}
This passes #1 (the edit url is user/123?action=edit) and #2 (the user instance is loaded only once). However, when user clicks the Back browser button, the URL changes as desired from user/123?action=edit to user/123 but I have no idea how to detect it and hide the edit form (the activate method is not called again). Therefore, this solution fails the #3 requirement.
EDIT:
In fact, I have found that I can detect the URL change and hide the edit form with event aggregator:
ea.subscribe("router:navigation:success",
(event) => this.editModalVisible = event.instruction.queryParams.action == 'edit');
But still, I want to know if there is a better way to achieve this.
The question is
How to cope with this situation in a clean and intuitive way?
How about adding a User class that will serve as the model and use dependency injection to use it in your view-models?
export class User {
currentUserId = 0;
userData = null;
retrieve(userId) {
if (userId !== this.currentUserId) {
retrieve the user data from the server;
place it into this.userData;
}
return this.userData;
}
}

Pass values to route

I have a list of items. When the user clicks on an item, the user will be taken to item details page.
I want to pass an object containing item details(like item's image URL) to the route. However, I don't want to expose it in the routes url.
If there were a way to do something like <a route-href="route: details; settings.bind({url: item.url})">${item.name}</a> that would be gold.
I have seen properties can be passed to a route if defined in the route configuration. However, I don't know how to change that from the template. Another way could be is to define a singleton and store the values there and inject the object to the destination route.
Is there a way to pass values to routes from view (like angular ui-routers param object)?
Okay so I figured out a way to achieve something closer to what I wanted:
Objective: Pass data to route without exposing them in the location bar.
Let's say, we have a list of users and we want to pass the username to the user's profile page without defining it as a query parameter.
In the view-model, first inject Router and then add data to the destination router:
goToUser(username) {
let userprofile = this.router.routes.find(x => x.name === 'userprofile');
userprofile.name = username;
this.router.navigateToRoute('userprofile');
}
Now when the route changes to userprofile, you can access the route settings as the second parameter of activate method:
activate(params, routeData) {
console.log(routeData.name); //user name
}
For those #Sayem's answer didn't worked, you can put any additional data (even objects) into setting property like this:
let editEmployeeRoute = this.router.routes.find(x => x.name === 'employees/edit');
editEmployeeRoute.settings.editObject = employeeToEdit;
this.router.navigateToRoute('employees/edit', {id: employeeToEdit.id});
So editObject will be delivered on the other side:
activate(params, routeConfig, navigationInstruction) {
console.log(params, routeConfig, navigationInstruction);
this.editId = params.id;
this.editObject = routeConfig.settings.editObject;
}
hopes this helps others encountering same problem as me. TG.

Allow Administrators to impersonate users using an iframe

I have an MVC project with three roles: Users, Account Managers, and Administrators.
Administrators have their own MVC Area where they have full control over Users and Account Managers. I'm trying to implement functionality to allow Administrators to view the site as any User or Account Manager.
In the Admin Area of the site, I have a View of a list of Users and Account Managers. The list contains a "View Site As User" button for each record.
I have never done anything like this before, but the ViewAs Controller Action is currently set up to create a Session with the selected User's information, like so:
ViewBag.SiteSession = Session["SiteSession"] = new SiteSession()
{
ID = user.ID,
AccountID = user.AccountID,
DisplayName = user.DisplayName,
IsManager = user.IsAdmin,
IsAdmin = false
};
The View relevant to this Action has the Model defined as a string, and nothing else but an iframe with the Model as the src attribute, like so:
#model string
<iframe src="#Model" >
</iframe>
What I'm trying to do is render whichever portion of the site was requested in this iframe. When an Administrator clicks "View As User," I'd like to direct to Home. The URL is generated through this call:
Url.Action("Index", "Home", new { Area = "" }));
The Area is set to nothing to avoid rendering the Admin Area's Home.
Currently, this is not working. I don't know where to even begin, minus what I already have.
I'm looking for any suggestions. All help is greatly appreciated, as this doesn't seem like an easy task.
If you don't know how to help, it would also be appreciated if you could direct this question to somebody that can.
Again, thanks in advance.
The way that I've done this in the past has been to use the concept of an an actual user and an effective user. Most display actions use the effective user to generate their content. Typically I've implemented it as "impersonation" rather than "preview" so the user is actually navigating the site as the user rather than displaying in a separate window. In this case I simply set both in the current session. Things that require admin permission (like switching to/from impersonation) obviously use the real user.
If you wanted to do preview then I'd think about using a parameter on each request to set the effective user. The code would need to understand to add this parameter to all links so that you could navigate in the iframe without messing up navigation in the original interface.
As for removing the area from the url, I think what you have (setting to the empty string) should work. If it's not working you might want to try lowercase area, Url.Action("Index", "Home", new { area = "" }). I'm pretty sure that the RouteValueDictionary that gets created under the hood uses a case insensitive key comparison, though, so it shouldn't matter.
For this task, I ended up creating a separate controller, ViewAsController, which had a controller-wide [Authorize] attribute that only allowed users with the Admin role to access its actions.
In the Start action, a Session object containing the selected User's information is created, like so:
[HttpGet]
public ActionResult Start(int id)
{
var user = db.Users
.First(u => u.ID == id);
Session["SiteSession"] = new SiteSession()
{
//Session data...
};
return PartialView("_IFrame");
}
This Action returns a Partial View that I ended up displaying in a jQuery UI modal dialog window.
Here's the code for that Partial View:
#{
ViewBag.SiteSession = (SiteSession)Session["SiteSession"];
}
<h2>Viewing Site As #ViewBag.SiteSession.DisplayName</h2>
<div>
<iframe src="#Url.Action("Index", "Home", new { Area = "" })"></iframe>
</div>
As you can see, it's extremely bare, and that's exactly what it needs to be. The <iframe> acts as a browser in a browser, allowing the Admin user full access to whichever Actions the selected User would.
For the sake of detail, here's the jQuery that creates the dialog and opens it:
$(function () {
$("#viewAsDialog").dialog({
modal: true,
autoOpen: false,
resizable: true,
draggable: true,
closeOnEscape: false,
height: $(window).height() * .9,
width: 1000,
closeText: '',
close: function () {
$.post("#Url.Action("End", "ViewAs", new { Area = "Admin" })")
.success(function (result) {
});
}
});
});
function viewAs(result) {
$("#viewAsDialog").html(result);
$("#viewAsDialog").dialog("open");
}
You can see here that the dialog is initialized on document-ready, and is not opened until the AJAX call that retrieves the Partial View is successfully completed.
Once the Admin closes the dialog, the server calls the End action in the ViewAs Controller, destroying the session:
[HttpPost]
public ActionResult End()
{
Session["SiteSession"] = null;
return new HttpStatusCodeResult(System.Net.HttpStatusCode.OK);
}