-
Notifications
You must be signed in to change notification settings - Fork 16
Refactor auth and enable static file serving #577
Conversation
1c2fa6b
to
4e8a0af
Compare
2189c2d
to
75fbdd0
Compare
@@ -44,37 +44,37 @@ const DenyPrivacyRequestModal = ({ | |||
returnFocusOnClose={false} | |||
> | |||
<ModalOverlay /> | |||
<ModalContent width='100%' maxWidth='456px'> | |||
<ModalContent width="100%" maxWidth="456px"> |
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.
@TheAndrewJackson I know you had set jsxSingleQuote
to true, but is there any chance we can keep double quotes in JSX? I've never worked with JSX single quotes before. I don't want to be pedantic but I'm wondering what the purpose of them is? Just consistency?
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.
We can definitely keep double quotes. I used single because I was following someone else who used single quotes. If double quotes are preferred then let's use them instead. Especially if the fidesctl
repo is using double quotes.
It's not a big deal anyway. As long as the formatter automatically sets it I'm fine with either.
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.
Cool. Let's update the Prettier file to use double quotes then – thanks for your flexibility here!
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.
Ah yes, the great debate.
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 looks good! I like a lot of the refactors and the tests. I finished my first pass of reading all of the code. I just have a couple small nits and questions. I learned a few things while reading this PR!
I'm going to do some functional testing now and see if anything visually pops up that I couldn't see while reviewing it.
|
||
To enable stable authentication you must supply a `NEXTAUTH_SECRET` environment | ||
variable. The best way to do this is by creating a `.env.local` file, which Next |
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 the .env.local file no longer needed for auth to work properly?
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.
Nope. This was an artifact of the previous library, which we're no longer using.
clients/admin-ui/src/app/store.ts
Outdated
storedAuthState = JSON.parse(storedAuthStateString); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.log(error); |
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 minor. We could have the log show up as an error.
console.log(error); | |
console.error(error); |
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.
Good one! Let's do that.
const router = useRouter(); | ||
const token = useSelector(selectToken); | ||
|
||
// TODO: check for token invalidation |
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 this still a TODO?
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.
Yes. Pending input from @seanpreston regarding whether or not we should have a token validation endpoint. I left this out of this PR because it's not currently supported either, so while definitely nice to have and important, it's an additional feature here.
children: JSX.Element; | ||
} | ||
|
||
const ProtectedRoute = ({ |
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 a cool component. I like this pattern.
} from '@reduxjs/toolkit'; | ||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; | ||
|
||
import type { RootState } from '../../app/store'; |
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.
Oh this is cool. I didn't know about importing/exporting types. I'll make sure to use this from now on.
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!
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's most helpful when you need types from two files that depend on one another – it avoids the circular dependency problem because the types can be statically inferred and the files don't need to be imported and executed, which is where you'd get that import loop.
|
||
export const { login, logout } = authSlice.actions; | ||
|
||
export const credentialStorage = createListenerMiddleware(); |
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.
createListenerMiddleware seems really convenient. I didn't know RTK had it. I like 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.
Yup! They make it really easy to build middleware in, and this is where any side effects of actions should be specified (you don't want to do that in reducers, since they need to be pure and not affect any external state whatsoever).
prepareHeaders: (headers, { getState }) => { | ||
const token = selectToken(getState() as RootState); | ||
headers.set('Access-Control-Allow-Origin', '*'); | ||
if (token) { | ||
headers.set('authorization', `Bearer ${token}`); | ||
} | ||
return headers; | ||
}, |
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 looks like all of the prepareHeaders
in each createApi
have the same code. Maybe the logic could be refactored out into one function that is shared between them.
I think there might be some new headers that get added to every request in an upcoming update for fideslog
for analytics. Something like X-Fides-Source: fidesops-admin-ui
. That would help simplify adding headers that are applied to each 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.
Yep, pretty sure they do! That's a great idea.
@@ -0,0 +1,11 @@ | |||
// components/Image.js | |||
import NextImage from 'next/image'; |
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 there any benefit to using the NextImage
over Chakra UIs Image
since we aren't optimizing images anymore?
https://v1.chakra-ui.com/docs/components/media-and-icons/image
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.
Hmm, I would say the only advantage is that it leaves the door open for easily refactoring to include image optimization when we do re-introduce the Node server, but honestly that's pretty trivial. I mainly made this change to avoid having to update any places where the Image component was being used in order get static building working.
In the future we should probably just reach for Chakra's Image component.
import theme from '../theme'; | ||
|
||
if (process.env.NEXT_PUBLIC_MOCK_API) { | ||
// eslint-disable-next-line global-require | ||
require('../mocks'); | ||
} | ||
|
||
const SafeHydrate: React.FC = ({ children }) => ( | ||
<div suppressHydrationWarning> | ||
{typeof window === 'undefined' ? null : children} |
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.
What is the hydration warning saying and doing? I'm guessing it's an annoying error message that we don't need while developing
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.
That's correct. Basically it's noting that what gets rendered on the server and what gets rendered on the client isn't the same, because we're no longer taking advantage of serverside rendering. This won't apply at all without a server, so the warning is meaningless.
User Management | ||
</BreadcrumbLink> | ||
</BreadcrumbItem> | ||
<ProtectedRoute> |
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 ProtectedRoute
component seems more elegant than the original getServerSideProps
solution. Just wrap the JSX with 1 more component and it's being authenticated.
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.
Yep! To be fair, we can definitely improve on the getServerSideProps
pattern, but I hadn't cleaned that up because I knew we were intending to rip it out.
I will say that one thing I noticed with this pattern is that it's easily possible to forget to authenticate a page, so we just need to either a) wrap all of the pages by default at the App.tsx
level, or stay on top of that while developing. I'm not super concerned, since the requests wouldn't be authenticated anyhow, so even if we do forget to wrap a page it's not the end of the world – and we don't create pages that frequently, so the error surface area is low. But it's just another place where we could introduce an error.
To be fair, that same logic applied to the previous solution too, so I guess it's not a new problem. Something to think about...
const { data } = useGetAllPrivacyRequestsQuery({ | ||
id: Array.isArray(id) ? id[0] : id, | ||
verbose: true, | ||
}); |
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.
In my functional testing I found that this page was doing 2 requests to the backend. The first was with no filtering id so it would get back multiple privacy requests. The second would have the id from the url as expected so it would filter and get the data.
This update makes it skip getting the data the url until the const { id = '' } = router.query;
line above has parsed the id as expected. Now it will only do 1 request.
const { data } = useGetAllPrivacyRequestsQuery({ | |
id: Array.isArray(id) ? id[0] : id, | |
verbose: true, | |
}); | |
const { data } = useGetAllPrivacyRequestsQuery( | |
{ | |
id: Array.isArray(id) ? id[0] : id, | |
verbose: true, | |
}, | |
{ | |
skip: id === '', | |
} | |
); |
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.
Ahh, really good catch. This is page is part I was the most worried about, since I just quickly made a few changes to get it up to date with the new auth system and no longer using the Next server. That's a great solution, and I didn't know about the skip
flag!
const body = | ||
data?.items.length === 0 ? ( | ||
!data || data?.items.length === 0 ? ( | ||
<Text>404 no subject request found</Text> |
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 looks like this page is flashing the 404 message for a split second while the page is loading the data. If the user has a slow connection it will display the 404 message while the page is loading. I guess we'll need to incorporate some kind of loading behavior now since we're no longer doing SSR. That might be out of scope of this PR though
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.
Good spot. Between Chakra UI and Redux Toolkit Query we actually have everything we need to solve this in like 5min, so I pushed up a quick fix for this. :)
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 much refactoring, amazing! 🤩 I don't have much to add outside of what Andrew already caught (👏 ), just some comments here and there. thanks for doing this!!
); | ||
it('renders the Subject Requests page by default when logged in', () => { | ||
render(<Home />, { | ||
preloadedState: { |
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.
whoa this is very cool 🤩
"private": true, | ||
"scripts": { | ||
"dev": "next dev", | ||
"dev:mock": "echo '🚨 Running with mock API'; NEXT_PUBLIC_MOCK_API=true next dev", | ||
"build": "next build", | ||
"build": "next build && next export", |
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.
we'll probably want to standardize this across the repos. fidesctl looks like this right now: https://github.com/ethyca/fides/blob/aking-706-edit-collection/clients/admin-ui/package.json/#L15-L17
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.
Yeah. Do you know off the top of your head if Next can export to a location in a parent directory? Would simplify a lot of the copy / paste / file moving stuff.
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.
looks like the answer is no, it has to stay in the project dir
https://nextjs.org/docs/api-reference/next.config.js/setting-a-custom-build-directory
distDir should not leave your project directory. For example, ../build is an invalid directory.
clients/admin-ui/src/constants.ts
Outdated
export interface UserResponse { | ||
id: string; | ||
} | ||
// eslint-disable-next-line import/prefer-default-export |
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 there are >1 exports now, so don't need this line anymore?
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.
Yup yup yup!
} from '@reduxjs/toolkit'; | ||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; | ||
|
||
import type { RootState } from '../../app/store'; |
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!
} | ||
if (!values.password) { | ||
errors.password = 'Required'; | ||
} |
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.
a note for the future, I'd love to start using yup for validation
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.
Sounds good. Happy to bring that in!
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.
@allisonking Is there a benefit to using yup that I'm not seeing? 👀
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 like that yup
makes validation declarative (you declare all the requirements a field has to meet at the same time you define the form (i.e. required, minimum 8 letters, etc.) and then it'll do the validation on submit (or I think you can also do it on touch) so then you don't have to do manual validation by looking through all the fields yourself with a bunch of if
s. since validation is defined in a schema, you can also reuse that schema which is handy. it's also nice that formik supports yup out of the box and they recommend it in their tutorial too: https://formik.org/docs/tutorial#schema-validation-with-yup
Thanks very much for your thorough review and great catches, folks! Really appreciate you taking the time to look this over and again, sorry about such a giant PR! Heh. |
try { | ||
storedAuthState = JSON.parse(storedAuthStateString); | ||
} catch (error) { | ||
// TODO: build in formal error logging system |
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.
What would this generally look like in practice?
<MenuList shadow="xl"> | ||
<Stack px={3} py={2} spacing={0}> | ||
<Text fontWeight="medium">{username}</Text> | ||
{/* This text should only show if actually an 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.
I think this is an artifact from when I was working on the admin ui. It's been a minute since I've worked on this, and you don't need to do this, but other than checking against a user's privileges, is there a way to check a users' "role"? Is it all based on specific privileges a user has? I'm a little confused on which privileges specifically make a user an "Admin" vs not.
@@ -44,37 +44,37 @@ const DenyPrivacyRequestModal = ({ | |||
returnFocusOnClose={false} | |||
> | |||
<ModalOverlay /> | |||
<ModalContent width='100%' maxWidth='456px'> | |||
<ModalContent width="100%" maxWidth="456px"> |
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.
Ah yes, the great debate.
{/* Only the associated user can change their own password */} | ||
{id === user.id && <UpdatePasswordModal id={id} />} | ||
{/* Only the owner of this profile can change their password */} | ||
{isOwnProfile ? <UpdatePasswordModal id={profileId} /> : null} |
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.
🎉
_hover={{ bg: 'primary.400' }} | ||
_active={{ bg: 'primary.500' }} | ||
colorScheme='primary' | ||
colorScheme="primary" | ||
// disabled={!(isValid && dirty)} |
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 dunno if this state is needed anymore.
} | ||
if (!values.password) { | ||
errors.password = 'Required'; | ||
} |
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.
@allisonking Is there a benefit to using yup that I'm not seeing? 👀
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 just finished my second pass and I think everything looks good now 🙂 I will be comfortable with merging once this PR also gets an approval from @allisonking and @LKCSmith
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 blocking the merge. I think any leftover issues can be addressed in future PRs.
* Rewrite auth * Remove unnecessary API files * Improve error handling in new user creation * Enable static build export and serving * Remove next-redux-wrapper * Correct breaking ESLint file * Clean up linting errors * Clean up merge artifacts * Update existing tests to use new auth * Implement auth testing * Cleanup merge artifact * Cleanup merge artifacts * Update Subject Requests detail page to use new auth * Update docs and changelog * Stop colocating tests in pages/ directory * Require authentication for request details page * Update error logging to use console.error * Improve privacy request details page loading sequence * Remove extraneous comment
Purpose
Make it possible to run the Next application without relying on the Node server by completely rewriting authentication layer and disabling image optimization.
Changes
next-redux-wrapper
and all hydration since we no longer render anything serversidebuild
script to executenext build && next export
to theout
dir for static filesChecklist
CHANGELOG.md
fileCHANGELOG.md
file is being appended toUnreleased
section in an appropriate category. Add a new category from the list at the top of the file if the needed one isn't already there.Run Unsafe PR Checks
label has been applied, and checks have passed, if this PR touches any external servicesTicket
Fixes https://github.com/ethyca/fidesui/issues/2