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",
"position": 4,
"link": { "id": "platform/authentication/index", "type": "doc" }
"position": 6
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
---
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.

View File

@@ -1,6 +1,6 @@
---
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.
@@ -21,7 +21,7 @@ Specifically, the following will be deployed:
## 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:

View File

@@ -1,4 +1,4 @@
{
"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",
"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)
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`.
```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
if (

View File

@@ -1,4 +1,4 @@
{
"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 Header from '../components/Header';
const nhost = new NhostClient({ backendUrl: 'my-app.nhost.run' });
function MyApp({ Component, pageProps }: AppProps) {
return (
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
<div>
<Header />
<Component {...pageProps} />
</div>
<Component {...pageProps} />
</NhostNextProvider>
);
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -4,19 +4,19 @@
"private": true,
"dependencies": {
"@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/react": "workspace:*",
"@nhost/react-apollo": "workspace:*",
"@rsuite/icons": "^1.0.2",
"graphql": "15.7.2",
"less": "^4.1.2",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-icons": "^4.3.1",
"react-json-view": "^1.21.3",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"rsuite": "^5.10.0"
"react-router-dom": "^6.3.0"
},
"lib": "workspace:*",
"scripts": {

View File

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

View File

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

View File

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

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'
export const AuthGate: React.FC = ({ children }) => {
export const AuthGate: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const { isLoading, isAuthenticated } = useAuthenticationStatus()
const location = useLocation()
if (isLoading) {
@@ -17,7 +17,7 @@ export const AuthGate: React.FC = ({ children }) => {
return <div>{children}</div>
}
export const PublicGate: React.FC = ({ children }) => {
export const PublicGate: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const { isLoading, isAuthenticated } = useAuthenticationStatus()
const location = useLocation()
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 { inspect } from '@xstate/inspect'
import 'rsuite/styles/index.less' // or 'rsuite/dist/rsuite.min.css'
import App from './App'
const nhost = new NhostClient({

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,100 +1,95 @@
import React, { useEffect, useState } from 'react'
import { NavLink } from 'react-router-dom'
import { Button, Divider, Input, Message } from 'rsuite'
/* eslint-disable react/react-in-jsx-scope */
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSignInEmailPassword } from '@nhost/react'
const Footer: React.FC = () => (
<div>
<Divider />
<Button as={NavLink} to="/sign-in" block appearance="link">
&#8592; Other Login Options
</Button>
</div>
)
import { Button, Modal, TextInput } from '@mantine/core'
import AuthLink from '../components/AuthLink'
import { showNotification } from '@mantine/notifications'
export const EmailPassword: React.FC = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [otp, setOtp] = useState('')
const { signInEmailPassword, error, needsMfaOtp, sendMfaOtp } = useSignInEmailPassword(
email,
password,
otp
)
const { signInEmailPassword, needsMfaOtp, sendMfaOtp } = useSignInEmailPassword()
const navigate = useNavigate()
const [errorMessage, setErrorMessage] = useState('')
// * 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 [emailVerificationToggle, setEmailVerificationToggle] = useState(false)
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)
return (
<div>
<Input
<>
<TextInput
value={otp}
onChange={setOtp}
onChange={(e) => setOtp(e.target.value)}
placeholder="One-time password"
size="lg"
autoFocus
style={{ marginBottom: '0.5em' }}
/>
{errorMessage && (
<Message showIcon type="error">
{errorMessage}
</Message>
)}
<Button appearance="primary" onClick={sendMfaOtp} block>
<Button fullWidth onClick={sendOtp}>
Send 2-step verification code
</Button>
<Footer />
</div>
</>
)
else
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}
onChange={setEmail}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email Address"
size="lg"
autoFocus
style={{ marginBottom: '0.5em' }}
/>
<Input
<TextInput
value={password}
onChange={setPassword}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
type="password"
size="lg"
style={{ marginBottom: '0.5em' }}
/>
{errorMessage && (
<Message showIcon type="error">
{errorMessage}
</Message>
)}
<Button
appearance="primary"
onClick={async () => {
const result = await signInEmailPassword(email, password)
console.log(result)
}}
block
>
<Button fullWidth onClick={signIn}>
Sign in
</Button>
<Button as={NavLink} block to="/sign-in/forgot-password">
<AuthLink link="/sign-in/forgot-password" variant="white">
Forgot password?
</Button>
<Footer />
</div>
</AuthLink>
</>
)
}

View File

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

View File

@@ -1,57 +1,43 @@
import React, { useEffect, useState } from 'react'
import { NavLink } from 'react-router-dom'
import { Button, Divider, Input, Message, Notification, toaster } from 'rsuite'
import React, { useState } from '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 = () => {
const [email, setEmail] = useState('')
const { resetPassword, isSent, error } = useResetPassword({ redirectTo: '/profile' })
const { resetPassword } = useResetPassword({ redirectTo: '/profile' })
const [errorMessage, setErrorMessage] = useState('')
// * 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])
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>
)
const reset = async () => {
const result = await resetPassword(email)
if (result.isError) {
showNotification({
color: 'red',
title: 'Error',
message: result.error?.message
})
}
}, [isSent])
}
return (
<div>
<Input
<>
<TextInput
value={email}
onChange={setEmail}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email Address"
size="lg"
autoFocus
style={{ marginBottom: '0.5em' }}
/>
{errorMessage && (
<Message showIcon type="error">
{errorMessage}
</Message>
)}
<Button appearance="primary" onClick={() => resetPassword(email)} block>
<Button onClick={reset} fullWidth>
Reset your password
</Button>
<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
</Button>
</div>
</AuthLink>
</>
)
}

View File

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

View File

@@ -1,105 +1,95 @@
/* eslint-disable react/react-in-jsx-scope */
import { useEffect, useMemo, useState } from 'react'
import { NavLink, useNavigate } from 'react-router-dom'
import { Button, Input, Message } from 'rsuite'
import { useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
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 = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [firstName, setFirstName] = 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(
() => ({ displayName: `${firstName} ${lastName}`, metadata: { firstName, lastName } }),
[firstName, lastName]
)
const navigate = useNavigate()
const [confirmPassword, setConfirmPassword] = useState('')
const { signUpEmailPassword, error, needsEmailVerification, isSuccess } =
useSignUpEmailPassword(options)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
if (needsEmailVerification) navigate('/sign-up/verification-email-sent')
else if (isSuccess) navigate('/')
const { signUpEmailPassword } = useSignUpEmailPassword(options)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [needsEmailVerification, isSuccess])
// * Set error message from the registration hook errors
useEffect(() => {
setErrorMessage(error?.message || '')
}, [error])
// * Reset error message every time the email or password input changed
useEffect(() => {
setErrorMessage('')
}, [email, password])
// * Show an error message when passwords are different
useEffect(() => {
if (password !== confirmPassword) setErrorMessage('Both passwords must be the same')
else setErrorMessage('')
}, [password, confirmPassword])
const signUp = async () => {
const result = await signUpEmailPassword(email, password, { metadata: { firstName, lastName } })
if (result.isError) {
showNotification({
color: 'red',
title: 'Error',
message: result.error?.message
})
} else if (result.needsEmailVerification) {
setEmailVerificationToggle(true)
} else {
navigate('/', { replace: true })
}
}
return (
<div>
<Input
value={firstName}
onChange={setFirstName}
placeholder="First name"
size="lg"
autoFocus
style={{ marginBottom: '0.5em' }}
/>
<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)
<>
<Modal
title="Verification email sent"
transition="fade"
centered
transitionDuration={600}
opened={emailVerificationToggle}
onClose={() => {
setEmailVerificationToggle(false)
}}
block
>
Sign up
</Button>
<Button as={NavLink} to="/sign-up" block appearance="link">
A email has been sent to {email}. Please follow the link to verify your email address and to
complete your registration.
</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
</Button>
</div>
</AuthLink>
</>
)
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,24 @@
# @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
### Minor Changes

View File

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

View File

@@ -75,38 +75,31 @@ export const createAuthMachine = ({
type: 'parallel',
states: {
authentication: {
initial: 'importingRefreshToken',
initial: 'starting',
on: {
SESSION_UPDATE: [
{
cond: 'hasSession',
actions: ['saveSession', 'persist', 'resetTimer', 'reportTokenChanged'],
actions: ['saveSession', 'resetTimer', 'reportTokenChanged'],
target: '.signedIn'
}
]
},
states: {
importingRefreshToken: {
starting: {
tags: ['loading'],
always: { cond: 'isSignedIn', target: 'signedIn' },
invoke: {
id: 'importRefreshToken',
src: 'importRefreshToken',
onDone: { actions: 'saveRefreshToken', target: 'starting' },
onDone: {
actions: ['saveSession', 'reportTokenChanged'],
target: 'signedIn'
},
onError: { actions: ['saveAuthenticationError'], target: 'signedOut' }
}
},
starting: {
always: [
{
cond: 'hasRefreshTokenWithoutSession',
target: 'authenticating.token'
},
{ cond: 'hasAuthenticationError', target: 'signedOut.failed' },
'signedOut'
]
},
signedOut: {
tags: ['ready'],
initial: 'noErrors',
entry: 'reportSignedOut',
states: {
@@ -236,7 +229,7 @@ export const createAuthMachine = ({
src: 'signInPasswordlessSmsOtp',
id: 'authenticatePasswordlessSmsOtp',
onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'],
actions: ['saveSession', 'reportTokenChanged'],
target: '#nhost.authentication.signedIn'
},
onError: {
@@ -256,7 +249,7 @@ export const createAuthMachine = ({
target: '#nhost.authentication.signedOut.needsMfa'
},
{
actions: ['saveSession', 'persist', 'reportTokenChanged'],
actions: ['saveSession', 'reportTokenChanged'],
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: {
invoke: {
src: 'signInAnonymous',
id: 'authenticateAnonymously',
onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'],
actions: ['saveSession', 'reportTokenChanged'],
target: '#nhost.authentication.signedIn'
},
onError: {
@@ -307,7 +286,7 @@ export const createAuthMachine = ({
src: 'signInMfaTotp',
id: 'signInMfaTotp',
onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'],
actions: ['saveSession', 'reportTokenChanged'],
target: '#nhost.authentication.signedIn'
},
onError: {
@@ -329,7 +308,7 @@ export const createAuthMachine = ({
{
cond: 'hasSession',
target: '#nhost.authentication.signedIn',
actions: ['saveSession', 'persist', 'reportTokenChanged']
actions: ['saveSession', 'reportTokenChanged']
},
{
target: '#nhost.authentication.signedOut.needsEmailVerification'
@@ -347,11 +326,9 @@ export const createAuthMachine = ({
]
}
},
signedIn: {
tags: ['ready'],
type: 'parallel',
entry: ['reportSignedIn', 'cleanUrl'],
entry: ['reportSignedIn', 'cleanUrl', 'broadcastToken'],
on: {
SIGNOUT: '#nhost.authentication.signedOut.signingOut',
DEANONYMIZE: {
@@ -401,12 +378,7 @@ export const createAuthMachine = ({
src: 'refreshToken',
id: 'refreshToken',
onDone: {
actions: [
'saveSession',
'persist',
'resetTimer',
'reportTokenChanged'
],
actions: ['saveSession', 'resetTimer', 'reportTokenChanged'],
target: 'pending'
},
onError: [
@@ -455,7 +427,7 @@ export const createAuthMachine = ({
src: 'refreshToken',
id: 'authenticateWithToken',
onDone: {
actions: ['saveSession', 'persist', 'reportTokenChanged'],
actions: ['saveSession', 'reportTokenChanged'],
target: ['#nhost.authentication.signedIn', 'idle.noErrors']
},
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({
user: (_, e: any) => e.data?.session?.user,
accessToken: (_, e) => ({
value: e.data?.session?.accessToken,
expiresAt: new Date(Date.now() + e.data?.session?.accessTokenExpiresIn * 1_000)
}),
refreshToken: (_, e) => ({ value: e.data?.session?.refreshToken })
user: (_, { data }: any) => data?.session?.user,
accessToken: (_, { data }: any) => {
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)
}
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({
mfa: (_, e: any) => e.data?.mfa ?? null
@@ -543,22 +530,7 @@ export const createAuthMachine = ({
saveNoMfaTicketError: assign({
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({
refreshToken: (_) => {
storageSetter(NHOST_REFRESH_TOKEN_KEY, null)
@@ -591,12 +563,9 @@ export const createAuthMachine = ({
guards: {
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,
noMfaTicket: (ctx, { ticket }) => !ticket && !ctx.mfa?.ticket,
hasRefreshToken: (ctx) => !!ctx.refreshToken.value,
hasAuthenticationError: (ctx) => !!ctx.errors.authentication,
isAutoRefreshDisabled: () => !autoRefreshToken,
refreshTimerShouldRefresh: (ctx) => {
const { expiresAt } = ctx.accessToken
@@ -686,15 +655,17 @@ export const createAuthMachine = ({
}),
importRefreshToken: async () => {
const stringExpiresAt = await storageGetter(NHOST_JWT_EXPIRES_AT_KEY)
const expiresAt = stringExpiresAt ? new Date(stringExpiresAt) : null
let refreshToken = await storageGetter(NHOST_REFRESH_TOKEN_KEY)
let error: ValidationErrorPayload | null = null
if (autoSignIn) {
const urlToken = getParameterByName('refreshToken') || null
if (urlToken) {
if (!refreshToken) {
// ? Which takes precedence? localStorage or the url?
refreshToken = urlToken
try {
const session = await postRequest('/token', {
refreshToken: urlToken
})
return { session }
} catch (exception) {
error = (exception as { error: ValidationErrorPayload }).error
}
} else {
const error = getParameterByName('error')
@@ -709,14 +680,19 @@ export const createAuthMachine = ({
}
}
}
return refreshToken
? {
refreshToken,
expiresAt
}
: Promise.reject<{ error: ValidationErrorPayload }>({
error: null
const storageToken = await storageGetter(NHOST_REFRESH_TOKEN_KEY)
if (storageToken) {
try {
const session = await postRequest('/token', {
refreshToken: storageToken
})
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: {
saveSession:
| 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| 'done.invoke.authenticatePasswordlessSmsOtp'
| '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.signInMfaTotp'
| 'done.invoke.registerUser'
@@ -26,22 +16,20 @@ export interface Typegen0 {
resetTimer: 'SESSION_UPDATE' | 'done.invoke.refreshToken' | ''
reportTokenChanged:
| 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser'
| 'done.invoke.refreshToken'
| 'done.invoke.authenticateWithToken'
saveRefreshToken: 'done.invoke.importRefreshToken'
saveAuthenticationError:
| 'error.platform.importRefreshToken'
| 'error.platform.authenticatePasswordlessEmail'
| 'error.platform.authenticatePasswordlessSms'
| 'error.platform.authenticatePasswordlessSmsOtp'
| 'error.platform.authenticateUserWithPassword'
| 'error.platform.signInToken'
| 'error.platform.authenticateAnonymously'
| 'error.platform.signInMfaTotp'
saveInvalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL'
@@ -51,39 +39,50 @@ export interface Typegen0 {
saveInvalidSignUpPassword: 'SIGNUP_EMAIL_PASSWORD'
saveNoMfaTicketError: 'SIGNIN_MFA_TOTP'
saveMfaTicket: 'done.invoke.authenticateUserWithPassword'
broadcastToken: 'done.invoke.signInToken'
saveRegisrationError: 'error.platform.registerUser'
saveRefreshAttempt: 'error.platform.refreshToken'
reportSignedOut:
| 'error.platform.importRefreshToken'
| ''
| 'error.platform.authenticateWithToken'
reportSignedOut: 'error.platform.importRefreshToken' | 'error.platform.authenticateWithToken'
resetAuthenticationError: 'xstate.init'
destroyRefreshToken: 'xstate.init'
clearContextExceptRefreshToken: 'SIGNOUT'
resetSignUpError: 'SIGNUP_EMAIL_PASSWORD'
reportSignedIn:
| 'SESSION_UPDATE'
| 'done.invoke.importRefreshToken'
| ''
| 'done.invoke.authenticatePasswordlessSmsOtp'
| 'done.invoke.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser'
| 'done.invoke.authenticateWithToken'
cleanUrl:
| '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.authenticateUserWithPassword'
| 'done.invoke.signInToken'
| 'done.invoke.authenticateAnonymously'
| 'done.invoke.signInMfaTotp'
| 'done.invoke.registerUser'
| 'done.invoke.authenticateWithToken'
}
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': {
type: 'done.invoke.authenticatePasswordlessSmsOtp'
data: unknown
@@ -94,11 +93,6 @@ export interface Typegen0 {
data: unknown
__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': {
type: 'done.invoke.authenticateAnonymously'
data: unknown
@@ -125,11 +119,6 @@ export interface Typegen0 {
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'': { 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': {
type: 'error.platform.importRefreshToken'
data: unknown
@@ -150,7 +139,6 @@ export interface Typegen0 {
type: 'error.platform.authenticateUserWithPassword'
data: unknown
}
'error.platform.signInToken': { type: 'error.platform.signInToken'; data: unknown }
'error.platform.authenticateAnonymously': {
type: 'error.platform.authenticateAnonymously'
data: unknown
@@ -190,13 +178,10 @@ export interface Typegen0 {
signInPasswordlessSms: 'done.invoke.authenticatePasswordlessSms'
signInPasswordlessSmsOtp: 'done.invoke.authenticatePasswordlessSmsOtp'
signInPassword: 'done.invoke.authenticateUserWithPassword'
refreshToken:
| 'done.invoke.signInToken'
| 'done.invoke.refreshToken'
| 'done.invoke.authenticateWithToken'
signInAnonymous: 'done.invoke.authenticateAnonymously'
signInMfaTotp: 'done.invoke.signInMfaTotp'
registerUser: 'done.invoke.registerUser'
refreshToken: 'done.invoke.refreshToken' | 'done.invoke.authenticateWithToken'
}
missingImplementations: {
actions: never
@@ -206,7 +191,6 @@ export interface Typegen0 {
}
eventsCausingServices: {
importRefreshToken: 'xstate.init'
refreshToken: '' | 'TRY_TOKEN'
signInPassword: 'SIGNIN_PASSWORD'
signInPasswordlessEmail: 'SIGNIN_PASSWORDLESS_EMAIL'
signInPasswordlessSms: 'SIGNIN_PASSWORDLESS_SMS'
@@ -215,12 +199,11 @@ export interface Typegen0 {
signInAnonymous: 'SIGNIN_ANONYMOUS'
signInMfaTotp: 'SIGNIN_MFA_TOTP'
signout: 'SIGNOUT'
refreshToken: '' | 'TRY_TOKEN'
}
eventsCausingGuards: {
hasSession: 'SESSION_UPDATE' | 'done.invoke.registerUser'
isSignedIn: '' | 'error.platform.authenticateWithToken'
hasRefreshTokenWithoutSession: ''
hasAuthenticationError: ''
invalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL' | 'SIGNUP_EMAIL_PASSWORD'
invalidPassword: 'SIGNIN_PASSWORD' | 'SIGNUP_EMAIL_PASSWORD'
invalidPhoneNumber: 'SIGNIN_PASSWORDLESS_SMS' | 'SIGNIN_PASSWORDLESS_SMS_OTP'
@@ -235,7 +218,6 @@ export interface Typegen0 {
eventsCausingDelays: {}
matchesStates:
| 'authentication'
| 'authentication.importingRefreshToken'
| 'authentication.starting'
| 'authentication.signedOut'
| 'authentication.signedOut.noErrors'
@@ -255,7 +237,6 @@ export interface Typegen0 {
| 'authentication.authenticating.passwordlessSms'
| 'authentication.authenticating.passwordlessSmsOtp'
| 'authentication.authenticating.password'
| 'authentication.authenticating.token'
| 'authentication.authenticating.anonymous'
| 'authentication.authenticating.mfa'
| 'authentication.authenticating.mfa.totp'
@@ -278,7 +259,6 @@ export interface Typegen0 {
| 'token.running'
| {
authentication?:
| 'importingRefreshToken'
| 'starting'
| 'signedOut'
| 'authenticating'
@@ -304,7 +284,6 @@ export interface Typegen0 {
| 'passwordlessSms'
| 'passwordlessSmsOtp'
| 'password'
| 'token'
| 'anonymous'
| 'mfa'
| { mfa?: 'totp' }
@@ -323,5 +302,5 @@ export interface Typegen0 {
}
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 = (
clientStorageType: ClientStorageType,
clientStorage?: ClientStorage
): StorageGetter => {
if (!clientStorage || clientStorageType === 'localStorage' || clientStorageType === 'web') {
if (clientStorageType === 'localStorage' || clientStorageType === 'web') {
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') {
return (key) => {
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 (clientStorage.getItem && clientStorage.removeItem) {
return clientStorage.getItem
@@ -89,25 +77,9 @@ export const localStorageSetter = (
clientStorageType: ClientStorageType,
clientStorage?: ClientStorage
): StorageSetter => {
if (!clientStorage || clientStorageType === 'localStorage' || clientStorageType === 'web') {
if (clientStorageType === 'localStorage' || clientStorageType === 'web') {
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') {
return (key, value) => {
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 (!clientStorage.removeItem) {
throw Error(

View File

@@ -1,5 +1,28 @@
# @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
### Patch Changes

View File

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

View File

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

View File

@@ -1,5 +1,29 @@
# @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
### Minor Changes

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ export const getNhostSession = async (
backendUrl: string,
context: GetServerSidePropsContext
): 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
return nhost.auth.isAuthenticated()
? {

View File

@@ -1,5 +1,19 @@
# @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
### Patch Changes

View File

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

View File

@@ -1,5 +1,21 @@
# @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
### Minor Changes

View File

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

View File

@@ -1,5 +1,25 @@
# @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
### Minor Changes

View File

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

View File

@@ -58,7 +58,7 @@ export const useNhostBackendUrl = () => {
*/
export const useAuthLoading = () => {
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,
(state) => ({
isAuthenticated: state.matches({ authentication: 'signedIn' }),
isLoading: !state.hasTag('ready'),
isLoading: state.hasTag('loading'),
error: state.context.errors.authentication || null,
isError: state.matches({ authentication: { signedOut: 'failed' } })
}),

287
pnpm-lock.yaml generated
View File

@@ -191,49 +191,49 @@ importers:
examples/react-apollo:
specifiers:
'@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/react': workspace:*
'@nhost/react-apollo': workspace:*
'@rsuite/icons': ^1.0.2
'@types/react': ^18.0.8
'@types/react-dom': ^18.0.3
'@vitejs/plugin-react': ^1.3.1
'@xstate/inspect': ^0.6.2
graphql: 15.7.2
less: ^4.1.2
react: ^18.1.0
react-dom: ^18.1.0
react-icons: ^4.3.1
react-json-view: ^1.21.3
react-router: ^6.3.0
react-router-dom: ^6.3.0
rsuite: ^5.10.0
typescript: ^4.6.3
vite: ^2.9.5
ws: ^8.6.0
xstate: ^4.31.0
dependencies:
'@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/react': link:../../packages/react
'@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
less: 4.1.2
react: 18.1.0
react-dom: 18.1.0_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-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:
'@types/react': 18.0.8
'@types/react-dom': 18.0.3
'@vitejs/plugin-react': 1.3.1
'@xstate/inspect': 0.6.5_ws@8.6.0+xstate@4.31.0
typescript: 4.6.3
vite: 2.9.5_less@4.1.2
vite: 2.9.5
ws: 8.6.0
xstate: 4.31.0
@@ -5189,10 +5189,6 @@ packages:
'@jridgewell/resolve-uri': 3.0.6
'@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:
resolution: {integrity: sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg==}
dev: false
@@ -5257,6 +5253,21 @@ packages:
react-transition-group: 4.4.2_react-dom@18.1.0+react@18.1.0
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:
resolution: {integrity: sha512-xZV+kAgiDIGoxTRXJFoY8LqjxTzmvX1YxRO5QCK0Ky7JuA/iiCwSDsHf5fRqGj6e/ZNJh0MHr/M52PdpYBrpuQ==}
peerDependencies:
@@ -5806,24 +5817,6 @@ packages:
picomatch: 2.3.1
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:
resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==}
dev: true
@@ -6285,10 +6278,6 @@ packages:
'@types/node': 17.0.25
dev: false
/@types/chai/4.3.1:
resolution: {integrity: sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==}
dev: false
/@types/connect-history-api-fallback/1.3.5:
resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==}
dependencies:
@@ -6448,10 +6437,6 @@ packages:
resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==}
dev: true
/@types/lodash/4.14.182:
resolution: {integrity: sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==}
dev: false
/@types/mdast/3.0.10:
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
dependencies:
@@ -6548,19 +6533,13 @@ packages:
'@types/history': 4.7.11
'@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:
resolution: {integrity: sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g==}
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2
csstype: 3.0.11
dev: true
/@types/react/18.0.5:
resolution: {integrity: sha512-UPxNGInDCIKlfqBrm8LDXYWNfLHwIdisWcsH5GpMyGjhEDLFgTtlRBaoWuCua9HcyuE0rMkmAeZ3FXV1pYLIYQ==}
@@ -8559,12 +8538,6 @@ packages:
keygrip: 1.1.0
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:
resolution: {integrity: sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==}
engines: {node: '>=12'}
@@ -9724,12 +9697,6 @@ packages:
csstype: 3.0.11
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:
resolution: {integrity: sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==}
dependencies:
@@ -9956,15 +9923,6 @@ packages:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
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:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
@@ -11239,18 +11197,6 @@ packages:
- encoding
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:
resolution: {integrity: sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==}
engines: {node: '>=4.0'}
@@ -12168,14 +12114,6 @@ packages:
engines: {node: '>=0.10.0'}
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:
resolution: {integrity: sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==}
engines: {node: '>=12.0.0'}
@@ -12291,10 +12229,6 @@ packages:
through: 2.3.8
dev: true
/insert-css/2.0.0:
resolution: {integrity: sha1-610Ql7dUL0x56jBg067gfQU4gPQ=}
dev: false
/install-artifact-from-github/1.3.0:
resolution: {integrity: sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA==}
hasBin: true
@@ -12704,10 +12638,6 @@ packages:
call-bind: 1.0.2
dev: true
/is-what/3.14.1:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
dev: false
/is-whitespace-character/1.0.4:
resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
dev: false
@@ -13689,24 +13619,6 @@ packages:
dependencies:
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:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -14038,16 +13950,6 @@ packages:
iconv-lite: 0.6.3
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:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
@@ -14506,18 +14408,6 @@ packages:
resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=}
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:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@@ -15088,11 +14978,6 @@ packages:
json-parse-even-better-errors: 2.3.1
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:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
dev: false
@@ -15220,6 +15105,7 @@ packages:
/pify/4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
dev: true
/pirates/4.0.5:
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
@@ -15803,6 +15689,14 @@ packages:
react: 17.0.2
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:
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
engines: {node: '>=6'}
@@ -15865,11 +15759,6 @@ packages:
resolution: {integrity: sha512-wapJ3h/w8fRSyPEG0y2WMV+tf9xwvj3nxM6aHVuPEOwKs/t5xLSKZb44ubNTiqq2T6lmEMHEWGMTaU2L6ddaFA==}
dev: false
/prr/1.0.1:
resolution: {integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=}
dev: false
optional: true
/pseudomap/1.0.2:
resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=}
dev: true
@@ -16163,23 +16052,6 @@ packages:
/react-is/17.0.2:
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:
resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==}
peerDependencies:
@@ -16404,22 +16276,6 @@ packages:
react-dom: 18.1.0_react@18.1.0
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:
resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==}
engines: {node: '>=0.10.0'}
@@ -16913,50 +16769,6 @@ packages:
fsevents: 2.3.2
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:
resolution: {integrity: sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==}
dev: false
@@ -17028,12 +16840,6 @@ packages:
dependencies:
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:
resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==}
engines: {node: '>= 8.9.0'}
@@ -19123,31 +18929,6 @@ packages:
fsevents: 2.3.2
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:
resolution: {integrity: sha512-5hH7aNQe8rJiTTqCtPNX/6mIKlGw+1wg8UXwAxDIIN8XaSR+Zx3GT2zSu7QKa1vIaBqfUODGh3vpwY8r0AW/jw==}
engines: {node: '>=12.2.0'}