webapp/src/context/session.tsx

259 lines
7.2 KiB
TypeScript
Raw Normal View History

import type { AuthModalSource } from '../components/Nav/AuthModal/types'
2023-11-28 13:18:25 +00:00
import type { Author, Result } from '../graphql/schema/core.gen'
import type { Accessor, JSX, Resource } from 'solid-js'
2023-12-14 11:49:55 +00:00
import {
VerifyEmailInput,
LoginInput,
AuthToken,
User,
Authorizer,
ConfigType,
} from '@authorizerdev/authorizer-js'
import {
createContext,
createEffect,
createMemo,
createResource,
createSignal,
2023-12-14 13:50:22 +00:00
on,
2023-12-14 11:49:55 +00:00
onMount,
useContext,
} from 'solid-js'
2023-11-28 13:18:25 +00:00
import { apiClient } from '../graphql/client/core'
import { showModal } from '../stores/ui'
2023-12-15 13:45:34 +00:00
import { useLocalize } from './localize'
import { useSnackbar } from './snackbar'
2023-12-16 14:13:14 +00:00
import { useRouter } from '../stores/router'
2023-12-14 11:49:55 +00:00
const config: ConfigType = {
authorizerURL: 'https://auth.discours.io',
redirectURL: 'https://discoursio-webapp.vercel.app/?modal=auth',
clientID: '9c113377-5eea-4c89-98e1-69302462fc08', // FIXME: use env?
}
2023-11-28 13:18:25 +00:00
export type SessionContextType = {
2023-12-14 11:49:55 +00:00
config: ConfigType
2023-11-28 13:18:25 +00:00
session: Resource<AuthToken>
2023-12-16 14:13:14 +00:00
author: Resource<Author | null>
2022-12-06 16:03:55 +00:00
isSessionLoaded: Accessor<boolean>
2023-11-28 13:18:25 +00:00
subscriptions: Accessor<Result>
isAuthenticated: Accessor<boolean>
2023-12-14 11:49:55 +00:00
isAuthWithCallback: Accessor<() => void>
actions: {
2023-12-03 10:22:42 +00:00
getToken: () => string
2023-11-28 13:18:25 +00:00
loadSession: () => AuthToken | Promise<AuthToken>
2023-12-16 14:13:14 +00:00
setSession: (token: AuthToken | null) => void // setSession
loadAuthor: (info?: unknown) => Author | Promise<Author>
loadSubscriptions: () => Promise<void>
requireAuthentication: (
callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource,
) => void
2023-11-28 13:18:25 +00:00
signIn: (params: LoginInput) => Promise<void>
signOut: () => Promise<void>
2023-12-16 14:13:14 +00:00
confirmEmail: (input: VerifyEmailInput) => Promise<void> // email confirm callback is in auth.discours.io
2023-12-14 11:49:55 +00:00
setIsSessionLoaded: (loaded: boolean) => void
authorizer: () => Authorizer
}
}
2022-11-14 10:02:08 +00:00
const SessionContext = createContext<SessionContextType>()
2022-11-14 10:02:08 +00:00
export function useSession() {
return useContext(SessionContext)
}
2023-11-02 17:43:22 +00:00
const EMPTY_SUBSCRIPTIONS = {
topics: [],
authors: [],
2023-11-02 17:43:22 +00:00
}
2023-12-14 11:49:55 +00:00
export const SessionProvider = (props: {
onStateChangeCallback(state: any): unknown
children: JSX.Element
}) => {
2023-02-17 09:21:02 +00:00
const { t } = useLocalize()
2023-02-10 11:11:24 +00:00
const {
actions: { showSnackbar },
2023-02-10 11:11:24 +00:00
} = useSnackbar()
2023-12-16 14:13:14 +00:00
const { searchParams, changeSearchParam } = useRouter()
2023-12-15 13:45:34 +00:00
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
2023-11-28 13:18:25 +00:00
const getSession = async (): Promise<AuthToken> => {
2022-12-06 16:03:55 +00:00
try {
2023-12-15 13:45:34 +00:00
const tkn = getToken()
console.debug('[context.session] token before: ', tkn)
2023-12-14 11:49:55 +00:00
const authResult = await authorizer().getSession({
2023-12-15 13:45:34 +00:00
Authorization: tkn,
2023-12-14 11:49:55 +00:00
})
if (authResult?.access_token) {
2023-12-16 14:13:14 +00:00
mutate(authResult)
2023-12-15 13:45:34 +00:00
console.debug('[context.session] token after: ', authResult.access_token)
await loadSubscriptions()
2023-12-14 11:49:55 +00:00
return authResult
2022-12-06 16:03:55 +00:00
}
} catch (error) {
2023-12-15 13:45:34 +00:00
console.error('[context.session] getSession error:', error)
2023-12-16 14:13:14 +00:00
mutate(null)
2022-12-06 16:03:55 +00:00
return null
2022-12-07 11:06:26 +00:00
} finally {
setTimeout(() => {
setIsSessionLoaded(true)
}, 0)
2022-12-06 16:03:55 +00:00
}
}
2023-11-28 13:18:25 +00:00
const [session, { refetch: loadSession, mutate }] = createResource<AuthToken>(getSession, {
2022-12-06 16:03:55 +00:00
ssrLoadFrom: 'initial',
initialValue: null,
2022-12-06 16:03:55 +00:00
})
2023-12-16 14:13:14 +00:00
const user = createMemo(() => session().user)
createEffect(() => {
// detect confirm redirect
const params = searchParams()
if (params?.access_token) {
console.debug('[context.session] access token presented, changing search params')
changeSearchParam({ modal: 'auth', mode: 'confirm-email', access_token: params?.access_token })
}
})
createEffect(() => {
// authorized graphql client
const tkn = getToken()
if (tkn) apiClient.connect(tkn)
})
const loadSubscriptions = async (): Promise<void> => {
const result = await apiClient.private?.getMySubscriptions()
if (result) {
setSubscriptions(result)
} else {
setSubscriptions(EMPTY_SUBSCRIPTIONS)
}
}
2023-11-28 13:18:25 +00:00
const [author, { refetch: loadAuthor }] = createResource<Author | null>(
async () => {
2023-11-28 18:04:51 +00:00
const u = session()?.user
if (u) {
2023-12-13 23:56:44 +00:00
return (await apiClient.getAuthorId({ user: u.id })) ?? null
2023-11-28 13:18:25 +00:00
}
return null
},
{
ssrLoadFrom: 'initial',
initialValue: null,
},
)
const isAuthenticated = createMemo(() => Boolean(session()?.user))
const signIn = async (params: LoginInput) => {
const authResult: AuthToken | void = await authorizer().login(params)
if (authResult && authResult.access_token) {
2023-12-16 14:13:14 +00:00
mutate(authResult)
2023-12-15 13:45:34 +00:00
await loadSubscriptions()
console.debug('[context.session] signed in')
} else {
console.info((authResult as AuthToken).message)
2023-11-28 13:18:25 +00:00
}
}
2023-12-14 11:49:55 +00:00
const authorizer = createMemo(
() =>
new Authorizer({
authorizerURL: config.authorizerURL,
redirectURL: config.redirectURL,
clientID: config.clientID,
}),
)
2023-12-14 13:50:22 +00:00
createEffect(
on(
() => props.onStateChangeCallback,
() => {
2023-12-16 14:13:14 +00:00
props.onStateChangeCallback(session())
2023-12-14 13:50:22 +00:00
},
{ defer: true },
),
)
2023-12-14 11:49:55 +00:00
const [configuration, setConfig] = createSignal<ConfigType>(config)
onMount(async () => {
setIsSessionLoaded(false)
console.log('[context.session] loading...')
const metaRes = await authorizer().getMetaData()
setConfig({ ...config, ...metaRes, redirectURL: window.location.origin + '/?modal=auth' })
console.log('[context.session] refreshing session...')
const s = await getSession()
2023-12-15 13:45:34 +00:00
console.debug('[context.session] session: ', s)
2023-12-14 11:49:55 +00:00
console.log('[context.session] loading author...')
2023-12-15 13:45:34 +00:00
const a = await loadAuthor()
console.debug('[context.session] author: ', a)
2023-12-14 11:49:55 +00:00
setIsSessionLoaded(true)
console.log('[context.session] loaded')
})
2023-12-14 11:49:55 +00:00
const [isAuthWithCallback, setIsAuthWithCallback] = createSignal<() => void>()
2023-11-02 17:43:22 +00:00
const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => {
setIsAuthWithCallback(() => callback)
2023-12-15 13:45:34 +00:00
const userdata = await authorizer().getProfile()
2023-12-16 14:13:14 +00:00
if (userdata) mutate({ ...session(), user: userdata })
2023-11-02 17:43:22 +00:00
if (!isAuthenticated()) {
showModal('auth', modalSource)
}
}
const signOut = async () => {
2023-11-28 13:18:25 +00:00
await authorizer().logout()
2023-12-16 14:13:14 +00:00
mutate(null)
2023-11-02 17:43:22 +00:00
setSubscriptions(EMPTY_SUBSCRIPTIONS)
2023-02-10 11:11:24 +00:00
showSnackbar({ body: t("You've successfully logged out") })
}
2023-11-28 13:18:25 +00:00
const confirmEmail = async (input: VerifyEmailInput) => {
2023-12-16 14:13:14 +00:00
console.log(`[context.session] calling authorizer's verify email with ${input}`)
2023-11-28 18:04:51 +00:00
const at: void | AuthToken = await authorizer().verifyEmail(input)
2023-12-16 14:13:14 +00:00
if (at) mutate(at)
console.log(`[context.session] confirmEmail got result ${at}`)
}
2023-12-16 14:13:14 +00:00
const getToken = createMemo(() => session()?.access_token)
2023-12-14 11:49:55 +00:00
const actions = {
2023-12-14 11:49:55 +00:00
getToken,
loadSession,
2023-12-14 11:49:55 +00:00
loadSubscriptions,
requireAuthentication,
signIn,
signOut,
confirmEmail,
2023-12-14 11:49:55 +00:00
setIsSessionLoaded,
2023-12-16 14:13:14 +00:00
setSession: mutate,
2023-12-14 11:49:55 +00:00
authorizer,
2023-12-16 14:13:14 +00:00
loadAuthor,
}
const value: SessionContextType = {
2023-12-14 11:49:55 +00:00
config: configuration(),
session,
subscriptions,
isSessionLoaded,
isAuthenticated,
2023-12-14 11:49:55 +00:00
author,
actions,
2023-12-14 11:49:55 +00:00
isAuthWithCallback,
}
2022-11-14 10:02:08 +00:00
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
}