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, Authorizer, ConfigType, SignupInput, } from '@authorizerdev/authorizer-js' import { createContext, createEffect, createMemo, createResource, createSignal, on, onCleanup, onMount, useContext, } from 'solid-js' import { inboxClient } from '../graphql/client/chat' import { apiClient } from '../graphql/client/core' import { notifierClient } from '../graphql/client/notifier' import { router, useRouter } from '../stores/router' import { showModal } from '../stores/ui' import { useLocalize } from './localize' import { useSnackbar } from './snackbar' import { openPage } from '@nanostores/router' const defaultConfig: ConfigType = { authorizerURL: 'https://auth.discours.io', redirectURL: 'https://discoursio-webapp.vercel.app', clientID: '9c113377-5eea-4c89-98e1-69302462fc08', // FIXME: use env? } export type SessionContextType = { config: ConfigType session: Resource author: Resource authError: Accessor isSessionLoaded: Accessor subscriptions: Accessor isAuthWithCallback: Accessor<() => void> isAuthenticated: Accessor actions: { loadSession: () => AuthToken | Promise setSession: (token: AuthToken | null) => void // setSession loadAuthor: (info?: unknown) => Author | Promise setAuthor: (a: Author) => void loadSubscriptions: () => Promise requireAuthentication: ( callback: (() => Promise) | (() => void), modalSource: AuthModalSource, ) => void signUp: (params: SignupInput) => Promise signIn: (params: LoginInput) => Promise signOut: () => Promise changePassword: (password: string, token: string) => void 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, changeSearchParams } = useRouter() // handle callback's redirect_uri createEffect(async () => { // TODO: handle oauth here too const token = searchParams()?.token const access_token = searchParams()?.access_token if (access_token) changeSearchParams({ mode: 'confirm-email', modal: 'auth', access_token }) else if (token) changeSearchParams({ mode: 'change-password', modal: 'auth', token }) }) // load let ta const [configuration, setConfig] = createSignal(defaultConfig) const authorizer = createMemo(() => new Authorizer(defaultConfig)) const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [authError, setAuthError] = createSignal('') const [session, { refetch: loadSession, mutate: setSession }] = createResource( async () => { try { const s = await authorizer().getSession() console.info('[context.session] loading session', s) ta = setTimeout(loadSession, s.expires_in * 1000) console.info(`[context.session] will refresh in ${s.expires_in / 60} mins`) return s } catch (error) { console.info('[context.session] cannot refresh session', error) setAuthError(error) return null } }, { ssrLoadFrom: 'initial', initialValue: null, }, ) onCleanup(() => { clearTimeout(ta) }) const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource( async () => { const u = session()?.user return u ? (await apiClient.getAuthorId({ user: u.id })) || null : null }, { ssrLoadFrom: 'initial', initialValue: null, }, ) const [subscriptions, setSubscriptions] = createSignal(EMPTY_SUBSCRIPTIONS) const loadSubscriptions = async (): Promise => { const result = await apiClient.getMySubscriptions() setSubscriptions(result || EMPTY_SUBSCRIPTIONS) } // when session is loaded createEffect(async () => { if (session()) { const token = session()?.access_token if (token) { // console.log('[context.session] token observer got token', token) if (!inboxClient.private) { apiClient.connect(token) notifierClient.connect(token) inboxClient.connect(token) } if (!author()) { const a = await loadAuthor() if (!a) reset() else await loadSubscriptions() } setIsSessionLoaded(true) } } }) const reset = () => { setIsSessionLoaded(true) setSubscriptions(EMPTY_SUBSCRIPTIONS) setSession(null) setAuthor(null) } // initial effect onMount(async () => { const metaRes = await authorizer().getMetaData() setConfig({ ...defaultConfig, ...metaRes, redirectURL: window.location.origin }) let s try { s = await loadSession() } catch (error) { console.warn('[context.session] load session failed', error) } if (!s) reset() }) // callback state updater createEffect( on( () => props.onStateChangeCallback, () => { props.onStateChangeCallback(session()) }, { defer: true }, ), ) // require auth wrapper const [isAuthWithCallback, setIsAuthWithCallback] = createSignal<() => void>() const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => { setIsAuthWithCallback(() => callback) await loadSession() if (!session()) { showModal('auth', modalSource) } } // authorizer api proxy methods const signUp = async (params) => { const authResult: void | AuthToken = await authorizer().signup({ ...params, confirm_password: params.password, }) if (authResult) setSession(authResult) } const signIn = async (params: LoginInput) => { const authResult: AuthToken | void = await authorizer().login(params) if (authResult) setSession(authResult) } const signOut = async () => { await authorizer().logout() reset() showSnackbar({ body: t("You've successfully logged out") }) } const changePassword = async (password: string, token: string) => { const resp = await authorizer().resetPassword({ password, token, confirm_password: password }) console.debug('[context.session] change password response:', resp) } const confirmEmail = async (input: VerifyEmailInput) => { console.debug(`[context.session] calling authorizer's verify email with`, input) try { const at: void | AuthToken = await authorizer().verifyEmail(input) if (at) setSession(at) return at } catch (error) { console.warn(error) } } const isAuthenticated = createMemo(() => Boolean(author())) const actions = { loadSession, loadSubscriptions, requireAuthentication, signUp, signIn, signOut, confirmEmail, setIsSessionLoaded, setSession, setAuthor, authorizer, loadAuthor, changePassword, } const value: SessionContextType = { authError, config: configuration(), session, subscriptions, isSessionLoaded, author, actions, isAuthWithCallback, isAuthenticated, } return {props.children} }