import type { AuthModalSource } from '../components/Nav/AuthModal/types' import type { Author, Result } from '../graphql/schema/core.gen' import type { Accessor, JSX, Resource } from 'solid-js' import { VerifyEmailInput, LoginInput, AuthToken, User, Authorizer, ConfigType, } from '@authorizerdev/authorizer-js' import { createContext, createEffect, createMemo, createResource, createSignal, on, onMount, useContext, } from 'solid-js' import { apiClient } from '../graphql/client/core' import { showModal } from '../stores/ui' import { useLocalize } from './localize' import { useSnackbar } from './snackbar' import { useRouter } from '../stores/router' 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? } export type SessionContextType = { config: ConfigType session: Resource author: Resource isSessionLoaded: Accessor subscriptions: Accessor isAuthenticated: Accessor isAuthWithCallback: Accessor<() => void> actions: { getToken: () => string loadSession: () => AuthToken | Promise setSession: (token: AuthToken | null) => void // setSession loadAuthor: (info?: unknown) => Author | Promise loadSubscriptions: () => Promise requireAuthentication: ( callback: (() => Promise) | (() => void), modalSource: AuthModalSource, ) => void signIn: (params: LoginInput) => Promise signOut: () => Promise confirmEmail: (input: VerifyEmailInput) => Promise // email confirm callback is in auth.discours.io setIsSessionLoaded: (loaded: boolean) => void authorizer: () => Authorizer } } const SessionContext = createContext() export function useSession() { return useContext(SessionContext) } const EMPTY_SUBSCRIPTIONS = { topics: [], authors: [], } export const SessionProvider = (props: { onStateChangeCallback(state: any): unknown children: JSX.Element }) => { const { t } = useLocalize() const { actions: { showSnackbar }, } = useSnackbar() const { searchParams, changeSearchParam } = useRouter() const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [subscriptions, setSubscriptions] = createSignal(EMPTY_SUBSCRIPTIONS) const getSession = async (): Promise => { try { const tkn = getToken() console.debug('[context.session] token before: ', tkn) const authResult = await authorizer().getSession({ Authorization: tkn, }) if (authResult?.access_token) { mutate(authResult) console.debug('[context.session] token after: ', authResult.access_token) await loadSubscriptions() return authResult } } catch (error) { console.error('[context.session] getSession error:', error) mutate(null) return null } finally { setTimeout(() => { setIsSessionLoaded(true) }, 0) } } const [session, { refetch: loadSession, mutate }] = createResource(getSession, { ssrLoadFrom: 'initial', initialValue: null, }) 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 => { const result = await apiClient.private?.getMySubscriptions() if (result) { setSubscriptions(result) } else { setSubscriptions(EMPTY_SUBSCRIPTIONS) } } const [author, { refetch: loadAuthor }] = createResource( async () => { const u = session()?.user if (u) { return (await apiClient.getAuthorId({ user: u.id })) ?? null } 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) { mutate(authResult) await loadSubscriptions() console.debug('[context.session] signed in') } else { console.info((authResult as AuthToken).message) } } const authorizer = createMemo( () => new Authorizer({ authorizerURL: config.authorizerURL, redirectURL: config.redirectURL, clientID: config.clientID, }), ) createEffect( on( () => props.onStateChangeCallback, () => { props.onStateChangeCallback(session()) }, { defer: true }, ), ) const [configuration, setConfig] = createSignal(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() console.debug('[context.session] session: ', s) console.log('[context.session] loading author...') const a = await loadAuthor() console.debug('[context.session] author: ', a) setIsSessionLoaded(true) console.log('[context.session] loaded') }) const [isAuthWithCallback, setIsAuthWithCallback] = createSignal<() => void>() const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => { setIsAuthWithCallback(() => callback) const userdata = await authorizer().getProfile() if (userdata) mutate({ ...session(), user: userdata }) if (!isAuthenticated()) { showModal('auth', modalSource) } } const signOut = async () => { await authorizer().logout() mutate(null) setSubscriptions(EMPTY_SUBSCRIPTIONS) showSnackbar({ body: t("You've successfully logged out") }) } const confirmEmail = async (input: VerifyEmailInput) => { console.log(`[context.session] calling authorizer's verify email with ${input}`) const at: void | AuthToken = await authorizer().verifyEmail(input) if (at) mutate(at) console.log(`[context.session] confirmEmail got result ${at}`) } const getToken = createMemo(() => session()?.access_token) const actions = { getToken, loadSession, loadSubscriptions, requireAuthentication, signIn, signOut, confirmEmail, setIsSessionLoaded, setSession: mutate, authorizer, loadAuthor, } const value: SessionContextType = { config: configuration(), session, subscriptions, isSessionLoaded, isAuthenticated, author, actions, isAuthWithCallback, } return {props.children} }