Compare commits

..

23 Commits

Author SHA1 Message Date
Pilou
dee93bb873 Merge pull request #526 from nhost/changeset-release/main
chore: update versions
2022-05-06 22:29:55 +02:00
github-actions[bot]
173b587802 chore: update versions 2022-05-06 20:24:16 +00:00
Pilou
30ef1660b4 Merge pull request #525 from nhost/fix/cookie-mode
fix: correct cookie storage type
2022-05-06 22:23:12 +02:00
Pierre-Louis Mercereau
a613aa9f0c refactor: unnest if block 2022-05-06 22:12:50 +02:00
Pierre-Louis Mercereau
3c03b9b46f refactor: remove dead code 2022-05-06 22:09:51 +02:00
Pierre-Louis Mercereau
65a3061146 fix: correct cookie storage type 2022-05-06 22:01:38 +02:00
Pilou
55864eac30 Merge pull request #522 from nhost/event-triggers-syntax-error
fixed syntax error in Event Triggers docs
2022-05-06 19:59:46 +02:00
Szilárd Dóró
28494d6c1f fixed syntax error in Event Triggers docs 2022-05-06 19:09:51 +02:00
Pilou
6777738c53 Merge pull request #519 from nhost/changeset-release/main
chore: update versions
2022-05-06 15:24:04 +02:00
github-actions[bot]
0d60693c27 chore: update versions 2022-05-06 11:43:21 +00:00
Pilou
c159c9c98c Merge pull request #518 from nhost/fix/refresh-token-load
fix: corrections and reshape the react-apollo example
2022-05-06 13:42:05 +02:00
Pierre-Louis Mercereau
58fa2a201c fix: corrections and reshape the react-apollo example 2022-05-06 12:55:17 +02:00
Johan Eliasson
db4607ccac Merge pull request #516 from nhost/docs-guides
Docs intro of Nhost
2022-05-06 11:50:05 +02:00
Johan Eliasson
95b14557a0 intro 2022-05-06 11:47:29 +02:00
Johan Eliasson
8b527d0fcb Merge pull request #445 from nhost/docs-guides
docs: intro, architecture and quickstarts
2022-05-06 11:43:41 +02:00
Pilou
fc50beec5e Merge pull request #513 from nhost/docs/clean-nextjs-intro
remove reference to useless component
2022-05-06 10:42:24 +02:00
Pierre-Louis Mercereau
ed0de2d930 remove reference to useless component 2022-05-05 21:53:37 +02:00
Johan Eliasson
2192fdc92e change cta 2022-05-04 09:04:53 +02:00
Johan Eliasson
eec2601a3a architecture 2022-05-04 08:55:17 +02:00
Johan Eliasson
93eaa85b47 Merge branch 'main' into docs-guides 2022-05-04 07:40:53 +02:00
Johan Eliasson
5a212aaa12 link fix 2022-04-22 22:34:45 +02:00
Johan Eliasson
79056d8b48 update 2022-04-22 22:30:02 +02:00
Johan Eliasson
f86883df88 new menu strucutre 2022-04-22 22:20:42 +02:00
72 changed files with 1065 additions and 1127 deletions

View File

@@ -1,5 +0,0 @@
{
"label": "The Nhost Platform",
"position": 1,
"link": { "type": "generated-index", "slug": "/platform" }
}

View File

@@ -1,5 +1,4 @@
{ {
"label": "Authentication", "label": "Authentication",
"position": 4, "position": 6
"link": { "id": "platform/authentication/index", "type": "doc" }
} }

View File

@@ -1,6 +1,6 @@
--- ---
title: 'Nhost CLI' title: 'CLI'
sidebar_position: 3 sidebar_position: 11
--- ---
import Tabs from '@theme/Tabs'; import Tabs from '@theme/Tabs';

View File

@@ -1,4 +1,4 @@
{ {
"label": "Database", "label": "Database",
"position": 2 "position": 4
} }

View File

@@ -1,6 +1,6 @@
--- ---
title: 'Environment variables' title: 'Environment variables'
sidebar_position: 1 sidebar_position: 9
--- ---
Environment variables are key-value pairs configured outside your source code. They are used to store environment-specific values such as API keys. Environment variables are key-value pairs configured outside your source code. They are used to store environment-specific values such as API keys.

View File

@@ -1,6 +1,6 @@
--- ---
title: 'GitHub integration' title: 'GitHub integration'
sidebar_position: 2 sidebar_position: 10
--- ---
You can connect your Nhost app to a GitHub repository. When you do this, any updates you push to your code will automatically be deployed. You can connect your Nhost app to a GitHub repository. When you do this, any updates you push to your code will automatically be deployed.
@@ -21,7 +21,7 @@ Specifically, the following will be deployed:
## Workflow ## Workflow
Create a new Nhost app. Then use [Nhost CLI](/platform/nhost/local-development) to initialize your Nhost app locally. Create a new Nhost app. Then use [Nhost CLI](/platform/cli) to initialize your Nhost app locally.
The workflow is as follows: The workflow is as follows:

View File

@@ -1,4 +1,4 @@
{ {
"label": "GraphQL", "label": "GraphQL",
"position": 3 "position": 5
} }

View File

@@ -1,11 +0,0 @@
---
title: 'The Nhost Platform'
sidebar_position: 1
---
- [Database](/platform/database)
- [GraphQL](/platform/graphql)
- [Authentication](/platform/authentication)
- [Storage](/platform/storage)
- [Serverless Functions](/platform/serverless-functions)
- [Nhost](/platform/nhost)

View File

@@ -0,0 +1,28 @@
---
title: 'Introduction to Nhost'
sidebar_label: Introduction
sidebar_position: 1
---
Nhost is the open source GraphQL backend (Firebase Alternative) and a development platform. Nhost is doing for the backend, what [Netlify](https://netlify.com/) and [Vercel](https://vercel.com/) are doing for the frontend.
We provide a modern backend with the general building blocks required to build fantastic digital products.
We make it easy to build and deploy this backend using our platform that takes care of configuration, security, and performance. Things just works and scale automatically so you can focus on your product and on your business.
## Quickstart
Get started quickly by following one of our quickstart guides:
- [Next.js](/platform/quickstarts/nextjs)
- [React](/platform/quickstarts/react)
## Products and features
Learn more about the product and features of Nhost.
- [Database](/platform/database)
- [GraphQL](/platform/graphql)
- [Authentication](/platform/authentication)
- [Storage](/platform/storage)
- [Serverless Functions](/platform/serverless-functions)

View File

@@ -1,4 +0,0 @@
{
"label": "Nhost",
"position": 7
}

View File

@@ -1,9 +0,0 @@
---
title: 'Overview'
---
Documentation for other platform features:
- [Environment variables](/platform/nhost/environment-variables)
- [GitHub integration](/platform/nhost/github-integration)
- [Local development](/platform/nhost/local-development)

View File

@@ -0,0 +1,5 @@
{
"label": "Overview",
"position": 2,
"collapsed": false
}

View File

@@ -0,0 +1,30 @@
---
title: 'Architecture'
sidebar_position: 2
---
Nhost is a backend as a service built with open source tools to provide developers the general building blocks required to build fantastic digital apps and products.
Here's a diagram of the Nhost stack on a high level:
![Nhost Architecture Diagram](/img/architecture/nhost-diagram.png)
As you see in the image above, Nhost provides endpoints for:
- GraphQL (`/graphql`)
- Authentication (`/auth`)
- Storage (`/storage`)
- Functions (`/functions`)
Data is stored in Postgres and files are stored in S3.
## Open Source
The open source tools used for the full Nhost stack are:
- Database: [Postgres](https://www.postgresql.org/)
- S3: [Minio](https://github.com/minio/minio)
- GraphQL: [Hasura](https://github.com/hasura/graphql-engine)
- Authentication: [Hasura Auth](https://github.com/nhost/hasura-auth)
- Storage: [Hasura Storage](https://github.com/nhost/hasura-storage)
- Functions: [Node.js](https://nodejs.org/en/)

View File

@@ -0,0 +1,5 @@
{
"label": "Quickstarts",
"position": 3,
"collapsed": false
}

View File

@@ -0,0 +1,8 @@
---
title: 'Quickstart: Next.js'
sidebar_position: 2
---
## Introduction
This is a quickstart guide for React with Nhost.

View File

@@ -0,0 +1,8 @@
---
title: 'Quickstart: React'
sidebar_position: 1
---
## Introduction
This is a quickstart guide for React with Nhost.

View File

@@ -1,4 +1,4 @@
{ {
"label": "Serverless Functions", "label": "Serverless Functions",
"position": 6 "position": 8
} }

View File

@@ -15,7 +15,7 @@ Event triggers are managed in Hasura. Go to Hasura, then select **Events** in th
![Creating event trigger in Hasura](/img/platform/hasura-create-event-trigger.png) ![Creating event trigger in Hasura](/img/platform/hasura-create-event-trigger.png)
Nhost's [environment variables](/platform/nhost/environment-variables) can be used in event trigger headers. For example, you can attach `NHOST_WEBHOOK_SECRET` to an outgoing webhook here. Nhost's [environment variables](/platform/environment-variables) can be used in event trigger headers. For example, you can attach `NHOST_WEBHOOK_SECRET` to an outgoing webhook here.
--- ---
@@ -43,7 +43,7 @@ In your serverless function, you need to make sure the request actually comes fr
- Check the header in the serverless function. It should match the environment variable `NHOST_WEBHOOK_SECRET`. - Check the header in the serverless function. It should match the environment variable `NHOST_WEBHOOK_SECRET`.
```js ```js
export default function async handler(req, res) { export default async function handler(req, res) {
// Check webhook secret to make sure the request is valid // Check webhook secret to make sure the request is valid
if ( if (

View File

@@ -1,4 +1,4 @@
{ {
"label": "Storage", "label": "Storage",
"position": 5 "position": 7
} }

View File

@@ -38,17 +38,12 @@ import type { AppProps } from 'next/app';
import { NhostClient, NhostNextProvider } from '@nhost/nextjs'; import { NhostClient, NhostNextProvider } from '@nhost/nextjs';
import Header from '../components/Header';
const nhost = new NhostClient({ backendUrl: 'my-app.nhost.run' }); const nhost = new NhostClient({ backendUrl: 'my-app.nhost.run' });
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return ( return (
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}> <NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
<div> <Component {...pageProps} />
<Header />
<Component {...pageProps} />
</div>
</NhostNextProvider> </NhostNextProvider>
); );
} }

View File

@@ -94,7 +94,7 @@ const config = {
href: 'https://app.nhost.io', href: 'https://app.nhost.io',
className: 'header-get-started-link', className: 'header-get-started-link',
position: 'right', position: 'right',
label: 'Create an app', label: 'Get started',
}, },
], ],
}, },

View File

@@ -156,6 +156,10 @@
color: var(--ifm-footer-link-hover-color); color: var(--ifm-footer-link-hover-color);
} }
article {
padding: 0 20px;
}
.header-github-link:hover { .header-github-link:hover {
opacity: 0.6; opacity: 0.6;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -4,19 +4,19 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/client": "^3.6.2", "@apollo/client": "^3.6.2",
"@mantine/core": "^4.2.2",
"@mantine/hooks": "^4.2.2",
"@mantine/notifications": "^4.2.2",
"@mantine/prism": "^4.2.2",
"@nhost/core": "workspace:*", "@nhost/core": "workspace:*",
"@nhost/react": "workspace:*", "@nhost/react": "workspace:*",
"@nhost/react-apollo": "workspace:*", "@nhost/react-apollo": "workspace:*",
"@rsuite/icons": "^1.0.2",
"graphql": "15.7.2", "graphql": "15.7.2",
"less": "^4.1.2",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-view": "^1.21.3",
"react-router": "^6.3.0", "react-router": "^6.3.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0"
"rsuite": "^5.10.0"
}, },
"lib": "workspace:*", "lib": "workspace:*",
"scripts": { "scripts": {

View File

@@ -1,7 +1,7 @@
import { Container, Title } from '@mantine/core'
import { useNhostClient } from '@nhost/react' import { useNhostClient } from '@nhost/react'
import React from 'react' import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Panel } from 'rsuite'
export const AboutPage: React.FC = () => { export const AboutPage: React.FC = () => {
const nhost = useNhostClient() const nhost = useNhostClient()
@@ -15,7 +15,8 @@ export const AboutPage: React.FC = () => {
console.log(req) console.log(req)
} }
return ( return (
<Panel header="About this example" bordered> <Container>
<Title>About this example</Title>
<p>This application demonstrates the available features of the Nhost stack.</p> <p>This application demonstrates the available features of the Nhost stack.</p>
<button onClick={fetch}>Fetch</button> <button onClick={fetch}>Fetch</button>
<div> <div>
@@ -32,13 +33,13 @@ export const AboutPage: React.FC = () => {
<ul> <ul>
<li>React</li> <li>React</li>
<li>React-router</li> <li>React-router</li>
<li>RSuite</li> <li>Mantine</li>
<li>and of course, the Nhost React client</li> <li>and of course, the Nhost React client</li>
</ul> </ul>
</div> </div>
<div> <div>
Noew let&apos;s go to the <Link to="/">index page</Link> Noew let&apos;s go to the <Link to="/">index page</Link>
</div> </div>
</Panel> </Container>
) )
} }

View File

@@ -1,10 +1,8 @@
/* eslint-disable react/react-in-jsx-scope */ /* eslint-disable react/react-in-jsx-scope */
import { useEffect } from 'react' import { useEffect } from 'react'
import { Link, Route, Routes, useLocation, useNavigate } from 'react-router-dom' import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import { Container, Content, Header, Nav, Navbar } from 'rsuite'
import { useAuthenticated, useSignOut } from '@nhost/react' import { useAuthenticated, useSignOut } from '@nhost/react'
import ExitIcon from '@rsuite/icons/Exit'
import { AuthGate, PublicGate } from './components/auth-gates' import { AuthGate, PublicGate } from './components/auth-gates'
import { AboutPage } from './About' import { AboutPage } from './About'
@@ -15,6 +13,10 @@ import { SignInPage } from './sign-in'
import { SignUpPage } from './sign-up' import { SignUpPage } from './sign-up'
import './App.css' import './App.css'
import NavBar from './components/NavBar'
import { MantineProvider, AppShell, Header } from '@mantine/core'
import { NotificationsProvider } from '@mantine/notifications'
const title = 'Nhost with React and Apollo'
function App() { function App() {
const isAuthenticated = useAuthenticated() const isAuthenticated = useAuthenticated()
@@ -27,74 +29,76 @@ function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [signedOut]) }, [signedOut])
return ( return (
<Container> <MantineProvider
<Header> withGlobalStyles
<Navbar appearance="inverse"> withNormalizeCSS
<Navbar.Brand as="div"> theme={{
<Link to="/"> /** Put your mantine theme override here */
<img src="/logo.svg" alt="logo" style={{ maxHeight: '100%' }} /> colorScheme: 'light'
</Link> }}
</Navbar.Brand> >
<Nav activeKey={location.pathname} onSelect={navigate}> <NotificationsProvider>
{isAuthenticated && <Nav.Item eventKey="/profile">Profile</Nav.Item>} <AppShell
{isAuthenticated && <Nav.Item eventKey="/apollo">Apollo GraphQL</Nav.Item>} padding="md"
<Nav.Item eventKey="/about">About</Nav.Item> navbar={<NavBar />}
</Nav> header={
<Nav pullRight> <Header height={60} p="xs">
{isAuthenticated && ( {title}
<Nav.Item icon={<ExitIcon />} onSelect={signOut}> </Header>
Sign Out }
</Nav.Item> styles={(theme) => ({
)} main: {
</Nav> backgroundColor:
</Navbar> theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0]
</Header>
<Content>
<Routes>
<Route
path="/"
element={
<AuthGate>
<Home />
</AuthGate>
} }
/> })}
<Route path="/about" element={<AboutPage />} /> >
<Route <Routes>
path="/sign-in/*" <Route
element={ path="/"
<PublicGate> element={
<SignInPage /> <AuthGate>
</PublicGate> <Home />
} </AuthGate>
/> }
<Route />
path="/sign-up/*" <Route path="/about" element={<AboutPage />} />
element={ <Route
<PublicGate> path="/sign-in/*"
<SignUpPage /> element={
</PublicGate> <PublicGate>
} <SignInPage />
/> </PublicGate>
<Route }
path="/profile" />
element={ <Route
<AuthGate> path="/sign-up/*"
<ProfilePage /> element={
</AuthGate> <PublicGate>
} <SignUpPage />
/> </PublicGate>
<Route }
path="/apollo" />
element={ <Route
<AuthGate> path="/profile"
<ApolloPage /> element={
</AuthGate> <AuthGate>
} <ProfilePage />
/> </AuthGate>
</Routes> }
</Content> />
</Container> <Route
path="/apollo"
element={
<AuthGate>
<ApolloPage />
</AuthGate>
}
/>
</Routes>
</AppShell>
</NotificationsProvider>
</MantineProvider>
) )
} }

View File

@@ -1,11 +1,12 @@
import { Container, Title } from '@mantine/core'
import React from 'react' import React from 'react'
import { Panel } from 'rsuite'
const HomePage: React.FC = () => { const HomePage: React.FC = () => {
return ( return (
<Panel header="Home page" bordered> <Container>
<Title>Home page</Title>
You are authenticated. You have now access to the authorised part of the application. You are authenticated. You have now access to the authorised part of the application.
</Panel> </Container>
) )
} }
export default HomePage export default HomePage

View File

@@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import { Panel, Table } from 'rsuite'
import { gql } from '@apollo/client' import { gql } from '@apollo/client'
import { useAuthQuery } from '@nhost/react-apollo' import { useAuthQuery } from '@nhost/react-apollo'
import { Container, Loader, Title } from '@mantine/core'
const GET_BOOKS = gql` const GET_BOOKS = gql`
query BooksQuery { query BooksQuery {
@@ -13,24 +13,22 @@ const GET_BOOKS = gql`
} }
` `
const { Column, Cell, HeaderCell } = Table
export const ApolloPage: React.FC = () => { export const ApolloPage: React.FC = () => {
const { loading, data } = useAuthQuery(GET_BOOKS, { const { loading, data } = useAuthQuery(GET_BOOKS, {
pollInterval: 5000, pollInterval: 5000,
fetchPolicy: 'cache-and-network' fetchPolicy: 'cache-and-network'
}) })
return ( return (
<Panel header="Apollo GraphQL"> <Container>
<Table loading={loading} data={data?.books || []} bordered cellBordered> <Title>Apollo GraphQL</Title>
<Column key="id" fixed width={300}> {loading && <Loader />}
<HeaderCell>Id</HeaderCell> {data?.books && (
<Cell dataKey="id" /> <ul>
</Column> {data.books.map((book) => (
<Column key="title" fixed flexGrow={1}> <li key={book.id}>{book.title}</li>
<HeaderCell>Title</HeaderCell> ))}
<Cell dataKey="title" /> </ul>
</Column> )}
</Table> </Container>
</Panel>
) )
} }

View File

@@ -0,0 +1,26 @@
import { Card, Container, Divider, SimpleGrid, Title } from '@mantine/core'
export const AuthLayout: React.FC<{
title?: string
footer?: React.ReactNode
children: React.ReactNode
}> = ({ title, footer, children }) => {
return (
<Container>
<Card shadow="sm" p="lg" m="lg">
{title && <Title p="lg">{title}</Title>}
<SimpleGrid cols={1} spacing={6}>
{children}
</SimpleGrid>
</Card>
{footer && (
<>
<Divider my="sm" />
{footer}
</>
)}
</Container>
)
}
export default AuthLayout

View File

@@ -0,0 +1,41 @@
import React from 'react'
import { Button, ButtonVariant } from '@mantine/core'
import { Link } from 'react-router-dom'
const AuthLink: React.FC<{
icon?: React.ReactNode
link: string
color?: string
children?: React.ReactNode
variant?: ButtonVariant
}> = ({ icon, color, link, variant, children }) => {
return (
// <Link to={link}>
<Button
component={Link}
fullWidth
radius="sm"
variant={variant}
to={link}
leftIcon={icon}
styles={(theme) => ({
root: {
backgroundColor: color,
'&:hover': {
backgroundColor: color && theme.fn.darken(color, 0.05)
}
},
leftIcon: {
marginRight: 15
}
})}
>
{children}
</Button>
// </Link>
)
}
export default AuthLink

View File

@@ -0,0 +1,80 @@
import { FaHouseUser, FaQuestion, FaSignOutAlt } from 'react-icons/fa'
import { SiApollographql } from 'react-icons/si'
import { Group, MantineColor, Navbar, Text, ThemeIcon, UnstyledButton } from '@mantine/core'
import { useAuthenticated, useSignOut } from '@nhost/react'
import { useNavigate, useLocation } from 'react-router'
import { Link } from 'react-router-dom'
interface MenuItemProps {
icon: React.ReactNode
color?: MantineColor
label: string
link?: string
action?: () => void
}
const MenuItem: React.FC<MenuItemProps> = ({ icon, color, label, link, action }) => {
const location = useLocation()
const active = location.pathname === link
const Button = (
<UnstyledButton
onClick={action}
sx={(theme) => ({
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: active
? theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 4 : 7]
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.black,
'&:hover': {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0]
}
})}
>
<Group>
<ThemeIcon color={color} variant="outline">
{icon}
</ThemeIcon>
<Text size="sm">{label}</Text>
</Group>
</UnstyledButton>
)
return link ? <Link to={link}>{Button}</Link> : Button
}
const data: MenuItemProps[] = [
{ icon: <FaHouseUser size={16} />, label: 'Home', link: '/' },
{ icon: <FaHouseUser size={16} />, label: 'Profile', link: '/profile' },
{ icon: <SiApollographql size={16} />, label: 'Apollo', link: '/apollo' },
{ icon: <FaQuestion size={16} />, label: 'About', link: '/about' }
]
export default function NavBar() {
const authenticated = useAuthenticated()
const { signOut } = useSignOut()
const navigate = useNavigate()
const links = data.map((link) => <MenuItem {...link} key={link.label} />)
return (
<Navbar width={{ sm: 300, lg: 400, base: 100 }}>
<Navbar.Section grow mt="md">
{links}
{authenticated && (
<MenuItem
icon={<FaSignOutAlt />}
label="Sign Out"
action={async () => {
await signOut()
navigate('/', { replace: true })
}}
/>
)}
</Navbar.Section>
</Navbar>
)
}

View File

@@ -0,0 +1,23 @@
import React from 'react'
import { FaFacebook, FaGithub, FaGoogle } from 'react-icons/fa'
import { useProviderLink } from '@nhost/react'
import AuthLink from './AuthLink'
export default function OauthLinks() {
const { github, google, facebook } = useProviderLink()
return (
<>
<AuthLink icon={<FaGithub />} link={github} color="#333">
Continue with GitHub
</AuthLink>
<AuthLink icon={<FaGoogle />} link={google} color="#de5246">
Continue with Google
</AuthLink>
<AuthLink icon={<FaFacebook />} link={facebook} color="#3b5998">
Continue with Facebook
</AuthLink>
</>
)
}

View File

@@ -0,0 +1,51 @@
import { useState } from 'react'
import { Button, Modal, SimpleGrid, TextInput } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useSignInEmailPasswordless } from '@nhost/react'
export const SignUpPasswordlessForm: React.FC = () => {
const { signInEmailPasswordless } = useSignInEmailPasswordless({ redirectTo: '/profile' })
const [emailVerificationToggle, setEmailVerificationToggle] = useState(false)
const [email, setEmail] = useState('')
const signIn = async () => {
const result = await signInEmailPasswordless(email)
if (result.isError) {
showNotification({
color: 'red',
title: 'Error',
message: result.error?.message || null
})
} else {
setEmailVerificationToggle(true)
}
}
return (
<SimpleGrid cols={1} spacing={6}>
<Modal
title="Verification email sent"
centered
opened={emailVerificationToggle}
onClose={() => {
setEmailVerificationToggle(false)
}}
>
A verification email has been sent. Please check your inbox and follow the link to complete
authentication. This page with automatically redirect to the authenticated home page once
the email has been verified.
</Modal>
<TextInput
type="email"
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Button fullWidth onClick={signIn}>
Continue with email
</Button>
</SimpleGrid>
)
}
export default SignUpPasswordlessForm

View File

@@ -3,7 +3,7 @@ import { Navigate, useLocation } from 'react-router-dom'
import { useAuthenticationStatus } from '@nhost/react' import { useAuthenticationStatus } from '@nhost/react'
export const AuthGate: React.FC = ({ children }) => { export const AuthGate: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const { isLoading, isAuthenticated } = useAuthenticationStatus() const { isLoading, isAuthenticated } = useAuthenticationStatus()
const location = useLocation() const location = useLocation()
if (isLoading) { if (isLoading) {
@@ -17,7 +17,7 @@ export const AuthGate: React.FC = ({ children }) => {
return <div>{children}</div> return <div>{children}</div>
} }
export const PublicGate: React.FC = ({ children }) => { export const PublicGate: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const { isLoading, isAuthenticated } = useAuthenticationStatus() const { isLoading, isAuthenticated } = useAuthenticationStatus()
const location = useLocation() const location = useLocation()
if (isLoading) { if (isLoading) {

View File

@@ -1,53 +0,0 @@
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button, Input, Message } from 'rsuite'
import { useSignInEmailPasswordless } from '@nhost/react'
export const EmailPasswordlessForm: React.FC = () => {
const [email, setEmail] = useState('')
const navigate = useNavigate()
const { signInEmailPasswordless, isError, isSuccess, error } = useSignInEmailPasswordless({
redirectTo: '/profile'
})
const [showError, setShowError] = useState(true)
useEffect(() => {
setShowError(false)
}, [email])
useEffect(() => {
if (isSuccess) {
navigate('/sign-in/verification-email-sent')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess])
return (
<div>
<Input
placeholder="Email Address"
value={email}
onChange={setEmail}
size="lg"
autoFocus
style={{ marginBottom: '0.5em' }}
/>
{showError && isError && (
<Message showIcon type="error">
{error?.message}
</Message>
)}
<Button
block
appearance="primary"
style={{ marginTop: '0.5em' }}
onClick={() => {
setShowError(true)
signInEmailPasswordless(email)
}}
>
Continue with email
</Button>
</div>
)
}

View File

@@ -1,3 +0,0 @@
export * from './auth-gates'
export * from './email-passwordless-form'
export * from './oauth-links'

View File

@@ -1,47 +0,0 @@
/* eslint-disable react/react-in-jsx-scope */
import { FaFacebook, FaGithub, FaGoogle } from 'react-icons/fa'
import { IconButton } from 'rsuite'
import { useAuthenticationStatus, useProviderLink } from '@nhost/react'
import { Icon } from '@rsuite/icons'
export const OAuthLinks: React.FC = () => {
// TODO show how to use options
const { github, google, facebook } = useProviderLink()
const { error } = useAuthenticationStatus()
return (
<div>
<IconButton
icon={<Icon as={FaGithub} style={{ backgroundColor: 'black' }} />}
as="a"
href={github}
block
appearance="primary"
style={{ backgroundColor: 'black' }}
>
Continue with GitHub
</IconButton>
<IconButton
icon={<Icon as={FaGoogle} />}
as="a"
href={google}
block
appearance="primary"
color="red"
>
Continue with Google
</IconButton>
<IconButton
icon={<Icon as={FaFacebook} />}
as="a"
href={facebook}
block
appearance="primary"
color="blue"
>
Continue with Facebook
</IconButton>
{error && <div>{error?.message}</div>}
</div>
)
}

View File

@@ -6,8 +6,6 @@ import { NhostClient, NhostReactProvider } from '@nhost/react'
import { NhostApolloProvider } from '@nhost/react-apollo' import { NhostApolloProvider } from '@nhost/react-apollo'
import { inspect } from '@xstate/inspect' import { inspect } from '@xstate/inspect'
import 'rsuite/styles/index.less' // or 'rsuite/dist/rsuite.min.css'
import App from './App' import App from './App'
const nhost = new NhostClient({ const nhost = new NhostClient({

View File

@@ -1,63 +1,55 @@
/* eslint-disable react/react-in-jsx-scope */ /* eslint-disable react/react-in-jsx-scope */
import { useEffect, useState } from 'react' import { useState } from 'react'
import { Button, FlexboxGrid, Input, Message, Notification, Panel, toaster } from 'rsuite'
import { useChangeEmail, useEmail } from '@nhost/react' import { useChangeEmail, useUserEmail } from '@nhost/react'
import { Button, Card, Grid, TextInput, Title } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
export const ChangeEmail: React.FC = () => { export const ChangeEmail: React.FC = () => {
const [newEmail, setNewEmail] = useState('') const [newEmail, setNewEmail] = useState('')
const email = useEmail() const email = useUserEmail()
const { changeEmail, error, needsEmailVerification } = useChangeEmail({ const { changeEmail } = useChangeEmail({
redirectTo: '/profile' redirectTo: '/profile'
}) })
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => { const change = async () => {
if (needsEmailVerification) { if (newEmail && email === newEmail) {
toaster.push( showNotification({
<Notification type="info" header="Info" closable> title: 'Error',
An email has been sent to {newEmail}. Please check your inbox and follow the link to message: 'You need to set a different email as the current one'
confirm the email change. })
</Notification>
)
setNewEmail('')
} }
// eslint-disable-next-line react-hooks/exhaustive-deps const result = await changeEmail(newEmail)
}, [needsEmailVerification]) if (result.needsEmailVerification) {
showNotification({
// * Set error message from the registration hook errors message: `An email has been sent to ${newEmail}. Please check your inbox and follow the link to confirm the email change.`
useEffect(() => { })
setErrorMessage(error?.message || '') }
}, [error]) if (result.error) {
// * Reset error message every time the email input changed showNotification({
useEffect(() => { color: 'red',
setErrorMessage('') title: 'Error',
}, [newEmail]) message: result.error.message
// * Show an error message when passwords are different })
useEffect(() => { }
if (newEmail && email === newEmail) }
setErrorMessage('You need to set a different email as the current one')
else setErrorMessage('')
}, [email, newEmail])
return ( return (
<Panel header="Change email" bordered> <Card shadow="sm" p="lg" m="sm">
<FlexboxGrid> <Title>Change email</Title>
<FlexboxGrid.Item colspan={12}> <Grid>
<Input value={newEmail} onChange={setNewEmail} placeholder="New email" /> <Grid.Col>
</FlexboxGrid.Item> <TextInput
<FlexboxGrid.Item colspan={12}> value={newEmail}
<Button onClick={() => changeEmail(email)} block appearance="primary"> onChange={(e) => setNewEmail(e.target.value)}
placeholder="New email"
/>
</Grid.Col>
<Grid.Col>
<Button onClick={change} fullWidth>
Change Change
</Button> </Button>
</FlexboxGrid.Item> </Grid.Col>
</FlexboxGrid> </Grid>
</Card>
{errorMessage && (
<Message showIcon type="error">
{errorMessage}
</Message>
)}
</Panel>
) )
} }

View File

@@ -1,59 +1,46 @@
import React from 'react' import React from 'react'
import { useEffect, useState } from 'react' import { useState } from 'react'
import { Button, FlexboxGrid, Input, Message, Notification, Panel, toaster } from 'rsuite'
import { useChangePassword } from '@nhost/react' import { useChangePassword } from '@nhost/react'
import { Button, Card, Grid, PasswordInput, Title } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
export const ChangePassword: React.FC = () => { export const ChangePassword: React.FC = () => {
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const { changePassword, isSuccess, error } = useChangePassword() const { changePassword } = useChangePassword()
const [errorMessage, setErrorMessage] = useState('')
// * See https://github.com/rsuite/rsuite/issues/2336 const change = async () => {
useEffect(() => { const result = await changePassword(password)
if (isSuccess) { if (result.isSuccess) {
setPassword('') showNotification({
toaster.push( message: `Password changed successfully.`
<Notification type="info" header="Info" closable> })
Password changed successfully.
</Notification>
)
setPassword('')
} }
}, [isSuccess]) if (result.error) {
showNotification({
// * Set error message from the registration hook errors color: 'red',
useEffect(() => { title: 'Error',
setErrorMessage(error?.message || '') message: result.error.message
}, [error]) })
// * Reset error message every time the password input changed }
useEffect(() => { }
setErrorMessage('')
}, [password])
return ( return (
<Panel header="Change password" bordered> <Card shadow="sm" p="lg" m="sm">
<FlexboxGrid> <Title>Change password</Title>
<FlexboxGrid.Item colspan={12}> <Grid>
<Input <Grid.Col>
<PasswordInput
value={password} value={password}
onChange={setPassword} onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="New password" placeholder="New password"
/> />
</FlexboxGrid.Item> </Grid.Col>
<FlexboxGrid.Item colspan={12}> <Grid.Col>
<Button onClick={() => changePassword(password)} block appearance="primary"> <Button onClick={change} fullWidth>
Change Change
</Button> </Button>
</FlexboxGrid.Item> </Grid.Col>
</FlexboxGrid> </Grid>
</Card>
{errorMessage && (
<Message showIcon type="error">
{errorMessage}
</Message>
)}
</Panel>
) )
} }

View File

@@ -1,59 +1,34 @@
import React from 'react' import React from 'react'
import ReactJson from 'react-json-view'
import { Button, Col, Panel, Row } from 'rsuite'
import { useHasuraClaims, useNhostClient, useUserData } from '@nhost/react' import { useHasuraClaims, useNhostClient, useUserData } from '@nhost/react'
import { ChangeEmail } from './change-email' import { ChangeEmail } from './change-email'
import { ChangePassword } from './change-password' import { ChangePassword } from './change-password'
import { Mfa } from './mfa' import { Mfa } from './mfa'
import { Button, Card, Container, Title } from '@mantine/core'
import { Prism } from '@mantine/prism'
export const ProfilePage: React.FC = () => { export const ProfilePage: React.FC = () => {
const claims = useHasuraClaims() const claims = useHasuraClaims()
const userData = useUserData() const userData = useUserData()
const nhost = useNhostClient() const nhost = useNhostClient()
return ( return (
<Panel header="Profile page" bordered> <Container>
<Row> <Title>Profile page</Title>
<Col md={12} sm={24}> <Mfa />
<Mfa /> <ChangeEmail />
</Col> <ChangePassword />
<Col md={12} sm={24}> <Card shadow="sm" p="lg" m="sm">
<ChangeEmail /> <Title>User information</Title>
</Col> {userData && <Prism language="json">{JSON.stringify(userData, null, 2)}</Prism>}
<Col md={12} sm={24}> </Card>
<ChangePassword /> <Card shadow="sm" p="lg" m="sm">
</Col> <Title>Hasura JWT claims</Title>
<Col md={12} sm={24}> <Button fullWidth onClick={() => nhost.auth.refreshSession()}>
<Panel header="User information" bordered> Refresh session
{userData && ( </Button>
<ReactJson {claims && <Prism language="json">{JSON.stringify(claims, null, 2)}</Prism>}
src={userData} </Card>
displayDataTypes={false} </Container>
displayObjectSize={false}
enableClipboard={false}
name={false}
/>
)}
</Panel>
</Col>
<Col md={12} sm={24}>
<Panel header="Hasura JWT claims" bordered>
<Button block appearance="primary" onClick={() => nhost.auth.refreshSession()}>
Refresh session
</Button>
{claims && (
<ReactJson
src={claims}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
name={false}
/>
)}
</Panel>
</Col>
</Row>
</Panel>
) )
} }

View File

@@ -1,30 +1,45 @@
import React from 'react' import React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { Button, Input, Panel } from 'rsuite'
import { useConfigMfa } from '@nhost/react' import { useConfigMfa } from '@nhost/react'
import { Card, Button, TextInput, Title } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
export const Mfa: React.FC = () => { export const Mfa: React.FC = () => {
const [code, setCode] = useState('') const [code, setCode] = useState('')
const { generateQrCode, activateMfa, isActivated, isGenerated, qrCodeDataUrl } = useConfigMfa() const { generateQrCode, activateMfa, isActivated, isGenerated, qrCodeDataUrl } = useConfigMfa()
const generate = async () => {
const result = await generateQrCode()
if (result.error) {
showNotification({
color: 'red',
title: 'Error',
message: result.error.message
})
}
}
return ( return (
<Panel header="Activate 2-step verification" bordered> <Card shadow="sm" p="lg" m="sm">
<Title>Activate 2-step verification</Title>
{!isGenerated && ( {!isGenerated && (
<Button block appearance="primary" onClick={generateQrCode}> <Button fullWidth onClick={generate}>
Generate Generate
</Button> </Button>
)} )}
{isGenerated && !isActivated && ( {isGenerated && !isActivated && (
<div> <div>
<img alt="qrcode" src={qrCodeDataUrl} /> <img alt="qrcode" src={qrCodeDataUrl} />
<Input value={code} onChange={setCode} placeholder="Enter activation code" /> <TextInput
<Button block appearance="primary" onClick={() => activateMfa(code)}> value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter activation code"
/>
<Button fullWidth onClick={() => activateMfa(code)}>
Activate Activate
</Button> </Button>
</div> </div>
)} )}
{isActivated && <div>MFA has been activated!!!</div>} {isActivated && <div>MFA has been activated!!!</div>}
</Panel> </Card>
) )
} }

View File

@@ -1,100 +1,95 @@
import React, { useEffect, useState } from 'react' /* eslint-disable react/react-in-jsx-scope */
import { NavLink } from 'react-router-dom' import { useState } from 'react'
import { Button, Divider, Input, Message } from 'rsuite' import { useNavigate } from 'react-router-dom'
import { useSignInEmailPassword } from '@nhost/react' import { useSignInEmailPassword } from '@nhost/react'
import { Button, Modal, TextInput } from '@mantine/core'
const Footer: React.FC = () => ( import AuthLink from '../components/AuthLink'
<div> import { showNotification } from '@mantine/notifications'
<Divider />
<Button as={NavLink} to="/sign-in" block appearance="link">
&#8592; Other Login Options
</Button>
</div>
)
export const EmailPassword: React.FC = () => { export const EmailPassword: React.FC = () => {
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [otp, setOtp] = useState('') const [otp, setOtp] = useState('')
const { signInEmailPassword, error, needsMfaOtp, sendMfaOtp } = useSignInEmailPassword( const { signInEmailPassword, needsMfaOtp, sendMfaOtp } = useSignInEmailPassword()
email, const navigate = useNavigate()
password,
otp
)
const [errorMessage, setErrorMessage] = useState('') const [emailVerificationToggle, setEmailVerificationToggle] = useState(false)
// * Set error message from the authentication hook errors
useEffect(() => {
setErrorMessage(error?.message || '')
}, [error])
// * Reset error message every time the email or password input changed
useEffect(() => {
setErrorMessage('')
}, [email, password])
const signIn = async () => {
const result = await signInEmailPassword(email, password)
if (result.isError) {
showNotification({
color: 'red',
title: 'Error',
message: result.error?.message
})
} else if (result.needsEmailVerification) {
setEmailVerificationToggle(true)
} else if (!result.needsEmailVerification) {
navigate('/', { replace: true })
}
}
const sendOtp = async () => {
sendMfaOtp(otp)
console.log('TODO')
}
if (needsMfaOtp) if (needsMfaOtp)
return ( return (
<div> <>
<Input <TextInput
value={otp} value={otp}
onChange={setOtp} onChange={(e) => setOtp(e.target.value)}
placeholder="One-time password" placeholder="One-time password"
size="lg" size="lg"
autoFocus autoFocus
style={{ marginBottom: '0.5em' }} style={{ marginBottom: '0.5em' }}
/> />
{errorMessage && ( <Button fullWidth onClick={sendOtp}>
<Message showIcon type="error">
{errorMessage}
</Message>
)}
<Button appearance="primary" onClick={sendMfaOtp} block>
Send 2-step verification code Send 2-step verification code
</Button> </Button>
<Footer /> </>
</div>
) )
else else
return ( return (
<div> <>
<Input <Modal
title="Verification email sent"
transition="fade"
centered
transitionDuration={600}
opened={emailVerificationToggle}
onClose={() => {
setEmailVerificationToggle(false)
}}
>
A email has been sent to {email}. Please follow the link to verify your email address and
to complete your registration.
</Modal>
<TextInput
value={email} value={email}
onChange={setEmail} onChange={(e) => setEmail(e.target.value)}
placeholder="Email Address" placeholder="Email Address"
size="lg" size="lg"
autoFocus autoFocus
style={{ marginBottom: '0.5em' }} style={{ marginBottom: '0.5em' }}
/> />
<Input <TextInput
value={password} value={password}
onChange={setPassword} onChange={(e) => setPassword(e.target.value)}
placeholder="Password" placeholder="Password"
type="password" type="password"
size="lg" size="lg"
style={{ marginBottom: '0.5em' }} style={{ marginBottom: '0.5em' }}
/> />
{errorMessage && ( <Button fullWidth onClick={signIn}>
<Message showIcon type="error">
{errorMessage}
</Message>
)}
<Button
appearance="primary"
onClick={async () => {
const result = await signInEmailPassword(email, password)
console.log(result)
}}
block
>
Sign in Sign in
</Button> </Button>
<Button as={NavLink} block to="/sign-in/forgot-password"> <AuthLink link="/sign-in/forgot-password" variant="white">
Forgot password? Forgot password?
</Button> </AuthLink>
<Footer /> </>
</div>
) )
} }

View File

@@ -1,15 +1,17 @@
import { Divider } from '@mantine/core'
import React from 'react' import React from 'react'
import { NavLink } from 'react-router-dom' import AuthLink from '../components/AuthLink'
import { Button } from 'rsuite'
import EmailPasswordlessForm from '../components/SignUpServerlessForm'
import { EmailPasswordlessForm } from '../components/email-passwordless-form'
export const EmailPasswordless: React.FC = () => { export const EmailPasswordless: React.FC = () => {
return ( return (
<div> <>
<EmailPasswordlessForm /> <EmailPasswordlessForm />
<Button as={NavLink} to="/sign-up" block appearance="link"> <Divider />
<AuthLink link="/sign-up" variant="white">
&#8592; Other Login Options &#8592; Other Login Options
</Button> </AuthLink>
</div> </>
) )
} }

View File

@@ -1,57 +1,43 @@
import React, { useEffect, useState } from 'react' import React, { useState } from 'react'
import { NavLink } from 'react-router-dom'
import { Button, Divider, Input, Message, Notification, toaster } from 'rsuite'
import { useResetPassword } from '@nhost/react' import { useResetPassword } from '@nhost/react'
import { showNotification } from '@mantine/notifications'
import { Button, Divider, TextInput } from '@mantine/core'
import AuthLink from '../components/AuthLink'
export const ForgotPassword: React.FC = () => { export const ForgotPassword: React.FC = () => {
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const { resetPassword, isSent, error } = useResetPassword({ redirectTo: '/profile' }) const { resetPassword } = useResetPassword({ redirectTo: '/profile' })
const [errorMessage, setErrorMessage] = useState('') const reset = async () => {
// * Set error message from the authentication hook errors const result = await resetPassword(email)
useEffect(() => { if (result.isError) {
setErrorMessage(error?.message || '') showNotification({
}, [error]) color: 'red',
// * Reset error message every time the email or password input changed title: 'Error',
useEffect(() => { message: result.error?.message
setErrorMessage('') })
}, [email])
useEffect(() => {
if (isSent) {
toaster.push(
<Notification type="info" header="Info" closable>
An email has been sent with a passwordless authentication link, so you will be able to
authenticate and change your password.
</Notification>
)
} }
}, [isSent]) }
return ( return (
<div> <>
<Input <TextInput
value={email} value={email}
onChange={setEmail} onChange={(e) => setEmail(e.target.value)}
placeholder="Email Address" placeholder="Email Address"
size="lg" size="lg"
autoFocus autoFocus
style={{ marginBottom: '0.5em' }} style={{ marginBottom: '0.5em' }}
/> />
{errorMessage && ( <Button onClick={reset} fullWidth>
<Message showIcon type="error">
{errorMessage}
</Message>
)}
<Button appearance="primary" onClick={() => resetPassword(email)} block>
Reset your password Reset your password
</Button> </Button>
<Divider /> <Divider />
<Button as={NavLink} to="/sign-in/email-password" block appearance="link">
<AuthLink link="/sign-in/email-password" variant="white">
&#8592; Sign in with email + password &#8592; Sign in with email + password
</Button> </AuthLink>
</div> </>
) )
} }

View File

@@ -1,60 +1,49 @@
import React from 'react' import React from 'react'
import { FaLock } from 'react-icons/fa' import { FaLock } from 'react-icons/fa'
import { Link, NavLink, Route, Routes } from 'react-router-dom' import { Link, Route, Routes } from 'react-router-dom'
import { Button, Divider, FlexboxGrid, IconButton, Panel } from 'rsuite'
import { Icon } from '@rsuite/icons' import OAuthLinks from '../components/OauthLinks'
import { OAuthLinks } from '../components'
import { VerificationEmailSent } from '../verification-email-sent'
import { EmailPassword } from './email-password' import { EmailPassword } from './email-password'
import { EmailPasswordless } from './email-passwordless' import { EmailPasswordless } from './email-passwordless'
import AuthLayout from '../components/AuthLayout'
import { Center, Text, Anchor, Divider } from '@mantine/core'
import AuthLink from '../components/AuthLink'
import { ForgotPassword } from './forgot-password' import { ForgotPassword } from './forgot-password'
// import { useSignInAnonymous } from '@nhost/react'
const Index: React.FC = () => ( const Index: React.FC = () => (
<div> <>
<OAuthLinks /> <OAuthLinks />
<Divider /> <Divider my="sm" />
<IconButton <AuthLink icon={<FaLock />} variant="outline" link="/sign-in/email-passwordless">
block
icon={<Icon as={FaLock} />}
appearance="ghost"
as={NavLink}
to="/sign-in/email-passwordless"
>
Continue with passwordless email Continue with passwordless email
</IconButton> </AuthLink>
<Button as={NavLink} to="/sign-in/email-password" block appearance="link"> <AuthLink variant="subtle" link="/sign-in/email-password">
Continue with email + password Continue with email + password
</Button> </AuthLink>
</div> </>
) )
export const SignInPage: React.FC = () => { export const SignInPage: React.FC = () => {
// const { signIn } = useSignInAnonymous()
return ( return (
<div style={{ textAlign: 'center' }}> <AuthLayout
<FlexboxGrid justify="center"> title="Log in to the Application"
<FlexboxGrid.Item colspan={12}> footer={
<Panel header={<h2>Log in to the Application</h2>} bordered> <Center>
<Routes> <Text>
<Route path="/" element={<Index />} /> Don&lsquo;t have an account?{' '}
<Route path="/email-passwordless" element={<EmailPasswordless />} /> <Anchor component={Link} to="/sign-up">
<Route path="/email-password" element={<EmailPassword />} /> Sign up
<Route path="/verification-email-sent" element={<VerificationEmailSent />} /> </Anchor>
<Route path="/forgot-password" element={<ForgotPassword />} /> </Text>
</Routes> </Center>
</Panel> }
</FlexboxGrid.Item> >
</FlexboxGrid> <Routes>
<Divider /> <Route path="/" element={<Index />} />
Don&lsquo;t have an account? <Link to="/sign-up">Sign up</Link> <Route path="/email-password" element={<EmailPassword />} />
{/* or{' '} <Route path="/forgot-password" element={<ForgotPassword />} />
<a href="#" onClick={signIn}> <Route path="/email-passwordless" element={<EmailPasswordless />} />
enter the app anonymously </Routes>
</a> */} </AuthLayout>
</div>
) )
} }

View File

@@ -1,105 +1,95 @@
/* eslint-disable react/react-in-jsx-scope */ /* eslint-disable react/react-in-jsx-scope */
import { useEffect, useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { NavLink, useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Button, Input, Message } from 'rsuite'
import { useSignUpEmailPassword } from '@nhost/react' import { useSignUpEmailPassword } from '@nhost/react'
import { Button, Divider, Modal, PasswordInput, SimpleGrid, TextInput } from '@mantine/core'
import AuthLink from '../components/AuthLink'
import { showNotification } from '@mantine/notifications'
export const EmailPassword: React.FC = () => { export const EmailPassword: React.FC = () => {
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [firstName, setFirstName] = useState('') const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('') const [lastName, setLastName] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [emailVerificationToggle, setEmailVerificationToggle] = useState(false)
const differentPassword = useMemo(
() => password && password !== confirmPassword && 'Should match the given password',
[password, confirmPassword]
)
const options = useMemo( const options = useMemo(
() => ({ displayName: `${firstName} ${lastName}`, metadata: { firstName, lastName } }), () => ({ displayName: `${firstName} ${lastName}`, metadata: { firstName, lastName } }),
[firstName, lastName] [firstName, lastName]
) )
const navigate = useNavigate() const navigate = useNavigate()
const [confirmPassword, setConfirmPassword] = useState('') const { signUpEmailPassword } = useSignUpEmailPassword(options)
const { signUpEmailPassword, error, needsEmailVerification, isSuccess } =
useSignUpEmailPassword(options)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
if (needsEmailVerification) navigate('/sign-up/verification-email-sent')
else if (isSuccess) navigate('/')
// eslint-disable-next-line react-hooks/exhaustive-deps const signUp = async () => {
}, [needsEmailVerification, isSuccess]) const result = await signUpEmailPassword(email, password, { metadata: { firstName, lastName } })
if (result.isError) {
// * Set error message from the registration hook errors showNotification({
useEffect(() => { color: 'red',
setErrorMessage(error?.message || '') title: 'Error',
}, [error]) message: result.error?.message
// * Reset error message every time the email or password input changed })
useEffect(() => { } else if (result.needsEmailVerification) {
setErrorMessage('') setEmailVerificationToggle(true)
}, [email, password]) } else {
// * Show an error message when passwords are different navigate('/', { replace: true })
useEffect(() => { }
if (password !== confirmPassword) setErrorMessage('Both passwords must be the same') }
else setErrorMessage('')
}, [password, confirmPassword])
return ( return (
<div> <>
<Input <Modal
value={firstName} title="Verification email sent"
onChange={setFirstName} transition="fade"
placeholder="First name" centered
size="lg" transitionDuration={600}
autoFocus opened={emailVerificationToggle}
style={{ marginBottom: '0.5em' }} onClose={() => {
/> setEmailVerificationToggle(false)
<Input
value={lastName}
onChange={setLastName}
placeholder="Last name"
size="lg"
style={{ marginBottom: '0.5em' }}
/>
<Input
value={email}
onChange={setEmail}
placeholder="Email Address"
size="lg"
style={{ marginBottom: '0.5em' }}
/>
<Input
value={password}
onChange={setPassword}
placeholder="Password"
type="password"
size="lg"
style={{ marginBottom: '0.5em' }}
/>
<Input
value={confirmPassword}
onChange={setConfirmPassword}
placeholder="Confirm Password"
type="password"
size="lg"
style={{ marginBottom: '0.5em' }}
/>
{errorMessage && (
<Message showIcon type="error">
{errorMessage}
</Message>
)}
<Button
appearance="primary"
onClick={async () => {
setErrorMessage('')
const result = await signUpEmailPassword(email, password)
console.log(result)
}} }}
block
> >
Sign up A email has been sent to {email}. Please follow the link to verify your email address and to
</Button> complete your registration.
<Button as={NavLink} to="/sign-up" block appearance="link"> </Modal>
<SimpleGrid cols={1} spacing={6}>
<TextInput
placeholder="First name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
<TextInput
placeholder="Last name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
<TextInput
type="email"
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<PasswordInput
placeholder="Confirm Password"
error={differentPassword}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<Button fullWidth onClick={signUp}>
Continue with email + password
</Button>
</SimpleGrid>
<Divider />
<AuthLink link="/sign-up" variant="white">
&#8592; Other Registration Options &#8592; Other Registration Options
</Button> </AuthLink>
</div> </>
) )
} }

View File

@@ -1,15 +1,17 @@
import { Divider } from '@mantine/core'
import React from 'react' import React from 'react'
import { NavLink } from 'react-router-dom' import AuthLink from '../components/AuthLink'
import { Button } from 'rsuite'
import EmailPasswordlessForm from '../components/SignUpServerlessForm'
import { EmailPasswordlessForm } from '../components/email-passwordless-form'
export const EmailPasswordless: React.FC = () => { export const EmailPasswordless: React.FC = () => {
return ( return (
<div> <>
<EmailPasswordlessForm /> <EmailPasswordlessForm />
<Button as={NavLink} to="/sign-up" block appearance="link"> <Divider />
<AuthLink link="/sign-up" variant="white">
&#8592; Other Registration Options &#8592; Other Registration Options
</Button> </AuthLink>
</div> </>
) )
} }

View File

@@ -1,52 +1,47 @@
import React from 'react' import React from 'react'
import { FaLock } from 'react-icons/fa' import { FaLock } from 'react-icons/fa'
import { Link, NavLink, Route, Routes } from 'react-router-dom' import { Link, Route, Routes } from 'react-router-dom'
import { Button, Divider, FlexboxGrid, IconButton, Panel } from 'rsuite'
import { Icon } from '@rsuite/icons' import OAuthLinks from '../components/OauthLinks'
import { OAuthLinks } from '../components'
import { VerificationEmailSent } from '../verification-email-sent'
import { EmailPassword } from './email-password' import { EmailPassword } from './email-password'
import { EmailPasswordless } from './email-passwordless' import { EmailPasswordless } from './email-passwordless'
import AuthLayout from '../components/AuthLayout'
import { Center, Text, Anchor, Divider } from '@mantine/core'
import AuthLink from '../components/AuthLink'
const Index: React.FC = () => ( const Index: React.FC = () => (
<div> <>
<OAuthLinks /> <OAuthLinks />
<Divider /> <Divider my="sm" />
<IconButton <AuthLink icon={<FaLock />} variant="outline" link="/sign-up/email-passwordless">
block
icon={<Icon as={FaLock} />}
appearance="ghost"
as={NavLink}
to="/sign-up/email-passwordless"
>
Continue with passwordless email Continue with passwordless email
</IconButton> </AuthLink>
<Button as={NavLink} to="/sign-up/email-password" block appearance="link"> <AuthLink variant="subtle" link="/sign-up/email-password">
Continue with email + password Continue with email + password
</Button> </AuthLink>
</div> </>
) )
export const SignUpPage: React.FC = () => { export const SignUpPage: React.FC = () => {
return ( return (
<div style={{ textAlign: 'center' }}> <AuthLayout
<FlexboxGrid justify="center"> title="Sign up"
<FlexboxGrid.Item colspan={12}> footer={
<Panel header={<h2>Sign up</h2>} bordered> <Center>
<Routes> <Text>
<Route path="/" element={<Index />} /> Already have an account?{' '}
<Route path="/email-password" element={<EmailPassword />} /> <Anchor component={Link} to="/sign-in">
<Route path="/email-passwordless" element={<EmailPasswordless />} /> Log in
<Route path="/verification-email-sent" element={<VerificationEmailSent />} /> </Anchor>
</Routes> </Text>
</Panel> </Center>
</FlexboxGrid.Item> }
</FlexboxGrid> >
<Divider /> <Routes>
Already have an account? <Link to="/sign-in">Log in</Link> <Route path="/" element={<Index />} />
</div> <Route path="/email-password" element={<EmailPassword />} />
<Route path="/email-passwordless" element={<EmailPasswordless />} />
</Routes>
</AuthLayout>
) )
} }

View File

@@ -1,20 +0,0 @@
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuthenticated } from '@nhost/react'
export const VerificationEmailSent: React.FC = () => {
const isAuthenticated = useAuthenticated()
const navigate = useNavigate()
useEffect(() => {
if (isAuthenticated) navigate('/')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated])
return (
<div>
A verification email has been sent. Please check your inbox and follow the link to complete
authentication. This page with automatically redirect to the authenticated home page once the
email has been verified.
</div>
)
}

View File

@@ -1,5 +1,20 @@
# @nhost/apollo # @nhost/apollo
## 0.5.2
### Patch Changes
- Updated dependencies [65a3061]
- @nhost/core@0.5.2
## 0.5.1
### Patch Changes
- Updated dependencies [58fa2a2]
- Updated dependencies [58fa2a2]
- @nhost/core@0.5.1
## 0.5.0 ## 0.5.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/apollo", "name": "@nhost/apollo",
"version": "0.5.0", "version": "0.5.2",
"description": "Nhost Apollo Client library", "description": "Nhost Apollo Client library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,24 @@
# @nhost/core # @nhost/core
## 0.5.2
### Patch Changes
- 65a3061: correct cookie storage type
## 0.5.1
### Patch Changes
- 58fa2a2: Improve loading status
The `loading` status indicates the authentication is not yet known to the client when it starts. Once the client is ready, the authentication status is either signed in, or signed out.
When the user was trying to authenticate, the `loading` status was set to `true` until the result of the authentication was known.
The client now only return `loading: true` on startup, and in no other cases.
- 58fa2a2: Look for a valid refresh token both the URL and local storage
When auto-signin was activated, the client was not taking into account the refresh token in the URL if a token was already stored locally.
The user was then not able to authenticate from a link when the refresh token stored locally was invalid or expired.
When auto-signin is activated, the client now checks and tries tokens from both the URL and the local storage, starting with the URL.
## 0.5.0 ## 0.5.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/core", "name": "@nhost/core",
"version": "0.5.0", "version": "0.5.2",
"description": "Nhost core client library", "description": "Nhost core client library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -75,38 +75,31 @@ export const createAuthMachine = ({
type: 'parallel', type: 'parallel',
states: { states: {
authentication: { authentication: {
initial: 'importingRefreshToken', initial: 'starting',
on: { on: {
SESSION_UPDATE: [ SESSION_UPDATE: [
{ {
cond: 'hasSession', cond: 'hasSession',
actions: ['saveSession', 'persist', 'resetTimer', 'reportTokenChanged'], actions: ['saveSession', 'resetTimer', 'reportTokenChanged'],
target: '.signedIn' target: '.signedIn'
} }
] ]
}, },
states: { states: {
importingRefreshToken: { starting: {
tags: ['loading'],
always: { cond: 'isSignedIn', target: 'signedIn' }, always: { cond: 'isSignedIn', target: 'signedIn' },
invoke: { invoke: {
id: 'importRefreshToken', id: 'importRefreshToken',
src: 'importRefreshToken', src: 'importRefreshToken',
onDone: { actions: 'saveRefreshToken', target: 'starting' }, onDone: {
actions: ['saveSession', 'reportTokenChanged'],
target: 'signedIn'
},
onError: { actions: ['saveAuthenticationError'], target: 'signedOut' } onError: { actions: ['saveAuthenticationError'], target: 'signedOut' }
} }
}, },
starting: {
always: [
{
cond: 'hasRefreshTokenWithoutSession',
target: 'authenticating.token'
},
{ cond: 'hasAuthenticationError', target: 'signedOut.failed' },
'signedOut'
]
},
signedOut: { signedOut: {
tags: ['ready'],
initial: 'noErrors', initial: 'noErrors',
entry: 'reportSignedOut', entry: 'reportSignedOut',
states: { states: {
@@ -236,7 +229,7 @@ export const createAuthMachine = ({
src: 'signInPasswordlessSmsOtp', src: 'signInPasswordlessSmsOtp',
id: 'authenticatePasswordlessSmsOtp', id: 'authenticatePasswordlessSmsOtp',
onDone: { onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'], actions: ['saveSession', 'reportTokenChanged'],
target: '#nhost.authentication.signedIn' target: '#nhost.authentication.signedIn'
}, },
onError: { onError: {
@@ -256,7 +249,7 @@ export const createAuthMachine = ({
target: '#nhost.authentication.signedOut.needsMfa' target: '#nhost.authentication.signedOut.needsMfa'
}, },
{ {
actions: ['saveSession', 'persist', 'reportTokenChanged'], actions: ['saveSession', 'reportTokenChanged'],
target: '#nhost.authentication.signedIn' target: '#nhost.authentication.signedIn'
} }
], ],
@@ -272,26 +265,12 @@ export const createAuthMachine = ({
] ]
} }
}, },
token: {
invoke: {
src: 'refreshToken',
id: 'signInToken',
onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged', 'broadcastToken'],
target: '#nhost.authentication.signedIn'
},
onError: {
actions: 'saveAuthenticationError',
target: '#nhost.authentication.signedOut.failed.server'
}
}
},
anonymous: { anonymous: {
invoke: { invoke: {
src: 'signInAnonymous', src: 'signInAnonymous',
id: 'authenticateAnonymously', id: 'authenticateAnonymously',
onDone: { onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'], actions: ['saveSession', 'reportTokenChanged'],
target: '#nhost.authentication.signedIn' target: '#nhost.authentication.signedIn'
}, },
onError: { onError: {
@@ -307,7 +286,7 @@ export const createAuthMachine = ({
src: 'signInMfaTotp', src: 'signInMfaTotp',
id: 'signInMfaTotp', id: 'signInMfaTotp',
onDone: { onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'], actions: ['saveSession', 'reportTokenChanged'],
target: '#nhost.authentication.signedIn' target: '#nhost.authentication.signedIn'
}, },
onError: { onError: {
@@ -329,7 +308,7 @@ export const createAuthMachine = ({
{ {
cond: 'hasSession', cond: 'hasSession',
target: '#nhost.authentication.signedIn', target: '#nhost.authentication.signedIn',
actions: ['saveSession', 'persist', 'reportTokenChanged'] actions: ['saveSession', 'reportTokenChanged']
}, },
{ {
target: '#nhost.authentication.signedOut.needsEmailVerification' target: '#nhost.authentication.signedOut.needsEmailVerification'
@@ -347,11 +326,9 @@ export const createAuthMachine = ({
] ]
} }
}, },
signedIn: { signedIn: {
tags: ['ready'],
type: 'parallel', type: 'parallel',
entry: ['reportSignedIn', 'cleanUrl'], entry: ['reportSignedIn', 'cleanUrl', 'broadcastToken'],
on: { on: {
SIGNOUT: '#nhost.authentication.signedOut.signingOut', SIGNOUT: '#nhost.authentication.signedOut.signingOut',
DEANONYMIZE: { DEANONYMIZE: {
@@ -401,12 +378,7 @@ export const createAuthMachine = ({
src: 'refreshToken', src: 'refreshToken',
id: 'refreshToken', id: 'refreshToken',
onDone: { onDone: {
actions: [ actions: ['saveSession', 'resetTimer', 'reportTokenChanged'],
'saveSession',
'persist',
'resetTimer',
'reportTokenChanged'
],
target: 'pending' target: 'pending'
}, },
onError: [ onError: [
@@ -455,7 +427,7 @@ export const createAuthMachine = ({
src: 'refreshToken', src: 'refreshToken',
id: 'authenticateWithToken', id: 'authenticateWithToken',
onDone: { onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'], actions: ['saveSession', 'reportTokenChanged'],
target: ['#nhost.authentication.signedIn', 'idle.noErrors'] target: ['#nhost.authentication.signedIn', 'idle.noErrors']
}, },
onError: [ onError: [
@@ -484,13 +456,28 @@ export const createAuthMachine = ({
} }
}), }),
// * Save session in the context, and persist the refresh token and the jwt expiration outside of the machine
saveSession: assign({ saveSession: assign({
user: (_, e: any) => e.data?.session?.user, user: (_, { data }: any) => data?.session?.user,
accessToken: (_, e) => ({ accessToken: (_, { data }: any) => {
value: e.data?.session?.accessToken, if (data.session.accessTokenExpiresIn) {
expiresAt: new Date(Date.now() + e.data?.session?.accessTokenExpiresIn * 1_000) const nextRefresh = new Date(
}), Date.now() + data.session.accessTokenExpiresIn * 1_000
refreshToken: (_, e) => ({ value: e.data?.session?.refreshToken }) ).toISOString()
storageSetter(NHOST_JWT_EXPIRES_AT_KEY, nextRefresh)
} else {
storageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
}
return {
value: data?.session?.accessToken,
expiresAt: new Date(Date.now() + data?.session?.accessTokenExpiresIn * 1_000)
}
},
refreshToken: (_, { data }: any) => {
storageSetter(NHOST_REFRESH_TOKEN_KEY, data.session.refreshToken)
return { value: data?.session?.refreshToken }
}
}), }),
saveMfaTicket: assign({ saveMfaTicket: assign({
mfa: (_, e: any) => e.data?.mfa ?? null mfa: (_, e: any) => e.data?.mfa ?? null
@@ -543,22 +530,7 @@ export const createAuthMachine = ({
saveNoMfaTicketError: assign({ saveNoMfaTicketError: assign({
errors: ({ errors }) => ({ ...errors, registration: NO_MFA_TICKET_ERROR }) errors: ({ errors }) => ({ ...errors, registration: NO_MFA_TICKET_ERROR })
}), }),
saveRefreshToken: assign({
accessToken: (ctx, e: any) => ({ ...ctx.accessToken, expiresAt: e.data.expiresAt }),
refreshToken: (ctx, e: any) => ({ ...ctx.refreshToken, value: e.data.refreshToken })
}),
// * Persist the refresh token and the jwt expiration outside of the machine
persist: (_, { data }: any) => {
storageSetter(NHOST_REFRESH_TOKEN_KEY, data.session.refreshToken)
if (data.session.accessTokenExpiresIn) {
const nextRefresh = new Date(
Date.now() + data.session.accessTokenExpiresIn * 1_000
).toISOString()
storageSetter(NHOST_JWT_EXPIRES_AT_KEY, nextRefresh)
} else {
storageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
}
},
destroyRefreshToken: assign({ destroyRefreshToken: assign({
refreshToken: (_) => { refreshToken: (_) => {
storageSetter(NHOST_REFRESH_TOKEN_KEY, null) storageSetter(NHOST_REFRESH_TOKEN_KEY, null)
@@ -591,12 +563,9 @@ export const createAuthMachine = ({
guards: { guards: {
isSignedIn: (ctx) => !!ctx.user && !!ctx.refreshToken.value && !!ctx.accessToken.value, isSignedIn: (ctx) => !!ctx.user && !!ctx.refreshToken.value && !!ctx.accessToken.value,
hasRefreshTokenWithoutSession: (ctx) =>
!!ctx.refreshToken.value && !ctx.user && !ctx.accessToken.value,
noToken: (ctx) => !ctx.refreshToken.value, noToken: (ctx) => !ctx.refreshToken.value,
noMfaTicket: (ctx, { ticket }) => !ticket && !ctx.mfa?.ticket, noMfaTicket: (ctx, { ticket }) => !ticket && !ctx.mfa?.ticket,
hasRefreshToken: (ctx) => !!ctx.refreshToken.value, hasRefreshToken: (ctx) => !!ctx.refreshToken.value,
hasAuthenticationError: (ctx) => !!ctx.errors.authentication,
isAutoRefreshDisabled: () => !autoRefreshToken, isAutoRefreshDisabled: () => !autoRefreshToken,
refreshTimerShouldRefresh: (ctx) => { refreshTimerShouldRefresh: (ctx) => {
const { expiresAt } = ctx.accessToken const { expiresAt } = ctx.accessToken
@@ -686,15 +655,17 @@ export const createAuthMachine = ({
}), }),
importRefreshToken: async () => { importRefreshToken: async () => {
const stringExpiresAt = await storageGetter(NHOST_JWT_EXPIRES_AT_KEY) let error: ValidationErrorPayload | null = null
const expiresAt = stringExpiresAt ? new Date(stringExpiresAt) : null
let refreshToken = await storageGetter(NHOST_REFRESH_TOKEN_KEY)
if (autoSignIn) { if (autoSignIn) {
const urlToken = getParameterByName('refreshToken') || null const urlToken = getParameterByName('refreshToken') || null
if (urlToken) { if (urlToken) {
if (!refreshToken) { try {
// ? Which takes precedence? localStorage or the url? const session = await postRequest('/token', {
refreshToken = urlToken refreshToken: urlToken
})
return { session }
} catch (exception) {
error = (exception as { error: ValidationErrorPayload }).error
} }
} else { } else {
const error = getParameterByName('error') const error = getParameterByName('error')
@@ -709,14 +680,19 @@ export const createAuthMachine = ({
} }
} }
} }
return refreshToken const storageToken = await storageGetter(NHOST_REFRESH_TOKEN_KEY)
? { if (storageToken) {
refreshToken, try {
expiresAt const session = await postRequest('/token', {
} refreshToken: storageToken
: Promise.reject<{ error: ValidationErrorPayload }>({
error: null
}) })
return { session }
} catch (exception) {
error = (exception as { error: ValidationErrorPayload }).error
}
}
return Promise.reject<{ error: ValidationErrorPayload }>({ error })
} }
} }
} }

View File

@@ -5,19 +5,9 @@ export interface Typegen0 {
eventsCausingActions: { eventsCausingActions: {
saveSession: saveSession:
| 'SESSION_UPDATE' | 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| 'done.invoke.authenticatePasswordlessSmsOtp' | 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword' | 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser'
| 'done.invoke.refreshToken'
| 'done.invoke.authenticateWithToken'
persist:
| 'SESSION_UPDATE'
| 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously' | 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp' | 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser' | 'done.invoke.registerUser'
@@ -26,22 +16,20 @@ export interface Typegen0 {
resetTimer: 'SESSION_UPDATE' | 'done.invoke.refreshToken' | '' resetTimer: 'SESSION_UPDATE' | 'done.invoke.refreshToken' | ''
reportTokenChanged: reportTokenChanged:
| 'SESSION_UPDATE' | 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| 'done.invoke.authenticatePasswordlessSmsOtp' | 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword' | 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously' | 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp' | 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser' | 'done.invoke.registerUser'
| 'done.invoke.refreshToken' | 'done.invoke.refreshToken'
| 'done.invoke.authenticateWithToken' | 'done.invoke.authenticateWithToken'
saveRefreshToken: 'done.invoke.importRefreshToken'
saveAuthenticationError: saveAuthenticationError:
| 'error.platform.importRefreshToken' | 'error.platform.importRefreshToken'
| 'error.platform.authenticatePasswordlessEmail' | 'error.platform.authenticatePasswordlessEmail'
| 'error.platform.authenticatePasswordlessSms' | 'error.platform.authenticatePasswordlessSms'
| 'error.platform.authenticatePasswordlessSmsOtp' | 'error.platform.authenticatePasswordlessSmsOtp'
| 'error.platform.authenticateUserWithPassword' | 'error.platform.authenticateUserWithPassword'
| 'error.platform.signInToken'
| 'error.platform.authenticateAnonymously' | 'error.platform.authenticateAnonymously'
| 'error.platform.signInMfaTotp' | 'error.platform.signInMfaTotp'
saveInvalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL' saveInvalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL'
@@ -51,39 +39,50 @@ export interface Typegen0 {
saveInvalidSignUpPassword: 'SIGNUP_EMAIL_PASSWORD' saveInvalidSignUpPassword: 'SIGNUP_EMAIL_PASSWORD'
saveNoMfaTicketError: 'SIGNIN_MFA_TOTP' saveNoMfaTicketError: 'SIGNIN_MFA_TOTP'
saveMfaTicket: 'done.invoke.authenticateUserWithPassword' saveMfaTicket: 'done.invoke.authenticateUserWithPassword'
broadcastToken: 'done.invoke.signInToken'
saveRegisrationError: 'error.platform.registerUser' saveRegisrationError: 'error.platform.registerUser'
saveRefreshAttempt: 'error.platform.refreshToken' saveRefreshAttempt: 'error.platform.refreshToken'
reportSignedOut: reportSignedOut: 'error.platform.importRefreshToken' | 'error.platform.authenticateWithToken'
| 'error.platform.importRefreshToken'
| ''
| 'error.platform.authenticateWithToken'
resetAuthenticationError: 'xstate.init' resetAuthenticationError: 'xstate.init'
destroyRefreshToken: 'xstate.init' destroyRefreshToken: 'xstate.init'
clearContextExceptRefreshToken: 'SIGNOUT' clearContextExceptRefreshToken: 'SIGNOUT'
resetSignUpError: 'SIGNUP_EMAIL_PASSWORD' resetSignUpError: 'SIGNUP_EMAIL_PASSWORD'
reportSignedIn: reportSignedIn:
| 'SESSION_UPDATE' | 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| '' | ''
| 'done.invoke.authenticatePasswordlessSmsOtp' | 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword' | 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously' | 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp' | 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser' | 'done.invoke.registerUser'
| 'done.invoke.authenticateWithToken' | 'done.invoke.authenticateWithToken'
cleanUrl: cleanUrl:
| 'SESSION_UPDATE' | 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| ''
| 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser'
| 'done.invoke.authenticateWithToken'
broadcastToken:
| 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| '' | ''
| 'done.invoke.authenticatePasswordlessSmsOtp' | 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword' | 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously' | 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp' | 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser' | 'done.invoke.registerUser'
| 'done.invoke.authenticateWithToken' | 'done.invoke.authenticateWithToken'
} }
internalEvents: { internalEvents: {
'done.invoke.importRefreshToken': {
type: 'done.invoke.importRefreshToken'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.authenticatePasswordlessSmsOtp': { 'done.invoke.authenticatePasswordlessSmsOtp': {
type: 'done.invoke.authenticatePasswordlessSmsOtp' type: 'done.invoke.authenticatePasswordlessSmsOtp'
data: unknown data: unknown
@@ -94,11 +93,6 @@ export interface Typegen0 {
data: unknown data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.' __tip: 'See the XState TS docs to learn how to strongly type this.'
} }
'done.invoke.signInToken': {
type: 'done.invoke.signInToken'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.authenticateAnonymously': { 'done.invoke.authenticateAnonymously': {
type: 'done.invoke.authenticateAnonymously' type: 'done.invoke.authenticateAnonymously'
data: unknown data: unknown
@@ -125,11 +119,6 @@ export interface Typegen0 {
__tip: 'See the XState TS docs to learn how to strongly type this.' __tip: 'See the XState TS docs to learn how to strongly type this.'
} }
'': { type: '' } '': { type: '' }
'done.invoke.importRefreshToken': {
type: 'done.invoke.importRefreshToken'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'error.platform.importRefreshToken': { 'error.platform.importRefreshToken': {
type: 'error.platform.importRefreshToken' type: 'error.platform.importRefreshToken'
data: unknown data: unknown
@@ -150,7 +139,6 @@ export interface Typegen0 {
type: 'error.platform.authenticateUserWithPassword' type: 'error.platform.authenticateUserWithPassword'
data: unknown data: unknown
} }
'error.platform.signInToken': { type: 'error.platform.signInToken'; data: unknown }
'error.platform.authenticateAnonymously': { 'error.platform.authenticateAnonymously': {
type: 'error.platform.authenticateAnonymously' type: 'error.platform.authenticateAnonymously'
data: unknown data: unknown
@@ -190,13 +178,10 @@ export interface Typegen0 {
signInPasswordlessSms: 'done.invoke.authenticatePasswordlessSms' signInPasswordlessSms: 'done.invoke.authenticatePasswordlessSms'
signInPasswordlessSmsOtp: 'done.invoke.authenticatePasswordlessSmsOtp' signInPasswordlessSmsOtp: 'done.invoke.authenticatePasswordlessSmsOtp'
signInPassword: 'done.invoke.authenticateUserWithPassword' signInPassword: 'done.invoke.authenticateUserWithPassword'
refreshToken:
| 'done.invoke.signInToken'
| 'done.invoke.refreshToken'
| 'done.invoke.authenticateWithToken'
signInAnonymous: 'done.invoke.authenticateAnonymously' signInAnonymous: 'done.invoke.authenticateAnonymously'
signInMfaTotp: 'done.invoke.signInMfaTotp' signInMfaTotp: 'done.invoke.signInMfaTotp'
registerUser: 'done.invoke.registerUser' registerUser: 'done.invoke.registerUser'
refreshToken: 'done.invoke.refreshToken' | 'done.invoke.authenticateWithToken'
} }
missingImplementations: { missingImplementations: {
actions: never actions: never
@@ -206,7 +191,6 @@ export interface Typegen0 {
} }
eventsCausingServices: { eventsCausingServices: {
importRefreshToken: 'xstate.init' importRefreshToken: 'xstate.init'
refreshToken: '' | 'TRY_TOKEN'
signInPassword: 'SIGNIN_PASSWORD' signInPassword: 'SIGNIN_PASSWORD'
signInPasswordlessEmail: 'SIGNIN_PASSWORDLESS_EMAIL' signInPasswordlessEmail: 'SIGNIN_PASSWORDLESS_EMAIL'
signInPasswordlessSms: 'SIGNIN_PASSWORDLESS_SMS' signInPasswordlessSms: 'SIGNIN_PASSWORDLESS_SMS'
@@ -215,12 +199,11 @@ export interface Typegen0 {
signInAnonymous: 'SIGNIN_ANONYMOUS' signInAnonymous: 'SIGNIN_ANONYMOUS'
signInMfaTotp: 'SIGNIN_MFA_TOTP' signInMfaTotp: 'SIGNIN_MFA_TOTP'
signout: 'SIGNOUT' signout: 'SIGNOUT'
refreshToken: '' | 'TRY_TOKEN'
} }
eventsCausingGuards: { eventsCausingGuards: {
hasSession: 'SESSION_UPDATE' | 'done.invoke.registerUser' hasSession: 'SESSION_UPDATE' | 'done.invoke.registerUser'
isSignedIn: '' | 'error.platform.authenticateWithToken' isSignedIn: '' | 'error.platform.authenticateWithToken'
hasRefreshTokenWithoutSession: ''
hasAuthenticationError: ''
invalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL' | 'SIGNUP_EMAIL_PASSWORD' invalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL' | 'SIGNUP_EMAIL_PASSWORD'
invalidPassword: 'SIGNIN_PASSWORD' | 'SIGNUP_EMAIL_PASSWORD' invalidPassword: 'SIGNIN_PASSWORD' | 'SIGNUP_EMAIL_PASSWORD'
invalidPhoneNumber: 'SIGNIN_PASSWORDLESS_SMS' | 'SIGNIN_PASSWORDLESS_SMS_OTP' invalidPhoneNumber: 'SIGNIN_PASSWORDLESS_SMS' | 'SIGNIN_PASSWORDLESS_SMS_OTP'
@@ -235,7 +218,6 @@ export interface Typegen0 {
eventsCausingDelays: {} eventsCausingDelays: {}
matchesStates: matchesStates:
| 'authentication' | 'authentication'
| 'authentication.importingRefreshToken'
| 'authentication.starting' | 'authentication.starting'
| 'authentication.signedOut' | 'authentication.signedOut'
| 'authentication.signedOut.noErrors' | 'authentication.signedOut.noErrors'
@@ -255,7 +237,6 @@ export interface Typegen0 {
| 'authentication.authenticating.passwordlessSms' | 'authentication.authenticating.passwordlessSms'
| 'authentication.authenticating.passwordlessSmsOtp' | 'authentication.authenticating.passwordlessSmsOtp'
| 'authentication.authenticating.password' | 'authentication.authenticating.password'
| 'authentication.authenticating.token'
| 'authentication.authenticating.anonymous' | 'authentication.authenticating.anonymous'
| 'authentication.authenticating.mfa' | 'authentication.authenticating.mfa'
| 'authentication.authenticating.mfa.totp' | 'authentication.authenticating.mfa.totp'
@@ -278,7 +259,6 @@ export interface Typegen0 {
| 'token.running' | 'token.running'
| { | {
authentication?: authentication?:
| 'importingRefreshToken'
| 'starting' | 'starting'
| 'signedOut' | 'signedOut'
| 'authenticating' | 'authenticating'
@@ -304,7 +284,6 @@ export interface Typegen0 {
| 'passwordlessSms' | 'passwordlessSms'
| 'passwordlessSmsOtp' | 'passwordlessSmsOtp'
| 'password' | 'password'
| 'token'
| 'anonymous' | 'anonymous'
| 'mfa' | 'mfa'
| { mfa?: 'totp' } | { mfa?: 'totp' }
@@ -323,5 +302,5 @@ export interface Typegen0 {
} }
token?: 'idle' | 'running' | { idle?: 'noErrors' | 'error' } token?: 'idle' | 'running' | { idle?: 'noErrors' | 'error' }
} }
tags: 'ready' tags: 'loading'
} }

View File

@@ -29,39 +29,13 @@ const defaultClientStorageSetter: StorageSetter = (key, value) => {
} }
} }
// TODO see https://github.com/nhost/nhost/pull/507#discussion_r865873389
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const checkStorageAccessors = (
clientStorage: ClientStorage,
accessors: Array<keyof ClientStorage>
) => {
accessors.forEach((key) => {
if (typeof clientStorage[key] !== 'function') {
console.error(`clientStorage.${key} is not a function`)
}
})
}
export const localStorageGetter = ( export const localStorageGetter = (
clientStorageType: ClientStorageType, clientStorageType: ClientStorageType,
clientStorage?: ClientStorage clientStorage?: ClientStorage
): StorageGetter => { ): StorageGetter => {
if (!clientStorage || clientStorageType === 'localStorage' || clientStorageType === 'web') { if (clientStorageType === 'localStorage' || clientStorageType === 'web') {
return defaultClientStorageGetter return defaultClientStorageGetter
} }
if (clientStorageType === 'react-native') {
// checkStorageAccessors(clientStorage, ['getItem'])
return (key) => clientStorage.getItem?.(key)
}
if (clientStorageType === 'capacitor') {
// checkStorageAccessors(clientStorage, ['get'])
return (key) => clientStorage.get?.({ key })
}
if (clientStorageType === 'expo-secure-storage') {
// checkStorageAccessors(clientStorage, ['getItemAsync'])
return (key) => clientStorage.getItemAsync?.(key)
}
if (clientStorageType === 'cookie') { if (clientStorageType === 'cookie') {
return (key) => { return (key) => {
if (isBrowser) { if (isBrowser) {
@@ -71,6 +45,20 @@ export const localStorageGetter = (
} }
} }
} }
if (!clientStorage) {
throw Error(
`clientStorageType is set to '${clientStorageType}' but no clienStorage has been given`
)
}
if (clientStorageType === 'react-native') {
return (key) => clientStorage.getItem?.(key)
}
if (clientStorageType === 'capacitor') {
return (key) => clientStorage.get?.({ key })
}
if (clientStorageType === 'expo-secure-storage') {
return (key) => clientStorage.getItemAsync?.(key)
}
if (clientStorageType === 'custom') { if (clientStorageType === 'custom') {
if (clientStorage.getItem && clientStorage.removeItem) { if (clientStorage.getItem && clientStorage.removeItem) {
return clientStorage.getItem return clientStorage.getItem
@@ -89,25 +77,9 @@ export const localStorageSetter = (
clientStorageType: ClientStorageType, clientStorageType: ClientStorageType,
clientStorage?: ClientStorage clientStorage?: ClientStorage
): StorageSetter => { ): StorageSetter => {
if (!clientStorage || clientStorageType === 'localStorage' || clientStorageType === 'web') { if (clientStorageType === 'localStorage' || clientStorageType === 'web') {
return defaultClientStorageSetter return defaultClientStorageSetter
} }
if (clientStorageType === 'react-native') {
// checkStorageAccessors(clientStorage, ['setItem', 'removeItem'])
return (key, value) =>
value ? clientStorage.setItem?.(key, value) : clientStorage.removeItem?.(key)
}
if (clientStorageType === 'capacitor') {
// checkStorageAccessors(clientStorage, ['set', 'remove'])
return (key, value) =>
value ? clientStorage.set?.({ key, value }) : clientStorage.remove?.({ key })
}
if (clientStorageType === 'expo-secure-storage') {
// checkStorageAccessors(clientStorage, ['setItemAsync', 'deleteItemAsync'])
return async (key, value) =>
value ? clientStorage.setItemAsync?.(key, value) : clientStorage.deleteItemAsync?.(key)
}
if (clientStorageType === 'cookie') { if (clientStorageType === 'cookie') {
return (key, value) => { return (key, value) => {
if (isBrowser) { if (isBrowser) {
@@ -119,6 +91,23 @@ export const localStorageSetter = (
} }
} }
} }
if (!clientStorage) {
throw Error(
`clientStorageType is set to '${clientStorageType}' but no clienStorage has been given`
)
}
if (clientStorageType === 'react-native') {
return (key, value) =>
value ? clientStorage.setItem?.(key, value) : clientStorage.removeItem?.(key)
}
if (clientStorageType === 'capacitor') {
return (key, value) =>
value ? clientStorage.set?.({ key, value }) : clientStorage.remove?.({ key })
}
if (clientStorageType === 'expo-secure-storage') {
return async (key, value) =>
value ? clientStorage.setItemAsync?.(key, value) : clientStorage.deleteItemAsync?.(key)
}
if (clientStorageType === 'custom') { if (clientStorageType === 'custom') {
if (!clientStorage.removeItem) { if (!clientStorage.removeItem) {
throw Error( throw Error(

View File

@@ -1,5 +1,28 @@
# @nhost/hasura-auth-js # @nhost/hasura-auth-js
## 1.1.4
### Patch Changes
- Updated dependencies [65a3061]
- @nhost/core@0.5.2
## 1.1.3
### Patch Changes
- 58fa2a2: Improve loading status
The `loading` status indicates the authentication is not yet known to the client when it starts. Once the client is ready, the authentication status is either signed in, or signed out.
When the user was trying to authenticate, the `loading` status was set to `true` until the result of the authentication was known.
The client now only return `loading: true` on startup, and in no other cases.
- 58fa2a2: Look for a valid refresh token both the URL and local storage
When auto-signin was activated, the client was not taking into account the refresh token in the URL if a token was already stored locally.
The user was then not able to authenticate from a link when the refresh token stored locally was invalid or expired.
When auto-signin is activated, the client now checks and tries tokens from both the URL and the local storage, starting with the URL.
- Updated dependencies [58fa2a2]
- Updated dependencies [58fa2a2]
- @nhost/core@0.5.1
## 1.1.2 ## 1.1.2
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/hasura-auth-js", "name": "@nhost/hasura-auth-js",
"version": "1.1.2", "version": "1.1.4",
"description": "Hasura-auth client", "description": "Hasura-auth client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -745,7 +745,7 @@ export class HasuraAuthClient {
if (!interpreter) { if (!interpreter) {
throw Error('Auth interpreter not set') throw Error('Auth interpreter not set')
} }
if (interpreter.state.hasTag('ready')) { if (!interpreter.state.hasTag('loading')) {
return Promise.resolve(interpreter) return Promise.resolve(interpreter)
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -754,7 +754,7 @@ export class HasuraAuthClient {
TIMEOUT_IN_SECONS * 1_000 TIMEOUT_IN_SECONS * 1_000
) )
interpreter.onTransition((state) => { interpreter.onTransition((state) => {
if (state.hasTag('ready')) { if (!state.hasTag('loading')) {
clearTimeout(timer) clearTimeout(timer)
return resolve(interpreter) return resolve(interpreter)
} }
@@ -763,7 +763,7 @@ export class HasuraAuthClient {
} }
private isReady() { private isReady() {
return !!this._client.interpreter?.state?.hasTag('ready') return !this._client.interpreter?.state?.hasTag('loading')
} }
get client() { get client() {

View File

@@ -1,5 +1,29 @@
# @nhost/nextjs # @nhost/nextjs
## 1.2.2
### Patch Changes
- @nhost/nhost-js@1.1.9
- @nhost/react@0.7.2
## 1.2.1
### Patch Changes
- 58fa2a2: Improve loading status
The `loading` status indicates the authentication is not yet known to the client when it starts. Once the client is ready, the authentication status is either signed in, or signed out.
When the user was trying to authenticate, the `loading` status was set to `true` until the result of the authentication was known.
The client now only return `loading: true` on startup, and in no other cases.
- 58fa2a2: Look for a valid refresh token both the URL and local storage
When auto-signin was activated, the client was not taking into account the refresh token in the URL if a token was already stored locally.
The user was then not able to authenticate from a link when the refresh token stored locally was invalid or expired.
When auto-signin is activated, the client now checks and tries tokens from both the URL and the local storage, starting with the URL.
- Updated dependencies [58fa2a2]
- Updated dependencies [58fa2a2]
- @nhost/react@0.7.1
- @nhost/nhost-js@1.1.8
## 1.2.0 ## 1.2.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/nextjs", "name": "@nhost/nextjs",
"version": "1.2.0", "version": "1.2.2",
"description": "Nhost NextJS library", "description": "Nhost NextJS library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -46,8 +46,9 @@ export const createServerSideClient = async (
autoSignIn: true autoSignIn: true
}) })
await waitFor(nhost.auth.client.interpreter!, (state: StateFrom<AuthMachine>) => await waitFor(
state.hasTag('ready') nhost.auth.client.interpreter!,
(state: StateFrom<AuthMachine>) => !state.hasTag('loading')
) )
return nhost return nhost
} }

View File

@@ -45,7 +45,7 @@ export const getNhostSession = async (
backendUrl: string, backendUrl: string,
context: GetServerSidePropsContext context: GetServerSidePropsContext
): Promise<NhostSession | null> => { ): Promise<NhostSession | null> => {
const nhost = await createServerSideClient(backendUrl, context as any) const nhost = await createServerSideClient(backendUrl, context)
const { accessToken, refreshToken, user } = nhost.auth.client.interpreter!.state.context const { accessToken, refreshToken, user } = nhost.auth.client.interpreter!.state.context
return nhost.auth.isAuthenticated() return nhost.auth.isAuthenticated()
? { ? {

View File

@@ -1,5 +1,19 @@
# @nhost/nhost-js # @nhost/nhost-js
## 1.1.9
### Patch Changes
- @nhost/hasura-auth-js@1.1.4
## 1.1.8
### Patch Changes
- Updated dependencies [58fa2a2]
- Updated dependencies [58fa2a2]
- @nhost/hasura-auth-js@1.1.3
## 1.1.7 ## 1.1.7
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/nhost-js", "name": "@nhost/nhost-js",
"version": "1.1.7", "version": "1.1.9",
"description": "Nhost JavaScript SDK", "description": "Nhost JavaScript SDK",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,21 @@
# @nhost/react-apollo # @nhost/react-apollo
## 4.2.2
### Patch Changes
- @nhost/apollo@0.5.2
- @nhost/react@0.7.2
## 4.2.1
### Patch Changes
- Updated dependencies [58fa2a2]
- Updated dependencies [58fa2a2]
- @nhost/react@0.7.1
- @nhost/apollo@0.5.1
## 4.2.0 ## 4.2.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react-apollo", "name": "@nhost/react-apollo",
"version": "4.2.0", "version": "4.2.2",
"description": "Nhost React Apollo client", "description": "Nhost React Apollo client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,25 @@
# @nhost/react # @nhost/react
## 0.7.2
### Patch Changes
- @nhost/nhost-js@1.1.9
## 0.7.1
### Patch Changes
- 58fa2a2: Improve loading status
The `loading` status indicates the authentication is not yet known to the client when it starts. Once the client is ready, the authentication status is either signed in, or signed out.
When the user was trying to authenticate, the `loading` status was set to `true` until the result of the authentication was known.
The client now only return `loading: true` on startup, and in no other cases.
- 58fa2a2: Look for a valid refresh token both the URL and local storage
When auto-signin was activated, the client was not taking into account the refresh token in the URL if a token was already stored locally.
The user was then not able to authenticate from a link when the refresh token stored locally was invalid or expired.
When auto-signin is activated, the client now checks and tries tokens from both the URL and the local storage, starting with the URL.
- @nhost/nhost-js@1.1.8
## 0.7.0 ## 0.7.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react", "name": "@nhost/react",
"version": "0.7.0", "version": "0.7.2",
"description": "Nhost React library", "description": "Nhost React library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -58,7 +58,7 @@ export const useNhostBackendUrl = () => {
*/ */
export const useAuthLoading = () => { export const useAuthLoading = () => {
const service = useAuthInterpreter() const service = useAuthInterpreter()
return useSelector(service, (state) => !state.hasTag('ready')) return useSelector(service, (state) => state.hasTag('loading'))
} }
/** /**
@@ -90,7 +90,7 @@ export const useAuthenticationStatus = () => {
service, service,
(state) => ({ (state) => ({
isAuthenticated: state.matches({ authentication: 'signedIn' }), isAuthenticated: state.matches({ authentication: 'signedIn' }),
isLoading: !state.hasTag('ready'), isLoading: state.hasTag('loading'),
error: state.context.errors.authentication || null, error: state.context.errors.authentication || null,
isError: state.matches({ authentication: { signedOut: 'failed' } }) isError: state.matches({ authentication: { signedOut: 'failed' } })
}), }),

287
pnpm-lock.yaml generated
View File

@@ -191,49 +191,49 @@ importers:
examples/react-apollo: examples/react-apollo:
specifiers: specifiers:
'@apollo/client': ^3.6.2 '@apollo/client': ^3.6.2
'@mantine/core': ^4.2.2
'@mantine/hooks': ^4.2.2
'@mantine/notifications': ^4.2.2
'@mantine/prism': ^4.2.2
'@nhost/core': workspace:* '@nhost/core': workspace:*
'@nhost/react': workspace:* '@nhost/react': workspace:*
'@nhost/react-apollo': workspace:* '@nhost/react-apollo': workspace:*
'@rsuite/icons': ^1.0.2
'@types/react': ^18.0.8 '@types/react': ^18.0.8
'@types/react-dom': ^18.0.3 '@types/react-dom': ^18.0.3
'@vitejs/plugin-react': ^1.3.1 '@vitejs/plugin-react': ^1.3.1
'@xstate/inspect': ^0.6.2 '@xstate/inspect': ^0.6.2
graphql: 15.7.2 graphql: 15.7.2
less: ^4.1.2
react: ^18.1.0 react: ^18.1.0
react-dom: ^18.1.0 react-dom: ^18.1.0
react-icons: ^4.3.1 react-icons: ^4.3.1
react-json-view: ^1.21.3
react-router: ^6.3.0 react-router: ^6.3.0
react-router-dom: ^6.3.0 react-router-dom: ^6.3.0
rsuite: ^5.10.0
typescript: ^4.6.3 typescript: ^4.6.3
vite: ^2.9.5 vite: ^2.9.5
ws: ^8.6.0 ws: ^8.6.0
xstate: ^4.31.0 xstate: ^4.31.0
dependencies: dependencies:
'@apollo/client': 3.6.2_graphql@15.7.2+react@18.1.0 '@apollo/client': 3.6.2_graphql@15.7.2+react@18.1.0
'@mantine/core': 4.2.2_ea9d0c51a4562a6544166c6277bf397c
'@mantine/hooks': 4.2.2_react@18.1.0
'@mantine/notifications': 4.2.2_af46c71ecf518c86fb9b76e8a89b700a
'@mantine/prism': 4.2.2_af46c71ecf518c86fb9b76e8a89b700a
'@nhost/core': link:../../packages/core '@nhost/core': link:../../packages/core
'@nhost/react': link:../../packages/react '@nhost/react': link:../../packages/react
'@nhost/react-apollo': link:../../packages/react-apollo '@nhost/react-apollo': link:../../packages/react-apollo
'@rsuite/icons': 1.0.2_react-dom@18.1.0+react@18.1.0
graphql: 15.7.2 graphql: 15.7.2
less: 4.1.2
react: 18.1.0 react: 18.1.0
react-dom: 18.1.0_react@18.1.0 react-dom: 18.1.0_react@18.1.0
react-icons: 4.3.1_react@18.1.0 react-icons: 4.3.1_react@18.1.0
react-json-view: 1.21.3_47ae209ab5c2ea051c9a2540426c6b2f
react-router: 6.3.0_react@18.1.0 react-router: 6.3.0_react@18.1.0
react-router-dom: 6.3.0_react-dom@18.1.0+react@18.1.0 react-router-dom: 6.3.0_react-dom@18.1.0+react@18.1.0
rsuite: 5.10.0_react-dom@18.1.0+react@18.1.0
devDependencies: devDependencies:
'@types/react': 18.0.8 '@types/react': 18.0.8
'@types/react-dom': 18.0.3 '@types/react-dom': 18.0.3
'@vitejs/plugin-react': 1.3.1 '@vitejs/plugin-react': 1.3.1
'@xstate/inspect': 0.6.5_ws@8.6.0+xstate@4.31.0 '@xstate/inspect': 0.6.5_ws@8.6.0+xstate@4.31.0
typescript: 4.6.3 typescript: 4.6.3
vite: 2.9.5_less@4.1.2 vite: 2.9.5
ws: 8.6.0 ws: 8.6.0
xstate: 4.31.0 xstate: 4.31.0
@@ -5189,10 +5189,6 @@ packages:
'@jridgewell/resolve-uri': 3.0.6 '@jridgewell/resolve-uri': 3.0.6
'@jridgewell/sourcemap-codec': 1.4.12 '@jridgewell/sourcemap-codec': 1.4.12
/@juggle/resize-observer/3.3.1:
resolution: {integrity: sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==}
dev: false
/@leichtgewicht/ip-codec/2.0.3: /@leichtgewicht/ip-codec/2.0.3:
resolution: {integrity: sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg==} resolution: {integrity: sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg==}
dev: false dev: false
@@ -5257,6 +5253,21 @@ packages:
react-transition-group: 4.4.2_react-dom@18.1.0+react@18.1.0 react-transition-group: 4.4.2_react-dom@18.1.0+react@18.1.0
dev: false dev: false
/@mantine/prism/4.2.2_af46c71ecf518c86fb9b76e8a89b700a:
resolution: {integrity: sha512-I3S7xAX74EQD8LNO/FuyR5EHNAhcNaWcXcEQZZITTYKbUv3/ieAcV8k5DybWa2FbFtVEhCSTrT+9YIjhIcGWOA==}
peerDependencies:
'@mantine/core': 4.2.2
'@mantine/hooks': 4.2.2
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@mantine/core': 4.2.2_ea9d0c51a4562a6544166c6277bf397c
'@mantine/hooks': 4.2.2_react@18.1.0
prism-react-renderer: 1.3.1_react@18.1.0
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
dev: false
/@mantine/ssr/4.2.2_b49546c5ecaa39f6ec18d9e420cad0ed: /@mantine/ssr/4.2.2_b49546c5ecaa39f6ec18d9e420cad0ed:
resolution: {integrity: sha512-xZV+kAgiDIGoxTRXJFoY8LqjxTzmvX1YxRO5QCK0Ky7JuA/iiCwSDsHf5fRqGj6e/ZNJh0MHr/M52PdpYBrpuQ==} resolution: {integrity: sha512-xZV+kAgiDIGoxTRXJFoY8LqjxTzmvX1YxRO5QCK0Ky7JuA/iiCwSDsHf5fRqGj6e/ZNJh0MHr/M52PdpYBrpuQ==}
peerDependencies: peerDependencies:
@@ -5806,24 +5817,6 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true
/@rsuite/icon-font/4.0.0:
resolution: {integrity: sha512-rZTgpTH3H3HLczCA2rnkWfoMKm0ZXoRzsrkVujfP/FfslnKUMvO6w56pa8pCvhWGpNEPUsLS2ULnFGpTEcup/Q==}
dev: false
/@rsuite/icons/1.0.2_react-dom@18.1.0+react@18.1.0:
resolution: {integrity: sha512-Y7vJNDQpJnFlyYSUXQ2iQ9Meg7+ZKcrIenhpYDdM3c7vYDE/L7pml+hrK28jk6QfV/QkVv5B504D+l7aM6AAJQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@rsuite/icon-font': 4.0.0
classnames: 2.3.1
insert-css: 2.0.0
lodash: 4.17.21
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
dev: false
/@rushstack/eslint-patch/1.1.3: /@rushstack/eslint-patch/1.1.3:
resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==} resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==}
dev: true dev: true
@@ -6285,10 +6278,6 @@ packages:
'@types/node': 17.0.25 '@types/node': 17.0.25
dev: false dev: false
/@types/chai/4.3.1:
resolution: {integrity: sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==}
dev: false
/@types/connect-history-api-fallback/1.3.5: /@types/connect-history-api-fallback/1.3.5:
resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==}
dependencies: dependencies:
@@ -6448,10 +6437,6 @@ packages:
resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==} resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==}
dev: true dev: true
/@types/lodash/4.14.182:
resolution: {integrity: sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==}
dev: false
/@types/mdast/3.0.10: /@types/mdast/3.0.10:
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
dependencies: dependencies:
@@ -6548,19 +6533,13 @@ packages:
'@types/history': 4.7.11 '@types/history': 4.7.11
'@types/react': 18.0.8 '@types/react': 18.0.8
/@types/react-virtualized/9.21.21:
resolution: {integrity: sha512-Exx6I7p4Qn+BBA1SRyj/UwQlZ0I0Pq7g7uhAp0QQ4JWzZunqEqNBGTmCmMmS/3N9wFgAGWuBD16ap7k8Y14VPA==}
dependencies:
'@types/prop-types': 15.7.5
'@types/react': 17.0.44
dev: false
/@types/react/17.0.44: /@types/react/17.0.44:
resolution: {integrity: sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g==} resolution: {integrity: sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g==}
dependencies: dependencies:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2 '@types/scheduler': 0.16.2
csstype: 3.0.11 csstype: 3.0.11
dev: true
/@types/react/18.0.5: /@types/react/18.0.5:
resolution: {integrity: sha512-UPxNGInDCIKlfqBrm8LDXYWNfLHwIdisWcsH5GpMyGjhEDLFgTtlRBaoWuCua9HcyuE0rMkmAeZ3FXV1pYLIYQ==} resolution: {integrity: sha512-UPxNGInDCIKlfqBrm8LDXYWNfLHwIdisWcsH5GpMyGjhEDLFgTtlRBaoWuCua9HcyuE0rMkmAeZ3FXV1pYLIYQ==}
@@ -8559,12 +8538,6 @@ packages:
keygrip: 1.1.0 keygrip: 1.1.0
dev: false dev: false
/copy-anything/2.0.6:
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
dependencies:
is-what: 3.14.1
dev: false
/copy-text-to-clipboard/3.0.1: /copy-text-to-clipboard/3.0.1:
resolution: {integrity: sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==} resolution: {integrity: sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -9724,12 +9697,6 @@ packages:
csstype: 3.0.11 csstype: 3.0.11
dev: false dev: false
/dom-lib/3.1.2:
resolution: {integrity: sha512-FKw/w51guhTwiSnyCQKTxEwglnZG7Q1qJO62asKwedcqCCHwrwQP8DnMPBTXL9y3JTePFDaPS95GLha/Q9nd/w==}
dependencies:
'@babel/runtime': 7.17.9
dev: false
/dom-serializer/0.1.1: /dom-serializer/0.1.1:
resolution: {integrity: sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==} resolution: {integrity: sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==}
dependencies: dependencies:
@@ -9956,15 +9923,6 @@ packages:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
dev: true dev: true
/errno/0.1.8:
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
hasBin: true
requiresBuild: true
dependencies:
prr: 1.0.1
dev: false
optional: true
/error-ex/1.3.2: /error-ex/1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies: dependencies:
@@ -11239,18 +11197,6 @@ packages:
- encoding - encoding
dev: false dev: false
/flux/4.0.3_react@18.1.0:
resolution: {integrity: sha512-yKAbrp7JhZhj6uiT1FTuVMlIAT1J4jqEyBpFApi1kxpGZCvacMVc/t1pMQyotqHhAgvoE3bNvAykhCo2CLjnYw==}
peerDependencies:
react: ^15.0.2 || ^16.0.0 || ^17.0.0
dependencies:
fbemitter: 3.0.0
fbjs: 3.0.4
react: 18.1.0
transitivePeerDependencies:
- encoding
dev: false
/follow-redirects/1.14.9: /follow-redirects/1.14.9:
resolution: {integrity: sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==} resolution: {integrity: sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -12168,14 +12114,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/image-size/0.5.5:
resolution: {integrity: sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=}
engines: {node: '>=0.10.0'}
hasBin: true
requiresBuild: true
dev: false
optional: true
/image-size/1.0.1: /image-size/1.0.1:
resolution: {integrity: sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==} resolution: {integrity: sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -12291,10 +12229,6 @@ packages:
through: 2.3.8 through: 2.3.8
dev: true dev: true
/insert-css/2.0.0:
resolution: {integrity: sha1-610Ql7dUL0x56jBg067gfQU4gPQ=}
dev: false
/install-artifact-from-github/1.3.0: /install-artifact-from-github/1.3.0:
resolution: {integrity: sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA==} resolution: {integrity: sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA==}
hasBin: true hasBin: true
@@ -12704,10 +12638,6 @@ packages:
call-bind: 1.0.2 call-bind: 1.0.2
dev: true dev: true
/is-what/3.14.1:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
dev: false
/is-whitespace-character/1.0.4: /is-whitespace-character/1.0.4:
resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==} resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
dev: false dev: false
@@ -13689,24 +13619,6 @@ packages:
dependencies: dependencies:
package-json: 6.5.0 package-json: 6.5.0
/less/4.1.2:
resolution: {integrity: sha512-EoQp/Et7OSOVu0aJknJOtlXZsnr8XE8KwuzTHOLeVSEx8pVWUICc8Q0VYRHgzyjX78nMEyC/oztWFbgyhtNfDA==}
engines: {node: '>=6'}
hasBin: true
dependencies:
copy-anything: 2.0.6
parse-node-version: 1.0.1
tslib: 2.3.1
optionalDependencies:
errno: 0.1.8
graceful-fs: 4.2.10
image-size: 0.5.5
make-dir: 2.1.0
mime: 1.6.0
needle: 2.9.1
source-map: 0.6.1
dev: false
/leven/3.1.0: /leven/3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -14038,16 +13950,6 @@ packages:
iconv-lite: 0.6.3 iconv-lite: 0.6.3
dev: true dev: true
/make-dir/2.1.0:
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
engines: {node: '>=6'}
requiresBuild: true
dependencies:
pify: 4.0.1
semver: 5.7.1
dev: false
optional: true
/make-dir/3.1.0: /make-dir/3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -14506,18 +14408,6 @@ packages:
resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=}
dev: true dev: true
/needle/2.9.1:
resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==}
engines: {node: '>= 4.4.x'}
hasBin: true
requiresBuild: true
dependencies:
debug: 3.2.7
iconv-lite: 0.4.24
sax: 1.2.4
dev: false
optional: true
/negotiator/0.6.3: /negotiator/0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -15088,11 +14978,6 @@ packages:
json-parse-even-better-errors: 2.3.1 json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
/parse-node-version/1.0.1:
resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
engines: {node: '>= 0.10'}
dev: false
/parse-numeric-range/1.3.0: /parse-numeric-range/1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
dev: false dev: false
@@ -15220,6 +15105,7 @@ packages:
/pify/4.0.1: /pify/4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true
/pirates/4.0.5: /pirates/4.0.5:
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
@@ -15803,6 +15689,14 @@ packages:
react: 17.0.2 react: 17.0.2
dev: false dev: false
/prism-react-renderer/1.3.1_react@18.1.0:
resolution: {integrity: sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ==}
peerDependencies:
react: '>=0.14.9'
dependencies:
react: 18.1.0
dev: false
/prismjs/1.27.0: /prismjs/1.27.0:
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -15865,11 +15759,6 @@ packages:
resolution: {integrity: sha512-wapJ3h/w8fRSyPEG0y2WMV+tf9xwvj3nxM6aHVuPEOwKs/t5xLSKZb44ubNTiqq2T6lmEMHEWGMTaU2L6ddaFA==} resolution: {integrity: sha512-wapJ3h/w8fRSyPEG0y2WMV+tf9xwvj3nxM6aHVuPEOwKs/t5xLSKZb44ubNTiqq2T6lmEMHEWGMTaU2L6ddaFA==}
dev: false dev: false
/prr/1.0.1:
resolution: {integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=}
dev: false
optional: true
/pseudomap/1.0.2: /pseudomap/1.0.2:
resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=} resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=}
dev: true dev: true
@@ -16163,23 +16052,6 @@ packages:
/react-is/17.0.2: /react-is/17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
/react-json-view/1.21.3_47ae209ab5c2ea051c9a2540426c6b2f:
resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==}
peerDependencies:
react: ^17.0.0 || ^16.3.0 || ^15.5.4
react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4
dependencies:
flux: 4.0.3_react@18.1.0
react: 18.1.0
react-base16-styling: 0.6.0
react-dom: 18.1.0_react@18.1.0
react-lifecycles-compat: 3.0.4
react-textarea-autosize: 8.3.3_@types+react@18.0.8+react@18.1.0
transitivePeerDependencies:
- '@types/react'
- encoding
dev: false
/react-json-view/1.21.3_react-dom@17.0.2+react@17.0.2: /react-json-view/1.21.3_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==} resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==}
peerDependencies: peerDependencies:
@@ -16404,22 +16276,6 @@ packages:
react-dom: 18.1.0_react@18.1.0 react-dom: 18.1.0_react@18.1.0
dev: false dev: false
/react-virtualized/9.22.3_react-dom@18.1.0+react@18.1.0:
resolution: {integrity: sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==}
peerDependencies:
react: ^15.3.0 || ^16.0.0-alpha
react-dom: ^15.3.0 || ^16.0.0-alpha
dependencies:
'@babel/runtime': 7.17.9
clsx: 1.1.1
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
react-lifecycles-compat: 3.0.4
dev: false
/react/17.0.2: /react/17.0.2:
resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -16913,50 +16769,6 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/rsuite-table/5.5.0_28428ba0c391cf6cfa125a69ce03058f:
resolution: {integrity: sha512-Bh4VUWsN8Q8q1oTvBx+Q4N05OpScsL7nEBi82M9ryY0gss7V0W8jaqBlJdvbxMZhZow3yfka0BLcbzeZfNADCA==}
peerDependencies:
prop-types: ^15.7.2
react: ^0.14.9 || >=15.3.0
react-dom: ^0.14.9 || >=15.3.0
dependencies:
'@babel/runtime': 7.17.9
'@juggle/resize-observer': 3.3.1
'@rsuite/icons': 1.0.2_react-dom@18.1.0+react@18.1.0
classnames: 2.3.1
dom-lib: 3.1.2
lodash: 4.17.21
prop-types: 15.8.1
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
react-is: 17.0.2
dev: false
/rsuite/5.10.0_react-dom@18.1.0+react@18.1.0:
resolution: {integrity: sha512-7yUzv6s8UVzNmyBMHTiybAeqS0k3IM4aIpFMfdBxStbWc92I1VCb5o8pv4FKvOWF7gzq5g1Kj759nRxTwhouVw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@babel/runtime': 7.17.9
'@juggle/resize-observer': 3.3.1
'@rsuite/icons': 1.0.2_react-dom@18.1.0+react@18.1.0
'@types/chai': 4.3.1
'@types/lodash': 4.14.182
'@types/prop-types': 15.7.5
'@types/react-virtualized': 9.21.21
classnames: 2.3.1
date-fns: 2.28.0
dom-lib: 3.1.2
lodash: 4.17.21
prop-types: 15.8.1
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
react-virtualized: 9.22.3_react-dom@18.1.0+react@18.1.0
rsuite-table: 5.5.0_28428ba0c391cf6cfa125a69ce03058f
schema-typed: 2.0.2
dev: false
/rtl-detect/1.0.4: /rtl-detect/1.0.4:
resolution: {integrity: sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==} resolution: {integrity: sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==}
dev: false dev: false
@@ -17028,12 +16840,6 @@ packages:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
/schema-typed/2.0.2:
resolution: {integrity: sha512-E8GAANjZ8oYwyQJEyf/93W1QERTCwu8N7aMUbgBWbYvqzZE8f/kAZaL5D9knr7yLEgMnwJQ4pd8x8EVZ0PpzUA==}
dependencies:
'@babel/runtime': 7.17.9
dev: false
/schema-utils/2.7.0: /schema-utils/2.7.0:
resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==}
engines: {node: '>= 8.9.0'} engines: {node: '>= 8.9.0'}
@@ -19123,31 +18929,6 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/vite/2.9.5_less@4.1.2:
resolution: {integrity: sha512-dvMN64X2YEQgSXF1lYabKXw3BbN6e+BL67+P3Vy4MacnY+UzT1AfkHiioFSi9+uiDUiaDy7Ax/LQqivk6orilg==}
engines: {node: '>=12.2.0'}
hasBin: true
peerDependencies:
less: '*'
sass: '*'
stylus: '*'
peerDependenciesMeta:
less:
optional: true
sass:
optional: true
stylus:
optional: true
dependencies:
esbuild: 0.14.37
less: 4.1.2
postcss: 8.4.12
resolve: 1.22.0
rollup: 2.70.2
optionalDependencies:
fsevents: 2.3.2
dev: true
/vite/2.9.7: /vite/2.9.7:
resolution: {integrity: sha512-5hH7aNQe8rJiTTqCtPNX/6mIKlGw+1wg8UXwAxDIIN8XaSR+Zx3GT2zSu7QKa1vIaBqfUODGh3vpwY8r0AW/jw==} resolution: {integrity: sha512-5hH7aNQe8rJiTTqCtPNX/6mIKlGw+1wg8UXwAxDIIN8XaSR+Zx3GT2zSu7QKa1vIaBqfUODGh3vpwY8r0AW/jw==}
engines: {node: '>=12.2.0'} engines: {node: '>=12.2.0'}