Merge pull request #21 from Discours/auth-fix

[DO NOT MERGE] auth fix WIP
This commit is contained in:
Igor Lobanov 2022-10-03 13:35:24 +02:00 committed by GitHub
commit 0e8117937b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 202 additions and 123 deletions

View File

@ -7,8 +7,7 @@ import { createEffect, createMemo, createSignal, For, onMount, Show } from 'soli
import type { Author, Reaction, Shout } from '../../graphql/types.gen' import type { Author, Reaction, Shout } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import { useStore } from '@nanostores/solid' import { useAuthStore } from '../../stores/auth'
import { session } from '../../stores/auth'
import { incrementView } from '../../stores/zine/articles' import { incrementView } from '../../stores/zine/articles'
import { renderMarkdown } from '@astrojs/markdown-remark' import { renderMarkdown } from '@astrojs/markdown-remark'
import { markdownOptions } from '../../../mdx.config' import { markdownOptions } from '../../../mdx.config'
@ -41,7 +40,7 @@ const formatDate = (date: Date) => {
export const FullArticle = (props: ArticleProps) => { export const FullArticle = (props: ArticleProps) => {
const [body, setBody] = createSignal(props.article.body?.startsWith('<') ? props.article.body : '') const [body, setBody] = createSignal(props.article.body?.startsWith('<') ? props.article.body : '')
const auth = useStore(session) const { session } = useAuthStore()
createEffect(() => { createEffect(() => {
if (body() || !props.article.body) { if (body() || !props.article.body) {
@ -180,12 +179,12 @@ export const FullArticle = (props: ArticleProps) => {
<ArticleComment <ArticleComment
comment={reaction} comment={reaction}
level={getCommentLevel(reaction)} level={getCommentLevel(reaction)}
canEdit={reaction.createdBy?.slug === auth()?.user?.slug} canEdit={reaction.createdBy?.slug === session()?.user?.slug}
/> />
)} )}
</For> </For>
</Show> </Show>
<Show when={!auth()?.user?.slug}> <Show when={!session()?.user?.slug}>
<div class="comment-warning" id="comments"> <div class="comment-warning" id="comments">
{t('To leave a comment you please')} {t('To leave a comment you please')}
<a <a
@ -199,7 +198,7 @@ export const FullArticle = (props: ArticleProps) => {
</a> </a>
</div> </div>
</Show> </Show>
<Show when={auth()?.user?.slug}> <Show when={session()?.user?.slug}>
<textarea class="write-comment" rows="1" placeholder={t('Write comment')} /> <textarea class="write-comment" rows="1" placeholder={t('Write comment')} />
</Show> </Show>
</div> </div>

View File

@ -6,10 +6,9 @@ import './Card.scss'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { session } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { locale } from '../../stores/ui' import { locale } from '../../stores/ui'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { useStore } from '@nanostores/solid'
interface AuthorCardProps { interface AuthorCardProps {
compact?: boolean compact?: boolean
@ -21,14 +20,15 @@ interface AuthorCardProps {
} }
export const AuthorCard = (props: AuthorCardProps) => { export const AuthorCard = (props: AuthorCardProps) => {
const auth = useStore(session) const { session } = useAuthStore()
const subscribed = createMemo( const subscribed = createMemo(
() => () =>
!!auth() !!session()
?.info?.authors?.filter((u) => u === props.author.slug) ?.info?.authors?.filter((u) => u === props.author.slug)
.pop() .pop()
) )
const canFollow = createMemo(() => !props.hideFollow && auth()?.user?.slug !== props.author.slug) const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
const bio = () => props.author.bio || t('Our regular contributor') const bio = () => props.author.bio || t('Our regular contributor')
const name = () => { const name = () => {
return props.author.name === 'Дискурс' && locale() !== 'ru' return props.author.name === 'Дискурс' && locale() !== 'ru'

View File

@ -1,7 +1,6 @@
import { useStore } from '@nanostores/solid'
import { For } from 'solid-js' import { For } from 'solid-js'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { session } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { Icon } from '../Nav/Icon' import { Icon } from '../Nav/Icon'
@ -15,7 +14,7 @@ type FeedSidebarProps = {
export const FeedSidebar = (props: FeedSidebarProps) => { export const FeedSidebar = (props: FeedSidebarProps) => {
const { getSeen: seen } = useSeenStore() const { getSeen: seen } = useSeenStore()
const auth = useStore(session) const { session } = useAuthStore()
const { authorEntities } = useAuthorsStore({ authors: props.authors }) const { authorEntities } = useAuthorsStore({ authors: props.authors })
const { articlesByTopic } = useArticlesStore() const { articlesByTopic } = useArticlesStore()
const { topicEntities } = useTopicsStore() const { topicEntities } = useTopicsStore()
@ -64,7 +63,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
</a> </a>
</li> </li>
<For each={auth()?.info?.authors}> <For each={session()?.info?.authors}>
{(authorSlug) => ( {(authorSlug) => (
<li> <li>
<a href={`/author/${authorSlug}`} classList={{ unread: checkAuthorIsSeen(authorSlug) }}> <a href={`/author/${authorSlug}`} classList={{ unread: checkAuthorIsSeen(authorSlug) }}>
@ -75,7 +74,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
)} )}
</For> </For>
<For each={auth()?.info?.topics}> <For each={session()?.info?.topics}>
{(topicSlug) => ( {(topicSlug) => (
<li> <li>
<a href={`/author/${topicSlug}`} classList={{ unread: checkTopicIsSeen(topicSlug) }}> <a href={`/author/${topicSlug}`} classList={{ unread: checkTopicIsSeen(topicSlug) }}>

View File

@ -5,11 +5,10 @@ import './AuthModal.scss'
import { Form } from 'solid-js-form' import { Form } from 'solid-js-form'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { hideModal, useModalStore } from '../../stores/ui' import { hideModal, useModalStore } from '../../stores/ui'
import { useStore } from '@nanostores/solid' import { useAuthStore, signIn, register } from '../../stores/auth'
import { session as sessionstore, signIn } from '../../stores/auth'
import { apiClient } from '../../utils/apiClient'
import { useValidator } from '../../utils/validators' import { useValidator } from '../../utils/validators'
import { baseUrl } from '../../graphql/publicGraphQLClient' import { baseUrl } from '../../graphql/publicGraphQLClient'
import { ApiError } from '../../utils/apiClient'
type AuthMode = 'sign-in' | 'sign-up' | 'forget' | 'reset' | 'resend' | 'password' type AuthMode = 'sign-in' | 'sign-up' | 'forget' | 'reset' | 'resend' | 'password'
@ -23,7 +22,7 @@ const statuses: { [key: string]: string } = {
const titles = { const titles = {
'sign-up': t('Create account'), 'sign-up': t('Create account'),
'sign-in': t('Enter the Discours'), 'sign-in': t('Enter the Discours'),
forget: t('Forget password?'), forget: t('Forgot password?'),
reset: t('Please, confirm your email to finish'), reset: t('Please, confirm your email to finish'),
resend: t('Resend code'), resend: t('Resend code'),
password: t('Enter your new password') password: t('Enter your new password')
@ -34,10 +33,10 @@ const titles = {
// FIXME !!! // FIXME !!!
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
export default (props: { code?: string; mode?: string }) => { export default (props: { code?: string; mode?: string }) => {
const session = useStore(sessionstore) const { session } = useAuthStore()
const [handshaking] = createSignal(false) const [handshaking] = createSignal(false)
const { getModal } = useModalStore() const { getModal } = useModalStore()
const [authError, setError] = createSignal('') const [authError, setError] = createSignal<string>('')
const [mode, setMode] = createSignal<AuthMode>('sign-in') const [mode, setMode] = createSignal<AuthMode>('sign-in')
const [validation, setValidation] = createSignal({}) const [validation, setValidation] = createSignal({})
const [initial, setInitial] = createSignal({}) const [initial, setInitial] = createSignal({})
@ -46,7 +45,7 @@ export default (props: { code?: string; mode?: string }) => {
let passElement: HTMLInputElement | undefined let passElement: HTMLInputElement | undefined
let codeElement: HTMLInputElement | undefined let codeElement: HTMLInputElement | undefined
// 3rd party providier auth handler // 3rd party provider auth handler
const oauth = (provider: string): void => { const oauth = (provider: string): void => {
const popup = window.open(`${baseUrl}/oauth/${provider}`, provider, 'width=740, height=420') const popup = window.open(`${baseUrl}/oauth/${provider}`, provider, 'width=740, height=420')
popup?.focus() popup?.focus()
@ -85,28 +84,50 @@ export default (props: { code?: string; mode?: string }) => {
setValidation(vs) setValidation(vs)
setInitial(ini) setInitial(ini)
} }
onMount(setupValidators) onMount(setupValidators)
const resetError = () => {
setError('')
}
const changeMode = (newMode: AuthMode) => {
setMode(newMode)
resetError()
}
// local auth handler // local auth handler
const localAuth = async () => { const localAuth = async () => {
console.log('[auth] native account processing') console.log('[auth] native account processing')
switch (mode()) { switch (mode()) {
case 'sign-in': case 'sign-in':
signIn({ email: emailElement?.value, password: passElement?.value }) try {
await signIn({ email: emailElement?.value, password: passElement?.value })
} catch (error) {
if (error instanceof ApiError) {
if (error.code === 'email_not_confirmed') {
setError(t('Please, confirm email'))
return
}
if (error.code === 'user_not_found') {
setError(t('Something went wrong, check email and password'))
return
}
}
setError(error.message)
}
break break
case 'sign-up': case 'sign-up':
if (pass2Element?.value !== passElement?.value) { if (pass2Element?.value !== passElement?.value) {
setError(t('Passwords are not equal')) setError(t('Passwords are not equal'))
} else { } else {
// FIXME use store actions await register({
const r = await apiClient.authRegiser({
email: emailElement?.value, email: emailElement?.value,
password: passElement?.value password: passElement?.value
}) })
if (r) {
console.debug('[auth] session update', r)
sessionstore.set(r)
}
} }
break break
case 'reset': case 'reset':
@ -130,6 +151,7 @@ export default (props: { code?: string; mode?: string }) => {
} }
} }
// FIXME move to handlers
createEffect(() => { createEffect(() => {
if (session()?.user?.slug && getModal() === 'auth') { if (session()?.user?.slug && getModal() === 'auth') {
// hiding itself if finished // hiding itself if finished
@ -141,7 +163,8 @@ export default (props: { code?: string; mode?: string }) => {
} else { } else {
console.log('[auth] session', session()) console.log('[auth] session', session())
} }
}, [session()]) })
return ( return (
<div class="row view" classList={{ 'view--sign-up': mode() === 'sign-up' }}> <div class="row view" classList={{ 'view--sign-up': mode() === 'sign-up' }}>
<div class="col-sm-6 d-md-none auth-image"> <div class="col-sm-6 d-md-none auth-image">
@ -174,7 +197,6 @@ export default (props: { code?: string; mode?: string }) => {
> >
<div class="auth__inner"> <div class="auth__inner">
<h4>{titles[mode()]}</h4> <h4>{titles[mode()]}</h4>
<div class={`auth-subtitle ${mode() === 'forget' ? '' : 'hidden'}`}> <div class={`auth-subtitle ${mode() === 'forget' ? '' : 'hidden'}`}>
<Show <Show
when={mode() === 'forget'} when={mode() === 'forget'}
@ -187,7 +209,6 @@ export default (props: { code?: string; mode?: string }) => {
{t('Everything is ok, please give us your email address')} {t('Everything is ok, please give us your email address')}
</Show> </Show>
</div> </div>
<Show when={authError()}> <Show when={authError()}>
<div class={`auth-info`}> <div class={`auth-info`}>
<ul> <ul>
@ -195,7 +216,6 @@ export default (props: { code?: string; mode?: string }) => {
</ul> </ul>
</div> </div>
</Show> </Show>
{/*FIXME*/} {/*FIXME*/}
{/*<Show when={false && mode() === 'sign-up'}>*/} {/*<Show when={false && mode() === 'sign-up'}>*/}
{/* <div class='pretty-form__item'>*/} {/* <div class='pretty-form__item'>*/}
@ -223,7 +243,6 @@ export default (props: { code?: string; mode?: string }) => {
<label for="email">{t('Email')}</label> <label for="email">{t('Email')}</label>
</div> </div>
</Show> </Show>
<Show when={mode() === 'sign-up' || mode() === 'sign-in' || mode() === 'password'}> <Show when={mode() === 'sign-up' || mode() === 'sign-in' || mode() === 'password'}>
<div class="pretty-form__item"> <div class="pretty-form__item">
<input <input
@ -250,7 +269,6 @@ export default (props: { code?: string; mode?: string }) => {
<label for="resetcode">{t('Reset code')}</label> <label for="resetcode">{t('Reset code')}</label>
</div> </div>
</Show> </Show>
<Show when={mode() === 'password' || mode() === 'sign-up'}> <Show when={mode() === 'password' || mode() === 'sign-up'}>
<div class="pretty-form__item"> <div class="pretty-form__item">
<input <input
@ -269,15 +287,19 @@ export default (props: { code?: string; mode?: string }) => {
{handshaking() ? '...' : titles[mode()]} {handshaking() ? '...' : titles[mode()]}
</button> </button>
</div> </div>
<Show when={mode() === 'sign-in'}> <Show when={mode() === 'sign-in'}>
<div class="auth-actions"> <div class="auth-actions">
<a href={''} onClick={() => setMode('forget')}> <a
{t('Forget password?')} href="#"
onClick={(ev) => {
ev.preventDefault()
changeMode('forget')
}}
>
{t('Forgot password?')}
</a> </a>
</div> </div>
</Show> </Show>
<Show when={mode() === 'sign-in' || mode() === 'sign-up'}> <Show when={mode() === 'sign-in' || mode() === 'sign-up'}>
<div class="social-provider"> <div class="social-provider">
<div class="providers-text">{t('Or continue with social network')}</div> <div class="providers-text">{t('Or continue with social network')}</div>
@ -297,25 +319,24 @@ export default (props: { code?: string; mode?: string }) => {
</div> </div>
</div> </div>
</Show> </Show>
<div class="auth-control"> <div class="auth-control">
<div classList={{ show: mode() === 'sign-up' }}> <div classList={{ show: mode() === 'sign-up' }}>
<span class="auth-link" onClick={() => setMode('sign-in')}> <span class="auth-link" onClick={() => changeMode('sign-in')}>
{t('I have an account')} {t('I have an account')}
</span> </span>
</div> </div>
<div classList={{ show: mode() === 'sign-in' }}> <div classList={{ show: mode() === 'sign-in' }}>
<span class="auth-link" onClick={() => setMode('sign-up')}> <span class="auth-link" onClick={() => changeMode('sign-up')}>
{t('I have no account yet')} {t('I have no account yet')}
</span> </span>
</div> </div>
<div classList={{ show: mode() === 'forget' }}> <div classList={{ show: mode() === 'forget' }}>
<span class="auth-link" onClick={() => setMode('sign-in')}> <span class="auth-link" onClick={() => changeMode('sign-in')}>
{t('I know the password')} {t('I know the password')}
</span> </span>
</div> </div>
<div classList={{ show: mode() === 'reset' }}> <div classList={{ show: mode() === 'reset' }}>
<span class="auth-link" onClick={() => setMode('resend')}> <span class="auth-link" onClick={() => changeMode('resend')}>
{t('Resend code')} {t('Resend code')}
</span> </span>
</div> </div>

View File

@ -6,8 +6,7 @@ import { Modal } from './Modal'
import AuthModal from './AuthModal' import AuthModal from './AuthModal'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui' import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
import { useStore } from '@nanostores/solid' import { useAuthStore } from '../../stores/auth'
import { session as ssession } from '../../stores/auth'
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router' import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
import './Header.scss' import './Header.scss'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
@ -38,7 +37,7 @@ export const Header = (props: Props) => {
const [visibleWarnings, setVisibleWarnings] = createSignal(false) const [visibleWarnings, setVisibleWarnings] = createSignal(false)
// stores // stores
const { getWarnings } = useWarningsStore() const { getWarnings } = useWarningsStore()
const session = useStore(ssession) const { session } = useAuthStore()
const { getModal } = useModalStore() const { getModal } = useModalStore()
const { getPage } = useRouter() const { getPage } = useRouter()

View File

@ -2,12 +2,11 @@ import type { Author } from '../../graphql/types.gen'
import Userpic from '../Author/Userpic' import Userpic from '../Author/Userpic'
import { Icon } from './Icon' import { Icon } from './Icon'
import './Private.scss' import './Private.scss'
import { session as sesstore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
export default () => { export default () => {
const session = useStore(sesstore) const { session } = useAuthStore()
const { getPage } = useRouter() const { getPage } = useRouter()
return ( return (

View File

@ -3,8 +3,7 @@ import { AuthorCard } from '../Author/Card'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { hideModal } from '../../stores/ui' import { hideModal } from '../../stores/ui'
import { session, signOut } from '../../stores/auth' import { useAuthStore, signOut } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
const quit = () => { const quit = () => {
@ -13,29 +12,32 @@ const quit = () => {
} }
export default () => { export default () => {
const auth = useStore(session) const { session } = useAuthStore()
const author = createMemo(() => {
const a = { const author = createMemo<Author>(() => {
const a: Author = {
name: 'anonymous', name: 'anonymous',
userpic: '', userpic: '',
slug: '' slug: ''
} as Author }
if (auth()?.user?.slug) {
const u = auth().user if (session()?.user?.slug) {
const u = session().user
a.name = u.name a.name = u.name
a.slug = u.slug a.slug = u.slug
a.userpic = u.userpic a.userpic = u.userpic
} }
return a return a
}) })
// TODO: ProfileModal markup and styles // TODO: ProfileModal markup and styles
return ( return (
<div class="row view profile"> <div class="row view profile">
<h1>{auth()?.user?.username}</h1> <h1>{session()?.user?.username}</h1>
<AuthorCard author={author()} /> <AuthorCard author={author()} />
<div class="profile-bio">{auth()?.user?.bio || ''}</div> <div class="profile-bio">{session()?.user?.bio || ''}</div>
<For each={auth()?.user?.links || []}>{(l: string) => <a href={l}>{l}</a>}</For> <For each={session()?.user?.links || []}>{(l: string) => <a href={l}>{l}</a>}</For>
<span onClick={quit}>{t('Quit')}</span> <span onClick={quit}>{t('Quit')}</span>
</div> </div>
) )

View File

@ -6,9 +6,11 @@ import type { Topic } from '../../graphql/types.gen'
import { FollowingEntity } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { locale } from '../../stores/ui' import { locale } from '../../stores/ui'
import { useStore } from '@nanostores/solid' import { useAuthStore } from '../../stores/auth'
import { session } from '../../stores/auth'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { getLogger } from '../../utils/logger'
const log = getLogger('TopicCard')
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
@ -19,63 +21,73 @@ interface TopicProps {
} }
export const TopicCard = (props: TopicProps) => { export const TopicCard = (props: TopicProps) => {
const auth = useStore(session) const { session } = useAuthStore()
const topic = createMemo(() => props.topic)
const subscribed = createMemo(() => { const subscribed = createMemo(() => {
return Boolean(auth()?.user?.slug) && topic().slug ? topic().slug in auth().info.topics : false if (!session()?.user?.slug || !session()?.info?.topics) {
return false
}
return props.topic.slug in session().info.topics
}) })
// FIXME use store actions // FIXME use store actions
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
if (really) { if (really) {
follow({ what: FollowingEntity.Topic, slug: topic().slug }) follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
} else { } else {
unfollow({ what: FollowingEntity.Topic, slug: topic().slug }) unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })
} }
} }
return ( return (
<div class="topic" classList={{ row: !props.compact && !props.subscribeButtonBottom }}> <div class="topic" classList={{ row: !props.compact && !props.subscribeButtonBottom }}>
<div classList={{ 'col-md-7': !props.compact && !props.subscribeButtonBottom }}> <div classList={{ 'col-md-7': !props.compact && !props.subscribeButtonBottom }}>
<Show when={topic().title}> <Show when={props.topic.title}>
<div class="topic-title"> <div class="topic-title">
<a href={`/topic/${topic().slug}`}>{capitalize(topic().title || '')}</a> <a href={`/topic/${props.topic.slug}`}>{capitalize(props.topic.title || '')}</a>
</div> </div>
</Show> </Show>
<Show when={topic().pic}> <Show when={props.topic.pic}>
<div class="topic__avatar"> <div class="topic__avatar">
<a href={topic().slug}> <a href={props.topic.slug}>
<img src={topic().pic} alt={topic().title} /> <img src={props.topic.pic} alt={props.topic.title} />
</a> </a>
</div> </div>
</Show> </Show>
<Show when={!props.compact && topic()?.body}> <Show when={!props.compact && props.topic?.body}>
<div class="topic-description" classList={{ 'topic-description--short': props.shortDescription }}> <div class="topic-description" classList={{ 'topic-description--short': props.shortDescription }}>
{topic().body} {props.topic.body}
</div> </div>
</Show> </Show>
<Show when={topic()?.stat}> <Show when={props.topic?.stat}>
<div class="topic-details"> <div class="topic-details">
<Show when={!props.compact}> <Show when={!props.compact}>
<span class="topic-details__item" classList={{ compact: props.compact }}> <span class="topic-details__item" classList={{ compact: props.compact }}>
{topic().stat?.shouts + {props.topic.stat?.shouts +
' ' + ' ' +
t('post') + t('post') +
plural(topic().stat?.shouts || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])} plural(
props.topic.stat?.shouts || 0,
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)}
</span> </span>
<span class="topic-details__item" classList={{ compact: props.compact }}> <span class="topic-details__item" classList={{ compact: props.compact }}>
{topic().stat?.authors + {props.topic.stat?.authors +
' ' + ' ' +
t('author') + t('author') +
plural(topic().stat?.authors || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])} plural(
props.topic.stat?.authors || 0,
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)}
</span> </span>
<span class="topic-details__item" classList={{ compact: props.compact }}> <span class="topic-details__item" classList={{ compact: props.compact }}>
{topic().stat?.followers + {props.topic.stat?.followers +
' ' + ' ' +
t('follower') + t('follower') +
plural( plural(
topic().stat?.followers || 0, props.topic.stat?.followers || 0,
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'] locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)} )}
</span> </span>

View File

@ -3,8 +3,7 @@ import { Show } from 'solid-js/web'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { FollowingEntity } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen'
import './Full.scss' import './Full.scss'
import { session } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
@ -13,8 +12,9 @@ type Props = {
} }
export const FullTopic = (props: Props) => { export const FullTopic = (props: Props) => {
const auth = useStore(session) const { session } = useAuthStore()
const subscribed = createMemo(() => auth()?.info?.topics?.includes(props.topic?.slug))
const subscribed = createMemo(() => session()?.info?.topics?.includes(props.topic?.slug))
return ( return (
<div class="topic-full container"> <div class="topic-full container">
<div class="row"> <div class="row">

View File

@ -7,8 +7,7 @@ import { Icon } from '../Nav/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
import { session } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import '../../styles/AllTopics.scss' import '../../styles/AllTopics.scss'
type AllAuthorsPageSearchParams = { type AllAuthorsPageSearchParams = {
@ -24,8 +23,10 @@ export const AllAuthorsView = (props: Props) => {
const [sortedAuthors, setSortedAuthors] = createSignal<Author[]>([]) const [sortedAuthors, setSortedAuthors] = createSignal<Author[]>([])
const [sortedKeys, setSortedKeys] = createSignal<string[]>([]) const [sortedKeys, setSortedKeys] = createSignal<string[]>([])
const [abc, setAbc] = createSignal([]) const [abc, setAbc] = createSignal([])
const auth = useStore(session)
const subscribed = (s) => Boolean(auth()?.info?.authors && auth()?.info?.authors?.includes(s || '')) const { session } = useAuthStore()
const subscribed = (s) => Boolean(session()?.info?.authors && session()?.info?.authors?.includes(s || ''))
const { getSearchParams } = useRouter<AllAuthorsPageSearchParams>() const { getSearchParams } = useRouter<AllAuthorsPageSearchParams>()

View File

@ -5,8 +5,7 @@ import { t } from '../../utils/intl'
import { setSortAllBy as setSortAllTopicsBy, useTopicsStore } from '../../stores/zine/topics' import { setSortAllBy as setSortAllTopicsBy, useTopicsStore } from '../../stores/zine/topics'
import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import { session } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import '../../styles/AllTopics.scss' import '../../styles/AllTopics.scss'
import { getLogger } from '../../utils/logger' import { getLogger } from '../../utils/logger'
@ -28,13 +27,13 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
sortBy: getSearchParams().by || 'shouts' sortBy: getSearchParams().by || 'shouts'
}) })
const auth = useStore(session) const { session } = useAuthStore()
createEffect(() => { createEffect(() => {
setSortAllTopicsBy(getSearchParams().by || 'shouts') setSortAllTopicsBy(getSearchParams().by || 'shouts')
}) })
const subscribed = (s) => Boolean(auth()?.info?.topics && auth()?.info?.topics?.includes(s || '')) const subscribed = (s) => Boolean(session()?.info?.topics && session()?.info?.topics?.includes(s || ''))
return ( return (
<div class="all-topics-page"> <div class="all-topics-page">

View File

@ -7,9 +7,8 @@ import { TopicCard } from '../Topic/Card'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/Card'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useStore } from '@nanostores/solid'
import { FeedSidebar } from '../Feed/Sidebar' import { FeedSidebar } from '../Feed/Sidebar'
import { session } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import CommentCard from '../Article/Comment' import CommentCard from '../Article/Comment'
import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles' import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
import { useReactionsStore } from '../../stores/zine/reactions' import { useReactionsStore } from '../../stores/zine/reactions'
@ -36,8 +35,7 @@ export const FeedView = (props: FeedProps) => {
const { sortedAuthors } = useAuthorsStore() const { sortedAuthors } = useAuthorsStore()
const { topTopics } = useTopicsStore() const { topTopics } = useTopicsStore()
const { topAuthors } = useTopAuthorsStore() const { topAuthors } = useTopAuthorsStore()
const { session } = useAuthStore()
const auth = useStore(session)
const topReactions = createMemo(() => sortBy(reactions(), byCreated)) const topReactions = createMemo(() => sortBy(reactions(), byCreated))
@ -71,7 +69,7 @@ export const FeedView = (props: FeedProps) => {
<div class="col-md-6"> <div class="col-md-6">
<ul class="feed-filter"> <ul class="feed-filter">
<Show when={!!auth()?.user?.slug}> <Show when={!!session()?.user?.slug}>
<li class="selected"> <li class="selected">
<a href="/feed/my">{t('My feed')}</a> <a href="/feed/my">{t('My feed')}</a>
</li> </li>

View File

@ -39,7 +39,7 @@
"Feedback": "Обратная связь", "Feedback": "Обратная связь",
"Follow": "Подписаться", "Follow": "Подписаться",
"Follow the topic": "Подписаться на тему", "Follow the topic": "Подписаться на тему",
"Forget password?": "Забыли пароль?", "Forgot password?": "Забыли пароль?",
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале", "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале",
"Help to edit": "Помочь редактировать", "Help to edit": "Помочь редактировать",
"Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики", "Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики",
@ -141,5 +141,7 @@
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"view": "просмотр", "view": "просмотр",
"zine": "журнал" "zine": "журнал",
"Please, confirm email": "Пожалуйста, подтвердите электронную почту",
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль"
} }

View File

@ -3,27 +3,28 @@ import type { AuthResult } from '../graphql/types.gen'
import { getLogger } from '../utils/logger' import { getLogger } from '../utils/logger'
import { resetToken, setToken } from '../graphql/privateGraphQLClient' import { resetToken, setToken } from '../graphql/privateGraphQLClient'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
import { createSignal } from 'solid-js'
const log = getLogger('auth-store') const log = getLogger('auth-store')
export const session = atom<AuthResult>() const [session, setSession] = createSignal<AuthResult | null>(null)
export const signIn = async (params) => { export const signIn = async (params) => {
const s = await apiClient.authLogin(params) const authResult = await apiClient.authLogin(params)
session.set(s) setSession(authResult)
setToken(s.token) setToken(authResult.token)
log.debug('signed in') log.debug('signed in')
} }
export const signUp = async (params) => { export const signUp = async (params) => {
const s = await apiClient.authRegiser(params) const authResult = await apiClient.authRegister(params)
session.set(s) setSession(authResult)
setToken(s.token) setToken(authResult.token)
log.debug('signed up') log.debug('signed up')
} }
export const signOut = () => { export const signOut = () => {
session.set(null) setSession(null)
resetToken() resetToken()
log.debug('signed out') log.debug('signed out')
} }
@ -36,6 +37,18 @@ export const signCheck = async (params) => {
export const resetCode = atom<string>() export const resetCode = atom<string>()
export const register = async ({ email, password }: { email: string; password: string }) => {
const authResult = await apiClient.authRegister({
email,
password
})
if (authResult && !authResult.error) {
log.debug('register session update', authResult)
setSession(authResult)
}
}
export const signSendLink = async (params) => { export const signSendLink = async (params) => {
await apiClient.authSendLink(params) // { email } await apiClient.authSendLink(params) // { email }
resetToken() resetToken()
@ -44,11 +57,15 @@ export const signSendLink = async (params) => {
export const signConfirm = async (params) => { export const signConfirm = async (params) => {
const auth = await apiClient.authConfirmCode(params) // { code } const auth = await apiClient.authConfirmCode(params) // { code }
setToken(auth.token) setToken(auth.token)
session.set(auth) setSession(auth)
} }
export const renewSession = async () => { export const renewSession = async () => {
const s = await apiClient.getSession() // token in header const authResult = await apiClient.getSession() // token in header
setToken(s.token) setToken(authResult.token)
session.set(s) setSession(authResult)
}
export const useAuthStore = () => {
return { session }
} }

View File

@ -5,7 +5,7 @@ import { addTopicsByAuthor } from './topics'
import { byStat } from '../../utils/sortby' import { byStat } from '../../utils/sortby'
import { getLogger } from '../../utils/logger' import { getLogger } from '../../utils/logger'
import { createMemo, createSignal } from 'solid-js' import { createSignal } from 'solid-js'
import { createLazyMemo } from '@solid-primitives/memo' import { createLazyMemo } from '@solid-primitives/memo'
const log = getLogger('articles store') const log = getLogger('articles store')

View File

@ -1,4 +1,4 @@
import type { Reaction, Shout, FollowingEntity } from '../graphql/types.gen' import type { Reaction, Shout, FollowingEntity, AuthResult } from '../graphql/types.gen'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import articleBySlug from '../graphql/query/article-by-slug' import articleBySlug from '../graphql/query/article-by-slug'
@ -36,15 +36,43 @@ const log = getLogger('api-client')
const FEED_SIZE = 50 const FEED_SIZE = 50
const REACTIONS_PAGE_SIZE = 100 const REACTIONS_PAGE_SIZE = 100
export const apiClient = { type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found'
// auth
authLogin: async ({ email, password }) => { export class ApiError extends Error {
code: ApiErrorCode
constructor(code: ApiErrorCode, message?: string) {
super(message)
this.code = code
}
}
export const apiClient = {
authLogin: async ({ email, password }): Promise<AuthResult> => {
const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise() const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise()
// log.debug('authLogin', { response })
if (response.error) {
if (response.error.message === '[GraphQL] User not found') {
throw new ApiError('user_not_found')
}
throw new ApiError('unknown', response.error.message)
}
if (response.data.signIn.error) {
if (response.data.signIn.error === 'please, confirm email') {
throw new ApiError('email_not_confirmed')
}
throw new ApiError('unknown', response.data.signIn.error)
}
return response.data.signIn return response.data.signIn
}, },
authRegiser: async ({ email, password }) => { authRegister: async ({ email, password }): Promise<AuthResult> => {
const response = await publicGraphQLClient.query(authRegisterMutation, { email, password }).toPromise() const response = await publicGraphQLClient
.mutation(authRegisterMutation, { email, password })
.toPromise()
return response.data.registerUser return response.data.registerUser
}, },
authSignOut: async () => { authSignOut: async () => {
@ -87,9 +115,12 @@ export const apiClient = {
return response.data.recentPublished return response.data.recentPublished
}, },
getRandomTopics: async ({ amount }: { amount: number }) => { getRandomTopics: async ({ amount }: { amount: number }) => {
log.debug('getRandomTopics')
const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise() const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise()
if (!response.data) {
log.error('getRandomTopics', response.error)
}
return response.data.topicsRandom return response.data.topicsRandom
}, },
getSearchResults: async ({ getSearchResults: async ({
@ -177,7 +208,7 @@ export const apiClient = {
return response.data.unfollow return response.data.unfollow
}, },
getSession: async () => { getSession: async (): Promise<AuthResult> => {
// renew session with auth token in header (!) // renew session with auth token in header (!)
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
return response.data.refreshSession return response.data.refreshSession