webapp/src/context/session.tsx

350 lines
10 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,
Authorizer,
ConfigType,
2023-12-24 08:16:41 +00:00
SignupInput,
2024-01-19 15:03:33 +00:00
AuthorizeResponse,
2024-01-19 18:24:37 +00:00
GraphqlQueryInput,
2023-12-14 11:49:55 +00:00
} from '@authorizerdev/authorizer-js'
import {
createContext,
createEffect,
createMemo,
createResource,
createSignal,
2023-12-14 13:50:22 +00:00
on,
2023-12-24 21:29:25 +00:00
onCleanup,
2023-12-14 11:49:55 +00:00
onMount,
useContext,
} from 'solid-js'
2023-12-19 09:34:24 +00:00
import { inboxClient } from '../graphql/client/chat'
2023-11-28 13:18:25 +00:00
import { apiClient } from '../graphql/client/core'
2023-12-19 09:34:24 +00:00
import { notifierClient } from '../graphql/client/notifier'
2023-12-25 05:47:11 +00:00
import { useRouter } from '../stores/router'
import { showModal } from '../stores/ui'
2023-12-25 07:32:53 +00:00
import { addAuthors } from '../stores/zine/authors'
2023-12-26 10:05:15 +00:00
import { useLocalize } from './localize'
import { useSnackbar } from './snackbar'
2023-12-24 08:16:41 +00:00
const defaultConfig: ConfigType = {
2023-12-14 11:49:55 +00:00
authorizerURL: 'https://auth.discours.io',
2024-01-19 15:03:33 +00:00
redirectURL: 'https://testing.discours.io',
clientID: 'b9038a34-ca59-41ae-a105-c7fbea603e24', // FIXME: use env?
2023-12-14 11:49:55 +00:00
}
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>
2023-12-24 21:12:42 +00:00
authError: Accessor<string>
2022-12-06 16:03:55 +00:00
isSessionLoaded: Accessor<boolean>
2023-11-28 13:18:25 +00:00
subscriptions: Accessor<Result>
2023-12-14 11:49:55 +00:00
isAuthWithCallback: Accessor<() => void>
2023-12-24 08:16:41 +00:00
isAuthenticated: Accessor<boolean>
actions: {
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>
2023-12-20 16:54:20 +00:00
setAuthor: (a: Author) => void
loadSubscriptions: () => Promise<void>
requireAuthentication: (
callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource,
) => void
2023-12-24 08:16:41 +00:00
signUp: (params: SignupInput) => Promise<AuthToken | void>
2023-11-28 13:18:25 +00:00
signIn: (params: LoginInput) => Promise<void>
signOut: () => Promise<void>
2024-01-19 18:24:37 +00:00
oauth: (provider: string) => Promise<void>
2023-12-24 08:16:41 +00:00
changePassword: (password: string, token: string) => void
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken | 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-24 08:16:41 +00:00
const { searchParams, changeSearchParams } = useRouter()
2024-01-19 18:24:37 +00:00
const [configuration, setConfig] = createSignal<ConfigType>(defaultConfig)
const authorizer = createMemo(() => new Authorizer(configuration()))
2023-12-15 13:45:34 +00:00
2024-01-19 18:24:37 +00:00
createEffect(() => {
if (authorizer()) {
}
})
const [oauthState, setOauthState] = createSignal<string>()
2023-12-24 08:16:41 +00:00
// handle callback's redirect_uri
createEffect(async () => {
2024-01-19 18:24:37 +00:00
// oauth
const state = searchParams()?.state
if (state) {
setOauthState((_s) => state)
const scope = searchParams()?.scope
? searchParams()?.scope?.toString().split(' ')
: ['openid', 'profile', 'email']
if (scope) console.info(`[context.session] scope: ${scope}`)
const url = searchParams()?.redirect_uri || searchParams()?.redirectURL || window.location.href
setConfig((c: ConfigType) => ({ ...c, redirectURL: url })) // .split('?')[0]
changeSearchParams({ mode: 'confirm-email', modal: 'auth' }, true)
}
})
// handle email confirm
createEffect(() => {
2023-12-24 08:16:41 +00:00
const token = searchParams()?.token
const access_token = searchParams()?.access_token
2023-12-24 23:37:30 +00:00
if (access_token) changeSearchParams({ mode: 'confirm-email', modal: 'auth', access_token })
else if (token) changeSearchParams({ mode: 'change-password', modal: 'auth', token })
2023-12-16 14:13:14 +00:00
})
2023-12-24 08:16:41 +00:00
// load
2024-01-18 08:31:45 +00:00
let minuteLater
2023-12-24 08:16:41 +00:00
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
2023-12-24 21:12:42 +00:00
const [authError, setAuthError] = createSignal('')
2023-12-24 08:16:41 +00:00
const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(
async () => {
try {
2023-12-24 21:29:25 +00:00
const s = await authorizer().getSession()
console.info('[context.session] loading session', s)
2024-01-18 08:31:45 +00:00
// Set session expiration time in local storage
const expires_at = new Date(Date.now() + s.expires_in * 1000)
localStorage.setItem('expires_at', `${expires_at.getTime()}`)
// Set up session expiration check timer
minuteLater = setTimeout(checkSessionIsExpired, 60 * 1000)
2023-12-24 21:29:25 +00:00
console.info(`[context.session] will refresh in ${s.expires_in / 60} mins`)
2024-01-18 08:31:45 +00:00
// Set the session loaded flag
setIsSessionLoaded(true)
2023-12-24 21:29:25 +00:00
return s
2023-12-25 04:05:04 +00:00
} catch (error) {
console.info('[context.session] cannot refresh session', error)
setAuthError(error)
2024-01-18 08:31:45 +00:00
// Set the session loaded flag even if there's an error
setIsSessionLoaded(true)
2023-12-24 08:16:41 +00:00
return null
2023-12-16 16:44:25 +00:00
}
2023-12-24 08:16:41 +00:00
},
{
ssrLoadFrom: 'initial',
initialValue: null,
},
)
2023-12-16 14:13:14 +00:00
2024-01-18 08:31:45 +00:00
const checkSessionIsExpired = () => {
const expires_at_data = localStorage.getItem('expires_at')
if (expires_at_data) {
const expires_at = Number.parseFloat(expires_at_data)
const current_time = Date.now()
// Check if the session has expired
if (current_time >= expires_at) {
console.info('[context.session] Session has expired, refreshing.')
loadSession()
} else {
// Schedule the next check
minuteLater = setTimeout(checkSessionIsExpired, 60 * 1000)
}
}
}
onCleanup(() => clearTimeout(minuteLater))
2023-12-24 21:29:25 +00:00
2023-12-20 16:54:20 +00:00
const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(
2023-11-28 13:18:25 +00:00
async () => {
2023-11-28 18:04:51 +00:00
const u = session()?.user
2024-01-10 12:39:02 +00:00
return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null
2023-11-28 13:18:25 +00:00
},
{
ssrLoadFrom: 'initial',
initialValue: null,
},
)
2023-12-24 08:16:41 +00:00
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
const loadSubscriptions = async (): Promise<void> => {
const result = await apiClient.getMySubscriptions()
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
}
2023-12-24 21:12:42 +00:00
// when session is loaded
2023-12-24 08:16:41 +00:00
createEffect(async () => {
if (session()) {
const token = session()?.access_token
if (token) {
2023-12-24 21:29:25 +00:00
// console.log('[context.session] token observer got token', token)
2023-12-24 08:16:41 +00:00
if (!inboxClient.private) {
apiClient.connect(token)
notifierClient.connect(token)
inboxClient.connect(token)
}
if (!author()) {
const a = await loadAuthor()
2023-12-25 07:32:53 +00:00
if (a) {
await loadSubscriptions()
addAuthors([a])
} else {
reset()
}
2023-12-24 08:16:41 +00:00
}
2023-12-24 21:12:42 +00:00
setIsSessionLoaded(true)
2023-12-24 08:16:41 +00:00
}
2023-12-24 12:56:30 +00:00
}
})
2023-12-24 21:44:01 +00:00
const reset = () => {
setIsSessionLoaded(true)
setSubscriptions(EMPTY_SUBSCRIPTIONS)
setSession(null)
setAuthor(null)
}
2023-12-24 08:16:41 +00:00
// initial effect
onMount(async () => {
const metaRes = await authorizer().getMetaData()
setConfig({ ...defaultConfig, ...metaRes, redirectURL: window.location.origin })
2024-01-18 15:57:10 +00:00
let s: AuthToken
2023-12-24 08:16:41 +00:00
try {
s = await loadSession()
2023-12-25 04:05:04 +00:00
} catch (error) {
console.warn('[context.session] load session failed', error)
2023-12-24 12:56:30 +00:00
}
2023-12-24 21:44:01 +00:00
if (!s) reset()
2023-12-24 08:16:41 +00:00
})
2023-12-14 11:49:55 +00:00
2023-12-24 08:16:41 +00:00
// callback state updater
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
2023-12-24 08:16:41 +00:00
// require auth wrapper
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-24 08:16:41 +00:00
await loadSession()
2023-11-02 17:43:22 +00:00
2023-12-24 08:16:41 +00:00
if (!session()) {
showModal('auth', modalSource)
}
}
2023-12-24 08:16:41 +00:00
// authorizer api proxy methods
2024-01-19 18:24:37 +00:00
const signUp = async (params: SignupInput) => {
const authResult: void | AuthToken = await authorizer().signup(params)
2023-12-24 08:16:41 +00:00
if (authResult) setSession(authResult)
}
const signIn = async (params: LoginInput) => {
const authResult: AuthToken | void = await authorizer().login(params)
if (authResult) setSession(authResult)
}
const signOut = async () => {
2023-11-28 13:18:25 +00:00
await authorizer().logout()
2023-12-24 21:44:01 +00:00
reset()
2023-02-10 11:11:24 +00:00
showSnackbar({ body: t("You've successfully logged out") })
}
2023-12-24 08:16:41 +00:00
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)
}
2023-11-28 13:18:25 +00:00
const confirmEmail = async (input: VerifyEmailInput) => {
2023-12-17 12:36:47 +00:00
console.debug(`[context.session] calling authorizer's verify email with`, input)
2023-12-24 21:12:42 +00:00
try {
const at: void | AuthToken = await authorizer().verifyEmail(input)
if (at) setSession(at)
return at
2023-12-25 04:05:04 +00:00
} catch (error) {
console.warn(error)
2023-12-24 21:12:42 +00:00
}
}
2024-01-19 18:24:37 +00:00
const oauth = async (oauthProvider: string) => {
2024-01-19 15:03:33 +00:00
console.debug(`[context.session] calling authorizer's oauth for`)
try {
2024-01-19 18:24:37 +00:00
// const data: GraphqlQueryInput = {}
// await authorizer().graphqlQuery(data)
2024-01-19 15:03:33 +00:00
const ar: AuthorizeResponse | void = await authorizer().oauthLogin(
oauthProvider,
[],
window.location.origin,
2024-01-19 18:24:37 +00:00
oauthState(),
2024-01-19 15:03:33 +00:00
)
console.debug(ar)
} catch (error) {
console.warn(error)
}
}
2023-12-24 08:16:41 +00:00
const isAuthenticated = createMemo(() => Boolean(author()))
const actions = {
loadSession,
2023-12-14 11:49:55 +00:00
loadSubscriptions,
requireAuthentication,
2023-12-24 08:16:41 +00:00
signUp,
signIn,
signOut,
confirmEmail,
2023-12-14 11:49:55 +00:00
setIsSessionLoaded,
2023-12-20 16:54:20 +00:00
setSession,
setAuthor,
2023-12-14 11:49:55 +00:00
authorizer,
2023-12-16 14:13:14 +00:00
loadAuthor,
2023-12-24 08:16:41 +00:00
changePassword,
2024-01-19 18:24:37 +00:00
oauth,
}
const value: SessionContextType = {
2023-12-24 21:12:42 +00:00
authError,
2023-12-14 11:49:55 +00:00
config: configuration(),
session,
subscriptions,
isSessionLoaded,
2023-12-14 11:49:55 +00:00
author,
actions,
2023-12-14 11:49:55 +00:00
isAuthWithCallback,
2023-12-24 08:16:41 +00:00
isAuthenticated,
}
2022-11-14 10:02:08 +00:00
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
}