-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Panoptes auth to app-root (#5459)
* add Panoptes auth to app-root - add the Panoptes auth client, from `panoptes-client`. - add `usePanoptesUser` for SWR-style user fetching. - add an express server to handle HTTPS in local development. - add the panoptes user to both page header and footer. * Use a global user object (experimental) * Add unread messages and notifications - add `swr`. - `useUnreadMessages` fetches your unread message count with `useSWR`. - `useUnreadNotifications` fetchs your unread notifications count with `useSWR`. * Add admin mode - persist the current user in local storage. - check admin mode with `useAdminMode`. - add the admin mode toggle to the footer, and the admin mode border to the page. * Global page context Add `PageContextProvider`, responsible for: - global page styles. - providing the Zooniverse Grommet theme. - providing Panoptes auth context (user and admin mode.) * Clean up usePanoptesUser - move `fetchPanoptesUser` into a helper. - add explanatory comments. - restore the missing `avatar_src` when getting the user object from a JWT. * Configure ESLint * Optimise package imports * Rewrite usePanoptesUser with useSWR - use `useSWR` to fetch the user object from Panoptes. - persist the returned data in local storage. - reset the current Panoptes session when auth changes. * Add named page headers
- Loading branch information
1 parent
8a48d12
commit 5a9a09f
Showing
20 changed files
with
482 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
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}`) | ||
}) | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { createContext } from 'react' | ||
|
||
const PanoptesAuthContext = createContext({}) | ||
|
||
export default PanoptesAuthContext |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as PanoptesAuthContext } from './PanoptesAuthContext.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as fetchPanoptesUser } from './fetchPanoptesUser.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} |
Oops, something went wrong.