all repos — caroster @ 46303167ec1138b375af3513350eb491415bb6a1

[Octree] Group carpool to your event https://caroster.io

feat: :lock: Setup Next Auth
Tim Izzo tim@octree.ch
Thu, 15 Sep 2022 11:30:47 +0000
commit

46303167ec1138b375af3513350eb491415bb6a1

parent

ba1f0945c383630d88192de37465dc72ebca0328

M .gitignore.gitignore

@@ -1,2 +1,3 @@

.DS_Store -.env*+.env* +!.env.example
A frontend/.env.example

@@ -0,0 +1,6 @@

+STRAPI_URL=http://localhost:1337 +SENTRY_SERVER_INIT_PATH=.next/server/sentry/initServerSDK.js +NEXTAUTH_SECRET=gewrsdfhgfh +NEXTAUTH_URL=http://localhost:3000/api/nauth +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET=
M frontend/components/Logo/index.jsfrontend/components/Logo/index.js

@@ -4,9 +4,9 @@ import useSettings from '../../hooks/useSettings';

const Logo = () => { const classes = useStyles(); - const {user} = useProfile(); + const {connected} = useProfile(); const settings = useSettings(); - const appLink = user ? '/dashboard' : settings?.['about_link'] || ''; + const appLink = connected ? '/dashboard' : settings?.['about_link'] || ''; return ( <div className={classes.layout}> <a href={appLink} className={classes.link}>
M frontend/containers/CreateEvent/Step1.tsxfrontend/containers/CreateEvent/Step1.tsx

@@ -7,14 +7,16 @@ import Checkbox from '@material-ui/core/Checkbox';

import FormControlLabel from '@material-ui/core/FormControlLabel'; import {useTranslation} from 'react-i18next'; import useDebounce from '../../hooks/useDebounce'; -import useProfile from '../../hooks/useProfile'; import {CardActions} from '@material-ui/core'; import {isValidEmail} from '../../lib/formValidation'; +import {useSession} from 'next-auth/react'; const Step1 = ({nextStep, event, addToEvent}) => { const {t} = useTranslation(); - const {connected, user} = useProfile(); - const classes = useStyles({connected}); + const session = useSession(); + const user = session?.data?.user; + const isAuthenticated = session.status === 'authenticated'; + const classes = useStyles({connected: isAuthenticated}); // States const [name, setName] = useState(event.name ?? '');

@@ -30,15 +32,15 @@

const canSubmit = useMemo(() => { const n = name.length > 0; const e = email.length > 0 && emailIsValid; - return connected ? n : n && e; - }, [name, email, emailIsValid, connected]); + return isAuthenticated ? n : n && e; + }, [name, email, emailIsValid, isAuthenticated]); const onNext = submitEvent => { if (submitEvent.preventDefault) submitEvent.preventDefault(); addToEvent({ name, - email: connected ? user.email : email, - newsletter: connected ? true : newsletter, + email: isAuthenticated ? user.email : email, + newsletter: isAuthenticated ? true : newsletter, }); nextStep(); return false;

@@ -56,7 +58,7 @@ onChange={e => setName(e.target.value)}

id="NewEventName" name="name" /> - {!connected && ( + {!isAuthenticated && ( <> <TextField label={t('event.creation.creator_email')}

@@ -94,7 +96,7 @@ >

{t('event.creation.next')} </Button> - {!connected && ( + {!isAuthenticated && ( <div className={classes.addFromAccountSection}> <Typography variant="body1"> {t('event.creation.addFromAccount.title')}
M frontend/containers/EventBar/index.tsxfrontend/containers/EventBar/index.tsx

@@ -10,7 +10,6 @@ import {makeStyles} from '@material-ui/core/styles';

import {useState} from 'react'; import {useRouter} from 'next/router'; import {useTranslation} from 'react-i18next'; -import useAuthStore from '../../stores/useAuthStore'; import useProfile from '../../hooks/useProfile'; import useShare from '../../hooks/useShare'; import GenericMenu from '../GenericMenu';

@@ -20,8 +19,7 @@ const {t} = useTranslation();

const router = useRouter(); const {share} = useShare(); const [anchorEl, setAnchorEl] = useState(null); - const token = useAuthStore(s => s.token); - const {user} = useProfile(); + const {profile, connected} = useProfile(); const classes = useStyles(); const signUp = () =>

@@ -69,16 +67,15 @@ },

{divider: true}, ]; - const menuActions = token ? loggedMenuActions : noUserMenuActions; - const userInfos = user - ? [{label: user.username, id: 'Email'}, {divider: true}] + const menuActions = connected ? loggedMenuActions : noUserMenuActions; + const appLink = connected ? '/dashboard' : `/e/${event.uuid}` || ''; + const userInfos = profile + ? [{label: profile.username, id: 'Email'}, {divider: true}] : []; - const appLink = user ? '/dashboard' : `/e/${event.uuid}` || ''; - - const UserIcon = user ? ( + const UserIcon = profile ? ( <Avatar className={classes.avatar}> - {`${user.username[0]}`.toUpperCase()} + {`${profile.username[0]}`.toUpperCase()} </Avatar> ) : ( <Icon>more_vert</Icon>
M frontend/containers/GenericMenu/index.tsxfrontend/containers/GenericMenu/index.tsx

@@ -1,10 +1,9 @@

import Menu from '@material-ui/core/Menu'; import {useTranslation} from 'react-i18next'; -import useAuthStore from '../../stores/useAuthStore'; -import useProfile from '../../hooks/useProfile'; import useSettings from '../../hooks/useSettings'; import Languages from '../Languages/MenuItem'; import Action, {ActionType} from './Action'; +import {signOut, useSession} from 'next-auth/react'; interface Props { anchorEl: Element;

@@ -16,15 +15,16 @@ const GenericMenu = (props: Props) => {

const {anchorEl, setAnchorEl, actions = []} = props; const {t} = useTranslation(); const settings = useSettings(); - const logout = useAuthStore(s => s.logout); - const {user} = useProfile(); + const session = useSession(); + const isAuthenticated = session.status === 'authenticated'; - const logoutMenuItem = user && { + const logoutMenuItem = isAuthenticated && { label: t('menu.logout'), onClick: () => { - logout(); - window.location.href = settings['about_link']; setAnchorEl(null); + signOut({ + callbackUrl: settings?.['about_link'] || '/', + }); }, id: 'LogoutTabs', };
M frontend/containers/GenericToolbar/index.tsxfrontend/containers/GenericToolbar/index.tsx

@@ -23,10 +23,10 @@ }) => {

const router = useRouter(); const [anchorEl, setAnchorEl] = useState(null); const classes = useStyles(); - const {user} = useProfile(); + const {profile} = useProfile(); - const userInfos = user - ? [{label: user.username, id: 'Email'}, {divider: true}] + const userInfos = profile + ? [{label: profile.username, id: 'Email'}, {divider: true}] : []; useEffect(() => {

@@ -67,9 +67,9 @@ edge="end"

id="MenuMoreInfo" onClick={e => setAnchorEl(e.currentTarget)} > - {user ? ( + {profile ? ( <Avatar className={classes.avatar}> - {`${user.username[0]}`.toUpperCase()} + {`${profile.username[0]}`.toUpperCase()} </Avatar> ) : ( <Icon>more_vert</Icon>
M frontend/containers/LostPassword/index.jsfrontend/containers/LostPassword/index.js

@@ -18,7 +18,7 @@ const LostPassword = () => {

const {t} = useTranslation(); const classes = useStyles(); const addToast = useToastStore(s => s.addToast); - const {user} = useProfile(); + const {profile} = useProfile(); const [sendForgotPassword, {loading}] = useForgotPasswordMutation(); const [isSent, setIsSent] = useState(false); const [error, setError] = useState('');

@@ -26,9 +26,9 @@ const [email, setEmail] = useState('');

const canSubmit = email.length > 4; useEffect(() => { - if (user?.confirmed) router.replace('/confirm'); - else if (user) router.replace('/dashboard'); - }, [user]); + if (profile?.confirmed) router.replace('/confirm'); + else if (profile) router.replace('/dashboard'); + }, [profile]); const onSubmit = useCallback( async e => {
M frontend/containers/NewPassengerDialog/AddPassengerToWaitingList.tsxfrontend/containers/NewPassengerDialog/AddPassengerToWaitingList.tsx

@@ -38,7 +38,7 @@ const [email, setEmail] = useState('');

const emailValidated = validateEmail(email); const [location, setlocation] = useState(''); const canAddPassenger = !!name && !!email; - const {user} = useProfile(); + const {profile, userId} = useProfile(); const {addPassenger} = usePassengersActions(); const onAddPassenger = async (e: FormEvent) => {

@@ -48,11 +48,11 @@ email,

name, location, }; - if (addSelf && user) + if (addSelf && profile) passenger = { - user: user.id, - email: user.email, - name: user.username, + user: userId, + email: profile.email, + name: profile.username, location, };
M frontend/containers/NewTravelDialog/useActions.tsfrontend/containers/NewTravelDialog/useActions.ts

@@ -8,7 +8,7 @@ EventByUuidDocument,

useCreateTravelMutation, TravelInput, FindUserVehiclesDocument, - Event + Event, } from '../../generated/graphql'; interface Props {

@@ -21,7 +21,7 @@ const {t} = useTranslation();

const addToast = useToastsStore(s => s.addToast); const {addToEvent} = useAddToEvents(); const [createTravelMutation] = useCreateTravelMutation(); - const {user} = useProfile(); + const {connected} = useProfile(); const createTravel = async ( travelInput: TravelInput,

@@ -35,7 +35,7 @@ uuid: event.uuid,

}, }, ]; - if (user) { + if (connected) { refetchQueries.push({ query: FindUserVehiclesDocument, });
M frontend/containers/PassengersList/Passenger.tsxfrontend/containers/PassengersList/Passenger.tsx

@@ -19,9 +19,9 @@ const Passenger = (props: Props) => {

const {passenger, button, isVehicle} = props; const {t} = useTranslation(); const classes = useStyles(); - const {user} = useProfile(); + const {userId} = useProfile(); - const isUser = user && `${user.id}` === passenger?.attributes.user?.data?.id; + const isUser = `${userId}` === passenger?.attributes.user?.data?.id; const showLocation = isVehicle ? false : passenger.attributes.location; if (passenger) {
M frontend/containers/SignInForm/index.tsxfrontend/containers/SignInForm/index.tsx

@@ -7,13 +7,12 @@ import Button from '@material-ui/core/Button';

import Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography'; import CardContent from '@material-ui/core/CardContent'; -import CircularProgress from '@material-ui/core/CircularProgress'; import FormHelperText from '@material-ui/core/FormHelperText'; import CardActions from '@material-ui/core/CardActions'; import {useTranslation} from 'react-i18next'; +import {signIn} from 'next-auth/react'; import useToastsStore from '../../stores/useToastStore'; import useLoginWithProvider from '../../hooks/useLoginWithProvider'; -import useLoginForm from '../../hooks/useLoginForm'; import useAddToEvents from '../../hooks/useAddToEvents'; const SignIn = () => {

@@ -24,7 +23,6 @@ const [error, setError] = useState('');

const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const addToast = useToastsStore(s => s.addToast); - const {login, loading} = useLoginForm(email, password); const {saveStoredEvents} = useAddToEvents(); const classes = useStyles();

@@ -36,7 +34,11 @@

const onSubmit = async e => { e.preventDefault?.(); try { - await login(); + await signIn('credentials', { + email, + password, + callbackUrl: '/', + }); saveStoredEvents(); router.push('/'); } catch (error) {

@@ -119,9 +121,6 @@ type="submit"

disabled={!canSubmit} aria-disabled={!canSubmit} id="SignInSubmit" - endIcon={ - loading && <CircularProgress className={classes.loader} size={20} /> - } > {t('signin.login')} </Button>

@@ -136,10 +135,6 @@ const useStyles = makeStyles(theme => ({

content: { display: 'flex', flexDirection: 'column', - }, - loader: { - marginLeft: '14px', - color: theme.palette.background.paper, }, actions: { justifyContent: 'center',
M frontend/containers/TravelColumns/index.tsxfrontend/containers/TravelColumns/index.tsx

@@ -27,7 +27,7 @@ const slider = useRef(null);

const {t} = useTranslation(); const addToast = useToastStore(s => s.addToast); const {addToEvent} = useAddToEvents(); - const {user} = useProfile(); + const {profile, userId, connected} = useProfile(); const classes = useStyles(); const [newPassengerTravelContext, toggleNewPassengerToTravel] = useState<{ travel: TravelType;

@@ -36,24 +36,24 @@ const {addPassenger} = usePassengersActions();

const sortedTravels = travels?.slice().sort(sortTravels); const canAddSelf = useMemo(() => { - if (!user) return false; + if (!connected) return false; const isInWaitingList = event?.waitingPassengers?.data.some( - passenger => passenger.attributes.user?.data?.id === `${user.id}` + passenger => passenger.attributes.user?.data?.id === `${userId}` ); const isInTravel = event?.travels?.data.some(travel => travel.attributes.passengers?.data.some( - passenger => passenger.attributes.user?.data?.id === `${user.id}` + passenger => passenger.attributes.user?.data?.id === `${userId}` ) ); return !(isInWaitingList || isInTravel); - }, [event, user]); + }, [event, userId]); const addSelfToTravel = async (travel: TravelType) => { try { await addPassenger({ - user: user?.id, - email: user.email, - name: user.username, + user: userId, + email: profile.email, + name: profile.username, travel: travel.id, }); addToEvent(event.id);
M frontend/containers/VehicleChoiceDialog/VehicleItem.tsxfrontend/containers/VehicleChoiceDialog/VehicleItem.tsx

@@ -19,12 +19,10 @@

const VehicleItem = ({vehicle, select}: Props) => { const {t} = useTranslation(); const classes = useStyles(); - const {user} = useProfile(); + const {userId} = useProfile(); const [deleteVehicleMutation] = useDeleteVehicleMutation({ variables: {id: vehicle.id}, - refetchQueries: [ - {query: FindUserVehiclesDocument, variables: {userId: user.id}}, - ], + refetchQueries: [{query: FindUserVehiclesDocument, variables: {userId}}], }); return (
M frontend/graphql/auth.gqlfrontend/graphql/auth.gql

@@ -14,15 +14,6 @@ }

} } -mutation login($identifier: String!, $password: String!) { - login(input: {identifier: $identifier, password: $password}) { - jwt - user { - ...MeFields - } - } -} - mutation forgotPassword($email: String!) { forgotPassword(email: $email) { ok
M frontend/hooks/useAddToEvents.tsfrontend/hooks/useAddToEvents.ts

@@ -1,8 +1,8 @@

import {useCallback} from 'react'; import {useUpdateMeMutation} from '../generated/graphql'; -import useAuthStore from '../stores/useAuthStore'; import create from 'zustand'; import {persist} from 'zustand/middleware'; +import {useSession} from 'next-auth/react'; type Store = { eventsToBeAdded: string[];

@@ -26,7 +26,8 @@ );

const useAddToEvents = () => { const [updateProfile] = useUpdateMeMutation(); - const isAuth = useAuthStore(s => !!s.token); + const session = useSession(); + const isAuth = session.status === 'authenticated'; const eventsToBeAdded = store(s => s.eventsToBeAdded); const addEvent = store(s => s.addEvent); const clearStore = store(s => s.clear);
D frontend/hooks/useLoginForm.ts

@@ -1,26 +0,0 @@

-import useAuthStore from '../stores/useAuthStore'; -import {useLoginMutation} from '../generated/graphql'; - -const useLoginForm = (identifier: string, password: string) => { - const setToken = useAuthStore(s => s.setToken); - const setUser = useAuthStore(s => s.setUser); - const [sendCreds, {loading}] = useLoginMutation(); - - const login = async () => { - const {data} = await sendCreds({ - variables: { - identifier, - password, - }, - }); - const token = data?.login?.jwt; - if (token) { - setToken(token); - setUser(data?.login?.user); - } else throw new Error('no_token'); - }; - - return {login, loading}; -}; - -export default useLoginForm;
M frontend/hooks/useLoginWithProvider.tsfrontend/hooks/useLoginWithProvider.ts

@@ -1,8 +1,6 @@

-import useAuthStore from '../stores/useAuthStore'; - const useLoginWithProvider = () => { - const setToken = useAuthStore(s => s.setToken); - const setUser = useAuthStore(s => s.setUser); + const setToken = () => {}; // DEV + const setUser = () => {}; // DEV const loginWithProvider = async (provider: string, search: string) => { const resultRaw = await fetch(`/api/auth/${provider}/callback${search}`);
M frontend/hooks/useProfile.tsfrontend/hooks/useProfile.ts

@@ -1,16 +1,16 @@

import {useEffect, useState} from 'react'; -import useAuthStore from '../stores/useAuthStore'; import {ProfileDocument, UsersPermissionsUser} from '../generated/graphql'; import {initializeApollo} from '../lib/apolloClient'; +import {useSession} from 'next-auth/react'; const useProfile = () => { - const token = useAuthStore(s => s.token); - const user = useAuthStore(s => s.user); + const session = useSession(); const [isReady, setIsReady] = useState(false); const [profile, setProfile] = useState<UsersPermissionsUser | null>(null); + const [userId, setUserId] = useState<string | null>(null); const fetchProfile = async () => { - const apolloClient = initializeApollo({}); + const apolloClient = initializeApollo('', session); try { const {data} = await apolloClient.query({

@@ -18,6 +18,7 @@ query: ProfileDocument,

}); const fetchedProfile = data?.me?.profile; setProfile(fetchedProfile); + setUserId(data?.me?.id); } catch (error) { console.error(error); } finally {

@@ -26,15 +27,14 @@ }

}; useEffect(() => { - if (profile) setIsReady(true); - else if (token) fetchProfile(); - else setIsReady(true); - }, [token, profile]); + if (session.status === 'authenticated') fetchProfile(); + else if (session.status === 'unauthenticated') setIsReady(true); + }, [session]); return { profile, - connected: !!token, - user, + userId, + connected: session.status === 'authenticated', isReady, }; };
M frontend/lib/apolloClient.tsfrontend/lib/apolloClient.ts

@@ -4,76 +4,51 @@ import {setContext} from '@apollo/client/link/context';

import {onError} from '@apollo/client/link/error'; import merge from 'deepmerge'; import isEqual from 'lodash/isEqual'; -import useAuthStore from '../stores/useAuthStore'; - -const {STRAPI_URL = 'http://localhost:1337'} = process?.env; +import {useSession} from 'next-auth/react'; +import {Session} from 'next-auth'; export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'; -let apolloClient; +let apolloClient: ApolloClient<any>; -const authLink = setContext((_, {headers}) => { - // get the authentication token from local storage if it exists - const {token} = useAuthStore.getState(); - // return the headers to the context so httpLink can read them - return { - headers: { - ...headers, - authorization: token ? `Bearer ${token}` : '', - }, - }; -}); +const authLink = (session: Session | null) => + setContext(async (_, {headers}) => { + const token = session?.token?.jwt; + // return the headers to the context so httpLink can read them + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : '', + }, + }; + }); -const errorLink = onError(error => { - const {networkError, graphQLErrors} = error; - console.error({networkError, graphQLErrors}); - const isUnauthorized = networkError?.response?.status === 401; +const errorLink = onError(({graphQLErrors = [], operation}) => { + console.error({graphQLErrors, operation}); + const message = graphQLErrors?.[0]?.message; - if (isUnauthorized) { - console.error('Unauthorized response received from GraphQL. Logout user.'); - useAuthStore.getState().setToken(); - useAuthStore.getState().setUser(); - localStorage.removeItem('token'); - localStorage.removeItem('user'); - } + if (message === 'Forbidden') window.location.href = '/auth/login'; }); -const httpLink = uri => +const httpLink = (uri: string) => new HttpLink({ - uri, + uri, // Server URL (must be absolute) credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers` }); -const createApolloClient = () => { +const createApolloClient = (uri: string, session: Session | null) => { return new ApolloClient({ ssrMode: typeof window === 'undefined', - link: from([authLink, errorLink, httpLink(`${STRAPI_URL}/graphql`)]), - cache: new InMemoryCache({ - typePolicies: { - Event: { - fields: { - waitingList: { - merge(_, incoming) { - return incoming; - }, - }, - }, - }, - Car: { - fields: { - passengers: { - merge(_, incoming) { - return incoming; - }, - }, - }, - }, - }, - }), + link: from([authLink(session), errorLink, httpLink(uri)]), + cache: new InMemoryCache(), }); }; -export const initializeApollo = (initialState = null) => { - const _apolloClient = apolloClient ?? createApolloClient(); +export const initializeApollo = ( + uri: string, + session: Session | null, + initialState = null +) => { + const _apolloClient = apolloClient ?? createApolloClient(uri, session); // If your page has Next.js data fetching methods that use Apollo Client, the initial state gets hydrated here if (initialState) {

@@ -99,15 +74,15 @@ if (!apolloClient) apolloClient = _apolloClient;

return _apolloClient; }; -export const addApolloState = (client, pageProps) => { +export const addApolloState = (client: ApolloClient<any>, pageProps: any) => { if (pageProps?.props) { pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract(); } return pageProps; }; -export const useApollo = pageProps => { +export const useApollo = (pageProps: any) => { const state = pageProps[APOLLO_STATE_PROP_NAME]; - - return useMemo(() => initializeApollo(state), [state]); + const {data: session} = useSession(); + return useMemo(() => initializeApollo('', session, state), [state, session]); };
M frontend/lib/i18n.tsfrontend/lib/i18n.ts

@@ -27,7 +27,7 @@ }

if (typeof window !== 'undefined' && typeof window.navigator !== 'undefined') if (navigator.language === 'fr' || navigator.language.includes('fr-')) return 'fr'; - return 'en'; + return 'fr'; }; i18n
A frontend/lib/pageUtils.ts

@@ -0,0 +1,53 @@

+import {ApolloClient} from '@apollo/client'; +import {getSession} from 'next-auth/react'; +import {ProfileDocument, SettingDocument} from '../generated/graphql'; +import {initializeApollo, APOLLO_STATE_PROP_NAME} from './apolloClient'; + +type ServerSideExtension = ( + context: any, + apolloClient: ApolloClient<any> +) => Promise<Object | void>; + +const getServerSideProps = + (extension?: ServerSideExtension) => async (context: any) => { + const session = await getSession(context); + const {STRAPI_URL = 'http://localhost:1337'} = process.env; + const apolloClient = await initializeApollo( + `${STRAPI_URL}/graphql`, + session + ); + const locale = session?.user?.lang || 'fr'; + + try { + await apolloClient.query({ + query: SettingDocument, + variables: {locale}, + }); + + if (session) + await apolloClient.query({ + query: ProfileDocument, + }); + + let extensionProps = {}; + if (extension) + extensionProps = (await extension(context, apolloClient)) || {}; + + return { + props: { + session, + [APOLLO_STATE_PROP_NAME]: apolloClient.cache.extract(), + ...extensionProps, + }, + }; + } catch (error) { + console.error(error); + return { + props: {session}, + }; + } + }; + +export default { + getServerSideProps, +};
M frontend/next.config.jsfrontend/next.config.js

@@ -24,6 +24,10 @@ source: '/graphql',

destination: `${STRAPI_URL}/graphql`, }, { + source: '/api/nauth/:slug*', + destination: `/api/nauth/:slug*`, + }, + { source: '/api/:slug*', destination: `${STRAPI_URL}/api/:slug*`, },
M frontend/package.jsonfrontend/package.json

@@ -22,6 +22,7 @@ "i18next": "^21.9.1",

"marked": "^4.1.0", "moment": "^2.29.4", "next": "^12.3.0", + "next-auth": "^4.10.3", "next-pwa": "^5.6.0", "react": "^18.2.0", "react-dom": "^18.2.0",
M frontend/pages/_app.tsxfrontend/pages/_app.tsx

@@ -4,6 +4,7 @@ import {ApolloProvider} from '@apollo/client';

import {ThemeProvider} from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; import {MuiPickersUtilsProvider} from '@material-ui/pickers'; +import {SessionProvider} from 'next-auth/react'; import moment from 'moment'; import MomentUtils from '@date-io/moment'; import {useApollo} from '../lib/apolloClient';

@@ -50,4 +51,10 @@ </ApolloProvider>

); }; -export default App; +const AppWrapper = (props: AppProps) => ( + <SessionProvider session={props?.pageProps.session} basePath="/api/nauth"> + <App {...props} /> + </SessionProvider> +); + +export default AppWrapper;
A frontend/pages/api/nauth/[...nextauth].js

@@ -0,0 +1,84 @@

+import NextAuth from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; +import GoogleProvider from 'next-auth/providers/google'; + +const {STRAPI_URL = 'http://localhost:1337'} = process.env; + +export default NextAuth({ + providers: [ + CredentialsProvider({ + name: 'Strapi', + credentials: { + email: {label: 'Email', type: 'text'}, + password: {label: 'Password', type: 'password'}, + }, + async authorize(credentials, req) { + try { + const response = await fetch(`${STRAPI_URL}/api/auth/local`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + identifier: credentials.email, + password: credentials.password, + }), + }); + const data = await response.json(); + const {user, jwt} = data; + return {...user, jwt}; + } catch (error) { + console.error({error}); + return null; + } + }, + }), + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), + ], + session: { + jwt: true, + }, + callbacks: { + jwt: async params => { + const {token, user, account} = params; + + // Google Auth + if (account?.provider === 'google') { + const strapiUrl = process.env.STRAPI_URL || 'http://localhost:1337'; + const response = await fetch( + `${strapiUrl}/api/auth/${account.provider}/callback?access_token=${account?.access_token}` + ); + const data = await response.json(); + token.id = data.user.id; + token.jwt = data.jwt; + token.email = data.user.email; + token.username = data.user.firstname; + token.lang = data.user.lang?.toLowerCase(); + } + + // Strapi Auth + else if (user) { + token.id = user.id; + token.jwt = user.jwt; + token.email = user.email; + token.username = user.firstname; + token.lang = user.lang?.toLowerCase(); + } + + return token; + }, + session: async params => { + const {session, token} = params; + if (session) { + session.token = token; + session.user.name = token.username; + session.user.lang = token.lang; + } + return session; + }, + }, + pages: { + signIn: '/auth/login', + }, +});
M frontend/pages/auth/login.tsxfrontend/pages/auth/login.tsx

@@ -1,24 +1,17 @@

-import {useEffect} from 'react'; -import {useRouter} from 'next/router'; import CardMedia from '@material-ui/core/CardMedia'; import Divider from '@material-ui/core/Divider'; import Card from '@material-ui/core/Card'; import {useTranslation} from 'react-i18next'; -import useAuthStore from '../../stores/useAuthStore'; import Layout from '../../layouts/Centered'; import Logo from '../../components/Logo'; import SignInForm from '../../containers/SignInForm'; import LoginGoogle from '../../containers/LoginGoogle'; import LanguagesIcon from '../../containers/Languages/Icon'; +import {getSession} from 'next-auth/react'; +import pageUtils from '../../lib/pageUtils'; const Login = () => { const {t} = useTranslation(); - const router = useRouter(); - const token = useAuthStore(s => s.token); - - useEffect(() => { - if (token) router.replace('/dashboard'); - }, [token]); return ( <Layout menuTitle={t('signin.title')} displayMenu={false}>

@@ -28,9 +21,22 @@ <SignInForm />

<Divider /> <LoginGoogle /> </Card> - <LanguagesIcon/> + <LanguagesIcon /> </Layout> ); +}; + +export const getServerSideProps = async (context: any) => { + const session = await getSession(context); + + if (session) + return { + redirect: { + destination: '/', + permanent: false, + }, + }; + else return pageUtils.getServerSideProps()(context); }; export default Login;
M frontend/pages/dashboard.tsxfrontend/pages/dashboard.tsx

@@ -1,26 +1,21 @@

-import {useMemo, useEffect} from 'react'; +import {useMemo} from 'react'; import {useRouter} from 'next/router'; import moment from 'moment'; import {useTranslation} from 'react-i18next'; -import useAuthStore from '../stores/useAuthStore'; import useProfile from '../hooks/useProfile'; import LayoutDefault from '../layouts/Default'; import DashboardEvents from '../containers/DashboardEvents'; import DashboardEmpty from '../containers/DashboardEmpty'; import Loading from '../containers/Loading'; import Fab from '../containers/Fab'; +import pageUtils from '../lib/pageUtils'; const Dashboard = () => { const {t} = useTranslation(); const router = useRouter(); - const isAuth = useAuthStore(s => !!s.token); const {profile, isReady} = useProfile(); const events = profile?.events?.data || []; - useEffect(() => { - if (!isAuth) router.push('/'); - }, [isAuth]); - const pastEvents = useMemo( () => events

@@ -57,7 +52,7 @@ id: 'ProfileTabs',

}, ]; - if (!events || !isAuth || !isReady) + if (!events || !isReady) return ( <LayoutDefault menuTitle={t('dashboard.title')}> <Loading />

@@ -83,5 +78,7 @@ );

}; const sortDesc = ({date: dateA}, {date: dateB}) => dateB.localeCompare(dateA); + +export const getServerSideProps = pageUtils.getServerSideProps(); export default Dashboard;
M frontend/pages/e/[uuid]/details.tsxfrontend/pages/e/[uuid]/details.tsx

@@ -16,11 +16,11 @@ import useEventStore from '../../../stores/useEventStore';

import useToastStore from '../../../stores/useToastStore'; import useSettings from '../../../hooks/useSettings'; import EventLayout, {TabComponent} from '../../../layouts/Event'; -import {initializeApollo} from '../../../lib/apolloClient'; import { EventByUuidDocument, useUpdateEventMutation, } from '../../../generated/graphql'; +import pageUtils from '../../../lib/pageUtils'; interface Props { eventUUID: string;

@@ -237,26 +237,25 @@ marginLeft: theme.spacing(2),

}, })); -export async function getServerSideProps(ctx) { - const {uuid} = ctx.query; - const apolloClient = initializeApollo(); - const {data = {}} = await apolloClient.query({ - query: EventByUuidDocument, - variables: {uuid}, - }); - const {eventByUUID: event} = data; - const {host = ''} = ctx.req.headers; +export const getServerSideProps = pageUtils.getServerSideProps( + async (context, apolloClient) => { + const {uuid} = context.query; + const {host = ''} = context.req.headers; + + // Fetch event + const {data} = await apolloClient.query({ + query: EventByUuidDocument, + variables: {uuid}, + }); + const event = data?.eventByUUID?.data; - return { - props: { - event, + return { eventUUID: uuid, metas: { - title: event?.name || '', - url: `https://${host}${ctx.resolvedUrl}`, + title: event?.attributes?.name || '', + url: `https://${host}${context.resolvedUrl}`, }, - }, - }; -} - + }; + } +); export default Page;
M frontend/pages/e/[uuid]/index.tsxfrontend/pages/e/[uuid]/index.tsx

@@ -1,4 +1,4 @@

-import {useState, useReducer, PropsWithChildren, useMemo} from 'react'; +import {useState, useReducer, PropsWithChildren} from 'react'; import {makeStyles} from '@material-ui/core/styles'; import {useTranslation} from 'react-i18next'; import EventLayout, {TabComponent} from '../../../layouts/Event';

@@ -7,14 +7,12 @@ import NewTravelDialog from '../../../containers/NewTravelDialog';

import VehicleChoiceDialog from '../../../containers/VehicleChoiceDialog'; import { EventByUuidDocument, - useFindUserVehiclesLazyQuery, + FindUserVehiclesDocument, + useFindUserVehiclesQuery, } from '../../../generated/graphql'; -import useProfile from '../../../hooks/useProfile'; import Fab from '../../../containers/Fab'; -import { - initializeApollo, - APOLLO_STATE_PROP_NAME, -} from '../../../lib/apolloClient'; +import pageUtils from '../../../lib/pageUtils'; +import {getSession, useSession} from 'next-auth/react'; interface Props { eventUUID: string;

@@ -27,18 +25,17 @@

const TravelsTab: TabComponent = (props: {event}) => { const classes = useStyles(); const {t} = useTranslation(); - const {user} = useProfile(); - const [findUserVehicle, {data}] = useFindUserVehiclesLazyQuery(); + const session = useSession(); + const isAuthenticated = session.status === 'authenticated'; + const {data} = useFindUserVehiclesQuery({ + skip: !isAuthenticated, + }); const vehicles = data?.me?.profile?.vehicles?.data || []; const [openNewTravelContext, toggleNewTravel] = useState({opened: false}); const [openVehicleChoice, toggleVehicleChoice] = useReducer(i => !i, false); - useMemo(() => { - if (user) findUserVehicle(); - }, [user]); - const addTravelClickHandler = - user && vehicles?.length != 0 + isAuthenticated && vehicles?.length != 0 ? toggleVehicleChoice : () => toggleNewTravel({opened: true});

@@ -79,32 +76,33 @@ },

}, })); -export async function getServerSideProps(ctx) { - const {uuid} = ctx.query; - const {host = ''} = ctx.req.headers; +export const getServerSideProps = pageUtils.getServerSideProps( + async (context, apolloClient) => { + const {uuid} = context.query; + const {host = ''} = context.req.headers; + const session = await getSession(context); - const apolloClient = initializeApollo(); - const {data: {eventByUUID: {data: event = null} = {}} = {}} = - await apolloClient.query({ + // Fetch event + const {data} = await apolloClient.query({ query: EventByUuidDocument, variables: {uuid}, }); + const event = data?.eventByUUID?.data; - try { + // Fetch user vehicles + if (session) + await apolloClient.query({ + query: FindUserVehiclesDocument, + }); + return { - props: { - [APOLLO_STATE_PROP_NAME]: apolloClient.cache.extract(), - eventUUID: uuid, - metas: { - title: event?.name || '', - url: `https://${host}${ctx.resolvedUrl}`, - }, + eventUUID: uuid, + metas: { + title: event?.attributes?.name || '', + url: `https://${host}${context.resolvedUrl}`, }, }; - } catch (error) { - console.error(error); - return {props: {}}; } -} +); export default Page;
M frontend/pages/e/[uuid]/waitingList.tsxfrontend/pages/e/[uuid]/waitingList.tsx

@@ -4,7 +4,7 @@ import {EventByUuidDocument} from '../../../generated/graphql';

import useProfile from '../../../hooks/useProfile'; import WaitingList from '../../../containers/WaitingList'; import {AddPassengerToWaitingList} from '../../../containers/NewPassengerDialog'; -import {initializeApollo} from '../../../lib/apolloClient'; +import pageUtils from '../../../lib/pageUtils'; interface NewPassengerDialogContext { addSelf: boolean;

@@ -19,22 +19,22 @@ return <EventLayout {...props} Tab={WaitingListTab} />;

}; const WaitingListTab: TabComponent = ({event}) => { - const {user} = useProfile(); + const {userId} = useProfile(); const [addPassengerToWaitingListContext, toggleNewPassengerToWaitingList] = useState<NewPassengerDialogContext | null>(null); const canAddSelf = useMemo(() => { - if (!user) return false; + if (!userId) return false; const isInWaitingList = event?.waitingPassengers?.data?.some( - passenger => passenger.attributes.user?.data?.id === `${user.id}` + passenger => passenger.attributes.user?.data?.id === `${userId}` ); const isInTravel = event?.travels?.data?.some(travel => travel.attributes.passengers?.data?.some( - passenger => passenger.attributes.user?.data?.id === `${user.id}` + passenger => passenger.attributes.user?.data?.id === `${userId}` ) ); return !(isInWaitingList || isInTravel); - }, [event, user]); + }, [event, userId]); return ( <>

@@ -54,26 +54,26 @@ </>

); }; -export async function getServerSideProps(ctx) { - const {uuid} = ctx.query; - const apolloClient = initializeApollo(); - const {data = {}} = await apolloClient.query({ - query: EventByUuidDocument, - variables: {uuid}, - }); - const {eventByUUID: event} = data; - const {host = ''} = ctx.req.headers; +export const getServerSideProps = pageUtils.getServerSideProps( + async (context, apolloClient) => { + const {uuid} = context.query; + const {host = ''} = context.req.headers; - return { - props: { - event, + // Fetch event + const {data} = await apolloClient.query({ + query: EventByUuidDocument, + variables: {uuid}, + }); + const event = data?.eventByUUID?.data; + + return { eventUUID: uuid, metas: { - title: event?.name || '', - url: `https://${host}${ctx.resolvedUrl}`, + title: event?.attributes?.name || '', + url: `https://${host}${context.resolvedUrl}`, }, - }, - }; -} + }; + } +); export default Page;
M frontend/pages/index.tsxfrontend/pages/index.tsx

@@ -1,16 +1,19 @@

import {useRouter} from 'next/router'; import {useTranslation} from 'react-i18next'; -import useProfile from '../hooks/useProfile'; import Layout from '../layouts/Centered'; import CreateEvent from '../containers/CreateEvent'; import LanguagesIcon from '../containers/Languages/Icon'; import Paper from '../components/Paper'; import Logo from '../components/Logo'; +import {useSession} from 'next-auth/react'; +import pageUtils from '../lib/pageUtils'; const Home = () => { const {t} = useTranslation(); const router = useRouter(); - const {isReady, profile} = useProfile(); + const session = useSession(); + const isAuthenticated = session.status === 'authenticated'; + const isReady = session.status !== 'loading'; const noUserMenuActions = [ {

@@ -38,7 +41,7 @@ id: 'ProfileTabs',

}, ]; - const menuActions = !!profile ? loggedMenuActions : noUserMenuActions; + const menuActions = isAuthenticated ? loggedMenuActions : noUserMenuActions; if (!isReady) return null;

@@ -46,15 +49,17 @@ return (

<Layout menuTitle={t('event.creation.title')} menuActions={menuActions} - displayMenu={!!profile} + displayMenu={isAuthenticated} > <Paper> <Logo /> <CreateEvent /> </Paper> - {!profile && <LanguagesIcon displayMenu={!!profile} />} + {!isAuthenticated && <LanguagesIcon displayMenu={false} />} </Layout> ); }; + +export const getServerSideProps = pageUtils.getServerSideProps(); export default Home;
M frontend/pages/profile.tsxfrontend/pages/profile.tsx

@@ -1,22 +1,23 @@

-import {useEffect} from 'react'; import {useRouter} from 'next/router'; import {useTranslation} from 'react-i18next'; -import useAuthStore from '../stores/useAuthStore'; -import useProfile from '../hooks/useProfile'; import Loading from '../containers/Loading'; import Profile from '../containers/Profile'; import Layout from '../layouts/Centered'; +import {useSession, signOut} from 'next-auth/react'; +import pageUtils from '../lib/pageUtils'; +import useProfile from '../hooks/useProfile'; +import {useEffect} from 'react'; const ProfilePage = () => { const router = useRouter(); const {t} = useTranslation(); - const isAuth = useAuthStore(s => !!s.token); - const logout = useAuthStore(s => s.logout); + const session = useSession(); + const isAuthenticated = session.status === 'authenticated'; const {profile} = useProfile(); useEffect(() => { - if (!isAuth) router.push('/'); - }, [isAuth]); + if (!isAuthenticated) router.push('/'); + }, [isAuthenticated]); const menuActions = [ {

@@ -31,13 +32,16 @@ id: 'DashboardTabs',

}, ]; - if (!profile) return <Loading />; + if (session.status === 'loading') return <Loading />; + else if (!isAuthenticated) return null; return ( <Layout menuTitle={t('profile.title')} menuActions={menuActions} goBack> - <Profile profile={profile} logout={logout} /> + {profile && <Profile profile={profile} logout={signOut} />} </Layout> ); }; + +export const getServerSideProps = pageUtils.getServerSideProps(); export default ProfilePage;
D frontend/stores/useAuthStore.ts

@@ -1,38 +0,0 @@

-import create from 'zustand'; -import {UsersPermissionsMe} from '../generated/graphql'; - -type State = { - token: string | null; - setToken: (token?: string) => void; - user: UsersPermissionsMe | null; - setUser: (user?: UsersPermissionsMe) => void; - logout: () => void; -}; - -const hasStorage = typeof localStorage !== 'undefined'; - -const useAuthStore = create<State>((set, get) => ({ - token: hasStorage ? localStorage.getItem('token') : null, - setToken: (token: string) => { - if (hasStorage) localStorage.setItem('token', token); - set({token}); - }, - user: - hasStorage && - localStorage.getItem('user') && - localStorage.getItem('user') !== 'undefined' - ? JSON.parse(localStorage.getItem('user')) - : null, - setUser: user => { - if (hasStorage) localStorage.setItem('user', JSON.stringify(user)); - set({user}); - }, - logout: () => { - set({token: null, user: null}); - localStorage.removeItem('token'); - localStorage.removeItem('user'); - window.location.href = '/auth/login'; - }, -})); - -export default useAuthStore;
M frontend/yarn.lockfrontend/yarn.lock

@@ -974,7 +974,7 @@ dependencies:

core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.14.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==

@@ -1657,6 +1657,11 @@ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==

dependencies: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" + +"@panva/hkdf@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.0.2.tgz#bab0f09d09de9fd83628220d496627681bc440d6" + integrity sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA== "@peculiar/asn1-schema@^2.1.6": version "2.3.0"

@@ -2619,6 +2624,11 @@ integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==

dependencies: safe-buffer "~5.1.1" +cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + core-js-compat@^3.21.0, core-js-compat@^3.22.1: version "3.25.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.1.tgz#6f13a90de52f89bbe6267e5620a412c7f7ff7e42"

@@ -4009,6 +4019,11 @@ "@types/node" "*"

merge-stream "^2.0.0" supports-color "^8.0.0" +jose@^4.1.4, jose@^4.3.7: + version "4.9.2" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.9.2.tgz#268994df4443b9c191de0b001c2e3796ac6cf368" + integrity sha512-EqKvu2PqJCD3Jrg3PvcYZVS7D21qMVLSYMDAFcOdGUEOpJSLNtJO7NjLANvu3SYHVl6pdP2ff7ve6EZW2nX7Nw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"

@@ -4529,6 +4544,21 @@ version "1.4.0"

resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +next-auth@^4.10.3: + version "4.10.3" + resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.3.tgz#0a952dd5004fd2ac2ba414c990922cf9b33951a3" + integrity sha512-7zc4aXYc/EEln7Pkcsn21V1IevaTZsMLJwapfbnKA4+JY0+jFzWbt5p/ljugesGIrN4VOZhpZIw50EaFZyghJQ== + dependencies: + "@babel/runtime" "^7.16.3" + "@panva/hkdf" "^1.0.1" + cookie "^0.4.1" + jose "^4.3.7" + oauth "^0.9.15" + openid-client "^5.1.0" + preact "^10.6.3" + preact-render-to-string "^5.1.19" + uuid "^8.3.2" + next-pwa@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4"

@@ -4614,11 +4644,21 @@ version "1.1.1"

resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== +oauth@^0.9.15: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-hash@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.12.2, object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"

@@ -4674,6 +4714,11 @@ call-bind "^1.0.2"

define-properties "^1.1.3" es-abstract "^1.19.1" +oidc-token-hash@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6" + integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"

@@ -4687,6 +4732,16 @@ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"

integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" + +openid-client@^5.1.0: + version "5.1.9" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.1.9.tgz#6887e75ad3fa8d0c78d0ed693a5ea107d39b54de" + integrity sha512-o/11Xos2fRPpK1zQrPfSIhIusFrAkqGSPwkD0UlUB+CCuRzd7zrrBJwIjgnVv3VUSif9ZGXh2d3GSJNH2Koh5g== + dependencies: + jose "^4.1.4" + lru-cache "^6.0.0" + object-hash "^2.0.1" + oidc-token-hash "^5.0.1" optimism@^0.16.1: version "0.16.1"

@@ -4916,6 +4971,18 @@ nanoid "^3.3.4"

picocolors "^1.0.0" source-map-js "^1.0.2" +preact-render-to-string@^5.1.19: + version "5.2.4" + resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.4.tgz#7d6a3f76e13fa9df8e81ccdef2f2b02489e5a3fd" + integrity sha512-iIPHb3BXUQ3Za6KNhkjN/waq11Oh+QWWtAgN3id3LrL+cszH3DYh8TxJPNQ6Aogsbu4JsqdJLBZltwPFpG6N6w== + dependencies: + pretty-format "^3.8.0" + +preact@^10.6.3: + version "10.11.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.0.tgz#26af45a0613f4e17a197cc39d7a1ea23e09b2532" + integrity sha512-Fk6+vB2kb6mSJfDgODq0YDhMfl0HNtK5+Uc9QqECO4nlyPAQwCI+BKyWO//idA7ikV7o+0Fm6LQmNuQi1wXI1w== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"

@@ -4925,6 +4992,11 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:

version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-format@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" + integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== progress@^2.0.0: version "2.0.3"

@@ -5962,6 +6034,11 @@ util-deprecate@^1.0.1:

version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache-lib@^3.0.1: version "3.0.1"
M nginx.confnginx.conf

@@ -59,4 +59,7 @@

location / { proxy_pass http://nextjs; } + location /api/nauth { + proxy_pass http://nextjs; + } }