Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Refactor auth and enable static file serving #577

Merged
merged 20 commits into from
Jun 9, 2022

Conversation

elliotbonneville
Copy link
Contributor

@elliotbonneville elliotbonneville commented May 27, 2022

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

  • Rewrite authentication layer and introduce auth slice to store auth state client-side
  • Remove next-redux-wrapper and all hydration since we no longer render anything serverside
  • Clean up user management code to use new auth slice
  • Disable image optimization
  • Update the build script to execute next build && next export to the out dir for static files

Checklist

  • Update CHANGELOG.md file
    • Merge in main so the most recent CHANGELOG.md file is being appended to
    • Add description within the Unreleased 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.
    • Add a link to this PR at the end of the description with the PR number as the text. example: #1
  • Applicable documentation updated (guides, quickstart, postman collections, tutorial, fidesdemo, database diagram.
  • If docs updated (select one):
    • documentation complete, or draft/outline provided (tag docs-team to complete/review on this branch)
    • documentation issue created (tag docs-team to complete issue separately)
  • Good unit test/integration test coverage
  • This PR contains a DB migration. If checked, the reviewer should confirm with the author that the down_revision correctly references the previous migration before merging
  • The Run Unsafe PR Checks label has been applied, and checks have passed, if this PR touches any external services

Ticket

Fixes https://github.com/ethyca/fidesui/issues/2

@@ -44,37 +44,37 @@ const DenyPrivacyRequestModal = ({
returnFocusOnClose={false}
>
<ModalOverlay />
<ModalContent width='100%' maxWidth='456px'>
<ModalContent width="100%" maxWidth="456px">
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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!

Copy link
Contributor

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.

@elliotbonneville elliotbonneville marked this pull request as ready for review June 7, 2022 18:57
@elliotbonneville elliotbonneville changed the title [WIP] Refactor auth and enable static file serving Refactor auth and enable static file serving Jun 8, 2022
Copy link
Contributor

@TheAndrewJackson TheAndrewJackson left a 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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

storedAuthState = JSON.parse(storedAuthStateString);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
Copy link
Contributor

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.

Suggested change
console.log(error);
console.error(error);

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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 = ({
Copy link
Contributor

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';
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same!

Copy link
Contributor Author

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();
Copy link
Contributor

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.

Copy link
Contributor Author

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).

Comment on lines +78 to +85
prepareHeaders: (headers, { getState }) => {
const token = selectToken(getState() as RootState);
headers.set('Access-Control-Allow-Origin', '*');
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
Copy link
Contributor

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.

Copy link
Contributor Author

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';
Copy link
Contributor

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

Copy link
Contributor Author

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}
Copy link
Contributor

@TheAndrewJackson TheAndrewJackson Jun 8, 2022

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

Copy link
Contributor Author

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>
Copy link
Contributor

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.

Copy link
Contributor Author

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...

Comment on lines 22 to 25
const { data } = useGetAllPrivacyRequestsQuery({
id: Array.isArray(id) ? id[0] : id,
verbose: true,
});
Copy link
Contributor

@TheAndrewJackson TheAndrewJackson Jun 8, 2022

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.

Suggested change
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 === '',
}
);

Copy link
Contributor Author

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>
Copy link
Contributor

@TheAndrewJackson TheAndrewJackson Jun 8, 2022

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

Copy link
Contributor Author

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. :)

Copy link
Contributor

@allisonking allisonking left a 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: {
Copy link
Contributor

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",
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

export interface UserResponse {
id: string;
}
// eslint-disable-next-line import/prefer-default-export
Copy link
Contributor

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?

Copy link
Contributor Author

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';
Copy link
Contributor

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';
}
Copy link
Contributor

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

Copy link
Contributor Author

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!

Copy link
Contributor

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? 👀

Copy link
Contributor

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 ifs. 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

@elliotbonneville
Copy link
Contributor Author

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
Copy link
Contributor

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 */}
Copy link
Contributor

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">
Copy link
Contributor

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}
Copy link
Contributor

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)}
Copy link
Contributor

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';
}
Copy link
Contributor

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? 👀

@TheAndrewJackson TheAndrewJackson self-requested a review June 8, 2022 19:53
Copy link
Contributor

@TheAndrewJackson TheAndrewJackson left a 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

Copy link
Contributor

@allisonking allisonking left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@TheAndrewJackson TheAndrewJackson dismissed LKCSmith’s stale review June 9, 2022 16:08

This is blocking the merge. I think any leftover issues can be addressed in future PRs.

@TheAndrewJackson TheAndrewJackson merged commit b8110c5 into main Jun 9, 2022
@TheAndrewJackson TheAndrewJackson deleted the feature/refactor-auth branch June 9, 2022 16:09
sanders41 pushed a commit that referenced this pull request Sep 22, 2022
* 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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants