-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
First attempt at effective name for admin mock #466
Conversation
app/session.server.ts
Outdated
export async function setEffectiveName(username: string, name: string): Promise<string> { | ||
const user = await getUserByUsername(username); | ||
|
||
if (user?.group.includes('admin')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's fix the User
returned in https:/DevelopingSpace/starchart/blob/main/app/models/user.server.ts#L7-L21 and re-use/modify the code in https:/DevelopingSpace/starchart/blob/main/app/models/user.server.ts#L61-L77 to add properties to the User
object that indicate if this is an admin.
User.isAdmin
This way we can do the /mycustomdomain(-dev)?-admins/.test(group)
stuff to set it on the object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is making use of isAdmin when setting effectiveName enough or should I do as you said and make a boolean property on a user for whether they are Admin?
Nvm :) got it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whatever you do, we need to put it on the User
so we can pass that info around. But using isAdmin()
like you are is good internally for your needs.
app/session.server.ts
Outdated
} | ||
|
||
export async function setEffectiveName(username: string, name: string): Promise<string> { | ||
const user = await getUserByUsername(username); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't have a user
here, should we throw?
app/session.server.ts
Outdated
return await effectiveNameCookie.serialize(name); | ||
} | ||
|
||
return await effectiveNameCookie.serialize(user?.username); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems odd to not have a user
at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, I had tried to use getUser() before and this is a relic from that. We should have a user and if we don't I will throw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking better, but I want to see us decorate the user
object with more details. The front-end is going to need to know the correct username
to use, whether this is an admin, etc via the User
we return in https:/DevelopingSpace/starchart/blob/main/app/root.tsx#L34.
Please add this info to that User
object so it can get passed around the app.
So it's ok to change the prisma schema for user? |
You don't need to change the database, change the object we send back when you query it, same as I do with |
app/models/user.server.ts
Outdated
@@ -17,7 +18,7 @@ export async function getUserByUsername(username: PrismaUser['username']) { | |||
} | |||
|
|||
// Decorate with the user's base domain as well | |||
return { ...user, baseDomain: buildUserBaseDomain(username) }; | |||
return { ...user, baseDomain: buildUserBaseDomain(username), isAdmin: isAdmin(username) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throw the effectiveUsername
on here too. So we know:
- are you an admin?
- what is your real username? (who are you really)
- what is your current effective username? (which user are you actually pretending to be)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how to do this since my effective user name is currently passed as a cookie from request to request on the header and we don't have access to the header in the getUserByUsername method.
app/models/user.server.ts
Outdated
} | ||
|
||
export async function getUserByUsername(username: PrismaUser['username']) { | ||
export async function getUserByUsername(username: PrismaUser['username'], request: Request) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I agree this is the wrong place to do this. Let's do it in the root.tsx
loader.
On the server side, we'll read the username/effective username out of session. On client, we'll send it with the root on the loader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So for client to use it I would make a utils method using useMathcesData() like we have for user?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The client would get it from the loader, which can access the request
and also the db, and put all the data on the User object we return in root.tsx
.
app/root.tsx
Outdated
@@ -30,8 +30,13 @@ export const links: LinksFunction = () => [ | |||
]; | |||
|
|||
export async function loader({ request, context }: LoaderArgs) { | |||
const effectiveUsername = await getEffectiveName(request); | |||
const userBeforeEffectiveName = await getUser(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: if both getEffectiveName
and getUser
both require request
, can you not return the effectiveUsername
on the result of getUser
(i.e., you could do this internally in getUser
)?
app/session.server.ts
Outdated
@@ -36,6 +38,20 @@ export async function getUsername(request: Request): Promise<User['username'] | | |||
return username; | |||
} | |||
|
|||
export async function getEffectiveName(request: Request): Promise<string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getEffectiveUsername
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, change all effectiveName
uses everywhere to effectiveUsername
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing this locally, here is what I get on the client:
{
"username": "user1",
"displayName": "Johannes Kepler",
"email": "[email protected]",
"group": "mycustomdomain-faculty",
"createdAt": "2023-03-28T17:18:24.557Z",
"updatedAt": "2023-03-28T17:18:24.557Z",
"deactivated": false,
"baseDomain": "user1.starchart.com",
"isAdmin": {},
"effectiveUsername": null
}
According to the type of User
, the effectiveUsername
is string
, but here we get null
. Should we set the effectiveUsername
to be the same as username
if it's not set (e.g., regular user)?
Next, the isAdmin
is an Object
vs. a boolean
.
app/session.server.ts
Outdated
const user = await getUserByUsername(username); | ||
|
||
if (user) { | ||
if (user.isAdmin) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (user?.isAdmin) {
return user;
}
throw redirect('/');
app/utils.ts
Outdated
@@ -105,6 +105,26 @@ export function useUser(): User { | |||
return maybeUser; | |||
} | |||
|
|||
export async function useEffectiveUser(): Promise<User> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be:
useEffectiveUsername()
The user
object will be identical. It's just that we're going to use this hook to get the specific "username" that the current user is using, which will either be:
user.effectiveUsername
if the cookie is set and we're using someone else's usernameuser.username
if the cookie is not set and we're using our own username
export async function useEffectiveUsername(): User['username'] {
// The user must be logged in to call this. Use `useOptionalUser()` if you're not sure
const { username, effectiveUsername } = useUser();
// Your username is either your real username, or the username you are temporarily using as an admin
return effectiveUsername || username;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how to go about this still, I have replaced what I had with what is suggested and it works for when we display it like on header instead of rendering a property of the user object I just render the effective username but we would have to have a different effective user object for the use of baseDomain for example. Or we could change how the method works so that getUserByUsername checks whether isAdmin and a non-empty effective username exist together and if that is the case will instead use the effectiveUsername when building domain. On second thought idk if this is possible because I don't think we have the extended User object in there since that is where we extend it in the first place. Maybe we just need to retrieve another effective user object like I was trying to do earlier?
app/models/user.server.ts
Outdated
@@ -13,7 +13,7 @@ export interface User extends PrismaUser { | |||
username, admins (identified by group) will start with an empty string | |||
that can be changed to another users username on the admin portal for | |||
them to impersonate */ | |||
effectiveUsername: string; | |||
effectiveUsername?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type here should be User['username']
so it matches what username
is when we pass it around.
app/session.server.ts
Outdated
export async function getEffectiveUsername(request: Request): Promise<string> { | ||
const cookie = request.headers.get('Cookie'); | ||
const effectiveUsername = await effectiveUsernameCookie.parse(cookie); | ||
return effectiveUsername; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's return this like so:
return effictiveUsername as User['username'];
And you can drop the return type on the function.
app/session.server.ts
Outdated
if (username === undefined) { | ||
return null; | ||
} | ||
|
||
const user = await getUserByUsername(username); | ||
if (user) { | ||
return user; | ||
return { ...user, effectiveUsername: effectiveUsername }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the property name and value are the same, just use it once:
return { ...user, effectiveUsername };
app/session.server.ts
Outdated
export async function getUser(request: Request) { | ||
const username = await getUsername(request); | ||
const effectiveUsername = await getEffectiveUsername(request); | ||
if (username === undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!username) {
app/models/user.server.ts
Outdated
username, admins (identified by group) will start with an empty string | ||
that can be changed to another users username on the admin portal for | ||
them to impersonate */ | ||
effectiveUsername?: User['username']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: if we're going to have a user and an effectiveUser (two different objects), is there any point having this value on the User
type now? Should both user
and effectiveUser
be of type User
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we do need it since everything kind of comes from the cookie now, is there a better way of doing this than with the cookie? Maybe keeping track of it on this property we are thinking about removing, but this is dynamically appended every time we load same as baseDomain so that does not make sense. Anyways, I am ok with removing it I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly it's kind of hard keeping track of stuff, and this isn't a big change either 😅
app/root.tsx
Outdated
return json( | ||
{ | ||
user: await getUser(request), | ||
effectiveUser: await getEffectiveUser(request), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move your comment from User to this, so it explains what this is vs. user
app/root.tsx
Outdated
} | ||
// , | ||
// { headers: { 'Set-Cookie': await setEffectiveUsername('user3', 'user1') } } | ||
// This is how I tested it, after refreshing once since user3 is admin it will mock user1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove this?
app/root.tsx
Outdated
); | ||
return json({ | ||
user: await getUser(request), | ||
/* the effectiveUsername, this is for admin impersonating users. Regular |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you format this differently?
user: await getUser(request),
/**
* effictiveUser: ...
* ...
* ...
*/
effectiveUser: await ...
Also, effectiveUser
vs. effectiveUsername
.
app/session.server.ts
Outdated
@@ -58,7 +59,7 @@ export async function getEffectiveUser(request: Request) { | |||
|
|||
const user = await getUserByUsername(effectiveUsername); | |||
if (user) { | |||
return { ...user, effectiveUsername }; // should we have an effectiveUsername when mocking? | |||
return user; // should we have an effectiveUsername when mocking? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can drop this comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is right.
In a follow-up, we should add/update e2e tests for this stuff.
Does anything call useUser()
now, or is it all useEffectiveUser()
? Maybe the header will do it when an admin logs in and we want to show different usernames?
We should also file an issue to hook up the isAdmin
stuff to the /admin
route.
Everything calls useEffectiveUser, if there is no effectiveUsername it will call getUser() in session server. As it stands there is no call to useUser(). I think we may be able to take it out of the root loader. |
OK, thanks for confirming. I think we'll still want it to show that an admin is impersonating another user, but probably in no other cases. |
@sfrunza13 can you rebase and fix this conflict? Then we can get this in too! |
yes, I will do this |
app/session.server.ts
Outdated
export async function getEffectiveUser(request: Request) { | ||
const username = await getUsername(request); | ||
// get the effective username in the cookie at the time | ||
const effectiveUsername = await getEffectiveUsername(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run these at the same time:
const [username, effectiveUsername] = await Promise.all([getUsername(request), getEffectiveUsername(request)]);
app/session.server.ts
Outdated
} | ||
|
||
// if there is no effective username let the admin be himself | ||
if (!effectiveUsername || effectiveUsername === '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes me nervous that effectiveUsername
could be ''
. When could this happen and why? Can we avoid that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was what I remembered from our discussion, admins would have an empty effectiveUsername until they select one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can "empty" be undefined
vs ''
?
app/session.server.ts
Outdated
return null; | ||
} | ||
|
||
// if there is no effective username let the admin be himself |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might update this wording slightly:
// if the user doesn't have an effective username, we're using their normal username (not impersonating)
app/session.server.ts
Outdated
|
||
// if there is no effective username let the admin be himself | ||
if (!effectiveUsername || effectiveUsername === '') { | ||
return await getUser(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you use getUser()
and in 69 you use getUserByUsername()
. Can we use the same path in both cases? Not critical, but it feels odd that we have different code paths.
app/session.server.ts
Outdated
@@ -62,12 +102,14 @@ export async function requireUsername( | |||
redirectTo: string = new URL(request.url).pathname | |||
) { | |||
const username = await getUsername(request); | |||
const effectiveUsername = await getEffectiveUsername(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above, run at same time.
Also, please squash this down into 1 commit at this point. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, thanks for persevering through the many reviews. I've left 2 final updates, and this is R+ from me.
app/session.server.ts
Outdated
|
||
export async function setEffectiveUsername( | ||
username: string, | ||
name: string | undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you can use name?: string
to mean "this is a string
or undefined".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should name
really be called effectiveUsername
? I think using name
is ambiguous.
The above was some misclicks |
Fixes #306 or trying to anyways.
I found that I could not set multiple key value pairs on session to commit it so I made a new cookie instead, curious to see what people think.