session-fx
Some checks failed
deploy / test (push) Failing after 1m2s
deploy / deploy (push) Has been skipped

This commit is contained in:
Untone 2023-12-24 11:16:41 +03:00
parent 62887f88c0
commit 9a55056b9d
16 changed files with 209 additions and 267 deletions

View File

@ -10,6 +10,7 @@ import { hideModal } from '../../../stores/ui'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
import { useSession } from '../../../context/session'
type FormFields = { type FormFields = {
password: string password: string
@ -18,28 +19,31 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>> type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
export const ChangePasswordForm = () => { export const ChangePasswordForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>() const { searchParams, changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const {
actions: { changePassword },
} = useSession()
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [newPassword, setNewPassword] = createSignal<string>() const [newPassword, setNewPassword] = createSignal<string>()
const [passwordError, setPasswordError] = createSignal<string>() const [passwordError, setPasswordError] = createSignal<string>()
const [isSuccess, setIsSuccess] = createSignal(false) const [isSuccess, setIsSuccess] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null } const authFormRef: { current: HTMLFormElement } = { current: null }
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
event.preventDefault() event.preventDefault()
setIsSubmitting(true) setIsSubmitting(true)
// Fake change password logic if (newPassword()) {
console.log('!!! sent new password:', newPassword) await changePassword(newPassword(), searchParams()?.token)
setTimeout(() => { setTimeout(() => {
setIsSubmitting(false) setIsSubmitting(false)
setIsSuccess(true) setIsSuccess(true)
}, 1000) }, 1000)
}
} }
const handlePasswordInput = (value) => { const handlePasswordInput = (value: string) => {
setNewPassword(value) setNewPassword(value)
if (passwordError()) { if (passwordError()) {
setValidationErrors((errors) => ({ ...errors, password: passwordError() })) setValidationErrors((errors) => ({ ...errors, password: passwordError() }))
@ -93,6 +97,12 @@ export const ChangePasswordForm = () => {
<div class={styles.title}>{t('Password updated!')}</div> <div class={styles.title}>{t('Password updated!')}</div>
<div class={styles.text}>{t('You can now login using your new password')}</div> <div class={styles.text}>{t('You can now login using your new password')}</div>
<div> <div>
<button
class={clsx('button', styles.submitButton)}
onClick={() => changeSearchParams({ mode: 'login' })}
>
{t('Enter')}
</button>
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}> <button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
{t('Back to main page')} {t('Back to main page')}
</button> </button>

View File

@ -1,11 +1,10 @@
import type { ConfirmEmailSearchParams } from './types' import type { ConfirmEmailSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, Show } from 'solid-js'
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 { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
@ -13,52 +12,21 @@ import styles from './AuthModal.module.scss'
export const EmailConfirm = () => { export const EmailConfirm = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { searchParams } = useRouter<ConfirmEmailSearchParams>()
const { const {
actions: { confirmEmail, loadSession, loadAuthor }, actions: { confirmEmail },
session, session,
} = useSession() } = useSession()
const [confirmedEmail, setConfirmedEmail] = createSignal<boolean>(false) const [isTokenExpired, setIsTokenExpired] = createSignal(false) // TODO: handle expired token in context/session
const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) // TODO: handle invalid token in context/session
const [isTokenExpired, setIsTokenExpired] = createSignal(false) createEffect(async () => {
const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) const token = searchParams()?.access_token
const { searchParams, changeSearchParam } = useRouter<ConfirmEmailSearchParams>() if (token) await confirmEmail({ token })
onMount(async () => {
const token = searchParams().access_token
if (token) {
changeSearchParam({})
try {
await confirmEmail({ token })
await loadSession()
await loadAuthor()
} catch (error) {
// TODO: adapt this code to authorizer
if (error instanceof ApiError) {
if (error.code === 'token_expired') {
setIsTokenExpired(true)
return
}
if (error.code === 'token_invalid') {
setIsTokenInvalid(true)
return
}
}
console.log(error)
}
}
})
createEffect(() => {
const confirmed = session()?.user?.email_verified
if (confirmed) {
console.debug(`[EmailConfirm] email successfully verified`)
setConfirmedEmail(confirmed)
}
}) })
const email = createMemo(() => session()?.user?.email) const email = createMemo(() => session()?.user?.email)
const confirmedEmail = createMemo(() => session()?.user?.email_verified)
return ( return (
<div> <div>

View File

@ -5,7 +5,7 @@ import { createSignal, JSX, Show } from 'solid-js'
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'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { validateEmail } from '../../../utils/validateEmail' import { validateEmail } from '../../../utils/validateEmail'
@ -33,17 +33,13 @@ export const ForgotPasswordForm = () => {
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [isUserNotFount, setIsUserNotFound] = createSignal(false) const [isUserNotFount, setIsUserNotFound] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null } const authFormRef: { current: HTMLFormElement } = { current: null }
const [message, setMessage] = createSignal<string>('') const [message, setMessage] = createSignal<string>('')
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
event.preventDefault() event.preventDefault()
setSubmitError('') setSubmitError('')
setIsUserNotFound(false) setIsUserNotFound(false)
const newValidationErrors: ValidationErrors = {} const newValidationErrors: ValidationErrors = {}
if (!email()) { if (!email()) {
@ -53,7 +49,6 @@ export const ForgotPasswordForm = () => {
} }
setValidationErrors(newValidationErrors) setValidationErrors(newValidationErrors)
const isValid = Object.keys(newValidationErrors).length === 0 const isValid = Object.keys(newValidationErrors).length === 0
if (!isValid) { if (!isValid) {
@ -68,18 +63,17 @@ export const ForgotPasswordForm = () => {
try { try {
const response = await authorizer().forgotPassword({ const response = await authorizer().forgotPassword({
email: email(), email: email(),
redirect_uri: window.location.href + '&success=1', // FIXME: redirect to success page accepting confirmation code redirect_uri: window.location.origin,
}) })
if (response) { console.debug('[ForgotPasswordForm] authorizer response: ', response)
console.debug('[ForgotPasswordForm]', response) if (response && response.message) setMessage(response.message)
if (response.message) setMessage(response.message)
}
} catch (error) { } catch (error) {
if (error instanceof ApiError && error.code === 'user_not_found') { console.error(error)
if (error?.code === 'user_not_found') {
setIsUserNotFound(true) setIsUserNotFound(true)
return return
} }
setSubmitError(error.message) setSubmitError(error?.message)
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@ -92,17 +86,17 @@ export const ForgotPasswordForm = () => {
ref={(el) => (authFormRef.current = el)} ref={(el) => (authFormRef.current = el)}
> >
<div> <div>
<h4>{t('Forgot password?')}</h4> <h4>{t('Restore password')}</h4>
<div class={styles.authSubtitle}> <div class={styles.authSubtitle}>
{t(message()) || t('Everything is ok, please give us your email address')} {t(message()) || t('Everything is ok, please give us your email address')}
</div> </div>
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email, 'pretty-form__item--error': validationErrors().email,
})} })}
> >
<input <input
disabled={Boolean(message())}
id="email" id="email"
name="email" name="email"
autocomplete="email" autocomplete="email"
@ -144,7 +138,11 @@ export const ForgotPasswordForm = () => {
</Show> </Show>
<div> <div>
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit"> <button
class={clsx('button', styles.submitButton)}
disabled={isSubmitting() || Boolean(message())}
type="submit"
>
{isSubmitting() ? '...' : t('Restore password')} {isSubmitting() ? '...' : t('Restore password')}
</button> </button>
</div> </div>

View File

@ -17,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 { VerifyEmailInput } from '@authorizerdev/authorizer-js'
type FormFields = { type FormFields = {
email: string email: string
@ -65,11 +66,12 @@ export const LoginForm = () => {
setIsLinkSent(true) setIsLinkSent(true)
setIsEmailNotConfirmed(false) setIsEmailNotConfirmed(false)
setSubmitError('') setSubmitError('')
const { changeSearchParams({ mode: 'forgot-password' }) // NOTE: temporary solition
actions: { authorizer, getToken }, /* FIXME:
} = useSession() const { actions: { authorizer } } = useSession()
const result = await authorizer().verifyEmail({ token: getToken() }) const result = await authorizer().verifyEmail({ token })
if (!result) setSubmitError('cant sign send link') // TODO: if (!result) setSubmitError('cant sign send link')
*/
} }
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
@ -110,6 +112,7 @@ export const LoginForm = () => {
showSnackbar({ body: t('Welcome!') }) showSnackbar({ body: t('Welcome!') })
} catch (error) { } catch (error) {
console.error(error)
if (error instanceof ApiError) { if (error instanceof ApiError) {
if (error.code === 'email_not_confirmed') { if (error.code === 'email_not_confirmed') {
setSubmitError(t('Please, confirm email')) setSubmitError(t('Please, confirm email'))
@ -184,16 +187,6 @@ export const LoginForm = () => {
> >
{t('Forgot password?')} {t('Forgot password?')}
</span> </span>
<span
class="link"
onClick={() =>
changeSearchParams({
mode: 'change-password',
})
}
>
{t('Change password')}
</span>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@ import { Show, createSignal } from 'solid-js'
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'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
@ -36,7 +36,7 @@ export const RegisterForm = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { emailChecks } = useEmailChecks() const { emailChecks } = useEmailChecks()
const { const {
actions: { authorizer }, actions: { signUp },
} = useSession() } = useSession()
const [submitError, setSubmitError] = createSignal('') const [submitError, setSubmitError] = createSignal('')
const [fullName, setFullName] = createSignal('') const [fullName, setFullName] = createSignal('')
@ -105,19 +105,20 @@ export const RegisterForm = () => {
} }
setIsSubmitting(true) setIsSubmitting(true)
try { try {
await authorizer().signup({ const opts = {
given_name: cleanName, given_name: cleanName,
email: cleanEmail, email: cleanEmail,
password: password(), password: password(),
confirm_password: password(), confirm_password: password(),
redirect_uri: window.location.origin, redirect_uri: window.location.origin,
}) }
await signUp(opts)
setIsSuccess(true) setIsSuccess(true)
} catch (error) { } catch (error) {
if (error instanceof ApiError && error.code === 'user_already_exists') { console.error(error)
if (error?.code === 'user_already_exists') {
return return
} }

View File

@ -11,6 +11,7 @@ export type AuthModalSource =
export type AuthModalSearchParams = { export type AuthModalSearchParams = {
mode: AuthModalMode mode: AuthModalMode
source?: AuthModalSource source?: AuthModalSource
token?: string
} }
export type ConfirmEmailSearchParams = { export type ConfirmEmailSearchParams = {

View File

@ -1,6 +0,0 @@
.center {
display: flex;
justify-content: center;
align-items: center;
height: 420px;
}

View File

@ -1,20 +0,0 @@
import './Confirmed.scss'
import { onMount } from 'solid-js'
import { useLocalize } from '../../context/localize'
export const Confirmed = (props: { token?: string }) => {
const { t } = useLocalize()
onMount(() => {
const token = props.token ?? document.cookie.split(';').at(0).replace('token=', '')
window.addEventListener('mousemove', () => window.close())
window.addEventListener('keydown', () => window.close())
window.addEventListener('click', () => window.close())
localStorage.setItem('token', token)
})
return (
<>
<div class="center">{t('You was successfully authorized')}</div>
</>
)
}

View File

@ -32,7 +32,7 @@ const MD_WIDTH_BREAKPOINT = 992
export const HeaderAuth = (props: Props) => { export const HeaderAuth = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { page } = useRouter() const { page } = useRouter()
const { author, isAuthenticated, isSessionLoaded } = useSession() const { session, author, isAuthenticated, isSessionLoaded } = useSession()
const { const {
unreadNotificationsCount, unreadNotificationsCount,
actions: { showNotificationsPanel }, actions: { showNotificationsPanel },
@ -139,12 +139,12 @@ export const HeaderAuth = (props: Props) => {
<div class={styles.button}> <div class={styles.button}>
<Icon <Icon
name="bell-white" name="bell-white"
counter={isAuthenticated() ? unreadNotificationsCount() : 1} counter={session() ? unreadNotificationsCount() || 0 : 1}
class={styles.icon} class={styles.icon}
/> />
<Icon <Icon
name="bell-white-hover" name="bell-white-hover"
counter={isAuthenticated() ? unreadNotificationsCount() : 1} counter={session() ? unreadNotificationsCount() || 0 : 1}
class={clsx(styles.icon, styles.iconHover)} class={clsx(styles.icon, styles.iconHover)}
/> />
</div> </div>

View File

@ -67,10 +67,11 @@ export const HomeView = (props: Props) => {
setIsLoadMoreButtonVisible(hasMore) setIsLoadMoreButtonVisible(hasMore)
} }
const { topic, shouts } = await apiClient.getRandomTopicShouts(RANDOM_TOPIC_SHOUTS_COUNT) const result = await apiClient.getRandomTopicShouts(RANDOM_TOPIC_SHOUTS_COUNT)
if (!result) console.warn('[apiClient.getRandomTopicShouts] failed')
batch(() => { batch(() => {
setRandomTopic(topic) if (result?.topic) setRandomTopic(result.topic)
setRandomTopicArticles(shouts) if (result?.shouts) setRandomTopicArticles(result.shouts)
}) })
}) })

View File

@ -1,7 +1,7 @@
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { Link } from '@solidjs/meta' import { Link } from '@solidjs/meta'
import { splitProps } from 'solid-js' import { createEffect, splitProps } from 'solid-js'
import { getImageUrl } from '../../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
@ -21,10 +21,9 @@ export const Image = (props: Props) => {
`${getImageUrl(local.src, { width: others.width * pixelDensity })} ${pixelDensity}x`, `${getImageUrl(local.src, { width: others.width * pixelDensity })} ${pixelDensity}x`,
) )
.join(', ') .join(', ')
return ( return (
<> <>
<Link rel="preload" as="image" imagesrcset={imageSrcSet} /> <Link rel="preload" as="image" imagesrcset={imageSrcSet} href={imageUrl} />
<img src={imageUrl} alt={local.alt} srcSet={imageSrcSet} {...others} /> <img src={imageUrl} alt={local.alt} srcSet={imageSrcSet} {...others} />
</> </>
) )

View File

@ -10,10 +10,10 @@ type ShowIfAuthenticatedProps = {
} }
export const ShowIfAuthenticated = (props: ShowIfAuthenticatedProps) => { export const ShowIfAuthenticated = (props: ShowIfAuthenticatedProps) => {
const { isAuthenticated } = useSession() const { author } = useSession()
return ( return (
<Show when={isAuthenticated()} fallback={props.fallback}> <Show when={author()} fallback={props.fallback}>
{props.children} {props.children}
</Show> </Show>
) )

View File

@ -9,8 +9,8 @@ const RECONNECT_TIMES = 2
export interface SSEMessage { export interface SSEMessage {
id: string id: string
entity: string entity: string // follower | shout | reaction
action: string action: string // create | delete | update | join | follow | seen
payload: any // Author | Shout | Reaction | Message payload: any // Author | Shout | Reaction | Message
created_at?: number // unixtime x1000 created_at?: number // unixtime x1000
seen?: boolean seen?: boolean
@ -29,9 +29,7 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
const [messageHandlers, setHandlers] = createSignal<Array<MessageHandler>>([]) const [messageHandlers, setHandlers] = createSignal<Array<MessageHandler>>([])
// const [messages, setMessages] = createSignal<Array<SSEMessage>>([]); // const [messages, setMessages] = createSignal<Array<SSEMessage>>([]);
const [connected, setConnected] = createSignal(false) const [connected, setConnected] = createSignal(false)
const { const { session } = useSession()
actions: { getToken },
} = useSession()
const addHandler = (handler: MessageHandler) => { const addHandler = (handler: MessageHandler) => {
setHandlers((hhh) => [...hhh, handler]) setHandlers((hhh) => [...hhh, handler])
@ -39,8 +37,9 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
const [retried, setRetried] = createSignal<number>(0) const [retried, setRetried] = createSignal<number>(0)
createEffect(async () => { createEffect(async () => {
const token = getToken() const token = session()?.access_token
if (token && !connected()) { if (token && !connected()) {
console.info('[context.connect] init SSE connection')
await fetchEventSource('https://connect.discours.io', { await fetchEventSource('https://connect.discours.io', {
method: 'GET', method: 'GET',
headers: { headers: {

View File

@ -8,6 +8,7 @@ import {
AuthToken, AuthToken,
Authorizer, Authorizer,
ConfigType, ConfigType,
SignupInput,
} from '@authorizerdev/authorizer-js' } from '@authorizerdev/authorizer-js'
import { import {
createContext, createContext,
@ -29,9 +30,9 @@ import { showModal } from '../stores/ui'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSnackbar } from './snackbar' import { useSnackbar } from './snackbar'
const config: ConfigType = { const defaultConfig: ConfigType = {
authorizerURL: 'https://auth.discours.io', authorizerURL: 'https://auth.discours.io',
redirectURL: 'https://discoursio-webapp.vercel.app/?modal=auth', redirectURL: 'https://discoursio-webapp.vercel.app',
clientID: '9c113377-5eea-4c89-98e1-69302462fc08', // FIXME: use env? clientID: '9c113377-5eea-4c89-98e1-69302462fc08', // FIXME: use env?
} }
@ -41,10 +42,9 @@ export type SessionContextType = {
author: Resource<Author | null> author: Resource<Author | null>
isSessionLoaded: Accessor<boolean> isSessionLoaded: Accessor<boolean>
subscriptions: Accessor<Result> subscriptions: Accessor<Result>
isAuthenticated: Accessor<boolean>
isAuthWithCallback: Accessor<() => void> isAuthWithCallback: Accessor<() => void>
isAuthenticated: Accessor<boolean>
actions: { actions: {
getToken: () => string
loadSession: () => AuthToken | Promise<AuthToken> loadSession: () => AuthToken | Promise<AuthToken>
setSession: (token: AuthToken | null) => void // setSession setSession: (token: AuthToken | null) => void // setSession
loadAuthor: (info?: unknown) => Author | Promise<Author> loadAuthor: (info?: unknown) => Author | Promise<Author>
@ -54,9 +54,11 @@ export type SessionContextType = {
callback: (() => Promise<void>) | (() => void), callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource, modalSource: AuthModalSource,
) => void ) => void
signUp: (params: SignupInput) => Promise<AuthToken | void>
signIn: (params: LoginInput) => Promise<void> signIn: (params: LoginInput) => Promise<void>
signOut: () => Promise<void> signOut: () => Promise<void>
confirmEmail: (input: VerifyEmailInput) => Promise<void> // email confirm callback is in auth.discours.io changePassword: (password: string, token: string) => void
confirmEmail: (input: VerifyEmailInput) => Promise<AuthToken | void> // email confirm callback is in auth.discours.io
setIsSessionLoaded: (loaded: boolean) => void setIsSessionLoaded: (loaded: boolean) => void
authorizer: () => Authorizer authorizer: () => Authorizer
} }
@ -81,75 +83,40 @@ export const SessionProvider = (props: {
const { const {
actions: { showSnackbar }, actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const { searchParams, changeSearchParam } = useRouter() const { searchParams, changeSearchParams } = useRouter()
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
const getSession = async (): Promise<AuthToken> => { // handle callback's redirect_uri
try { createEffect(async () => {
const tkn = getToken() // TODO: handle oauth here too
// console.debug('[context.session] token before:', tkn) const token = searchParams()?.token
const authResult = await authorizer().getSession({ const access_token = searchParams()?.access_token
Authorization: tkn, if (token) {
changeSearchParams({
mode: 'change-password',
modal: 'auth',
})
} else if (access_token) {
changeSearchParams({
mode: 'confirm-email',
modal: 'auth',
}) })
if (authResult?.access_token) {
setSession(authResult)
// console.debug('[context.session] token after:', authResult.access_token)
await loadSubscriptions()
return authResult
}
} catch (error) {
console.error('[context.session] getSession error:', error)
setSession(null)
return null
} finally {
setTimeout(() => {
setIsSessionLoaded(true)
}, 0)
}
}
const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(getSession, {
ssrLoadFrom: 'initial',
initialValue: null,
})
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(() => { // load
const token = getToken()
if (!inboxClient.private && token) {
apiClient.connect(token)
notifierClient.connect(token)
inboxClient.connect(token)
}
})
const loadSubscriptions = async (): Promise<void> => { const [configuration, setConfig] = createSignal<ConfigType>(defaultConfig)
if (apiClient.private) { const authorizer = createMemo(() => new Authorizer(defaultConfig))
const result = await apiClient.getMySubscriptions() const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
if (result) { const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(
setSubscriptions(result)
} else {
setSubscriptions(EMPTY_SUBSCRIPTIONS)
}
}
}
const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(
async () => { async () => {
const u = session()?.user try {
if (u) { console.info('[context.session] loading session')
return (await apiClient.getAuthorId({ user: u.id })) ?? null return await authorizer().getSession()
} catch (_) {
console.info('[context.session] cannot refresh session')
return null
} }
return null
}, },
{ {
ssrLoadFrom: 'initial', ssrLoadFrom: 'initial',
@ -157,29 +124,71 @@ export const SessionProvider = (props: {
}, },
) )
const isAuthenticated = createMemo(() => Boolean(session()?.user)) const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(
async () => {
const signIn = async (params: LoginInput) => { const u = session()?.user
const authResult: AuthToken | void = await authorizer().login(params) return u ? (await apiClient.getAuthorId({ user: u.id })) || null : null
},
if (authResult && authResult.access_token) { {
setSession(authResult) ssrLoadFrom: 'initial',
await loadSubscriptions() initialValue: null,
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,
}),
) )
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
const loadSubscriptions = async (): Promise<void> => {
const result = await apiClient.getMySubscriptions()
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
}
// session postload effect
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()
await loadSubscriptions()
if (a) {
console.log('[context.session] author profile and subs loaded', author())
} else {
console.warn('[context.session] author is not loaded')
}
setIsSessionLoaded(true)
}
}
} else {
console.log('[context.session] setting session null')
if (session() === null && author() !== null) {
setIsSessionLoaded(true)
setAuthor(null)
setSubscriptions(EMPTY_SUBSCRIPTIONS)
}
}
})
// initial effect
onMount(async () => {
const metaRes = await authorizer().getMetaData()
setConfig({ ...defaultConfig, ...metaRes, redirectURL: window.location.origin })
let s
try {
s = await loadSession()
} catch (e) {}
if (!s) {
setIsSessionLoaded(true)
setSession(null)
setAuthor(null)
}
})
// callback state updater
createEffect( createEffect(
on( on(
() => props.onStateChangeCallback, () => props.onStateChangeCallback,
@ -190,56 +199,57 @@ export const SessionProvider = (props: {
), ),
) )
const [configuration, setConfig] = createSignal<ConfigType>(config) // require auth wrapper
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 [isAuthWithCallback, setIsAuthWithCallback] = createSignal<() => void>()
const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => { const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => {
setIsAuthWithCallback(() => callback) setIsAuthWithCallback(() => callback)
const userdata = await authorizer().getProfile() await loadSession()
if (userdata) setSession({ ...session(), user: userdata })
if (!isAuthenticated()) { if (!session()) {
showModal('auth', modalSource) 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 () => { const signOut = async () => {
await authorizer().logout() await authorizer().logout()
setSession(null) setSession(null)
setSubscriptions(EMPTY_SUBSCRIPTIONS)
showSnackbar({ body: t("You've successfully logged out") }) 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) => { const confirmEmail = async (input: VerifyEmailInput) => {
console.debug(`[context.session] calling authorizer's verify email with`, input) console.debug(`[context.session] calling authorizer's verify email with`, input)
const at: void | AuthToken = await authorizer().verifyEmail(input) const at: void | AuthToken = await authorizer().verifyEmail(input)
if (at) setSession(at) if (at) setSession(at)
console.log(`[context.session] confirmEmail got result ${at}`) return at
} }
const getToken = createMemo(() => session()?.access_token) const isAuthenticated = createMemo(() => Boolean(author()))
const actions = { const actions = {
getToken,
loadSession, loadSession,
loadSubscriptions, loadSubscriptions,
requireAuthentication, requireAuthentication,
signUp,
signIn, signIn,
signOut, signOut,
confirmEmail, confirmEmail,
@ -248,16 +258,17 @@ export const SessionProvider = (props: {
setAuthor, setAuthor,
authorizer, authorizer,
loadAuthor, loadAuthor,
changePassword,
} }
const value: SessionContextType = { const value: SessionContextType = {
config: configuration(), config: configuration(),
session, session,
subscriptions, subscriptions,
isSessionLoaded, isSessionLoaded,
isAuthenticated,
author, author,
actions, actions,
isAuthWithCallback, isAuthWithCallback,
isAuthenticated,
} }
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider> return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>

View File

@ -53,39 +53,28 @@ export const apiClient = {
getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => { getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => {
const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise() const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise()
if (!response.data) { if (!response.data) console.error('[graphql.core] load_shouts_random_top failed', response)
console.error('[graphql.core] getRandomTopShouts error', response.error)
}
return response.data.load_shouts_random_top return response.data.load_shouts_random_top
}, },
getUnratedShouts: async (limit = 50, offset = 0) => { getUnratedShouts: async (limit = 50, offset = 0) => {
const response = await apiClient.private.query(loadShoutsUnrated, { limit, offset }).toPromise() const response = await apiClient.private.query(loadShoutsUnrated, { limit, offset }).toPromise()
if (!response.data) console.error('[graphql.core] load_shouts_unrated', response)
if (!response.data) {
console.error('[graphql.core] getUnratedShouts error', response.error)
}
return response.data.load_shouts_unrated return response.data.load_shouts_unrated
}, },
getRandomTopics: async ({ amount }: { amount: number }) => { getRandomTopics: async ({ amount }: { amount: number }) => {
const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise() const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise()
if (!response.data) console.error('[graphql.client.core] get_topics_random failed', response)
if (!response.data) {
console.error('[graphql.client.core] getRandomTopics', response.error)
}
return response.data.get_topics_random return response.data.get_topics_random
}, },
getRandomTopicShouts: async (limit: number): Promise<{ topic: Topic; shouts: Shout[] }> => { getRandomTopicShouts: async (limit: number): Promise<{ topic: Topic; shouts: Shout[] }> => {
const resp = await publicGraphQLClient.query(articlesLoadRandomTopic, { limit }).toPromise() const resp = await publicGraphQLClient.query(articlesLoadRandomTopic, { limit }).toPromise()
if (!resp.data) console.error('[graphql.client.core] load_shouts_random_topic', resp)
if (resp.error) {
console.error(resp)
}
return resp.data.load_random_topics_shouts return resp.data.load_random_topics_shouts
}, },
@ -100,16 +89,14 @@ export const apiClient = {
getAllTopics: async () => { getAllTopics: async () => {
const response = await publicGraphQLClient.query(topicsAll, {}).toPromise() const response = await publicGraphQLClient.query(topicsAll, {}).toPromise()
if (response.error) { if (!response.data) console.error('[graphql.client.core] get_topics_all', response)
console.debug('[graphql.client.core] get_topics_all', response.error)
}
return response.data.get_topics_all return response.data.get_topics_all
}, },
getAllAuthors: async (limit: number = 50, offset: number = 0) => { getAllAuthors: async (limit: number = 50, offset: number = 0) => {
const response = await publicGraphQLClient.query(authorsAll, { limit, offset }).toPromise() const response = await publicGraphQLClient.query(authorsAll, { limit, offset }).toPromise()
if (response.error) { if (!response.data) console.error('[graphql.client.core] load_authors_all', response)
console.debug('[graphql.client.core] load_authors_all', response.error)
}
return response.data.load_authors_all return response.data.load_authors_all
}, },
getAuthor: async (params: { slug?: string; author_id?: number }): Promise<Author> => { getAuthor: async (params: { slug?: string; author_id?: number }): Promise<Author> => {

View File

@ -1,8 +1,8 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
query LoadRandomTopShoutsQuery($params: LoadRandomTopShoutsParams) { query LoadRandomTopShoutsQuery($options: LoadShoutsOptions) {
load_shouts_random_top(params: $params) { load_shouts_random_top(options: $options) {
id id
title title
# lead # lead