diff --git a/src/components/App.tsx b/src/components/App.tsx index 4b39c741..18952224 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4,7 +4,6 @@ import { Meta, MetaProvider } from '@solidjs/meta' import { Component, createEffect, createMemo } from 'solid-js' import { Dynamic } from 'solid-js/web' -import { AuthorizerProvider } from '../context/authorizer' import { ConfirmProvider } from '../context/confirm' import { ConnectProvider } from '../context/connect' import { EditorProvider } from '../context/editor' @@ -120,17 +119,15 @@ export const App = (props: Props) => { - - - - - - - - - - - + + + + + + + + + diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index 09b4bf1b..3038fb73 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -3,7 +3,6 @@ import type { ConfirmEmailSearchParams } from './types' import { clsx } from 'clsx' import { createMemo, createSignal, onMount, Show } from 'solid-js' -import { useAuthorizer } from '../../../context/authorizer' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' import { ApiError } from '../../../graphql/error' @@ -17,7 +16,7 @@ export const EmailConfirm = () => { const { actions: { confirmEmail }, } = useSession() - const [{ user }] = useAuthorizer() + const { user } = useSession() const [isTokenExpired, setIsTokenExpired] = createSignal(false) const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index ff2b5176..ea7e48f0 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -3,7 +3,6 @@ import type { AuthModalSearchParams } from './types' import { clsx } from 'clsx' import { createSignal, JSX, Show } from 'solid-js' -import { useAuthorizer } from '../../../context/authorizer' import { useLocalize } from '../../../context/localize' import { ApiError } from '../../../graphql/error' import { useRouter } from '../../../stores/router' @@ -12,6 +11,7 @@ import { validateEmail } from '../../../utils/validateEmail' import { email, setEmail } from './sharedLogic' import styles from './AuthModal.module.scss' +import { useSession } from '../../../context/session' type FormFields = { email: string @@ -26,7 +26,9 @@ export const ForgotPasswordForm = () => { setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setEmail(newEmail) } - const [, { authorizer }] = useAuthorizer() + const { + actions: { authorizer }, + } = useSession() const [submitError, setSubmitError] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index a7e4f994..851045e5 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -3,7 +3,6 @@ import type { AuthModalSearchParams } from './types' import { clsx } from 'clsx' import { createSignal, Show } from 'solid-js' -import { useAuthorizer } from '../../../context/authorizer' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' import { useSnackbar } from '../../../context/snackbar' @@ -67,8 +66,10 @@ export const LoginForm = () => { setIsLinkSent(true) setIsEmailNotConfirmed(false) setSubmitError('') - const [{ token }, { authorizer }] = useAuthorizer() - const result = await authorizer().verifyEmail({ token: token.id_token }) + const { + actions: { authorizer, getToken }, + } = useSession() + const result = await authorizer().verifyEmail({ token: getToken() }) if (!result) setSubmitError('cant sign send link') // TODO: } diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index fc797367..b83c3190 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -4,7 +4,6 @@ import type { JSX } from 'solid-js' import { clsx } from 'clsx' import { Show, createSignal } from 'solid-js' -import { useAuthorizer } from '../../../context/authorizer' import { useLocalize } from '../../../context/localize' import { ApiError } from '../../../graphql/error' import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' @@ -18,6 +17,7 @@ import { email, setEmail } from './sharedLogic' import { SocialProviders } from './SocialProviders' import styles from './AuthModal.module.scss' +import { useSession } from '../../../context/session' type FormFields = { fullName: string @@ -35,7 +35,9 @@ export const RegisterForm = () => { const { changeSearchParam } = useRouter() const { t } = useLocalize() const { emailChecks } = useEmailChecks() - const [, { authorizer }] = useAuthorizer() + const { + actions: { authorizer }, + } = useSession() const [submitError, setSubmitError] = createSignal('') const [fullName, setFullName] = createSignal('') const [password, setPassword] = createSignal('') diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index 480f1938..d5026dab 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -4,7 +4,7 @@ import deepEqual from 'fast-deep-equal' import { createEffect, createSignal, For, lazy, Match, onCleanup, onMount, Show, Switch } from 'solid-js' import { createStore } from 'solid-js/store' -import { useAuthorizer } from '../../context/authorizer' +import { useSession } from '../../context/session' import { useConfirm } from '../../context/confirm' import { useLocalize } from '../../context/localize' import { useProfileForm } from '../../context/profile' @@ -49,7 +49,9 @@ export const ProfileSettings = () => { actions: { showSnackbar }, } = useSnackbar() - const [, { setUser, authorizer }] = useAuthorizer() + const { + actions: { setUser, authorizer }, + } = useSession() const { actions: { showConfirm }, diff --git a/src/context/authorizer.tsx b/src/context/authorizer.tsx deleted file mode 100644 index 705a245e..00000000 --- a/src/context/authorizer.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import type { ParentComponent } from 'solid-js' - -import { Authorizer, User, AuthToken, ConfigType } from '@authorizerdev/authorizer-js' -import { createContext, createEffect, createMemo, onMount, useContext } from 'solid-js' -import { createStore } from 'solid-js/store' - -export type AuthorizerState = { - user: User | null - token: AuthToken | null - loading: boolean - config: ConfigType -} - -type AuthorizerContextActions = { - setLoading: (loading: boolean) => void - setToken: (token: AuthToken | null) => void - setUser: (user: User | null) => void - setAuthData: (data: AuthorizerState) => void - authorizer: () => Authorizer - logout: () => Promise -} -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? -} - -const AuthorizerContext = createContext<[AuthorizerState, AuthorizerContextActions]>([ - { - config, - user: null, - token: null, - loading: false, - }, - { - setLoading: () => {}, - setToken: () => {}, - setUser: () => {}, - setAuthData: () => {}, - authorizer: () => new Authorizer(config), - logout: async () => {}, - }, -]) - -type AuthorizerProviderProps = { - onStateChangeCallback?: (stateData: AuthorizerState) => void -} - -export const AuthorizerProvider: ParentComponent = (props) => { - const [state, setState] = createStore({ - user: null, - token: null, - loading: true, - config, - }) - const authorizer = createMemo( - () => - new Authorizer({ - authorizerURL: state.config.authorizerURL, - redirectURL: state.config.redirectURL, - clientID: state.config.clientID, - }), - ) - - createEffect(() => { - if (props.onStateChangeCallback) { - props.onStateChangeCallback(state) - } - }) - - // Actions - const setLoading = (loading: boolean) => { - setState('loading', loading) - } - - const handleTokenChange = (token: AuthToken | null) => { - setState('token', token) - } - - const setUser = (user: User | null) => { - setState('user', user) - } - - const setAuthData = (data: AuthorizerState) => { - setState(data) - } - - const logout = async () => { - setState('loading', true) - setState('user', null) - } - - const interval: number | null = null - - const getToken = async () => { - setState('loading', true) - const metaRes = await authorizer().getMetaData() - setState('config', (cfg) => ({ ...cfg, ...metaRes })) - setState('loading', false) - } - - onMount(() => { - setState('config', { ...config, redirectURL: window.location.origin + '/?modal=auth' }) - }) - - return ( - - {props.children} - - ) -} - -export const useAuthorizer = () => useContext(AuthorizerContext) diff --git a/src/context/connect.tsx b/src/context/connect.tsx index 3bb1828a..311032d4 100644 --- a/src/context/connect.tsx +++ b/src/context/connect.tsx @@ -3,7 +3,6 @@ import type { Accessor, JSX } from 'solid-js' import { fetchEventSource } from '@microsoft/fetch-event-source' import { createContext, useContext, createSignal, createEffect } from 'solid-js' -import { useAuthorizer } from './authorizer' import { useSession } from './session' export interface SSEMessage { diff --git a/src/context/editor.tsx b/src/context/editor.tsx index fe07fee1..737026da 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -11,8 +11,8 @@ import { router, useRouter } from '../stores/router' import { slugify } from '../utils/slugify' import { useLocalize } from './localize' -import { useSnackbar } from './snackbar' import { useSession } from './session' +import { useSnackbar } from './snackbar' type WordCounter = { characters: number diff --git a/src/context/session.tsx b/src/context/session.tsx index f79770ea..3ae1dab1 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -2,22 +2,44 @@ 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 } from '@authorizerdev/authorizer-js' -import { createContext, createMemo, createResource, createSignal, onMount, useContext } from 'solid-js' +import { + VerifyEmailInput, + LoginInput, + AuthToken, + User, + Authorizer, + ConfigType, +} from '@authorizerdev/authorizer-js' +import { + createContext, + createEffect, + createMemo, + createResource, + createSignal, + onMount, + useContext, +} from 'solid-js' import { apiClient } from '../graphql/client/core' import { showModal } from '../stores/ui' - -import { useAuthorizer } from './authorizer' import { useLocalize } from './localize' import { useSnackbar } from './snackbar' +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 = { + user: User | null + config: ConfigType session: Resource isSessionLoaded: Accessor subscriptions: Accessor author: Resource isAuthenticated: Accessor + isAuthWithCallback: Accessor<() => void> actions: { getToken: () => string loadSession: () => AuthToken | Promise @@ -29,6 +51,10 @@ export type SessionContextType = { signIn: (params: LoginInput) => Promise signOut: () => Promise confirmEmail: (input: VerifyEmailInput) => Promise + setIsSessionLoaded: (loaded: boolean) => void + setToken: (token: AuthToken | null) => void // setSession + setUser: (user: User | null) => void + authorizer: () => Authorizer } } @@ -43,15 +69,18 @@ const EMPTY_SUBSCRIPTIONS = { authors: [], } -export const SessionProvider = (props: { children: JSX.Element }) => { +export const SessionProvider = (props: { + onStateChangeCallback(state: any): unknown + children: JSX.Element +}) => { const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [subscriptions, setSubscriptions] = createSignal(EMPTY_SUBSCRIPTIONS) const { t } = useLocalize() const { actions: { showSnackbar }, } = useSnackbar() - const [{ token }, { setUser, setToken, authorizer }] = useAuthorizer() - const getToken = () => token.access_token + const [token, setToken] = createSignal() + const [user, setUser] = createSignal() const loadSubscriptions = async (): Promise => { const result = await apiClient.getMySubscriptions() if (result) { @@ -63,17 +92,16 @@ export const SessionProvider = (props: { children: JSX.Element }) => { const getSession = async (): Promise => { try { - if (token) { - const authResult = await authorizer().getSession() - if (authResult && authResult.access_token) { - console.log(authResult) - setToken(authResult) - if (authResult.user) setUser(authResult.user) - loadSubscriptions() - return authResult - } + const authResult = await authorizer().getSession({ + Authorization: getToken(), + }) + if (authResult?.access_token) { + console.log(authResult) + setToken(authResult) + if (authResult.user) setUser(authResult.user) + loadSubscriptions() + return authResult } - return null } catch (error) { console.error('getSession error:', error) setToken(null) @@ -120,8 +148,39 @@ export const SessionProvider = (props: { children: JSX.Element }) => { } } - const [isAuthWithCallback, setIsAuthWithCallback] = createSignal(null) + const authorizer = createMemo( + () => + new Authorizer({ + authorizerURL: config.authorizerURL, + redirectURL: config.redirectURL, + clientID: config.clientID, + }), + ) + createEffect(() => { + if (props.onStateChangeCallback) { + props.onStateChangeCallback(token()) + } + }) + + 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.log(`[context.session] ${s}`) + setToken(s) + console.log('[context.session] loading author...') + await loadAuthor() + setIsSessionLoaded(true) + console.log('[context.session] loaded') + }) + + const [isAuthWithCallback, setIsAuthWithCallback] = createSignal<() => void>() const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => { setIsAuthWithCallback(() => callback) @@ -132,12 +191,6 @@ export const SessionProvider = (props: { children: JSX.Element }) => { } } - onMount(async () => { - // Load the session and author data on mount - await loadSession() - loadAuthor() - }) - const signOut = async () => { await authorizer().logout() mutate(null) @@ -155,26 +208,32 @@ export const SessionProvider = (props: { children: JSX.Element }) => { } } + const getToken = createMemo(() => token()?.access_token) + const actions = { + getToken, loadSession, + loadSubscriptions, requireAuthentication, signIn, signOut, confirmEmail, - loadSubscriptions, - getToken, + setIsSessionLoaded, + setToken, + setUser, + authorizer, } const value: SessionContextType = { + user: user(), + config: configuration(), session, subscriptions, isSessionLoaded, - author, isAuthenticated, + author, actions, + isAuthWithCallback, } - onMount(() => { - loadSession() - }) return {props.children} }