session-context-wip
Some checks failed
deploy / test (push) Failing after 1m6s
deploy / deploy (push) Has been skipped

This commit is contained in:
Untone 2023-12-14 14:49:55 +03:00
parent a59cc9c28e
commit f23c3fded1
10 changed files with 116 additions and 180 deletions

View File

@ -4,7 +4,6 @@ import { Meta, MetaProvider } from '@solidjs/meta'
import { Component, createEffect, createMemo } from 'solid-js' import { Component, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web' import { Dynamic } from 'solid-js/web'
import { AuthorizerProvider } from '../context/authorizer'
import { ConfirmProvider } from '../context/confirm' import { ConfirmProvider } from '../context/confirm'
import { ConnectProvider } from '../context/connect' import { ConnectProvider } from '../context/connect'
import { EditorProvider } from '../context/editor' import { EditorProvider } from '../context/editor'
@ -120,17 +119,15 @@ export const App = (props: Props) => {
<LocalizeProvider> <LocalizeProvider>
<SnackbarProvider> <SnackbarProvider>
<ConfirmProvider> <ConfirmProvider>
<AuthorizerProvider onStateChangeCallback={console.log}> <SessionProvider onStateChangeCallback={console.log}>
<SessionProvider> <ConnectProvider>
<ConnectProvider> <NotificationsProvider>
<NotificationsProvider> <EditorProvider>
<EditorProvider> <Dynamic component={pageComponent()} {...props} />
<Dynamic component={pageComponent()} {...props} /> </EditorProvider>
</EditorProvider> </NotificationsProvider>
</NotificationsProvider> </ConnectProvider>
</ConnectProvider> </SessionProvider>
</SessionProvider>
</AuthorizerProvider>
</ConfirmProvider> </ConfirmProvider>
</SnackbarProvider> </SnackbarProvider>
</LocalizeProvider> </LocalizeProvider>

View File

@ -3,7 +3,6 @@ import type { ConfirmEmailSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, onMount, Show } from 'solid-js' import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useAuthorizer } from '../../../context/authorizer'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { ApiError } from '../../../graphql/error' import { ApiError } from '../../../graphql/error'
@ -17,7 +16,7 @@ export const EmailConfirm = () => {
const { const {
actions: { confirmEmail }, actions: { confirmEmail },
} = useSession() } = useSession()
const [{ user }] = useAuthorizer() const { user } = useSession()
const [isTokenExpired, setIsTokenExpired] = createSignal(false) const [isTokenExpired, setIsTokenExpired] = createSignal(false)
const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) const [isTokenInvalid, setIsTokenInvalid] = createSignal(false)

View File

@ -3,7 +3,6 @@ import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, JSX, Show } from 'solid-js' import { createSignal, JSX, Show } from 'solid-js'
import { useAuthorizer } from '../../../context/authorizer'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { ApiError } from '../../../graphql/error' import { ApiError } from '../../../graphql/error'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
@ -12,6 +11,7 @@ import { validateEmail } from '../../../utils/validateEmail'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
import { useSession } from '../../../context/session'
type FormFields = { type FormFields = {
email: string email: string
@ -26,7 +26,9 @@ export const ForgotPasswordForm = () => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
setEmail(newEmail) setEmail(newEmail)
} }
const [, { authorizer }] = useAuthorizer() const {
actions: { authorizer },
} = useSession()
const [submitError, setSubmitError] = createSignal('') const [submitError, setSubmitError] = createSignal('')
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})

View File

@ -3,7 +3,6 @@ import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
import { useAuthorizer } from '../../../context/authorizer'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
@ -67,8 +66,10 @@ export const LoginForm = () => {
setIsLinkSent(true) setIsLinkSent(true)
setIsEmailNotConfirmed(false) setIsEmailNotConfirmed(false)
setSubmitError('') setSubmitError('')
const [{ token }, { authorizer }] = useAuthorizer() const {
const result = await authorizer().verifyEmail({ token: token.id_token }) actions: { authorizer, getToken },
} = useSession()
const result = await authorizer().verifyEmail({ token: getToken() })
if (!result) setSubmitError('cant sign send link') // TODO: if (!result) setSubmitError('cant sign send link') // TODO:
} }

View File

@ -4,7 +4,6 @@ import type { JSX } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useAuthorizer } from '../../../context/authorizer'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { ApiError } from '../../../graphql/error' import { ApiError } from '../../../graphql/error'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
@ -18,6 +17,7 @@ import { email, setEmail } from './sharedLogic'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
import { useSession } from '../../../context/session'
type FormFields = { type FormFields = {
fullName: string fullName: string
@ -35,7 +35,9 @@ export const RegisterForm = () => {
const { changeSearchParam } = useRouter<AuthModalSearchParams>() const { changeSearchParam } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const { emailChecks } = useEmailChecks() const { emailChecks } = useEmailChecks()
const [, { authorizer }] = useAuthorizer() const {
actions: { authorizer },
} = useSession()
const [submitError, setSubmitError] = createSignal('') const [submitError, setSubmitError] = createSignal('')
const [fullName, setFullName] = createSignal('') const [fullName, setFullName] = createSignal('')
const [password, setPassword] = createSignal('') const [password, setPassword] = createSignal('')

View File

@ -4,7 +4,7 @@ import deepEqual from 'fast-deep-equal'
import { createEffect, createSignal, For, lazy, Match, onCleanup, onMount, Show, Switch } from 'solid-js' import { createEffect, createSignal, For, lazy, Match, onCleanup, onMount, Show, Switch } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { useAuthorizer } from '../../context/authorizer' import { useSession } from '../../context/session'
import { useConfirm } from '../../context/confirm' import { useConfirm } from '../../context/confirm'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useProfileForm } from '../../context/profile' import { useProfileForm } from '../../context/profile'
@ -49,7 +49,9 @@ export const ProfileSettings = () => {
actions: { showSnackbar }, actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const [, { setUser, authorizer }] = useAuthorizer() const {
actions: { setUser, authorizer },
} = useSession()
const { const {
actions: { showConfirm }, actions: { showConfirm },

View File

@ -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<void>
}
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<AuthorizerProviderProps> = (props) => {
const [state, setState] = createStore<AuthorizerState>({
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 (
<AuthorizerContext.Provider
value={[
state,
{
setUser,
setLoading,
setToken: handleTokenChange,
setAuthData,
authorizer,
logout,
},
]}
>
{props.children}
</AuthorizerContext.Provider>
)
}
export const useAuthorizer = () => useContext(AuthorizerContext)

View File

@ -3,7 +3,6 @@ import type { Accessor, JSX } from 'solid-js'
import { fetchEventSource } from '@microsoft/fetch-event-source' import { fetchEventSource } from '@microsoft/fetch-event-source'
import { createContext, useContext, createSignal, createEffect } from 'solid-js' import { createContext, useContext, createSignal, createEffect } from 'solid-js'
import { useAuthorizer } from './authorizer'
import { useSession } from './session' import { useSession } from './session'
export interface SSEMessage { export interface SSEMessage {

View File

@ -11,8 +11,8 @@ import { router, useRouter } from '../stores/router'
import { slugify } from '../utils/slugify' import { slugify } from '../utils/slugify'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSnackbar } from './snackbar'
import { useSession } from './session' import { useSession } from './session'
import { useSnackbar } from './snackbar'
type WordCounter = { type WordCounter = {
characters: number characters: number

View File

@ -2,22 +2,44 @@ import type { AuthModalSource } from '../components/Nav/AuthModal/types'
import type { Author, Result } from '../graphql/schema/core.gen' import type { Author, Result } from '../graphql/schema/core.gen'
import type { Accessor, JSX, Resource } from 'solid-js' import type { Accessor, JSX, Resource } from 'solid-js'
import { VerifyEmailInput, LoginInput, AuthToken, User } from '@authorizerdev/authorizer-js' import {
import { createContext, createMemo, createResource, createSignal, onMount, useContext } from 'solid-js' 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 { apiClient } from '../graphql/client/core'
import { showModal } from '../stores/ui' import { showModal } from '../stores/ui'
import { useAuthorizer } from './authorizer'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSnackbar } from './snackbar' 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 = { export type SessionContextType = {
user: User | null
config: ConfigType
session: Resource<AuthToken> session: Resource<AuthToken>
isSessionLoaded: Accessor<boolean> isSessionLoaded: Accessor<boolean>
subscriptions: Accessor<Result> subscriptions: Accessor<Result>
author: Resource<Author | null> author: Resource<Author | null>
isAuthenticated: Accessor<boolean> isAuthenticated: Accessor<boolean>
isAuthWithCallback: Accessor<() => void>
actions: { actions: {
getToken: () => string getToken: () => string
loadSession: () => AuthToken | Promise<AuthToken> loadSession: () => AuthToken | Promise<AuthToken>
@ -29,6 +51,10 @@ export type SessionContextType = {
signIn: (params: LoginInput) => Promise<void> signIn: (params: LoginInput) => Promise<void>
signOut: () => Promise<void> signOut: () => Promise<void>
confirmEmail: (input: VerifyEmailInput) => Promise<void> confirmEmail: (input: VerifyEmailInput) => Promise<void>
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: [], 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 [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS) const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
const { t } = useLocalize() const { t } = useLocalize()
const { const {
actions: { showSnackbar }, actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const [{ token }, { setUser, setToken, authorizer }] = useAuthorizer() const [token, setToken] = createSignal<AuthToken>()
const getToken = () => token.access_token const [user, setUser] = createSignal<User>()
const loadSubscriptions = async (): Promise<void> => { const loadSubscriptions = async (): Promise<void> => {
const result = await apiClient.getMySubscriptions() const result = await apiClient.getMySubscriptions()
if (result) { if (result) {
@ -63,17 +92,16 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
const getSession = async (): Promise<AuthToken> => { const getSession = async (): Promise<AuthToken> => {
try { try {
if (token) { const authResult = await authorizer().getSession({
const authResult = await authorizer().getSession() Authorization: getToken(),
if (authResult && authResult.access_token) { })
console.log(authResult) if (authResult?.access_token) {
setToken(authResult) console.log(authResult)
if (authResult.user) setUser(authResult.user) setToken(authResult)
loadSubscriptions() if (authResult.user) setUser(authResult.user)
return authResult loadSubscriptions()
} return authResult
} }
return null
} catch (error) { } catch (error) {
console.error('getSession error:', error) console.error('getSession error:', error)
setToken(null) 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<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()
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) => { const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => {
setIsAuthWithCallback(() => callback) 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 () => { const signOut = async () => {
await authorizer().logout() await authorizer().logout()
mutate(null) mutate(null)
@ -155,26 +208,32 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
} }
} }
const getToken = createMemo(() => token()?.access_token)
const actions = { const actions = {
getToken,
loadSession, loadSession,
loadSubscriptions,
requireAuthentication, requireAuthentication,
signIn, signIn,
signOut, signOut,
confirmEmail, confirmEmail,
loadSubscriptions, setIsSessionLoaded,
getToken, setToken,
setUser,
authorizer,
} }
const value: SessionContextType = { const value: SessionContextType = {
user: user(),
config: configuration(),
session, session,
subscriptions, subscriptions,
isSessionLoaded, isSessionLoaded,
author,
isAuthenticated, isAuthenticated,
author,
actions, actions,
isAuthWithCallback,
} }
onMount(() => {
loadSession()
})
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider> return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
} }