import Link from 'next/link' import { useTranslation } from 'next-i18next' import { useAccount } from 'shared/hooks/use-account.mjs' import { useBackend } from 'shared/hooks/use-backend.mjs' import { roles } from 'config/roles.mjs' import { useEffect, useState } from 'react' import { Loading } from 'shared/components/spinner.mjs' import { horFlexClasses } from 'shared/utils.mjs' import { LockIcon, PlusIcon } from 'shared/components/icons.mjs' import { ConsentForm, ns as gdprNs } from 'shared/components/gdpr/form.mjs' export const ns = ['auth', gdprNs] const Wrap = ({ children }) => ( <div className="m-auto max-w-xl text-center mt-8 p-8">{children}</div> ) const ContactSupport = ({ t }) => ( <div className="flex flex-row items-center justify-center gap-4 mt-8"> <Link href="/support" className="btn btn-success w-full"> {t('contactSupport')} </Link> </div> ) const AuthRequired = ({ t, banner }) => ( <Wrap> {banner} <h2>{t('authRequired')}</h2> <p>{t('membersOnly')}</p> <div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-8"> <Link href="/signup" className={`${horFlexClasses} btn btn-secondary w-full`}> <PlusIcon /> {t('signUp')} </Link> <Link href="/signin" className={`${horFlexClasses} btn btn-secondary btn-outline w-full`}> <LockIcon /> {t('signIn')} </Link> </div> </Wrap> ) const AccountInactive = ({ t, banner }) => ( <Wrap> {banner} <h1>{t('accountInactive')}</h1> <p>{t('accountInactiveMsg')}</p> <p>{t('signupAgain')}</p> <div className="flex flex-row items-center justify-center gap-4 mt-8"> <Link href="/signup" className="btn btn-primary w-full"> {t('signUp')} </Link> </div> </Wrap> ) const AccountDisabled = ({ t, banner }) => ( <Wrap> {banner} <h1>{t('accountDisabled')}</h1> <p>{t('accountDisabledMsg')}</p> <ContactSupport t={t} /> </Wrap> ) const AccountProhibited = ({ t, banner }) => ( <Wrap> {banner} <h1>{t('accountProhibited')}</h1> <p>{t('accountProhibitedMsg')}</p> <ContactSupport t={t} /> </Wrap> ) const AccountStatusUnknown = ({ t, banner }) => ( <Wrap> {banner} <h1>{t('statusUnknown')}</h1> <p>{t('statusUnknownMsg')}</p> <ContactSupport t={t} /> </Wrap> ) const RoleLacking = ({ t, requiredRole, role, banner }) => ( <Wrap> {banner} <h1>{t('roleLacking')}</h1> <p dangerouslySetInnerHTML={{ __html: t('roleLackingMsg', { requiredRole, role }) }} /> <ContactSupport t={t} /> </Wrap> ) const ConsentLacking = ({ banner, refresh }) => { const { setAccount, setToken, setSeenUser } = useAccount() const backend = useBackend() const updateConsent = async ({ consent1, consent2 }) => { let consent = 0 if (consent1) consent = 1 if (consent1 && consent2) consent = 2 if (consent > 0) { const result = await backend.updateConsent(consent) console.log({ result }) if (result.success) { setToken(result.data.token) setAccount(result.data.account) setSeenUser(result.data.account.username) refresh() } else { console.log('something went wrong', result) refresh() } } } return ( <Wrap> <div className="text-left"> {banner} <ConsentForm submit={updateConsent} /> </div> </Wrap> ) } export const AuthWrapper = ({ children, requiredRole = 'user' }) => { const { t } = useTranslation(ns) const { account, setAccount, token, admin, stopImpersonating, signOut } = useAccount() const backend = useBackend() const [ready, setReady] = useState(false) const [impersonating, setImpersonating] = useState(false) const [error, setError] = useState(false) const [refreshCount, setRefreshCount] = useState(0) /* * Avoid hydration errors */ useEffect(() => { const verifyAdmin = async () => { const result = await backend.adminPing(admin.token) if (result.success && result.data.account.role === 'admin') { setImpersonating({ admin: result.data.account.username, user: account.username, }) } setReady(true) } const verifyUser = async () => { const result = await backend.ping() if (result.success) { // Refresh account in local storage setAccount({ ...account, ...result.data.account, }) } else { if (result.data?.error?.error) setError(result.data.error.error) else signOut() } setReady(true) } if (admin && admin.token) verifyAdmin() if (token) verifyUser() else setReady(true) }, [admin, token, refreshCount, account, setAccount, backend, signOut]) const refresh = () => { setRefreshCount(refreshCount + 1) setError(false) } if (!ready) return ( <> <p>not ready</p> <Loading /> </> ) const banner = impersonating ? ( <div className="bg-warning rounded-lg shadow py-4 px-6 flex flex-row items-center gap-4 justify-between"> <span className="text-base-100 text-left"> Hi <b>{impersonating.admin}</b>, you are currently impersonating <b>{impersonating.user}</b> </span> <button className="btn btn-neutral" onClick={stopImpersonating}> Stop Impersonating </button> </div> ) : null const childProps = { t, banner } if (!token || !account.username) return <AuthRequired {...childProps} /> if (error) { if (error === 'accountInactive') return <AccountInactive {...childProps} /> if (error === 'accountDisabled') return <AccountDisabled {...childProps} /> if (error === 'accountBlocked') return <AccountProhibited {...childProps} /> if (error === 'consentLacking') return <ConsentLacking {...childProps} refresh={refresh} /> return <AccountStatusUnknown {...childProps} /> } if (!roles.levels[account.role] || roles.levels[account.role] < roles.levels[requiredRole]) { return <RoleLacking {...childProps} role={account.role} requiredRole={requiredRole} /> } return children }