Skip to content

Commit

Permalink
♻️ refactor authentication with hooks (#822)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpetetot committed Aug 2, 2020
1 parent 15dfe22 commit 69d6bda
Show file tree
Hide file tree
Showing 80 changed files with 694 additions and 804 deletions.
17 changes: 10 additions & 7 deletions src/app.jsx
Expand Up @@ -4,6 +4,7 @@ import cn from 'classnames'
import { provider } from '@k-ramel/react'

import withTheme from 'styles/themes/withTheme'
import { AuthProvider } from './features/auth'
import NotFound from './screens/components/notFound'
import Conference from './screens/conference'
import Organizer from './screens/organizer'
Expand All @@ -15,13 +16,15 @@ import store from './store'
import './styles'

const App = ({ className }) => (
<div className={cn('app', className)}>
<Conference />
<Organizer />
<Speaker />
<Invite />
<NotFound />
</div>
<AuthProvider>
<div className={cn('app', className)}>
<Conference />
<Organizer />
<Speaker />
<Invite />
<NotFound />
</div>
</AuthProvider>
)

App.propTypes = {
Expand Down
110 changes: 110 additions & 0 deletions src/features/auth/context.js
@@ -0,0 +1,110 @@
/* eslint-disable react/jsx-filename-extension */
import React, { useState, useEffect, useContext, useCallback, useMemo } from 'react'
import { inject } from '@k-ramel/react'
import PropTypes from 'prop-types'
import firebase from 'firebase/app'
import pick from 'lodash/pick'

import userCrud from 'firebase/user'
import { preloadFunctions } from 'firebase/functionCalls'

const AuthContext = React.createContext()

export const useAuth = () => useContext(AuthContext)

export const AuthContextProvider = ({ children, resetStore, goToHome }) => {
const [user, setUser] = useState()
const [loading, setLoading] = useState(true)

useEffect(() => {
firebase.auth().onAuthStateChanged(async (authUser) => {
if (authUser) {
// check if user exists in database
const userRef = await userCrud.read(authUser.uid)
if (userRef.exists) {
// get user info from db
setUser(userRef.data())
} else {
// first connexion, add user in database
const userData = pick(authUser, ['uid', 'displayName', 'photoURL', 'email'])
await userCrud.create(userData)
setUser(userData)
}
// preload cloud functions
preloadFunctions()
} else {
setUser(null)
resetStore()
}
setLoading(false)
})
}, [resetStore])

const signin = useCallback(async (providerName) => {
setLoading(true)

let provider
switch (providerName) {
case 'google':
provider = new firebase.auth.GoogleAuthProvider()
break
case 'twitter':
provider = new firebase.auth.TwitterAuthProvider()
break
case 'github':
provider = new firebase.auth.GithubAuthProvider()
break
case 'facebook':
provider = new firebase.auth.FacebookAuthProvider()
break
default:
return
}
provider.setCustomParameters({
prompt: 'select_account',
})

firebase.auth().signInWithRedirect(provider)
}, [])

const signout = useCallback(async () => {
setLoading(true)
firebase.auth().signOut()
goToHome()
localStorage.removeItem('currentEventId')
}, [goToHome])

const updateUser = useCallback(
async (data) => {
const updatedUser = { ...user, ...data }
setUser(updatedUser)
return userCrud.update(updatedUser)
},
[user],
)

const resetUserFromProvider = useCallback(async () => {
const data = pick(firebase.auth().currentUser, ['uid', 'email', 'displayName', 'photoURL'])
return updateUser(data)
}, [updateUser])

const value = useMemo(
() => ({ user, loading, signin, signout, updateUser, resetUserFromProvider }),
[user, loading, signin, signout, updateUser, resetUserFromProvider],
)

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}

AuthContextProvider.propTypes = {
resetStore: PropTypes.func.isRequired,
goToHome: PropTypes.func.isRequired,
children: PropTypes.any.isRequired,
}

export const AuthProvider = inject((store, props, { router }) => {
return {
resetStore: () => store.data.reset(),
goToHome: () => router.push('home'),
}
})(AuthContextProvider)
2 changes: 2 additions & 0 deletions src/features/auth/index.js
@@ -0,0 +1,2 @@
export { AuthProvider, useAuth } from './context'
export { default as protect } from './protect'
41 changes: 41 additions & 0 deletions src/features/auth/protect.js
@@ -0,0 +1,41 @@
/* eslint-disable react/jsx-filename-extension */
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { inject } from '@k-ramel/react'

import { useAuth } from 'features/auth'
import { LoadingIndicator } from 'components/loader'

export default (Component) => {
const ProtectedComponent = ({ redirectLogin, ...rest }) => {
const { user, loading } = useAuth()

useEffect(() => {
if (!user && !loading) {
redirectLogin()
}
}, [user, loading, redirectLogin])

if (loading) {
return <LoadingIndicator />
}

if (!user) {
return null
}

return <Component userId={user.uid} {...rest} />
}

ProtectedComponent.propTypes = {
redirectLogin: PropTypes.func.isRequired,
}

return inject((store) => {
return {
redirectLogin: () => {
store.dispatch({ type: '@@router/REPLACE_WITH_NEXT_URL', payload: 'login' })
},
}
})(ProtectedComponent)
}
2 changes: 2 additions & 0 deletions src/features/beta/index.js
@@ -0,0 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as restrictBeta } from './restrictBeta'
33 changes: 33 additions & 0 deletions src/features/beta/restrictBeta.js
@@ -0,0 +1,33 @@
/* eslint-disable react/jsx-filename-extension */
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { inject } from '@k-ramel/react'
import { useAuth } from 'features/auth'

const SKIP_BETA_ACCESS = process.env.NODE_ENV === 'development'

export default (Component) => {
const BetaRestricted = ({ redirectBetaAccess, ...rest }) => {
const { user } = useAuth()
const { betaAccess } = user

useEffect(() => {
if (SKIP_BETA_ACCESS) return
if (!betaAccess) redirectBetaAccess()
}, [betaAccess, redirectBetaAccess])

return SKIP_BETA_ACCESS || betaAccess ? <Component {...rest} /> : null
}

BetaRestricted.propTypes = {
redirectBetaAccess: PropTypes.func.isRequired,
}

return inject((store) => {
return {
redirectBetaAccess: () => {
store.dispatch({ type: '@@router/REPLACE_WITH_NEXT_URL', payload: 'beta-access' })
},
}
})(BetaRestricted)
}
8 changes: 7 additions & 1 deletion src/firebase/betaAccess.js
@@ -1,3 +1,9 @@
/* eslint-disable import/prefer-default-export */
import crud from './crud'

export default crud('betaAccess', 'id')
const betaAccess = crud('betaAccess', 'id')

export const isValidBetaAccessKey = async (accessKey) => {
const accessRef = await betaAccess.read(accessKey)
return accessRef.exists
}
10 changes: 1 addition & 9 deletions src/layout/avatarDropdown/avatarDropdown.container.js
Expand Up @@ -3,22 +3,14 @@ import { inject } from '@k-ramel/react'
import AvatarDropdown from './avatarDropdown'

const mapStore = (store, props, { router }) => {
const { uid } = store.auth.get()
const { displayName, photoURL } = store.data.users.get(uid) || {}

let contributorsRoute = 'public-contributors'
if (router.getParam('root') === 'speaker') {
contributorsRoute = 'speaker-contributors'
} else if (router.getParam('root') === 'organizer') {
contributorsRoute = 'organizer-contributors'
}

return {
displayName,
photoURL,
contributorsRoute,
signout: () => store.dispatch('@@ui/SIGN_OUT'),
}
return { contributorsRoute }
}

export default inject(mapStore)(AvatarDropdown)
17 changes: 8 additions & 9 deletions src/layout/avatarDropdown/avatarDropdown.jsx
Expand Up @@ -5,11 +5,18 @@ import { Link } from '@k-redux-router/react-k-ramel'
import IconLabel from 'components/iconLabel'
import Avatar from 'components/avatar'
import Dropdown from 'components/dropdown'
import { useAuth } from 'features/auth'

import './avatarDropdown.css'

const AvatarDropdown = ({ displayName, photoURL, contributorsRoute, signout }) => {
const AvatarDropdown = ({ contributorsRoute }) => {
const { user, signout } = useAuth()

if (!user) return null

const { displayName, photoURL } = user
const avatar = <Avatar src={photoURL} name={displayName} className="avatar-dropdown-button" />

return (
<Dropdown className="avatar-dropdown" action={avatar} darkMode>
<div>{displayName}</div>
Expand All @@ -27,15 +34,7 @@ const AvatarDropdown = ({ displayName, photoURL, contributorsRoute, signout }) =
}

AvatarDropdown.propTypes = {
displayName: PropTypes.string,
photoURL: PropTypes.string,
contributorsRoute: PropTypes.string.isRequired,
signout: PropTypes.func.isRequired,
}

AvatarDropdown.defaultProps = {
displayName: undefined,
photoURL: undefined,
}

export default AvatarDropdown
2 changes: 1 addition & 1 deletion src/screens/components/addUserModal/inviteLink/index.js
@@ -1 +1 @@
export { default } from './inviteLink.container'
export { default } from './inviteLink'

This file was deleted.

8 changes: 5 additions & 3 deletions src/screens/components/addUserModal/inviteLink/inviteLink.jsx
Expand Up @@ -4,16 +4,19 @@ import PropTypes from 'prop-types'
import CopyInput from 'components/copyInput'
import Button from 'components/button'
import IconLabel from 'components/iconLabel/iconLabel'
import { useAuth } from 'features/auth'

import useInviteLink from './useInviteLink'
import styles from './inviteLink.module.css'

const InviteLink = ({ entity, entityId, entityTitle, uid }) => {
const InviteLink = ({ entity, entityId, entityTitle }) => {
const { user } = useAuth()

const { generate, revoke, loading, inviteLink } = useInviteLink({
entity,
entityId,
entityTitle,
uid,
uid: user.uid,
})

if (!inviteLink || loading) {
Expand All @@ -38,7 +41,6 @@ InviteLink.propTypes = {
entity: PropTypes.string.isRequired,
entityId: PropTypes.string.isRequired,
entityTitle: PropTypes.string.isRequired,
uid: PropTypes.string.isRequired,
}

export default InviteLink
7 changes: 3 additions & 4 deletions src/screens/components/hasRole/hasRole.container.js
Expand Up @@ -2,14 +2,13 @@ import { inject } from '@k-ramel/react'

import HasRole from './hasRole'

const mapStore = (store, { of, forEventId, forOrganizationId }) => {
const mapStore = (store, { forEventId, forOrganizationId }) => {
const event = store.data.events.get(forEventId)
const organization = store.data.organizations.get()[event?.organization ?? forOrganizationId]
const { uid } = store.auth.get()
const roles = Array.isArray(of) ? of : [of]

return {
authorized: roles.includes(organization?.members?.[uid]) || event.owner === uid,
eventOwner: event?.owner,
orgaMembers: organization?.members,
}
}

Expand Down
17 changes: 12 additions & 5 deletions src/screens/components/hasRole/hasRole.jsx
@@ -1,20 +1,27 @@
import { bool, node } from 'prop-types'
import { node } from 'prop-types'
import { useAuth } from 'features/auth'

const HasRole = ({ authorized, children, otherwise }) => {
if (!authorized) return otherwise
// TODO Refactor later with a hook
const HasRole = ({ of, orgaMembers, eventOwner, children, otherwise }) => {
const { user } = useAuth()
const roles = Array.isArray(of) ? of : [of]

if (!roles.includes(orgaMembers?.[user.uid]) && eventOwner !== user.uid) {
return otherwise
}

return children
}

HasRole.propTypes = {
authorized: bool,
children: node.isRequired,
otherwise: node,
}

HasRole.defaultProps = {
authorized: false,
otherwise: null,
orgaMembers: null,
eventOwner: null,
}

export default HasRole

0 comments on commit 69d6bda

Please sign in to comment.