-
Notifications
You must be signed in to change notification settings - Fork 29
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
Add Panoptes auth to app-root #5459
Changes from all commits
7a7133d
bb96440
afe0f0d
e6290e3
b12b88b
9d9f9ff
a2cda99
e1b6239
8eefa1c
b90779c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"extends": [ | ||
"next/core-web-vitals", | ||
"plugin:jsx-a11y/recommended", | ||
"plugin:@next/next/recommended" | ||
], | ||
"rules": { | ||
"consistent-return": "error" | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": [ | ||
"src/**/*.stories.js" | ||
], | ||
"rules": { | ||
"import/no-anonymous-default-export": "off" | ||
} | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,53 @@ | ||||||
if (process.env.NEWRELIC_LICENSE_KEY) { | ||||||
await import('newrelic') | ||||||
} | ||||||
|
||||||
import express from 'express' | ||||||
import next from 'next' | ||||||
|
||||||
const port = parseInt(process.env.PORT, 10) || 3000 | ||||||
const dev = process.env.NODE_ENV !== 'production' | ||||||
|
||||||
const APP_ENV = process.env.APP_ENV || 'development' | ||||||
|
||||||
const hostnames = { | ||||||
development: 'local.zooniverse.org', | ||||||
branch: 'fe-project-branch.preview.zooniverse.org', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change will crash when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it’s just in all the server setups by default. The content pages app defines it too.
|
||||||
staging: 'frontend.preview.zooniverse.org', | ||||||
production : 'www.zooniverse.org' | ||||||
} | ||||||
const hostname = hostnames[APP_ENV] | ||||||
|
||||||
const app = next({ dev, hostname, port }) | ||||||
const handle = app.getRequestHandler() | ||||||
|
||||||
app.prepare().then(async () => { | ||||||
const server = express() | ||||||
|
||||||
server.get('*', (req, res) => { | ||||||
return handle(req, res) | ||||||
}) | ||||||
|
||||||
let selfsigned | ||||||
try { | ||||||
selfsigned = await import('selfsigned') | ||||||
} catch (error) { | ||||||
console.error(error) | ||||||
} | ||||||
if (APP_ENV === 'development' && selfsigned) { | ||||||
const https = await import('https') | ||||||
|
||||||
const attrs = [{ name: 'commonName', value: hostname }]; | ||||||
const { cert, private: key } = selfsigned.generate(attrs, { days: 365 }) | ||||||
return https.createServer({ cert, key }, server) | ||||||
.listen(port, err => { | ||||||
if (err) throw err | ||||||
console.log(`> Ready on https://${hostname}:${port}`) | ||||||
}) | ||||||
} else { | ||||||
return server.listen(port, err => { | ||||||
if (err) throw err | ||||||
console.log(`> Ready on http://${hostname}:${port}`) | ||||||
}) | ||||||
} | ||||||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
'use client' | ||
|
||
import zooTheme from '@zooniverse/grommet-theme' | ||
import { Grommet } from 'grommet' | ||
import { createGlobalStyle } from 'styled-components' | ||
|
||
import { PanoptesAuthContext } from '../contexts' | ||
import { useAdminMode, usePanoptesUser } from '../hooks' | ||
|
||
const GlobalStyle = createGlobalStyle` | ||
body { | ||
margin: 0; | ||
} | ||
` | ||
|
||
/** | ||
Context for every page: | ||
- global page styles. | ||
- Zooniverse Grommet theme. | ||
- Panoptes auth (user account and admin mode.) | ||
*/ | ||
export default function PageContextProviders({ children }) { | ||
const { data: user, error, isLoading } = usePanoptesUser() | ||
const { adminMode, toggleAdmin } = useAdminMode(user) | ||
const authContext = { adminMode, error, isLoading, toggleAdmin, user } | ||
|
||
return ( | ||
<PanoptesAuthContext.Provider value={authContext}> | ||
<GlobalStyle /> | ||
<Grommet | ||
background={{ | ||
dark: 'dark-1', | ||
light: 'light-1' | ||
}} | ||
theme={zooTheme} | ||
> | ||
{children} | ||
</Grommet> | ||
</PanoptesAuthContext.Provider> | ||
) | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
'use client' | ||
import { AdminCheckbox, ZooFooter } from '@zooniverse/react-components' | ||
import { useContext } from 'react' | ||
|
||
import { PanoptesAuthContext } from '../contexts' | ||
|
||
export default function PageFooter() { | ||
const { adminMode, toggleAdmin, user } = useContext(PanoptesAuthContext) | ||
|
||
return ( | ||
<ZooFooter | ||
adminContainer={user?.admin ? <AdminCheckbox onChange={toggleAdmin} checked={adminMode} /> : null} | ||
/> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
'use client' | ||
import { ZooHeader } from '@zooniverse/react-components' | ||
import { useContext } from 'react' | ||
|
||
import { | ||
useUnreadMessages, | ||
useUnreadNotifications | ||
} from '../hooks' | ||
|
||
import { PanoptesAuthContext } from '../contexts' | ||
|
||
export default function PageHeader() { | ||
const { adminMode, user } = useContext(PanoptesAuthContext) | ||
const { data: unreadMessages }= useUnreadMessages(user) | ||
const { data: unreadNotifications }= useUnreadNotifications(user) | ||
|
||
return ( | ||
<header aria-label='Zooniverse site header'> | ||
<ZooHeader | ||
isAdmin={adminMode} | ||
unreadMessages={unreadMessages} | ||
unreadNotifications={unreadNotifications} | ||
user={user} | ||
/> | ||
</header> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,15 @@ | ||
'use client' | ||
/** | ||
* Note that all child components are now client components. | ||
* If we want children of RootLayout to be server components | ||
* a ZooHeaderContainer and ZooFooterContainer could be created instead. | ||
*/ | ||
|
||
import { createGlobalStyle } from 'styled-components' | ||
import { Grommet } from 'grommet' | ||
import zooTheme from '@zooniverse/grommet-theme' | ||
import ZooHeader from '@zooniverse/react-components/ZooHeader' | ||
import ZooFooter from '@zooniverse/react-components/ZooFooter' | ||
|
||
const GlobalStyle = createGlobalStyle` | ||
body { | ||
margin: 0; | ||
} | ||
` | ||
import PageContextProviders from './PageContextProviders.js' | ||
import PageHeader from './PageHeader.js' | ||
import PageFooter from './PageFooter.js' | ||
|
||
export default function RootLayout({ children }) { | ||
return ( | ||
<body> | ||
<GlobalStyle /> | ||
<Grommet | ||
background={{ | ||
dark: 'dark-1', | ||
light: 'light-1' | ||
}} | ||
theme={zooTheme} | ||
> | ||
<ZooHeader /> | ||
<PageContextProviders> | ||
<PageHeader /> | ||
{children} | ||
<ZooFooter /> | ||
</Grommet> | ||
<PageFooter /> | ||
</PageContextProviders> | ||
</body> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { createContext } from 'react' | ||
|
||
const PanoptesAuthContext = createContext({}) | ||
|
||
export default PanoptesAuthContext |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as PanoptesAuthContext } from './PanoptesAuthContext.js' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import auth from 'panoptes-client/lib/auth' | ||
import { auth as authHelpers } from '@zooniverse/panoptes-js' | ||
|
||
/** | ||
Get a Panoptes user from a Panoptes JSON Web Token (JWT), if we have one, or from | ||
the Panoptes API otherwise. | ||
*/ | ||
export default async function fetchPanoptesUser({ user: storedUser }) { | ||
try { | ||
const jwt = await auth.checkBearerToken() | ||
/* | ||
`crypto.subtle` is needed to decrypt the Panoptes JWT. | ||
It will only exist for https:// URLs. | ||
*/ | ||
const isSecure = crypto?.subtle | ||
if (jwt && isSecure) { | ||
/* | ||
avatar_src isn't encoded in the Panoptes JWT, so we need to add it. | ||
https://github.com/zooniverse/panoptes/issues/4217 | ||
*/ | ||
const { user, error } = await authHelpers.decodeJWT(jwt) | ||
if (user) { | ||
const { admin, display_name, id, login } = user | ||
return { | ||
avatar_src: storedUser.avatar_src, | ||
...user | ||
} | ||
} | ||
if (error) { | ||
throw error | ||
} | ||
} | ||
} catch (error) { | ||
console.log(error) | ||
} | ||
const { admin, avatar_src, display_name, id, login } = await auth.checkCurrent() | ||
return { admin, avatar_src, display_name, id, login } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as fetchPanoptesUser } from './fetchPanoptesUser.js' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export { default as useAdminMode } from './useAdminMode.js' | ||
export { default as usePanoptesUser } from './usePanoptesUser.js' | ||
export { default as useUnreadMessages } from './useUnreadMessages.js' | ||
export { default as useUnreadNotifications } from './useUnreadNotifications.js' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useEffect, useState } from 'react' | ||
|
||
const isBrowser = typeof window !== 'undefined' | ||
const localStorage = isBrowser ? window.localStorage : null | ||
const storedAdminFlag = !!localStorage?.getItem('adminFlag') | ||
const adminBorderImage = 'repeating-linear-gradient(45deg,#000,#000 25px,#ff0 25px,#ff0 50px) 5' | ||
|
||
export default function useAdminMode(user) { | ||
const [adminState, setAdminState] = useState(storedAdminFlag) | ||
const adminMode = user?.admin && adminState | ||
|
||
useEffect(function onUserChange() { | ||
const isAdmin = user?.admin | ||
if (isAdmin) { | ||
const adminFlag = !!localStorage?.getItem('adminFlag') | ||
setAdminState(adminFlag) | ||
} else { | ||
localStorage?.removeItem('adminFlag') | ||
} | ||
}, [user?.admin]) | ||
|
||
useEffect(function onAdminChange() { | ||
if (adminMode) { | ||
document.body.style.border = '5px solid' | ||
document.body.style.borderImage = adminBorderImage | ||
} | ||
return () => { | ||
document.body.style.border = '' | ||
document.body.style.borderImage = '' | ||
} | ||
}, [adminMode]) | ||
|
||
function toggleAdmin() { | ||
let newAdminState = !adminState | ||
setAdminState(newAdminState) | ||
if (newAdminState) { | ||
localStorage?.setItem('adminFlag', true) | ||
} else { | ||
localStorage?.removeItem('adminFlag') | ||
} | ||
} | ||
|
||
return { adminMode, toggleAdmin } | ||
} |
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.
Because this express server is only used for local development, do we need new relic? Just want to think through this server while we have the chance and consider the build bug from app-content-pages too: #5428
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 express server runs on
yarn start
too, but we need to test that on Kubernetes.Your comment reminds me that the live content pages app isn't logging to New Relic 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.
Right! I think that's what I'm getting at. The content pages app will probably never use a custom express server in deployment again, so New Relic should not be setup for app-root either. Thoughts?
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.
How do we debug slow pages and Postgres queries without New Relic?
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 mean, content pages should be using New Relic in production too. It should be imported when the Next app loads and starts, at the top of
next.config.js
.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.
Does the debugging via New Relic happen via logging from a production deploy, a local build, or both?
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.
🤔 app-project does have New Relic imported into
next.config.js
but app-content-pages does not.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.
Any environment that has
NEWRELIC_LICENSE_KEY
set, according to the code. That’s production and staging deploys.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 they only have examples for the pages router.
https://github.com/newrelic-experimental/newrelic-nextjs-integration
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 addition to lack of New Relic + App Router documentation, I strongly suggest that New Relic isn't included in app-root (for now). We don't really know what deployment of this app will look like yet, and I'd prefer to setup logging tools that are used in staging + production envs when we get to that step. I've added it as a to-do in the project board.
Otherwise everything here is looking good!