Compare commits
17 Commits
@nhost/rea
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd6f37f2a6 | ||
|
|
39df4d5b9c | ||
|
|
e91215bbac | ||
|
|
ccaa4c4bba | ||
|
|
ab36f90cec | ||
|
|
cfbe2db430 | ||
|
|
6838ac6201 | ||
|
|
0caf43037d | ||
|
|
4ed626d5b5 | ||
|
|
9ff9abee6a | ||
|
|
9d3f0521a5 | ||
|
|
744fd6929f | ||
|
|
f43f52e766 | ||
|
|
fd4c54ee91 | ||
|
|
b30ff6f507 | ||
|
|
ff7ae21a87 | ||
|
|
6d2c7b26c0 |
@@ -99,8 +99,8 @@ Nhost libraries and tools
|
||||
- [JavaScript/TypeScript SDK](https://docs.nhost.io/reference/sdk)
|
||||
- [Dart and Flutter SDK](https://github.com/nhost/nhost-dart)
|
||||
- [Nhost CLI](https://docs.nhost.io/reference/cli)
|
||||
- [Nhost React Auth](https://docs.nhost.io/reference/supporting-libraries/react-auth)
|
||||
- [Nhost React Apollo](https://docs.nhost.io/reference/supporting-libraries/react-apollo)
|
||||
- [Nhost React](https://docs.nhost.io/reference/react)
|
||||
- [Nhost Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
|
||||
## Community ❤️
|
||||
|
||||
|
||||
@@ -39,10 +39,10 @@ HTTP endpoints are automatically generated based on the file structure under `fu
|
||||
As such, given this file structure:
|
||||
|
||||
```js
|
||||
functions / index.js
|
||||
users / index.ts
|
||||
active.ts
|
||||
my - company.js
|
||||
functions/index.js
|
||||
functions/users/index.ts
|
||||
functions/active.ts
|
||||
functions/my-company.js
|
||||
```
|
||||
|
||||
The following endpoints will be available:
|
||||
|
||||
@@ -20,27 +20,27 @@ npm install @nhost/react @nhost/nextjs
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuring Nhost with Next.js follows the same logic as React, except we are initializing with `NhostSSR` instead of `Nhost`.
|
||||
Under the hood, `NhostSSR` uses cookies to store the refresh token, and disables auto-refresh and auto-login when running on the server-side.
|
||||
Configuring Nhost with Next.js follows the same logic as React, except we are initializing with the `NhostClient` from the `@nhost/nextjs` package.
|
||||
Under the hood, `NhostClient` uses cookies to store the refresh token, and disables auto-refresh and auto-login when running on the server-side.
|
||||
|
||||
```jsx
|
||||
// {project-root}/pages/_app.tsx
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
import { NhostSSR, NhostProvider } from '@nhost/nextjs'
|
||||
import { NhostClient, NhostNextProvider } from '@nhost/nextjs'
|
||||
|
||||
import Header from '../components/Header'
|
||||
|
||||
const nhost = new NhostSSR({ backendUrl: 'my-app.nhost.run' })
|
||||
const nhost = new NhostClient({ backendUrl: 'my-app.nhost.run' })
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<NhostProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<div>
|
||||
<Header />
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</NhostProvider>
|
||||
</NhostNextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ export default MyApp
|
||||
The logic is the same as in a classic React application:
|
||||
|
||||
```jsx
|
||||
// {project-root}/pages/csr-page.jsx
|
||||
// {project-root}/pages/csr-page.tsx
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
import { useAccessToken, useAuthenticated, useUserData } from '@nhost/react'
|
||||
import { useAccessToken, useAuthenticated, useUserData } from '@nhost/nextjs'
|
||||
|
||||
const ClientSidePage: React.FC = () => {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
@@ -83,7 +83,7 @@ export default ClientSidePage
|
||||
You need to load the session from the server first from `getServerSideProps`. Once it is done, the `_app` component will make sure to load or update the session through `pageProps`.
|
||||
|
||||
```jsx
|
||||
// {project-root}/pages/ssr-page.jsx
|
||||
// {project-root}/pages/ssr-page.tsx
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
@@ -93,7 +93,7 @@ import {
|
||||
useAccessToken,
|
||||
useAuthenticated,
|
||||
useUserData
|
||||
} from '@nhost/react'
|
||||
} from '@nhost/nextjs'
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const nhostSession = await getNhostSession('my-app.nhost.run', context)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: 'Introduction'
|
||||
---
|
||||
|
||||
It is possible to use [`@nhost/react`](/reference/react) in any Next.js page that would be configured to render on the client-side.
|
||||
All the React hooks and helpers from [`@nhost/react`](/reference/react) are available in Next.js and are exported in the `@nhost/nextjs` package.
|
||||
|
||||
When rendering a page from the server-side, Next.js needs to get some information from the client to determine their authentication status. Such communication is only available from cookies, and the Nhost client is designed to enable such a mechanism.
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ Create a `auth-protected.js` file:
|
||||
|
||||
```jsx
|
||||
import { useRouter } from 'next/router'
|
||||
import { useAuthLoading, useAuthenticated } from '@nhost/react'
|
||||
import { useAuthenticationStatus } from '@nhost/nextjs'
|
||||
|
||||
export function authProtected(Comp) {
|
||||
return function AuthProtected(props) {
|
||||
const router = useRouter()
|
||||
const isLoading = useAuthLoading()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
|
||||
@@ -7,38 +7,37 @@ title: 'Apollo GraphQL'
|
||||
With Yarn:
|
||||
|
||||
```sh
|
||||
yarn add @nhost/react @nhost/react-apollo
|
||||
yarn add @nhost/react @nhost/react-apollo @apollo/client
|
||||
```
|
||||
|
||||
With Npm:
|
||||
|
||||
```sh
|
||||
npm install @nhost/react @nhost/react-apollo
|
||||
npm install @nhost/react @nhost/react-apollo @apollo/client
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Let's add a `NhostApolloProvider`. Make sure the Apollo Provider is nested into `NhostProvider`, as it will need the Nhost context to determine the authentication headers to be sent to the GraphQL endpoint.
|
||||
Let's add a `NhostApolloProvider`. Make sure the Apollo Provider is nested into `NhostReactProvider`, as it will need the Nhost context to determine the authentication headers to be sent to the GraphQL endpoint.
|
||||
|
||||
```jsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import { NhostClient, NhostReactProvider } from '@nhost/react'
|
||||
|
||||
const nhost = new Nhost({
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: 'http://localhost:1337'
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<NhostApolloProvider>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<App />
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</NhostReactProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
@@ -7,36 +7,36 @@ title: 'Hooks'
|
||||
### Email and Password Sign-Un
|
||||
|
||||
```js
|
||||
const { signUp, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
useEmailPasswordSignUp(email?: string, password?: string, options?: Options )
|
||||
const { signUpEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useSignUpEmailPassword(email?: string, password?: string, options?: Options )
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signUp` | (email?: string, password?: string) => void | Used for a new user to sign up. The email/password arguments will take precedence over the possible state values used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsVerification` | boolean | Returns `true` if the sign-up has been accepted, but a verificaiton email has been sent and is awaiting. |
|
||||
| `isSuccess` | boolean | Returns `true` if the sign-up suceeded. Returns `false` if the new email needs to be verified first, or if an error occurred. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined | |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------ | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signUpEmailPassword` | (email?: string, password?: string) => void | Used for a new user to sign up. The email/password arguments will take precedence over the possible state values used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsEmailVerification` | boolean | Returns `true` if the sign-up has been accepted, but a verificaiton email has been sent and is awaiting. |
|
||||
| `isSuccess` | boolean | Returns `true` if the sign-up suceeded. Returns `false` if the new email needs to be verified first, or if an error occurred. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined | |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useEmailPasswordSignUp } from '@nhost/react'
|
||||
import { useSignUpEmailPassword } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const { signUp, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
useEmailPasswordSignUp(email, password)
|
||||
const { signUpEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useSignUpEmailPassword(email, password)
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Email" />
|
||||
@@ -45,9 +45,9 @@ const Component = () => {
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<button onClick={signUp}>Register</button>
|
||||
<button onClick={signUpEmailPassword}>Register</button>
|
||||
{isSuccess && <div>Your account have beed created! You are now authenticated</div>}
|
||||
{needsVerification && (
|
||||
{needsEmailVerification && (
|
||||
<div>Please check your mailbox and follow the verification link to verify your email</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -58,30 +58,32 @@ const Component = () => {
|
||||
### Email and Password Sign-In
|
||||
|
||||
```js
|
||||
const { signIn, isLoading, needsVerification, isSuccess, isError, error } =
|
||||
useEmailPasswordSignIn(email?: string, password?: string)
|
||||
const { signInEmailPassword, isLoading, needsEmailVerification, needsMfaOtp, sendMfaOtp, isSuccess, isError, error } =
|
||||
useSignInEmailPassword(email?: string, password?: string)
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signIn` | (email?: string, password?: string) => void | Will try to authenticate. The email/password arguments will take precedence over the possible state values used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsVerification` | boolean | Returns `true` if the user email is still pending verification. |
|
||||
| `isSuccess` | boolean | Returns `true` if the user has successfully authenticated. Returns `false` in case or error or if the new email needs to be verified first. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signInEmailPassword` | (email?: string, password?: string) => void | Will try to authenticate. The email/password arguments will take precedence over the possible state values used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsEmailVerification` | boolean | Returns `true` if the user email is still pending email verification. |
|
||||
| `needsMfaOtp` | boolean | Returns `true` if the server is awaiting an MFA one-time password to complete the authentication. |
|
||||
| `sendMfaOtp` | (otp: string) => void | Sends MFA One-time password. Will turn either `isSuccess` or `isError` to true, and store potential error in `error`. |
|
||||
| `isSuccess` | boolean | Returns `true` if the user has successfully authenticated. Returns `false` in case or error or if the new email needs to be verified first. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useEmailPasswordSignIn } from '@nhost/react'
|
||||
import { useSignInEmailPassword } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const { signIn, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
useEmailPasswordSignIn(email, password)
|
||||
const { signInEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useSignInEmailPassword(email, password)
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Email" />
|
||||
@@ -90,9 +92,9 @@ const Component = () => {
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<button onClick={signUp}>Register</button>
|
||||
<button onClick={signInEmailPassword}>Register</button>
|
||||
{isSuccess && <div>Authentication suceeded</div>}
|
||||
{needsVerification && (
|
||||
{needsEmailVerification && (
|
||||
<div>
|
||||
You must verify your email to sign in. Check your mailbox and follow the instructions to
|
||||
verify your email.
|
||||
@@ -105,49 +107,69 @@ const Component = () => {
|
||||
|
||||
### Oauth Providers
|
||||
|
||||
```js
|
||||
const providerLink = useProviderLink(options?: Options)
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasua Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```js
|
||||
import { useProviderLink } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const { github } = useProviderLink()
|
||||
return <a href={github}>Authenticate with GitHub</a>
|
||||
const { facebook, github } = useProviderLink()
|
||||
return
|
||||
;<div>
|
||||
<a href={facebook}>Authenticate with Facebook</a>
|
||||
<a href={github}>Authenticate with GitHub</a>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Passwordless email authentication
|
||||
|
||||
```js
|
||||
const { signIn, isLoading, isSuccess, isError, error } =
|
||||
useEmailPasswordlessSignIn(email?: string, options?: Options)
|
||||
const { signInEmailPasswordless, isLoading, isSuccess, isError, error } =
|
||||
useSignInEmailPasswordless(email?: string, options?: Options)
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signIn` | (email?: string) => void | Sends a magic link to the given email The email argument will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSuccess` | boolean | Returns `true` if the magic link email user has successfully send. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} | Provides details about the error. |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasua Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signInEmailPasswordless` | (email?: string) => void | Sends a magic link to the given email The email argument will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSuccess` | boolean | Returns `true` if the magic link email user has successfully send. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} | Provides details about the error. |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasua Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useEmailPasswordlessSignIn } from '@nhost/react'
|
||||
import { useSignInEmailPasswordless } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const { signIn, isLoading, isSuccess, isError, error } = useEmailPasswordlessSignIn(email)
|
||||
const { signInEmailPasswordless, isLoading, isSuccess, isError, error } =
|
||||
useSignInEmailPasswordless(email)
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Email" />
|
||||
<button onClick={signUp}>Register</button>
|
||||
<button onClick={signInEmailPasswordless}>Authenticate</button>
|
||||
{isSuccess && (
|
||||
<div>
|
||||
An email has been sent to {email}. Please check your mailbox and click on the
|
||||
@@ -192,22 +214,23 @@ const Component = () => {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication status
|
||||
|
||||
### `useAuthLoading`
|
||||
### `useAuthenticationStatus`
|
||||
|
||||
The Nhost client may need some initial steps to determine the authentication status during startup, like fetching a new JWT from an existing refresh token.
|
||||
|
||||
`useAuthLoading` will return `true` until the authentication status is known.
|
||||
`isLoading` will return `true` until the authentication status is known.
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useAuthLoading, useAuthenticated } from '@nhost/react'
|
||||
import { useAuthenticationStatus } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const isLoading = useAuthLoading()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
if (isLoading) return <div>Loading Nhost authentication status...</div>
|
||||
else if (isAuthenticated) return <div>User is authenticated</div>
|
||||
else return <div>Public section</div>
|
||||
@@ -216,29 +239,31 @@ const Component = () => {
|
||||
|
||||
### Get the JWT access token
|
||||
|
||||
<!-- TODO better documentation -->
|
||||
<!-- TODO ellaborate -->
|
||||
|
||||
```js
|
||||
const accessToken = useAccessToken()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User management
|
||||
|
||||
### Change email
|
||||
|
||||
```js
|
||||
const { changeEmail, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
const { changeEmail, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useChangeEmail(email?: string, options?: { redirectTo?: string })
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `changeEmail` | (email?: string) => void | Rrequests the email change. The arguement password will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsVerification` | boolean | Returns `true` if the email change has been requested, but that a email has been sent to the user to verify the new email. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------ | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `changeEmail` | (email?: string) => void | Requests the email change. The arguement password will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsEmailVerification` | boolean | Returns `true` if the email change has been requested, but that a email has been sent to the user to verify the new email. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
@@ -248,12 +273,13 @@ import { useChangeEmail } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const { changeEmail, isLoading, needsVerification, isError, error } = useChangeEmail(password)
|
||||
const { changeEmail, isLoading, needsEmailVerification, isError, error } =
|
||||
useChangeEmail(password)
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} />
|
||||
<button onClick={changeEmail}>Change password</button>
|
||||
{needsVerification && (
|
||||
{needsEmailVerification && (
|
||||
<div>
|
||||
Please check your mailbox and follow the verification link to confirm your new email
|
||||
</div>
|
||||
@@ -330,9 +356,48 @@ const Component = () => {
|
||||
}
|
||||
```
|
||||
|
||||
### Send email verification
|
||||
|
||||
```js
|
||||
const { sendEmail, isLoading, isSent, isError, error } =
|
||||
useSendVerificationEmail(email?: string, options?: { redirectTo?: string })
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ------------ | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `sendEmail` | (email?: string) => void | Requests the email change. The arguement password will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSent` | boolean | Returns `true` if the verification email has been sent |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useChangeEmail } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const { sendEmail, isLoading, isSent, isError, error } = useChangeEmail(password)
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} />
|
||||
<button onClick={sendEmail}>Send email verification</button>
|
||||
{isSent && (
|
||||
<div>Please check your mailbox and follow the verification link to confirm your email</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User data
|
||||
|
||||
<!-- TODO document -->
|
||||
<!-- TODO ellaborate -->
|
||||
|
||||
```js
|
||||
const userData = useUserData()
|
||||
|
||||
@@ -20,26 +20,25 @@ npm install @nhost/react
|
||||
|
||||
## Configuration
|
||||
|
||||
`@nhost/react` exports a React provider `NhostProvider` that makes the authentication state and the several hooks available in your application. Wrap this component around your whole App.
|
||||
`@nhost/react` exports a React provider `NhostReactProvider` that makes the authentication state and the several hooks available in your application. Wrap this component around your whole App.
|
||||
|
||||
```jsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import { NhostClient, NhostReactProvider } from '@nhost/react'
|
||||
|
||||
import App from './App'
|
||||
|
||||
const nhost = new Nhost({
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: 'http://localhost:1337'
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<App />
|
||||
</NhostProvider>
|
||||
</NhostReactProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
@@ -50,13 +49,20 @@ ReactDOM.render(
|
||||
### Options
|
||||
|
||||
```js
|
||||
const nhost = new Nhost({ backendUrl, autoSignIn, autoRefreshToken, storageGetter, storageSetter })
|
||||
const nhost = new NhostClient({
|
||||
backendUrl,
|
||||
autoLogin,
|
||||
autoRefreshToken,
|
||||
clientStorageGetter,
|
||||
clientStorageSetter
|
||||
})
|
||||
```
|
||||
|
||||
| Name | Type | Default | Notes |
|
||||
| ------------------ | ----------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `backendUrl` | string | | The Nhost app url, for instance `https://my-app.nhost.run`. When using the CLI, its value is `http://localhost:1337` |
|
||||
| `autoSignIn` | boolean | `true` | If set to `true`, the client will detect credentials in the current URL that could have been sent during an email verification or an Oauth authentication. It will also automatically authenticate all the active tabs in the current browser. |
|
||||
| `autoRefreshToken` | boolean | `true` | If set to `true`, the JWT (access token) will be automatically refreshed before it expires. |
|
||||
| `storageGetter` | (key:string) => string \| null | use localStorage | Nhost stores a refresh token in `localStorage` so the session can be restored when starting the browser. |
|
||||
| `storageSetter` | (key: string, value: string \| null | use localStorage | |
|
||||
| Name | Type | Default | Notes |
|
||||
| --------------------- | ----------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `backendUrl` | string | | The Nhost app url, for instance `https://my-app.nhost.run`. When using the CLI, its value is `http://localhost:1337` |
|
||||
| `autoLogin` | boolean | `true` | If set to `true`, the client will detect credentials in the current URL that could have been sent during an email verification or an Oauth authentication. It will also automatically authenticate all the active tabs in the current browser. |
|
||||
| `autoRefreshToken` | boolean | `true` | If set to `true`, the JWT (access token) will be automatically refreshed before it expires. |
|
||||
| `clientStorageGetter` | (key:string) => string \| null | use localStorage | Nhost stores a refresh token in `localStorage` so the session can be restored when starting the browser. |
|
||||
| `clientStorageGetter` | (key: string, value: string \| null | use localStorage | |
|
||||
| `refreshIntervalTime` | | |
|
||||
|
||||
@@ -8,11 +8,10 @@ You can protect routes by creating an `AuthGate` component when using `@nhost/re
|
||||
|
||||
```jsx
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import { useAuthLoading, useAuthenticated } from '@nhost/react'
|
||||
import { useAuthenticationStatus } from '@nhost/react'
|
||||
|
||||
export function AuthGate(children) {
|
||||
const isLoading = useAuthLoading()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- @nhost/react-apollo@3.0.0
|
||||
- @nhost/apollo@0.2.0
|
||||
|
||||
@@ -7,7 +7,8 @@ export default function Header() {
|
||||
<nav>
|
||||
<Link href="/">Index</Link> <br />
|
||||
<Link href="/second">Second</Link> <br />
|
||||
<Link href="/third">Third</Link> <br />
|
||||
<Link href="/third">SSR auth-guarded page</Link> <br />
|
||||
<Link href="/client-side-auth-guard">CSR auth-guarded page</Link> <br />
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
|
||||
21
examples/nextjs/components/protected-route.tsx
Normal file
21
examples/nextjs/components/protected-route.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useAuthenticationStatus } from '@nhost/nextjs'
|
||||
|
||||
export function authProtected(Comp) {
|
||||
return function AuthProtected(props) {
|
||||
const router = useRouter()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
console.log('Authentication guard: check auth status', { isLoading, isAuthenticated })
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
router.push('/')
|
||||
return null
|
||||
}
|
||||
|
||||
return <Comp {...props} />
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
environment:
|
||||
hasura_graphql_enable_remote_schema_permissions: false
|
||||
auth:
|
||||
version: 0.2.1
|
||||
version: 0.4.2
|
||||
auth:
|
||||
access_control:
|
||||
email:
|
||||
|
||||
@@ -4,7 +4,6 @@ table:
|
||||
configuration:
|
||||
custom_column_names:
|
||||
id: id
|
||||
redirect_url: redirectUrl
|
||||
custom_name: authProviderRequests
|
||||
custom_root_fields:
|
||||
delete: deleteAuthProviderRequests
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -9,17 +9,18 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nhost/nextjs": "^0.2.0",
|
||||
"@nhost/react": "^0.2.0",
|
||||
"@nhost/react-apollo": "^3.0.0",
|
||||
"@apollo/client": "^3.5.10",
|
||||
"@nhost/nextjs": "^1.0.0",
|
||||
"@nhost/react": "^0.3.0",
|
||||
"@nhost/react-apollo": "^4.0.0",
|
||||
"graphql": "^16.3.0",
|
||||
"next": "12.1.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.17",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "17.0.43",
|
||||
"@xstate/inspect": "^0.6.2",
|
||||
"eslint": "8.8.0",
|
||||
"eslint-config-next": "12.0.10",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { AppProps } from 'next/app'
|
||||
import React from 'react'
|
||||
|
||||
import { NhostSSR } from '@nhost/client'
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { NhostClient, NhostNextProvider } from '@nhost/nextjs'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
import { inspect } from '@xstate/inspect'
|
||||
|
||||
@@ -17,18 +16,18 @@ if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_DEBUG) {
|
||||
iframe: false
|
||||
})
|
||||
}
|
||||
const nhost = new NhostSSR({ backendUrl: BACKEND_URL })
|
||||
const nhost = new NhostClient({ backendUrl: BACKEND_URL })
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<NhostProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<NhostApolloProvider>
|
||||
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<div className="App">
|
||||
<Header />
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</NhostNextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
17
examples/nextjs/pages/client-side-auth-guard.tsx
Normal file
17
examples/nextjs/pages/client-side-auth-guard.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
|
||||
import { useAccessToken } from '@nhost/nextjs'
|
||||
|
||||
import { authProtected } from '../components/protected-route'
|
||||
|
||||
const ClientSideAuthPage: React.FC = () => {
|
||||
const accessToken = useAccessToken()
|
||||
return (
|
||||
<div>
|
||||
<h1>Client-side rendered page only accessible to authenticated users</h1>
|
||||
<div>Access token: {accessToken}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default authProtected(ClientSideAuthPage)
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
useAuthenticated,
|
||||
useChangeEmail,
|
||||
useChangePassword,
|
||||
useEmailPasswordlessSignIn,
|
||||
useEmailPasswordSignIn,
|
||||
useEmailPasswordSignUp,
|
||||
useSignOut
|
||||
} from '@nhost/react'
|
||||
useSignInEmailPassword,
|
||||
useSignInEmailPasswordless,
|
||||
useSignOut,
|
||||
useSignUpEmailPassword
|
||||
} from '@nhost/nextjs'
|
||||
import { useAuthQuery } from '@nhost/react-apollo'
|
||||
|
||||
import { BOOKS_QUERY } from '../helpers'
|
||||
@@ -25,9 +25,9 @@ const Home: NextPage = () => {
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const accessToken = useAccessToken()
|
||||
const { signOut } = useSignOut()
|
||||
const { signUp, ...signUpResult } = useEmailPasswordSignUp(email, password)
|
||||
const { signIn } = useEmailPasswordSignIn(email, password)
|
||||
const { signIn: passwordlessSignIn } = useEmailPasswordlessSignIn(email)
|
||||
const { signUpEmailPassword, ...signUpResult } = useSignUpEmailPassword(email, password)
|
||||
const { signInEmailPassword } = useSignInEmailPassword(email, password)
|
||||
const { signInEmailPasswordless } = useSignInEmailPasswordless(email)
|
||||
const { changeEmail, ...changeEmailResult } = useChangeEmail(newEmail)
|
||||
const { changePassword, ...changePasswordResult } = useChangePassword(newPassword)
|
||||
const { loading, data, error } = useAuthQuery(BOOKS_QUERY)
|
||||
@@ -46,11 +46,11 @@ const Home: NextPage = () => {
|
||||
) : (
|
||||
<>
|
||||
<input value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<button onClick={passwordlessSignIn}>Passwordless signin</button>
|
||||
<button onClick={signInEmailPasswordless}>Passwordless signin</button>
|
||||
<div>{JSON.stringify(signUpResult)}</div>
|
||||
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
|
||||
<button onClick={signUp}>Email + password sign-up</button>
|
||||
<button onClick={signIn}>Email + password sign-in</button>
|
||||
<button onClick={signUpEmailPassword}>Email + password sign-up</button>
|
||||
<button onClick={signInEmailPassword}>Email + password sign-in</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
import { getNhostSession, NhostSession } from '@nhost/nextjs'
|
||||
import { useAccessToken, useAuthenticated, useUserData } from '@nhost/react'
|
||||
import { NhostSession } from '@nhost/core'
|
||||
import { getNhostSession, useAccessToken, useAuthenticated, useUserData } from '@nhost/nextjs'
|
||||
|
||||
import { BACKEND_URL } from '../helpers'
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
import { getNhostSession, NhostSession } from '@nhost/nextjs'
|
||||
import { useAccessToken, useAuthenticated } from '@nhost/react'
|
||||
import { NhostSession } from '@nhost/core'
|
||||
import { getNhostSession, useAccessToken } from '@nhost/nextjs'
|
||||
|
||||
import { authProtected } from '../components/protected-route'
|
||||
import { BACKEND_URL } from '../helpers'
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
@@ -17,15 +18,12 @@ export async function getServerSideProps(context: NextPageContext) {
|
||||
|
||||
const RefetchPage: React.FC<{ initial: NhostSession }> = () => {
|
||||
const accessToken = useAccessToken()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
if (!isAuthenticated) return <div>User it not authenticated </div>
|
||||
return (
|
||||
<div>
|
||||
<h1>Third page</h1>
|
||||
User is authenticated: {isAuthenticated ? 'yes' : 'no'}
|
||||
<h1>SSR page only accessible to authenticated users</h1>
|
||||
<div>Access token: {accessToken}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefetchPage
|
||||
export default authProtected(RefetchPage)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
# @nhost-examples/react-apollo
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [207ae38]
|
||||
- @nhost/react-apollo@3.0.0
|
||||
- @nhost/nhost-js@0.3.11
|
||||
- @nhost/react-auth@2.0.9
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
style: {
|
||||
postcss: {
|
||||
plugins: [require("tailwindcss"), require("autoprefixer")],
|
||||
},
|
||||
},
|
||||
};
|
||||
7
examples/react-apollo-crm/functions/_utils/nhost.ts
Normal file
7
examples/react-apollo-crm/functions/_utils/nhost.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: process.env.NHOST_BACKEND_URL!
|
||||
})
|
||||
|
||||
export { nhost }
|
||||
@@ -1,107 +1,105 @@
|
||||
import { Request, Response } from "express";
|
||||
import { nhost } from "../../../src/utils/nhost";
|
||||
import { Request, Response } from 'express'
|
||||
import { nhost } from '../../_utils/nhost'
|
||||
|
||||
const handler = async (req: Request, res: Response) => {
|
||||
if (
|
||||
req.headers["nhsot-webhook-secret"] !== process.env.NHSOT_WEBHOOK_SECRET
|
||||
) {
|
||||
return res.status(401).send("Unauthorized");
|
||||
}
|
||||
if (req.headers['nhsot-webhook-secret'] !== process.env.NHSOT_WEBHOOK_SECRET) {
|
||||
return res.status(401).send('Unauthorized')
|
||||
}
|
||||
|
||||
// User who just signed up
|
||||
const user = req.body.event.data.new;
|
||||
// User who just signed up
|
||||
const user = req.body.event.data.new
|
||||
|
||||
// Get the user's email domain
|
||||
const emailDomain = user.email.split("@")[1];
|
||||
// Get the user's email domain
|
||||
const emailDomain = user.email.split('@')[1]
|
||||
|
||||
// Check if a company with the user's email domain already exists.
|
||||
const GET_COMPANY_WITH_EMAIL_DOMAIN = `
|
||||
// Check if a company with the user's email domain already exists.
|
||||
const GET_COMPANY_WITH_EMAIL_DOMAIN = `
|
||||
query getCompanyWithEmailDomain($emailDomain: String!) {
|
||||
companies(where: { emailDomain: { _eq: $emailDomain } }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
GET_COMPANY_WITH_EMAIL_DOMAIN,
|
||||
{
|
||||
emailDomain,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-hasura-admin-secret": process.env.NHOST_ADMIN_SECRET,
|
||||
`
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
GET_COMPANY_WITH_EMAIL_DOMAIN,
|
||||
{
|
||||
emailDomain
|
||||
},
|
||||
}
|
||||
);
|
||||
{
|
||||
headers: {
|
||||
'x-hasura-admin-secret': process.env.NHOST_ADMIN_SECRET
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return res.status(500).send(error);
|
||||
}
|
||||
if (error) {
|
||||
return res.status(500).send(error)
|
||||
}
|
||||
|
||||
const { companies } = data as any;
|
||||
const { companies } = data as any
|
||||
|
||||
let companyId;
|
||||
if (companies.length === 1) {
|
||||
// if a company already exists, use that company's id
|
||||
companyId = companies[0].id;
|
||||
} else {
|
||||
// else, create a new company for the newly created user with the same email domain as the user
|
||||
const CREATE_NEW_COMPANY = `
|
||||
let companyId
|
||||
if (companies.length === 1) {
|
||||
// if a company already exists, use that company's id
|
||||
companyId = companies[0].id
|
||||
} else {
|
||||
// else, create a new company for the newly created user with the same email domain as the user
|
||||
const CREATE_NEW_COMPANY = `
|
||||
mutation insertCompany($emailDomain: String!) {
|
||||
insertCompany(object: { name: $emailDomain, emailDomain: $emailDomain }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
CREATE_NEW_COMPANY,
|
||||
{
|
||||
emailDomain,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-hasura-admin-secret": process.env.NHOST_ADMIN_SECRET,
|
||||
},
|
||||
`
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
CREATE_NEW_COMPANY,
|
||||
{
|
||||
emailDomain
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-hasura-admin-secret': process.env.NHOST_ADMIN_SECRET
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return res.status(500).send(error)
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return res.status(500).send(error);
|
||||
}
|
||||
const { insertCompany } = data as any
|
||||
|
||||
const { insertCompany } = data as any;
|
||||
companyId = insertCompany.id
|
||||
}
|
||||
|
||||
companyId = insertCompany.id;
|
||||
}
|
||||
// We now have the company id of an existing, or a newly created company.
|
||||
// Now let's add the user to the company.
|
||||
|
||||
// We now have the company id of an existing, or a newly created company.
|
||||
// Now let's add the user to the company.
|
||||
|
||||
const ADD_USER_TO_COMPANY = `
|
||||
const ADD_USER_TO_COMPANY = `
|
||||
mutation addUserToCompany($userId: uuid!, $companyId: uuid!) {
|
||||
insertCompanyUser(object: {userId: $userId, companyId: $companyId}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { error: addUserToCompanyError } = await nhost.graphql.request(
|
||||
ADD_USER_TO_COMPANY,
|
||||
{
|
||||
userId: user.id,
|
||||
companyId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-hasura-admin-secret": process.env.NHOST_ADMIN_SECRET,
|
||||
`
|
||||
const { error: addUserToCompanyError } = await nhost.graphql.request(
|
||||
ADD_USER_TO_COMPANY,
|
||||
{
|
||||
userId: user.id,
|
||||
companyId
|
||||
},
|
||||
}
|
||||
);
|
||||
{
|
||||
headers: {
|
||||
'x-hasura-admin-secret': process.env.NHOST_ADMIN_SECRET
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (addUserToCompanyError) {
|
||||
return res.status(500).send(error);
|
||||
}
|
||||
if (addUserToCompanyError) {
|
||||
return res.status(500).send(error)
|
||||
}
|
||||
|
||||
res.status(200).send(`OK`);
|
||||
};
|
||||
res.status(200).send(`OK`)
|
||||
}
|
||||
|
||||
export default handler;
|
||||
export default handler
|
||||
|
||||
46182
examples/react-apollo-crm/package-lock.json
generated
46182
examples/react-apollo-crm/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,11 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.4.16",
|
||||
"@craco/craco": "^6.4.0",
|
||||
"@headlessui/react": "^1.4.2",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@nhost/nhost-js": "^0.3.4",
|
||||
"@nhost/react-apollo": "^2.0.7-0",
|
||||
"@nhost/react-auth": "^2.0.3",
|
||||
"@saeris/apollo-server-vercel": "^1.0.1",
|
||||
"@nhost/nhost-js": "^1.0.0",
|
||||
"@nhost/react": "^0.3.0",
|
||||
"@nhost/react-apollo": "^4.0.0",
|
||||
"@tailwindcss/forms": "^0.3.4",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
@@ -27,14 +25,14 @@
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-scripts": "^5.0.0",
|
||||
"typescript": "^4.1.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"codegen": "graphql-codegen --config codegen.yaml --errors-only"
|
||||
},
|
||||
@@ -68,4 +66,4 @@
|
||||
"postcss": "^7.0.39",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import "./App.css";
|
||||
import { NhostAuthProvider } from "@nhost/react-auth";
|
||||
import { NhostApolloProvider } from "@nhost/react-apollo";
|
||||
import { nhost } from "./utils/nhost";
|
||||
import { Route, Routes } from "react-router";
|
||||
import { Layout } from "./components/ui/Layout";
|
||||
import { Customers } from "./components/Customers";
|
||||
import { Dashboard } from "./components/Dashboard";
|
||||
import { NewCustomer } from "./components/NewCustomer";
|
||||
import { RequireAuth } from "./components/RequireAuth";
|
||||
import { Customer } from "./components/Customer";
|
||||
import { SignUp } from "./components/SignUp";
|
||||
import { SignIn } from "./components/SignIn";
|
||||
import { ResetPassword } from "./components/ResetPassword";
|
||||
import './App.css'
|
||||
import { NhostReactProvider } from '@nhost/react'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
import { nhost } from './utils/nhost'
|
||||
import { Route, Routes } from 'react-router'
|
||||
import { Layout } from './components/ui/Layout'
|
||||
import { Customers } from './components/Customers'
|
||||
import { Dashboard } from './components/Dashboard'
|
||||
import { NewCustomer } from './components/NewCustomer'
|
||||
import { RequireAuth } from './components/RequireAuth'
|
||||
import { Customer } from './components/Customer'
|
||||
import { SignUp } from './components/SignUp'
|
||||
import { SignIn } from './components/SignIn'
|
||||
import { ResetPassword } from './components/ResetPassword'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<NhostAuthProvider nhost={nhost}>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<AppRouter />
|
||||
</NhostApolloProvider>
|
||||
</NhostAuthProvider>
|
||||
);
|
||||
</NhostReactProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function AppRouter() {
|
||||
@@ -55,7 +55,7 @@ function AppRouter() {
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
||||
@@ -1,98 +1,93 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { CheckIcon } from "@heroicons/react/outline";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function ChangePasswordModal() {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [open, setOpen] = useState(true)
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.changePassword({ newPassword });
|
||||
const { error } = await nhost.auth.changePassword({ newPassword })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
}
|
||||
if (error) {
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
onClose={setOpen}
|
||||
>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" onClose={setOpen}>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span
|
||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg leading-6 font-medium text-gray-900"
|
||||
>
|
||||
Change Password
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
tabIndex={2}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span
|
||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg leading-6 font-medium text-gray-900"
|
||||
>
|
||||
Change Password
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
tabIndex={2}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm"
|
||||
>
|
||||
Set new password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm"
|
||||
>
|
||||
Set new password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import React from "react";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import React from 'react'
|
||||
import { Navigate, useLocation } from 'react-router'
|
||||
|
||||
export function RequireAuth({ children }: { children: JSX.Element }) {
|
||||
const { isAuthenticated, isLoading } = useNhostAuth();
|
||||
const location = useLocation();
|
||||
const { isAuthenticated, isLoading } = useNhostAuth()
|
||||
const location = useLocation()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading user data...</div>;
|
||||
return <div>Loading user data...</div>
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/sign-in" state={{ from: location }} />;
|
||||
return <Navigate to="/sign-in" state={{ from: location }} />
|
||||
}
|
||||
|
||||
return children;
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function ResetPassword() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const { isAuthenticated } = useNhostAuth();
|
||||
const { isAuthenticated } = useNhostAuth()
|
||||
|
||||
let navigate = useNavigate();
|
||||
let navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.resetPassword({ email });
|
||||
const { error } = await nhost.auth.resetPassword({ email })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
alert("Check out email inbox");
|
||||
};
|
||||
alert('Check out email inbox')
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate("/");
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -33,19 +33,14 @@ export function ResetPassword() {
|
||||
<div className="flex justify-center">
|
||||
<div className="text-2xl font-bold text-blue-700">AquaSystem</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900">
|
||||
Reset Password
|
||||
</h2>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900">Reset Password</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -77,5 +72,5 @@ export function ResetPassword() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function SignIn() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const { isAuthenticated } = useNhostAuth();
|
||||
const { isAuthenticated } = useNhostAuth()
|
||||
|
||||
let navigate = useNavigate();
|
||||
let navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.signIn({ email, password });
|
||||
const { error } = await nhost.auth.signIn({ email, password })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
navigate("/", { replace: true });
|
||||
};
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate("/");
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -44,10 +44,7 @@ export function SignIn() {
|
||||
<div className="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -66,10 +63,7 @@ export function SignIn() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -110,7 +104,7 @@ export function SignIn() {
|
||||
</form>
|
||||
</div>
|
||||
<div className="text-center py-4">
|
||||
Don't have an account?{" "}
|
||||
Don't have an account?{' '}
|
||||
<Link to="/sign-up" className="text-blue-600 hover:text-blue-500">
|
||||
Sign Up
|
||||
</Link>
|
||||
@@ -118,5 +112,5 @@ export function SignIn() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function SignUp() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const { isAuthenticated } = useNhostAuth();
|
||||
const { isAuthenticated } = useNhostAuth()
|
||||
|
||||
let navigate = useNavigate();
|
||||
let navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.signUp({ email, password });
|
||||
const { error } = await nhost.auth.signUp({ email, password })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
navigate("/", { replace: true });
|
||||
};
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate("/");
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -44,10 +44,7 @@ export function SignUp() {
|
||||
<div className="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -66,10 +63,7 @@ export function SignUp() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -100,7 +94,7 @@ export function SignUp() {
|
||||
</div>
|
||||
|
||||
<div className="text-center py-4">
|
||||
Already have an account?{" "}
|
||||
Already have an account?{' '}
|
||||
<Link to="/sign-in" className="text-blue-600 hover:text-blue-500">
|
||||
Sign In
|
||||
</Link>
|
||||
@@ -108,5 +102,5 @@ export function SignUp() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import { Dialog, Menu, Transition } from '@headlessui/react'
|
||||
import {
|
||||
FolderIcon,
|
||||
HomeIcon,
|
||||
InboxIcon,
|
||||
MenuAlt2Icon,
|
||||
UsersIcon,
|
||||
XIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { SearchIcon } from "@heroicons/react/solid";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { nhost } from "../../utils/nhost";
|
||||
import { ChangePasswordModal } from "../ChangePasswordModal";
|
||||
XIcon
|
||||
} from '@heroicons/react/outline'
|
||||
import { SearchIcon } from '@heroicons/react/solid'
|
||||
import { NavLink, Outlet } from 'react-router-dom'
|
||||
import { nhost } from '../../utils/nhost'
|
||||
import { ChangePasswordModal } from '../ChangePasswordModal'
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/", icon: HomeIcon, current: true },
|
||||
{ name: "Orders", href: "/orders", icon: UsersIcon, current: false },
|
||||
{ name: "Customers", href: "/customers", icon: FolderIcon, current: false },
|
||||
{ name: "Settings", href: "/settings", icon: InboxIcon, current: false },
|
||||
];
|
||||
{ name: 'Dashboard', href: '/', icon: HomeIcon, current: true },
|
||||
{ name: 'Orders', href: '/orders', icon: UsersIcon, current: false },
|
||||
{ name: 'Customers', href: '/customers', icon: FolderIcon, current: false },
|
||||
{ name: 'Settings', href: '/settings', icon: InboxIcon, current: false }
|
||||
]
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export function Layout() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false)
|
||||
|
||||
console.log("Layout Reload");
|
||||
console.log('Layout Reload')
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useEffect RUN");
|
||||
console.log('useEffect RUN')
|
||||
|
||||
if (window.location.hash.search("type=passwordReset") !== -1) {
|
||||
console.log("FOUND!");
|
||||
if (window.location.hash.search('type=passwordReset') !== -1) {
|
||||
console.log('FOUND!')
|
||||
|
||||
setShowChangePasswordModal(true);
|
||||
setShowChangePasswordModal(true)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showChangePasswordModal && <ChangePasswordModal />}
|
||||
<div>
|
||||
<Transition.Root show={sidebarOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed inset-0 z-40 flex md:hidden"
|
||||
onClose={setSidebarOpen}
|
||||
>
|
||||
<Dialog as="div" className="fixed inset-0 z-40 flex md:hidden" onClose={setSidebarOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
@@ -87,10 +83,7 @@ export function Layout() {
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XIcon
|
||||
className="w-6 h-6 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon className="w-6 h-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
@@ -109,11 +102,9 @@ export function Layout() {
|
||||
to={item.href}
|
||||
className={({ isActive }) => {
|
||||
return classNames(
|
||||
isActive
|
||||
? "bg-blue-800 text-white"
|
||||
: "text-blue-100 hover:bg-blue-600",
|
||||
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
|
||||
);
|
||||
isActive ? 'bg-blue-800 text-white' : 'text-blue-100 hover:bg-blue-600',
|
||||
'group flex items-center px-2 py-2 text-base font-medium rounded-md'
|
||||
)
|
||||
}}
|
||||
>
|
||||
<item.icon
|
||||
@@ -138,9 +129,7 @@ export function Layout() {
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex flex-col flex-grow pt-5 overflow-y-auto bg-blue-700">
|
||||
<div className="flex items-center flex-shrink-0 px-4">
|
||||
<span className="text-lg font-semibold text-white">
|
||||
AquaSystem
|
||||
</span>
|
||||
<span className="text-lg font-semibold text-white">AquaSystem</span>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-5">
|
||||
<nav className="flex-1 px-2 pb-4 space-y-1">
|
||||
@@ -150,11 +139,9 @@ export function Layout() {
|
||||
to={item.href}
|
||||
className={({ isActive }) => {
|
||||
return classNames(
|
||||
isActive
|
||||
? "bg-blue-800 text-white"
|
||||
: "text-blue-100 hover:bg-blue-600",
|
||||
"group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||
);
|
||||
isActive ? 'bg-blue-800 text-white' : 'text-blue-100 hover:bg-blue-600',
|
||||
'group flex items-center px-2 py-2 text-sm font-medium rounded-md'
|
||||
)
|
||||
}}
|
||||
>
|
||||
<item.icon
|
||||
@@ -234,11 +221,11 @@ export function Layout() {
|
||||
<div
|
||||
// to={"/login"}
|
||||
onClick={async () => {
|
||||
await nhost.auth.signOut();
|
||||
await nhost.auth.signOut()
|
||||
}}
|
||||
className={classNames(
|
||||
active ? "bg-gray-100" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700"
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700'
|
||||
)}
|
||||
>
|
||||
Sign out
|
||||
@@ -260,5 +247,5 @@ export function Layout() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NhostClient } from "@nhost/nhost-js";
|
||||
import { NhostClient } from '@nhost/react'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: process.env.REACT_APP_BACKEND_URL!,
|
||||
});
|
||||
backendUrl: process.env.REACT_APP_BACKEND_URL!
|
||||
})
|
||||
|
||||
export { nhost };
|
||||
export { nhost }
|
||||
|
||||
11360
examples/react-apollo-crm/yarn.lock
Normal file
11360
examples/react-apollo-crm/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ services:
|
||||
environment:
|
||||
hasura_graphql_enable_remote_schema_permissions: false
|
||||
auth:
|
||||
version: 0.2.1
|
||||
version: 0.4.2
|
||||
auth:
|
||||
access_control:
|
||||
email:
|
||||
@@ -124,10 +124,10 @@ auth:
|
||||
allowed_roles: user,me
|
||||
default_allowed_roles: user,me
|
||||
default_role: user
|
||||
mfa:
|
||||
enabled: false
|
||||
issuer: nhost
|
||||
signin_email_verified_required: true
|
||||
mfa:
|
||||
enabled: true
|
||||
issuer: nhost
|
||||
storage:
|
||||
force_download_for_content_types: text/html,application/javascript
|
||||
version: 3
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-apollo",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@nhost/react": "^0.2.0",
|
||||
"@nhost/react-apollo": "^3.0.0",
|
||||
"@apollo/client": "^3.5.10",
|
||||
"@nhost/react": "^0.3.0",
|
||||
"@nhost/react-apollo": "^4.0.0",
|
||||
"@rsuite/icons": "^1.0.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"react": "^17.0.2",
|
||||
@@ -14,7 +15,6 @@
|
||||
"react-router-dom": "^6.2.1",
|
||||
"rsuite": "^5.6.2"
|
||||
},
|
||||
"lib": "workspace:*",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Container, Header, Navbar, Content, Nav } from 'rsuite'
|
||||
import { useEffect } from 'react'
|
||||
import { SignInPage } from './sign-in'
|
||||
import { AuthGate } from './components/auth-gates'
|
||||
import { AuthGate, PublicGate } from './components/auth-gates'
|
||||
import Home from './Home'
|
||||
import { ProfilePage } from './profile'
|
||||
import { ApolloPage } from './apollo'
|
||||
@@ -57,8 +57,22 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/sign-in/*" element={<SignInPage />} />
|
||||
<Route path="/sign-up/*" element={<SignUpPage />} />
|
||||
<Route
|
||||
path="/sign-in/*"
|
||||
element={
|
||||
<PublicGate>
|
||||
<SignInPage />
|
||||
</PublicGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sign-up/*"
|
||||
element={
|
||||
<PublicGate>
|
||||
<SignUpPage />
|
||||
</PublicGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthenticated, useAuthLoading } from '@nhost/react'
|
||||
import { useAuthenticationStatus } from '@nhost/react'
|
||||
|
||||
export const AuthGate: React.FC = ({ children }) => {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const isLoading = useAuthLoading()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
const location = useLocation()
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
@@ -17,16 +16,14 @@ export const AuthGate: React.FC = ({ children }) => {
|
||||
}
|
||||
|
||||
export const PublicGate: React.FC = ({ children }) => {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const isLoading = useAuthLoading()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
const location = useLocation()
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
// ? stay on the same route - is it the best way to do so?
|
||||
return <Navigate to={location} state={{ from: location }} replace />
|
||||
return <Navigate to={'/'} state={{ from: location }} replace />
|
||||
}
|
||||
|
||||
return <div>{children}</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Button, Input, Message } from 'rsuite'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useEmailPasswordlessSignIn } from '@nhost/react'
|
||||
import { useSignInEmailPasswordless } from '@nhost/react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export const EmailPasswordlessForm: React.FC = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const navigate = useNavigate()
|
||||
const { signIn, isError, isSuccess, error } = useEmailPasswordlessSignIn(email, {
|
||||
const { signInEmailPasswordless, isError, isSuccess, error } = useSignInEmailPasswordless(email, {
|
||||
redirectTo: '/profile'
|
||||
})
|
||||
const [showError, setShowError] = useState(true)
|
||||
@@ -42,7 +42,7 @@ export const EmailPasswordlessForm: React.FC = () => {
|
||||
style={{ marginTop: '0.5em' }}
|
||||
onClick={() => {
|
||||
setShowError(true)
|
||||
signIn()
|
||||
signInEmailPasswordless()
|
||||
}}
|
||||
>
|
||||
Continue with email
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Icon } from '@rsuite/icons'
|
||||
import { useProviderLink } from '@nhost/react'
|
||||
|
||||
export const OAuthLinks: React.FC = () => {
|
||||
// TODO show how to use options
|
||||
const { github, google, facebook } = useProviderLink()
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import { NhostClient, NhostReactProvider } from '@nhost/react'
|
||||
import 'rsuite/styles/index.less' // or 'rsuite/dist/rsuite.min.css'
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
|
||||
const nhost = new Nhost({
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: import.meta.env.VITE_NHOST_URL || 'http://localhost:1337'
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<NhostApolloProvider>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<App />
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</NhostReactProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Button, FlexboxGrid, Input, Message, Panel, toaster, Notification } fro
|
||||
export const ChangeEmail: React.FC = () => {
|
||||
const [newEmail, setNewEmail] = useState('')
|
||||
const email = useEmail()
|
||||
const { changeEmail, error, needsVerification } = useChangeEmail(newEmail, {
|
||||
const { changeEmail, error, needsEmailVerification } = useChangeEmail(newEmail, {
|
||||
redirectTo: '/profile'
|
||||
})
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (needsVerification) {
|
||||
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
|
||||
@@ -21,7 +21,7 @@ export const ChangeEmail: React.FC = () => {
|
||||
setNewEmail('')
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [needsVerification])
|
||||
}, [needsEmailVerification])
|
||||
|
||||
// * Set error message from the registration hook errors
|
||||
useEffect(() => {
|
||||
@@ -33,7 +33,8 @@ export const ChangeEmail: React.FC = () => {
|
||||
}, [newEmail])
|
||||
// * Show an error message when passwords are different
|
||||
useEffect(() => {
|
||||
if (email === newEmail) setErrorMessage('You need to set a different email as the current one')
|
||||
if (newEmail && email === newEmail)
|
||||
setErrorMessage('You need to set a different email as the current one')
|
||||
else setErrorMessage('')
|
||||
}, [email, newEmail])
|
||||
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import decode from 'jwt-decode'
|
||||
import ReactJson from 'react-json-view'
|
||||
import { Col, Panel, Row } from 'rsuite'
|
||||
import { useAccessToken, useUserData } from '@nhost/react'
|
||||
import { Button, Col, Panel, Row } from 'rsuite'
|
||||
import { useAccessToken, useNhostClient, useUserData } from '@nhost/react'
|
||||
|
||||
import { ChangeEmail } from './change-email'
|
||||
import { ChangePassword } from './change-password'
|
||||
import { Mfa } from './mfa'
|
||||
|
||||
export const ProfilePage: React.FC = () => {
|
||||
const accessToken = useAccessToken()
|
||||
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>
|
||||
@@ -33,6 +38,9 @@ export const ProfilePage: React.FC = () => {
|
||||
</Col>
|
||||
<Col md={12} sm={24}>
|
||||
<Panel header="JWT" bordered>
|
||||
<Button block appearance="primary" onClick={() => nhost.auth.refreshSession()}>
|
||||
Refresh session
|
||||
</Button>
|
||||
{accessToken && (
|
||||
<ReactJson
|
||||
src={decode(accessToken)}
|
||||
|
||||
29
examples/react-apollo/src/profile/mfa.tsx
Normal file
29
examples/react-apollo/src/profile/mfa.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useConfigMfa } from '@nhost/react'
|
||||
import { useState } from 'react'
|
||||
import { Button, Input, Panel } from 'rsuite'
|
||||
|
||||
export const Mfa: React.FC = () => {
|
||||
const [code, setCode] = useState('')
|
||||
const { generateQrCode, activateMfa, isActivated, isGenerated, qrCodeDataUrl } =
|
||||
useConfigMfa(code)
|
||||
|
||||
return (
|
||||
<Panel header="Activate 2-step verification" bordered>
|
||||
{!isGenerated && (
|
||||
<Button block appearance="primary" onClick={generateQrCode}>
|
||||
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}>
|
||||
Activate
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isActivated && <div>MFA has been activated!!!</div>}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,26 @@
|
||||
import { Button, Divider, Input, Message } from 'rsuite'
|
||||
import { useEmailPasswordSignIn } from '@nhost/react'
|
||||
import { useSignInEmailPassword } from '@nhost/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
|
||||
const Footer: React.FC = () => (
|
||||
<div>
|
||||
<Divider />
|
||||
<Button as={NavLink} to="/sign-in" block appearance="link">
|
||||
← Other Login Options
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const EmailPassword: React.FC = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const { signIn, error } = useEmailPasswordSignIn(email, password)
|
||||
const [otp, setOtp] = useState('')
|
||||
const { signInEmailPassword, error, needsMfaOtp, sendMfaOtp } = useSignInEmailPassword(
|
||||
email,
|
||||
password,
|
||||
otp
|
||||
)
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
// * Set error message from the authentication hook errors
|
||||
@@ -18,41 +32,61 @@ export const EmailPassword: React.FC = () => {
|
||||
setErrorMessage('')
|
||||
}, [email, password])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
placeholder="Email Address"
|
||||
size="lg"
|
||||
autoFocus
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
if (needsMfaOtp)
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={otp}
|
||||
onChange={setOtp}
|
||||
placeholder="One-time password"
|
||||
size="lg"
|
||||
autoFocus
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<Message showIcon type="error">
|
||||
{errorMessage}
|
||||
</Message>
|
||||
)}
|
||||
<Button appearance="primary" onClick={sendMfaOtp} block>
|
||||
Send 2-step verification code
|
||||
</Button>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
placeholder="Email Address"
|
||||
size="lg"
|
||||
autoFocus
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
|
||||
{errorMessage && (
|
||||
<Message showIcon type="error">
|
||||
{errorMessage}
|
||||
</Message>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<Message showIcon type="error">
|
||||
{errorMessage}
|
||||
</Message>
|
||||
)}
|
||||
|
||||
<Button appearance="primary" onClick={signIn} block>
|
||||
Sign in
|
||||
</Button>
|
||||
<Button as={NavLink} block to="/sign-in/forgot-password">
|
||||
Forgot password?
|
||||
</Button>
|
||||
<Divider />
|
||||
<Button as={NavLink} to="/sign-in" block appearance="link">
|
||||
← Other Login Options
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
<Button appearance="primary" onClick={signInEmailPassword} block>
|
||||
Sign in
|
||||
</Button>
|
||||
<Button as={NavLink} block to="/sign-in/forgot-password">
|
||||
Forgot password?
|
||||
</Button>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import { NavLink, Route, Routes } from 'react-router-dom'
|
||||
import { Link, NavLink, Route, Routes } from 'react-router-dom'
|
||||
import { Button, Divider, FlexboxGrid, IconButton, Panel } from 'rsuite'
|
||||
import { Icon } from '@rsuite/icons'
|
||||
import { FaLock } from 'react-icons/fa'
|
||||
@@ -9,7 +9,7 @@ import { VerificationEmailSent } from '../verification-email-sent'
|
||||
import { EmailPassword } from './email-password'
|
||||
import { ForgotPassword } from './forgot-password'
|
||||
import { EmailPasswordless } from './email-passwordless'
|
||||
// import { useAnonymousSignIn } from '@nhost/react'
|
||||
// import { useSignInAnonymous } from '@nhost/react'
|
||||
|
||||
const Index: React.FC = () => (
|
||||
<div>
|
||||
@@ -31,7 +31,7 @@ const Index: React.FC = () => (
|
||||
)
|
||||
|
||||
export const SignInPage: React.FC = () => {
|
||||
// const { signIn } = useAnonymousSignIn()
|
||||
// const { signIn } = useSignInAnonymous()
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<FlexboxGrid justify="center">
|
||||
@@ -48,7 +48,8 @@ export const SignInPage: React.FC = () => {
|
||||
</FlexboxGrid.Item>
|
||||
</FlexboxGrid>
|
||||
<Divider />
|
||||
{/* Don't have an account? <Link to="/sign-up">Sign up</Link> or{' '}
|
||||
Don't have an account? <Link to="/sign-up">Sign up</Link>
|
||||
{/* or{' '}
|
||||
<a href="#" onClick={signIn}>
|
||||
enter the app anonymously
|
||||
</a> */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Input, Message } from 'rsuite'
|
||||
import { useEmailPasswordSignUp } from '@nhost/react'
|
||||
import { useSignUpEmailPassword } from '@nhost/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { NavLink, useNavigate } from 'react-router-dom'
|
||||
|
||||
@@ -14,18 +14,18 @@ export const EmailPassword: React.FC = () => {
|
||||
)
|
||||
const navigate = useNavigate()
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const { signUp, error, needsVerification, isSuccess } = useEmailPasswordSignUp(
|
||||
const { signUpEmailPassword, error, needsEmailVerification, isSuccess } = useSignUpEmailPassword(
|
||||
email,
|
||||
password,
|
||||
options
|
||||
)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
useEffect(() => {
|
||||
if (needsVerification) navigate('/sign-up/verification-email-sent')
|
||||
if (needsEmailVerification) navigate('/sign-up/verification-email-sent')
|
||||
else if (isSuccess) navigate('/')
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [needsVerification, isSuccess])
|
||||
}, [needsEmailVerification, isSuccess])
|
||||
|
||||
// * Set error message from the registration hook errors
|
||||
useEffect(() => {
|
||||
@@ -37,7 +37,7 @@ export const EmailPassword: React.FC = () => {
|
||||
}, [email, password])
|
||||
// * Show an error message when passwords are different
|
||||
useEffect(() => {
|
||||
if (password !== confirmPassword) setErrorMessage('Provided passwords must be the same')
|
||||
if (password !== confirmPassword) setErrorMessage('Both passwords must be the same')
|
||||
else setErrorMessage('')
|
||||
}, [password, confirmPassword])
|
||||
return (
|
||||
@@ -91,7 +91,7 @@ export const EmailPassword: React.FC = () => {
|
||||
appearance="primary"
|
||||
onClick={() => {
|
||||
setErrorMessage('')
|
||||
signUp()
|
||||
signUpEmailPassword()
|
||||
}}
|
||||
block
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ services:
|
||||
environment:
|
||||
hasura_graphql_enable_remote_schema_permissions: false
|
||||
auth:
|
||||
version: 0.2.1
|
||||
version: 0.4.2
|
||||
auth:
|
||||
access_control:
|
||||
email:
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 744fd69: Unify vanilla, react and next APIs so they can work together
|
||||
React and NextJS libraries now works together with `@nhost/nhost-js`. It also means the Nhost client needs to be initiated before passing it to the React provider.
|
||||
See the [React](https://docs.nhost.io/reference/react#configuration) and [NextJS](https://docs.nhost.io/reference/nextjs/configuration) configuration documentation for additional information.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [744fd69]
|
||||
- Updated dependencies [744fd69]
|
||||
- @nhost/core@0.3.0
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Nhost
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -36,26 +36,30 @@
|
||||
"main": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.umd.js",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.umd.js"
|
||||
"require": "./dist/index.cjs.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@apollo/client": "^3.5.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.5.8",
|
||||
"@nhost/client": "workspace:*",
|
||||
"@nhost/core": "workspace:^",
|
||||
"graphql": "16",
|
||||
"subscriptions-transport-ws": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"xstate": "^4.30.5"
|
||||
"@apollo/client": "^3.5.8",
|
||||
"xstate": "^4.30.5",
|
||||
"@nhost/nhost-js": "workspace:^"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
import { setContext } from '@apollo/client/link/context'
|
||||
import { WebSocketLink } from '@apollo/client/link/ws'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
export type NhostApolloClientOptions = {
|
||||
nhost?: Nhost
|
||||
nhost?: NhostClient
|
||||
graphqlUrl?: string
|
||||
headers?: any
|
||||
publicRole?: string
|
||||
fetchPolicy?: WatchQueryFetchPolicy
|
||||
@@ -28,6 +29,7 @@ export type NhostApolloClientOptions = {
|
||||
|
||||
export const createApolloClient = ({
|
||||
nhost,
|
||||
graphqlUrl,
|
||||
headers = {},
|
||||
publicRole = 'public',
|
||||
fetchPolicy,
|
||||
@@ -35,11 +37,13 @@ export const createApolloClient = ({
|
||||
connectToDevTools = isBrowser && process.env.NODE_ENV === 'development',
|
||||
onError
|
||||
}: NhostApolloClientOptions) => {
|
||||
if (!nhost?.interpreter) {
|
||||
console.error("Nhost has not be initiated. Apollo client can't be created")
|
||||
let backendUrl = graphqlUrl || nhost?.graphql.getUrl()
|
||||
if (!backendUrl) {
|
||||
console.error("Can't initialize the Apollo Client: no backend Url has been provided")
|
||||
return null
|
||||
}
|
||||
const { interpreter, backendUrl } = nhost
|
||||
const interpreter = nhost?.auth.client.interpreter
|
||||
|
||||
let token: string | null = null
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
@@ -61,7 +65,7 @@ export const createApolloClient = ({
|
||||
return resHeaders
|
||||
}
|
||||
|
||||
const uri = `${backendUrl}/v1/graphql`
|
||||
const uri = backendUrl
|
||||
const wsUri = uri.startsWith('https') ? uri.replace(/^https/, 'wss') : uri.replace(/^http/, 'ws')
|
||||
|
||||
let webSocketClient: SubscriptionClient | null = null
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Nhost
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,48 +0,0 @@
|
||||
import { BroadcastChannel } from 'broadcast-channel'
|
||||
import { InterpreterFrom } from 'xstate'
|
||||
|
||||
import { createNhostMachine, NhostMachine, NhostMachineOptions } from './machines'
|
||||
import { defaultStorageGetter, defaultStorageSetter } from './storage'
|
||||
|
||||
export type NhostClientOptions = NhostMachineOptions
|
||||
|
||||
export class Nhost {
|
||||
readonly backendUrl: string
|
||||
readonly clientUrl: string
|
||||
readonly machine: NhostMachine
|
||||
interpreter?: InterpreterFrom<NhostMachine>
|
||||
#channel?: BroadcastChannel
|
||||
|
||||
constructor({
|
||||
backendUrl,
|
||||
clientUrl = typeof window !== 'undefined' ? window.location.origin : '',
|
||||
storageGetter = defaultStorageGetter,
|
||||
storageSetter = defaultStorageSetter,
|
||||
autoSignIn = true,
|
||||
autoRefreshToken = true
|
||||
}: NhostClientOptions) {
|
||||
this.backendUrl = backendUrl
|
||||
this.clientUrl = clientUrl
|
||||
|
||||
const machine = createNhostMachine({
|
||||
backendUrl,
|
||||
clientUrl,
|
||||
storageGetter,
|
||||
storageSetter,
|
||||
autoSignIn,
|
||||
autoRefreshToken
|
||||
})
|
||||
|
||||
this.machine = machine
|
||||
|
||||
if (typeof window !== 'undefined' && autoSignIn) {
|
||||
this.#channel = new BroadcastChannel<string>('nhost')
|
||||
this.#channel.addEventListener('message', (token) => {
|
||||
const existingToken = this.interpreter?.state.context.refreshToken
|
||||
if (this.interpreter && token !== existingToken) {
|
||||
this.interpreter.send({ type: 'TRY_TOKEN', token })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Nhost, NhostClientOptions } from './client'
|
||||
import { cookieStorageGetter, cookieStorageSetter } from './storage'
|
||||
const isBrowser = typeof window !== undefined
|
||||
|
||||
export class NhostSSR extends Nhost {
|
||||
constructor({ backendUrl }: NhostClientOptions) {
|
||||
super({
|
||||
backendUrl,
|
||||
autoSignIn: isBrowser,
|
||||
autoRefreshToken: isBrowser,
|
||||
storageGetter: cookieStorageGetter,
|
||||
storageSetter: cookieStorageSetter
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ErrorPayload } from '../errors'
|
||||
import type { NhostSession, PasswordlessOptions, SignUpOptions } from '../types'
|
||||
|
||||
export type NhostEvents =
|
||||
| { type: 'SESSION_UPDATE'; data: { session: NhostSession } }
|
||||
| { type: 'TRY_TOKEN'; token: string }
|
||||
| { type: 'SIGNIN_ANONYMOUS' }
|
||||
| { type: 'SIGNIN_PASSWORD'; email?: string; password?: string }
|
||||
| {
|
||||
type: 'SIGNIN_PASSWORDLESS_EMAIL'
|
||||
email?: string
|
||||
options?: PasswordlessOptions
|
||||
}
|
||||
| { type: 'SIGNUP_EMAIL_PASSWORD'; email?: string; password?: string; options?: SignUpOptions }
|
||||
| { type: 'TOKEN_REFRESH_ERROR'; error: ErrorPayload }
|
||||
| { type: 'SIGNOUT'; all?: boolean }
|
||||
@@ -1,4 +1,19 @@
|
||||
# @nhost/client
|
||||
# @nhost/core
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 744fd69: Unify vanilla, react and next APIs so they can work together
|
||||
React and NextJS libraries now works together with `@nhost/nhost-js`. It also means the Nhost client needs to be initiated before passing it to the React provider.
|
||||
See the [React](https://docs.nhost.io/reference/react#configuration) and [NextJS](https://docs.nhost.io/reference/nextjs/configuration) configuration documentation for additional information.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 744fd69: Rename `@nhost/client` to `@nhost/core`
|
||||
The `@nhost/client` name was somehow misleading, as it was implying it could somehow work as a vanilla client, whereas it only contained the state machine that could be used for vanilla or framework specific libraries e.g. `@nhost/react`.
|
||||
|
||||
It is therefore renamed to `@nhost/core`, and keeps the same versionning and changelog.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/client",
|
||||
"version": "0.2.1",
|
||||
"name": "@nhost/core",
|
||||
"version": "0.3.0",
|
||||
"description": "Nhost core client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -35,13 +35,13 @@
|
||||
"main": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.umd.js",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.umd.js"
|
||||
"require": "./dist/index.cjs.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -51,7 +51,6 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.25.0",
|
||||
"broadcast-channel": "^4.10.0",
|
||||
"immer": "^9.0.12",
|
||||
"js-cookie": "^3.0.1",
|
||||
"xstate": "^4.30.5"
|
||||
}
|
||||
71
packages/core/src/client.ts
Normal file
71
packages/core/src/client.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { BroadcastChannel } from 'broadcast-channel'
|
||||
import { interpret } from 'xstate'
|
||||
|
||||
import { MIN_TOKEN_REFRESH_INTERVAL } from './constants'
|
||||
import { AuthMachine, AuthMachineOptions, createAuthMachine } from './machines'
|
||||
import { defaultClientStorageGetter, defaultClientStorageSetter } from './storage'
|
||||
import type { AuthInterpreter } from './types'
|
||||
|
||||
export type NhostClientOptions = AuthMachineOptions & { start?: boolean }
|
||||
|
||||
export class AuthClient {
|
||||
readonly backendUrl: string
|
||||
readonly clientUrl: string
|
||||
readonly machine: AuthMachine
|
||||
#interpreter?: AuthInterpreter
|
||||
#channel?: BroadcastChannel
|
||||
#subscriptions: Set<(client: AuthClient) => void> = new Set()
|
||||
|
||||
constructor({
|
||||
backendUrl,
|
||||
clientUrl = typeof window !== 'undefined' ? window.location.origin : '',
|
||||
clientStorageGetter = defaultClientStorageGetter,
|
||||
clientStorageSetter = defaultClientStorageSetter,
|
||||
refreshIntervalTime = MIN_TOKEN_REFRESH_INTERVAL,
|
||||
autoSignIn = true,
|
||||
autoRefreshToken = true,
|
||||
start = true
|
||||
}: NhostClientOptions) {
|
||||
this.backendUrl = backendUrl
|
||||
this.clientUrl = clientUrl
|
||||
|
||||
this.machine = createAuthMachine({
|
||||
backendUrl,
|
||||
clientUrl,
|
||||
refreshIntervalTime,
|
||||
clientStorageGetter,
|
||||
clientStorageSetter,
|
||||
autoSignIn,
|
||||
autoRefreshToken
|
||||
})
|
||||
|
||||
if (start) {
|
||||
this.interpreter = interpret(this.machine)
|
||||
this.interpreter.start()
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && autoSignIn) {
|
||||
this.#channel = new BroadcastChannel<string>('nhost')
|
||||
this.#channel.addEventListener('message', (token) => {
|
||||
const existingToken = this.interpreter?.state.context.refreshToken
|
||||
if (this.interpreter && token !== existingToken) {
|
||||
this.interpreter.send({ type: 'TRY_TOKEN', token })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get interpreter(): AuthInterpreter | undefined {
|
||||
return this.#interpreter
|
||||
}
|
||||
set interpreter(interpreter: AuthInterpreter | undefined) {
|
||||
this.#interpreter = interpreter
|
||||
if (interpreter) {
|
||||
this.#subscriptions.forEach((fn) => fn(this))
|
||||
}
|
||||
}
|
||||
|
||||
onStart(fn: (interpreter: AuthClient) => void) {
|
||||
this.#subscriptions.add(fn)
|
||||
}
|
||||
}
|
||||
17
packages/core/src/coookie-client.ts
Normal file
17
packages/core/src/coookie-client.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AuthClient, NhostClientOptions } from './client'
|
||||
import { cookieStorageGetter, cookieStorageSetter } from './storage'
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
export class AuthClientSSR extends AuthClient {
|
||||
constructor({
|
||||
...options
|
||||
}: Omit<NhostClientOptions, 'clientStorageGetter' | 'clientStorageSetter'>) {
|
||||
super({
|
||||
...options,
|
||||
autoSignIn: isBrowser && options.autoSignIn,
|
||||
autoRefreshToken: isBrowser && options.autoRefreshToken,
|
||||
clientStorageGetter: cookieStorageGetter,
|
||||
clientStorageSetter: cookieStorageSetter
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,26 @@ export const INVALID_EMAIL_ERROR: ValidationErrorPayload = {
|
||||
message: 'Email is incorrectly formatted'
|
||||
}
|
||||
|
||||
export const INVALID_MFA_TYPE_ERROR: ValidationErrorPayload = {
|
||||
status: VALIDATION_ERROR_CODE,
|
||||
error: 'invalid-mfa-type',
|
||||
message: 'MFA type is invalid'
|
||||
}
|
||||
|
||||
export const INVALID_PASSWORD_ERROR: ValidationErrorPayload = {
|
||||
status: VALIDATION_ERROR_CODE,
|
||||
error: 'invalid-password',
|
||||
message: 'Password is incorrectly formatted'
|
||||
}
|
||||
|
||||
export const INVALID_PHONE_NUMBER_ERROR: ValidationErrorPayload = {
|
||||
status: VALIDATION_ERROR_CODE,
|
||||
error: 'invalid-phone-number',
|
||||
message: 'Phone number is incorrectly formatted'
|
||||
}
|
||||
|
||||
export const NO_MFA_TICKET_ERROR: ValidationErrorPayload = {
|
||||
status: VALIDATION_ERROR_CODE,
|
||||
error: 'no-mfa-ticket',
|
||||
message: 'No MFA ticket has been provided'
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
export type { NhostClientOptions } from './client'
|
||||
export { Nhost } from './client'
|
||||
export { AuthClient } from './client'
|
||||
export * from './constants'
|
||||
export { NhostSSR } from './coookie-client'
|
||||
export { AuthClientSSR } from './coookie-client'
|
||||
export * from './machines'
|
||||
export * from './storage'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
@@ -1,21 +1,26 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { Nhost } from '../client'
|
||||
import { AuthClient } from '../client'
|
||||
import { ErrorPayload, INVALID_EMAIL_ERROR } from '../errors'
|
||||
import { nhostApiClient } from '../hasura-auth'
|
||||
import { ChangeEmailOptions } from '../types'
|
||||
import { rewriteRedirectTo } from '../utils'
|
||||
import { isValidEmail } from '../validators'
|
||||
|
||||
export type ChangeEmailContext = {
|
||||
error: ErrorPayload | null
|
||||
}
|
||||
export type ChangeEmailEvents = {
|
||||
type: 'REQUEST_CHANGE'
|
||||
email?: string
|
||||
options?: ChangeEmailOptions
|
||||
}
|
||||
|
||||
export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }: Nhost) => {
|
||||
export type ChangeEmailEvents =
|
||||
| {
|
||||
type: 'REQUEST'
|
||||
email?: string
|
||||
options?: ChangeEmailOptions
|
||||
}
|
||||
| { type: 'SUCCESS' }
|
||||
| { type: 'ERROR'; error: ErrorPayload | null }
|
||||
|
||||
export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
@@ -24,13 +29,14 @@ export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }:
|
||||
events: {} as ChangeEmailEvents
|
||||
},
|
||||
tsTypes: {} as import('./change-email.typegen').Typegen0,
|
||||
preserveActionOrder: true,
|
||||
id: 'changeEmail',
|
||||
initial: 'idle',
|
||||
context: { error: null },
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
REQUEST_CHANGE: [
|
||||
REQUEST: [
|
||||
{
|
||||
cond: 'invalidEmail',
|
||||
actions: 'saveInvalidEmailError',
|
||||
@@ -52,8 +58,8 @@ export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }:
|
||||
invoke: {
|
||||
src: 'requestChange',
|
||||
id: 'requestChange',
|
||||
onDone: 'idle.success',
|
||||
onError: { actions: 'saveRequestError', target: 'idle.error' }
|
||||
onDone: { target: 'idle.success', actions: 'reportSuccess' },
|
||||
onError: { actions: ['saveRequestError', 'reportError'], target: 'idle.error' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,9 +68,10 @@ export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }:
|
||||
actions: {
|
||||
saveInvalidEmailError: assign({ error: (_) => INVALID_EMAIL_ERROR }),
|
||||
saveRequestError: assign({
|
||||
// TODO type
|
||||
error: (_, { data: { error } }: any) => error
|
||||
})
|
||||
}),
|
||||
reportError: send((ctx) => ({ type: 'ERROR', error: ctx.error })),
|
||||
reportSuccess: send('SUCCESS')
|
||||
},
|
||||
guards: {
|
||||
invalidEmail: (_, { email }) => !isValidEmail(email)
|
||||
@@ -72,14 +79,10 @@ export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }:
|
||||
services: {
|
||||
requestChange: async (_, { email, options }) => {
|
||||
const res = await api.post(
|
||||
'/v1/auth/user/email/change',
|
||||
'/user/email/change',
|
||||
{
|
||||
newEmail: email,
|
||||
options: {
|
||||
redirectTo: options?.redirectTo?.startsWith('/')
|
||||
? clientUrl + options.redirectTo
|
||||
: options?.redirectTo
|
||||
}
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -3,17 +3,19 @@
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true
|
||||
eventsCausingActions: {
|
||||
saveInvalidEmailError: 'REQUEST_CHANGE'
|
||||
saveInvalidEmailError: 'REQUEST'
|
||||
reportSuccess: 'done.invoke.requestChange'
|
||||
saveRequestError: 'error.platform.requestChange'
|
||||
reportError: 'error.platform.requestChange'
|
||||
}
|
||||
internalEvents: {
|
||||
'error.platform.requestChange': { type: 'error.platform.requestChange'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
'done.invoke.requestChange': {
|
||||
type: 'done.invoke.requestChange'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.requestChange': { type: 'error.platform.requestChange'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
}
|
||||
invokeSrcNameMap: {
|
||||
requestChange: 'done.invoke.requestChange'
|
||||
@@ -25,10 +27,10 @@ export interface Typegen0 {
|
||||
delays: never
|
||||
}
|
||||
eventsCausingServices: {
|
||||
requestChange: 'REQUEST_CHANGE'
|
||||
requestChange: 'REQUEST'
|
||||
}
|
||||
eventsCausingGuards: {
|
||||
invalidEmail: 'REQUEST_CHANGE'
|
||||
invalidEmail: 'REQUEST'
|
||||
}
|
||||
eventsCausingDelays: {}
|
||||
matchesStates:
|
||||
@@ -1,6 +1,6 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { Nhost } from '../client'
|
||||
import { AuthClient } from '../client'
|
||||
import { ErrorPayload, INVALID_PASSWORD_ERROR } from '../errors'
|
||||
import { nhostApiClient } from '../hasura-auth'
|
||||
import { isValidPassword } from '../validators'
|
||||
@@ -8,12 +8,15 @@ import { isValidPassword } from '../validators'
|
||||
export type ChangePasswordContext = {
|
||||
error: ErrorPayload | null
|
||||
}
|
||||
export type ChangePasswordEvents = {
|
||||
type: 'REQUEST_CHANGE'
|
||||
password?: string
|
||||
}
|
||||
export type ChangePasswordEvents =
|
||||
| {
|
||||
type: 'REQUEST'
|
||||
password?: string
|
||||
}
|
||||
| { type: 'SUCCESS' }
|
||||
| { type: 'ERROR'; error: ErrorPayload | null }
|
||||
|
||||
export const createChangePasswordMachine = ({ backendUrl, interpreter }: Nhost) => {
|
||||
export const createChangePasswordMachine = ({ backendUrl, interpreter }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
@@ -22,13 +25,14 @@ export const createChangePasswordMachine = ({ backendUrl, interpreter }: Nhost)
|
||||
events: {} as ChangePasswordEvents
|
||||
},
|
||||
tsTypes: {} as import('./change-password.typegen').Typegen0,
|
||||
preserveActionOrder: true,
|
||||
id: 'changePassword',
|
||||
initial: 'idle',
|
||||
context: { error: null },
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
REQUEST_CHANGE: [
|
||||
REQUEST: [
|
||||
{
|
||||
cond: 'invalidPassword',
|
||||
actions: 'saveInvalidPasswordError',
|
||||
@@ -50,8 +54,8 @@ export const createChangePasswordMachine = ({ backendUrl, interpreter }: Nhost)
|
||||
invoke: {
|
||||
src: 'requestChange',
|
||||
id: 'requestChange',
|
||||
onDone: 'idle.success',
|
||||
onError: { actions: 'saveRequestError', target: 'idle.error' }
|
||||
onDone: { target: 'idle.success', actions: 'reportSuccess' },
|
||||
onError: { actions: ['saveRequestError', 'reportError'], target: 'idle.error' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,12 +64,13 @@ export const createChangePasswordMachine = ({ backendUrl, interpreter }: Nhost)
|
||||
actions: {
|
||||
saveInvalidPasswordError: assign({ error: (_) => INVALID_PASSWORD_ERROR }),
|
||||
saveRequestError: assign({
|
||||
// TODO type
|
||||
error: (_, { data: { error } }: any) => {
|
||||
console.log(error)
|
||||
return error
|
||||
}
|
||||
})
|
||||
}),
|
||||
reportError: send((ctx) => ({ type: 'ERROR', error: ctx.error })),
|
||||
reportSuccess: send('SUCCESS')
|
||||
},
|
||||
guards: {
|
||||
invalidPassword: (_, { password }) => !isValidPassword(password)
|
||||
@@ -73,7 +78,7 @@ export const createChangePasswordMachine = ({ backendUrl, interpreter }: Nhost)
|
||||
services: {
|
||||
requestChange: (_, { password }) =>
|
||||
api.post<string, { data: { error?: ErrorPayload } }>(
|
||||
'/v1/auth/user/password',
|
||||
'/user/password',
|
||||
{ newPassword: password },
|
||||
{
|
||||
headers: {
|
||||
@@ -3,17 +3,19 @@
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true
|
||||
eventsCausingActions: {
|
||||
saveInvalidPasswordError: 'REQUEST_CHANGE'
|
||||
saveInvalidPasswordError: 'REQUEST'
|
||||
reportSuccess: 'done.invoke.requestChange'
|
||||
saveRequestError: 'error.platform.requestChange'
|
||||
reportError: 'error.platform.requestChange'
|
||||
}
|
||||
internalEvents: {
|
||||
'error.platform.requestChange': { type: 'error.platform.requestChange'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
'done.invoke.requestChange': {
|
||||
type: 'done.invoke.requestChange'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.requestChange': { type: 'error.platform.requestChange'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
}
|
||||
invokeSrcNameMap: {
|
||||
requestChange: 'done.invoke.requestChange'
|
||||
@@ -25,10 +27,10 @@ export interface Typegen0 {
|
||||
delays: never
|
||||
}
|
||||
eventsCausingServices: {
|
||||
requestChange: 'REQUEST_CHANGE'
|
||||
requestChange: 'REQUEST'
|
||||
}
|
||||
eventsCausingGuards: {
|
||||
invalidPassword: 'REQUEST_CHANGE'
|
||||
invalidPassword: 'REQUEST'
|
||||
}
|
||||
eventsCausingDelays: {}
|
||||
matchesStates:
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ErrorPayload } from '../errors'
|
||||
import type { ErrorPayload } from '../errors'
|
||||
import { User } from '../types'
|
||||
|
||||
export type NhostContext = {
|
||||
export type AuthContext = {
|
||||
user: User | null
|
||||
mfa: boolean
|
||||
mfa: {
|
||||
ticket: string
|
||||
} | null
|
||||
accessToken: {
|
||||
value: string | null
|
||||
expiresAt: Date
|
||||
@@ -15,12 +17,12 @@ export type NhostContext = {
|
||||
refreshToken: {
|
||||
value: string | null
|
||||
}
|
||||
errors: Partial<Record<'registration' | 'authentication', ErrorPayload>>
|
||||
errors: Partial<Record<'registration' | 'authentication' | 'signout', ErrorPayload>>
|
||||
}
|
||||
|
||||
export const INITIAL_MACHINE_CONTEXT: NhostContext = {
|
||||
export const INITIAL_MACHINE_CONTEXT: AuthContext = {
|
||||
user: null,
|
||||
mfa: false,
|
||||
mfa: null,
|
||||
accessToken: {
|
||||
value: null,
|
||||
expiresAt: new Date()
|
||||
134
packages/core/src/machines/enable-mfa.ts
Normal file
134
packages/core/src/machines/enable-mfa.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { AuthClient } from '../client'
|
||||
import { ErrorPayload, INVALID_MFA_TYPE_ERROR } from '../errors'
|
||||
import { nhostApiClient } from '../hasura-auth'
|
||||
|
||||
export type EnableMfaContext = {
|
||||
error: ErrorPayload | null
|
||||
imageUrl: string | null
|
||||
secret: string | null
|
||||
}
|
||||
|
||||
export type EnableMfaEvents =
|
||||
| {
|
||||
type: 'GENERATE'
|
||||
}
|
||||
| {
|
||||
type: 'ACTIVATE'
|
||||
code?: string
|
||||
activeMfaType: 'totp'
|
||||
}
|
||||
| { type: 'GENERATED' }
|
||||
| { type: 'GENERATED_ERROR'; error: ErrorPayload | null }
|
||||
| { type: 'SUCCESS' }
|
||||
| { type: 'ERROR'; error: ErrorPayload | null }
|
||||
|
||||
export const createEnableMfaMachine = ({ backendUrl, interpreter }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
context: {} as EnableMfaContext,
|
||||
events: {} as EnableMfaEvents
|
||||
},
|
||||
tsTypes: {} as import('./enable-mfa.typegen').Typegen0,
|
||||
preserveActionOrder: true,
|
||||
id: 'enableMfa',
|
||||
initial: 'idle',
|
||||
context: { error: null, imageUrl: null, secret: null },
|
||||
states: {
|
||||
idle: {
|
||||
initial: 'initial',
|
||||
on: {
|
||||
GENERATE: 'generating'
|
||||
},
|
||||
states: {
|
||||
initial: {},
|
||||
error: {}
|
||||
}
|
||||
},
|
||||
generating: {
|
||||
invoke: {
|
||||
src: 'generate',
|
||||
id: 'generate',
|
||||
onDone: { target: 'generated', actions: ['reportGeneratedSuccess', 'saveGeneration'] },
|
||||
onError: { actions: ['saveError', 'reportGeneratedError'], target: 'idle.error' }
|
||||
}
|
||||
},
|
||||
generated: {
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
initial: 'idle',
|
||||
on: {
|
||||
ACTIVATE: [
|
||||
{
|
||||
cond: 'invalidMfaType',
|
||||
actions: 'saveInvalidMfaTypeError',
|
||||
target: '.error'
|
||||
},
|
||||
{
|
||||
target: 'activating'
|
||||
}
|
||||
]
|
||||
},
|
||||
states: { idle: {}, error: {} }
|
||||
},
|
||||
activating: {
|
||||
invoke: {
|
||||
src: 'activate',
|
||||
id: 'activate',
|
||||
onDone: { target: 'activated', actions: 'reportSuccess' },
|
||||
onError: { actions: ['saveError', 'reportError'], target: 'idle.error' }
|
||||
}
|
||||
},
|
||||
activated: { type: 'final' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
saveInvalidMfaTypeError: assign({ error: (_) => INVALID_MFA_TYPE_ERROR }),
|
||||
saveError: assign({
|
||||
error: (_, { data: { error } }: any) => error
|
||||
}),
|
||||
saveGeneration: assign({
|
||||
imageUrl: (_, { data: { imageUrl } }: any) => imageUrl,
|
||||
secret: (_, { data: { totpSecret } }: any) => totpSecret
|
||||
}),
|
||||
reportError: send((ctx) => ({ type: 'ERROR', error: ctx.error })),
|
||||
reportSuccess: send('SUCCESS'),
|
||||
reportGeneratedSuccess: send('GENERATED'),
|
||||
reportGeneratedError: send((ctx) => ({ type: 'GENERATED_ERROR', error: ctx.error }))
|
||||
},
|
||||
guards: {
|
||||
invalidMfaType: (_, { activeMfaType }) => !activeMfaType || activeMfaType !== 'totp'
|
||||
},
|
||||
services: {
|
||||
generate: async (_) => {
|
||||
const { data } = await api.get('/mfa/totp/generate', {
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.state.context.accessToken.value}`
|
||||
}
|
||||
})
|
||||
return data
|
||||
},
|
||||
activate: (_, { code, activeMfaType }) =>
|
||||
api.post(
|
||||
'/user/mfa',
|
||||
{
|
||||
code,
|
||||
activeMfaType
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.state.context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
63
packages/core/src/machines/enable-mfa.typegen.ts
Normal file
63
packages/core/src/machines/enable-mfa.typegen.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true
|
||||
eventsCausingActions: {
|
||||
reportGeneratedSuccess: 'done.invoke.generate'
|
||||
saveGeneration: 'done.invoke.generate'
|
||||
saveError: 'error.platform.generate' | 'error.platform.activate'
|
||||
reportGeneratedError: 'error.platform.generate'
|
||||
saveInvalidMfaTypeError: 'ACTIVATE'
|
||||
reportSuccess: 'done.invoke.activate'
|
||||
reportError: 'error.platform.activate'
|
||||
}
|
||||
internalEvents: {
|
||||
'done.invoke.generate': {
|
||||
type: 'done.invoke.generate'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.generate': { type: 'error.platform.generate'; data: unknown }
|
||||
'error.platform.activate': { type: 'error.platform.activate'; data: unknown }
|
||||
'done.invoke.activate': {
|
||||
type: 'done.invoke.activate'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
}
|
||||
invokeSrcNameMap: {
|
||||
generate: 'done.invoke.generate'
|
||||
activate: 'done.invoke.activate'
|
||||
}
|
||||
missingImplementations: {
|
||||
actions: never
|
||||
services: never
|
||||
guards: never
|
||||
delays: never
|
||||
}
|
||||
eventsCausingServices: {
|
||||
generate: 'GENERATE'
|
||||
activate: 'ACTIVATE'
|
||||
}
|
||||
eventsCausingGuards: {
|
||||
invalidMfaType: 'ACTIVATE'
|
||||
}
|
||||
eventsCausingDelays: {}
|
||||
matchesStates:
|
||||
| 'idle'
|
||||
| 'idle.initial'
|
||||
| 'idle.error'
|
||||
| 'generating'
|
||||
| 'generated'
|
||||
| 'generated.idle'
|
||||
| 'generated.idle.idle'
|
||||
| 'generated.idle.error'
|
||||
| 'generated.activating'
|
||||
| 'generated.activated'
|
||||
| {
|
||||
idle?: 'initial' | 'error'
|
||||
generated?: 'idle' | 'activating' | 'activated' | { idle?: 'idle' | 'error' }
|
||||
}
|
||||
tags: never
|
||||
}
|
||||
30
packages/core/src/machines/events.ts
Normal file
30
packages/core/src/machines/events.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { DeanonymizeOptions, NhostSession, PasswordlessOptions, SignUpOptions } from '../types'
|
||||
|
||||
export type AuthEvents =
|
||||
| { type: 'SESSION_UPDATE'; data: { session: NhostSession } }
|
||||
| { type: 'TRY_TOKEN'; token: string }
|
||||
| { type: 'SIGNIN_ANONYMOUS' }
|
||||
| {
|
||||
type: 'DEANONYMIZE'
|
||||
signInMethod: 'email-password' | 'passwordless'
|
||||
connection?: 'email' | 'sms'
|
||||
options: DeanonymizeOptions
|
||||
}
|
||||
| { type: 'SIGNIN_PASSWORD'; email?: string; password?: string }
|
||||
| {
|
||||
type: 'SIGNIN_PASSWORDLESS_EMAIL'
|
||||
email?: string
|
||||
options?: PasswordlessOptions
|
||||
}
|
||||
| {
|
||||
type: 'SIGNIN_PASSWORDLESS_SMS'
|
||||
phoneNumber?: string
|
||||
options?: PasswordlessOptions
|
||||
}
|
||||
| { type: 'SIGNIN_PASSWORDLESS_SMS_OTP'; phoneNumber?: string; otp?: string }
|
||||
| { type: 'SIGNUP_EMAIL_PASSWORD'; email?: string; password?: string; options?: SignUpOptions }
|
||||
| { type: 'SIGNOUT'; all?: boolean }
|
||||
| { type: 'SIGNIN_MFA_TOTP'; ticket?: string; otp?: string }
|
||||
| { type: 'SIGNED_IN' }
|
||||
| { type: 'SIGNED_OUT' }
|
||||
| { type: 'TOKEN_CHANGED' }
|
||||
@@ -1,46 +1,57 @@
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { BroadcastChannel } from 'broadcast-channel'
|
||||
import produce from 'immer'
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import {
|
||||
MIN_TOKEN_REFRESH_INTERVAL,
|
||||
NHOST_JWT_EXPIRES_AT_KEY,
|
||||
NHOST_REFRESH_TOKEN_KEY,
|
||||
TOKEN_REFRESH_MARGIN
|
||||
} from '../constants'
|
||||
import { INVALID_EMAIL_ERROR, INVALID_PASSWORD_ERROR } from '../errors'
|
||||
import {
|
||||
INVALID_EMAIL_ERROR,
|
||||
INVALID_PASSWORD_ERROR,
|
||||
INVALID_PHONE_NUMBER_ERROR,
|
||||
NO_MFA_TICKET_ERROR
|
||||
} from '../errors'
|
||||
import { nhostApiClient } from '../hasura-auth'
|
||||
import { StorageGetter, StorageSetter } from '../storage'
|
||||
import { isValidEmail, isValidPassword } from '../validators'
|
||||
import { Mfa, NhostSession } from '../types'
|
||||
import { rewriteRedirectTo } from '../utils'
|
||||
import { isValidEmail, isValidPassword, isValidPhoneNumber } from '../validators'
|
||||
|
||||
import { INITIAL_MACHINE_CONTEXT, NhostContext } from './context'
|
||||
import { NhostEvents } from './events'
|
||||
import { AuthContext, INITIAL_MACHINE_CONTEXT } from './context'
|
||||
import { AuthEvents } from './events'
|
||||
|
||||
export type { NhostContext, NhostEvents }
|
||||
export type { AuthContext, AuthEvents }
|
||||
export * from './change-email'
|
||||
export * from './change-password'
|
||||
export * from './enable-mfa'
|
||||
export * from './reset-password'
|
||||
export * from './send-verification-email'
|
||||
|
||||
export type NhostMachineOptions = {
|
||||
export type AuthMachineOptions = {
|
||||
backendUrl: string
|
||||
clientUrl?: string
|
||||
storageGetter?: StorageGetter
|
||||
storageSetter?: StorageSetter
|
||||
refreshIntervalTime?: number
|
||||
clientStorageGetter?: StorageGetter
|
||||
clientStorageSetter?: StorageSetter
|
||||
autoSignIn?: boolean
|
||||
autoRefreshToken?: boolean
|
||||
}
|
||||
|
||||
export type NhostMachine = ReturnType<typeof createNhostMachine>
|
||||
export type AuthMachine = ReturnType<typeof createAuthMachine>
|
||||
|
||||
export const createNhostMachine = ({
|
||||
// TODO actions typings
|
||||
|
||||
export const createAuthMachine = ({
|
||||
backendUrl,
|
||||
clientUrl,
|
||||
storageSetter,
|
||||
storageGetter,
|
||||
clientStorageGetter,
|
||||
clientStorageSetter,
|
||||
refreshIntervalTime,
|
||||
autoRefreshToken = true,
|
||||
autoSignIn = true
|
||||
}: Required<NhostMachineOptions>) => {
|
||||
}: Required<AuthMachineOptions>) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
const postRequest = async <T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
@@ -53,44 +64,45 @@ export const createNhostMachine = ({
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
context: {} as NhostContext,
|
||||
events: {} as NhostEvents
|
||||
context: {} as AuthContext,
|
||||
events: {} as AuthEvents
|
||||
},
|
||||
tsTypes: {} as import('./index.typegen').Typegen0,
|
||||
context: produce<NhostContext>(INITIAL_MACHINE_CONTEXT, (ctx) => {
|
||||
const expiresAt = storageGetter(NHOST_JWT_EXPIRES_AT_KEY)
|
||||
if (expiresAt) ctx.accessToken.expiresAt = new Date(expiresAt)
|
||||
ctx.refreshToken.value = storageGetter(NHOST_REFRESH_TOKEN_KEY)
|
||||
}),
|
||||
context: INITIAL_MACHINE_CONTEXT,
|
||||
preserveActionOrder: true,
|
||||
id: 'nhost',
|
||||
type: 'parallel',
|
||||
states: {
|
||||
authentication: {
|
||||
initial: 'checkAutoSignIn',
|
||||
on: {
|
||||
TRY_TOKEN: '#nhost.token.running',
|
||||
SESSION_UPDATE: [
|
||||
{
|
||||
cond: 'hasSession',
|
||||
actions: ['saveSession', 'persist', 'resetTimer'],
|
||||
actions: ['saveSession', 'persist', 'resetTimer', 'reportTokenChanged'],
|
||||
target: '.signedIn'
|
||||
}
|
||||
]
|
||||
},
|
||||
states: {
|
||||
checkAutoSignIn: {
|
||||
always: [{ cond: 'isAutoSignInDisabled', target: 'starting' }],
|
||||
invoke: [
|
||||
{
|
||||
id: 'autoSignIn',
|
||||
src: 'autoSignIn',
|
||||
onDone: {
|
||||
target: 'signedIn',
|
||||
actions: ['saveSession', 'persist']
|
||||
},
|
||||
onError: 'starting'
|
||||
}
|
||||
]
|
||||
always: [{ cond: 'isAutoSignInDisabled', target: 'importingRefreshToken' }],
|
||||
invoke: {
|
||||
id: 'autoSignIn',
|
||||
src: 'autoSignIn',
|
||||
onDone: {
|
||||
target: 'signedIn',
|
||||
actions: ['saveSession', 'persist', 'reportTokenChanged']
|
||||
},
|
||||
onError: 'importingRefreshToken'
|
||||
}
|
||||
},
|
||||
importingRefreshToken: {
|
||||
invoke: {
|
||||
id: 'importRefreshToken',
|
||||
src: 'importRefreshToken',
|
||||
onDone: { actions: 'saveRefreshToken', target: 'starting' }
|
||||
}
|
||||
},
|
||||
starting: {
|
||||
always: [
|
||||
@@ -108,48 +120,53 @@ export const createNhostMachine = ({
|
||||
signedOut: {
|
||||
tags: ['ready'],
|
||||
initial: 'noErrors',
|
||||
entry: 'reportSignedOut',
|
||||
states: {
|
||||
noErrors: {},
|
||||
success: {},
|
||||
needsVerification: {},
|
||||
needsEmailVerification: {},
|
||||
needsSmsOtp: {},
|
||||
needsMfa: {},
|
||||
failed: {
|
||||
exit: 'resetAuthenticationError',
|
||||
initial: 'server',
|
||||
states: {
|
||||
server: {
|
||||
entry: 'saveAuthenticationError'
|
||||
},
|
||||
server: {},
|
||||
validation: {
|
||||
states: {
|
||||
password: {
|
||||
entry: 'saveInvalidPassword'
|
||||
},
|
||||
email: {
|
||||
entry: 'saveInvalidEmail'
|
||||
}
|
||||
password: {},
|
||||
email: {},
|
||||
phoneNumber: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
signingOut: {
|
||||
entry: 'destroyToken',
|
||||
exit: 'clearContext',
|
||||
invoke: {
|
||||
src: 'signout',
|
||||
id: 'signingOut',
|
||||
onDone: 'success',
|
||||
onError: 'failed.server' // TODO save error
|
||||
onDone: {
|
||||
target: 'success'
|
||||
},
|
||||
onError: {
|
||||
target: 'failed.server'
|
||||
// TODO save error
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
on: {
|
||||
// TODO change input validation - see official xstate form example
|
||||
SIGNIN_PASSWORD: [
|
||||
{
|
||||
cond: 'invalidEmail',
|
||||
actions: ['saveInvalidEmail'],
|
||||
target: '.failed.validation.email'
|
||||
},
|
||||
{
|
||||
cond: 'invalidPassword',
|
||||
actions: ['saveInvalidPassword'],
|
||||
target: '.failed.validation.password'
|
||||
},
|
||||
'#nhost.authentication.authenticating.password'
|
||||
@@ -157,22 +174,49 @@ export const createNhostMachine = ({
|
||||
SIGNIN_PASSWORDLESS_EMAIL: [
|
||||
{
|
||||
cond: 'invalidEmail',
|
||||
actions: 'saveInvalidEmail',
|
||||
target: '.failed.validation.email'
|
||||
},
|
||||
'#nhost.authentication.authenticating.passwordlessEmail'
|
||||
],
|
||||
SIGNIN_PASSWORDLESS_SMS: [
|
||||
{
|
||||
cond: 'invalidPhoneNumber',
|
||||
actions: 'saveInvalidPhoneNumber',
|
||||
target: '.failed.validation.phoneNumber'
|
||||
},
|
||||
'#nhost.authentication.authenticating.passwordlessSms'
|
||||
],
|
||||
SIGNIN_PASSWORDLESS_SMS_OTP: [
|
||||
{
|
||||
cond: 'invalidPhoneNumber',
|
||||
actions: 'saveInvalidPhoneNumber',
|
||||
target: '.failed.validation.phoneNumber'
|
||||
},
|
||||
'#nhost.authentication.authenticating.passwordlessSmsOtp'
|
||||
],
|
||||
SIGNUP_EMAIL_PASSWORD: [
|
||||
{
|
||||
cond: 'invalidEmail',
|
||||
actions: 'saveInvalidSignUpEmail',
|
||||
target: '.failed.validation.email'
|
||||
},
|
||||
{
|
||||
cond: 'invalidPassword',
|
||||
actions: 'saveInvalidSignUpPassword',
|
||||
target: '.failed.validation.password'
|
||||
},
|
||||
'#nhost.authentication.registering'
|
||||
],
|
||||
SIGNIN_ANONYMOUS: '#nhost.authentication.authenticating.anonymous'
|
||||
SIGNIN_ANONYMOUS: '#nhost.authentication.authenticating.anonymous',
|
||||
SIGNIN_MFA_TOTP: [
|
||||
{
|
||||
cond: 'noMfaTicket',
|
||||
actions: ['saveNoMfaTicketError'],
|
||||
target: '.failed'
|
||||
},
|
||||
'#nhost.authentication.authenticating.mfa.totp'
|
||||
]
|
||||
}
|
||||
},
|
||||
authenticating: {
|
||||
@@ -181,24 +225,60 @@ export const createNhostMachine = ({
|
||||
invoke: {
|
||||
src: 'signInPasswordlessEmail',
|
||||
id: 'authenticatePasswordlessEmail',
|
||||
onDone: '#nhost.authentication.signedOut.needsVerification',
|
||||
onError: '#nhost.authentication.signedOut.failed.server'
|
||||
onDone: '#nhost.authentication.signedOut.needsEmailVerification',
|
||||
onError: {
|
||||
actions: 'saveAuthenticationError',
|
||||
target: '#nhost.authentication.signedOut.failed.server'
|
||||
}
|
||||
}
|
||||
},
|
||||
passwordlessSms: {
|
||||
invoke: {
|
||||
src: 'signInPasswordlessSms',
|
||||
id: 'authenticatePasswordlessSms',
|
||||
onDone: '#nhost.authentication.signedOut.needsSmsOtp',
|
||||
onError: {
|
||||
actions: 'saveAuthenticationError',
|
||||
target: '#nhost.authentication.signedOut.failed.server'
|
||||
}
|
||||
}
|
||||
},
|
||||
passwordlessSmsOtp: {
|
||||
invoke: {
|
||||
src: 'signInPasswordlessSmsOtp',
|
||||
id: 'authenticatePasswordlessSmsOtp',
|
||||
onDone: {
|
||||
actions: ['saveSession', 'persist', 'reportTokenChanged'],
|
||||
target: '#nhost.authentication.signedIn'
|
||||
},
|
||||
onError: {
|
||||
actions: 'saveAuthenticationError',
|
||||
target: '#nhost.authentication.signedOut.failed.server'
|
||||
}
|
||||
}
|
||||
},
|
||||
password: {
|
||||
invoke: {
|
||||
src: 'signInPassword',
|
||||
id: 'authenticateUserWithPassword',
|
||||
onDone: {
|
||||
actions: ['saveSession', 'persist'],
|
||||
target: '#nhost.authentication.signedIn'
|
||||
},
|
||||
onDone: [
|
||||
{
|
||||
cond: 'hasMfaTicket',
|
||||
actions: ['saveMfaTicket'],
|
||||
target: '#nhost.authentication.signedOut.needsMfa'
|
||||
},
|
||||
{
|
||||
actions: ['saveSession', 'persist', 'reportTokenChanged'],
|
||||
target: '#nhost.authentication.signedIn'
|
||||
}
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
cond: 'unverified',
|
||||
target: '#nhost.authentication.signedOut.needsVerification'
|
||||
target: '#nhost.authentication.signedOut.needsEmailVerification'
|
||||
},
|
||||
{
|
||||
actions: 'saveAuthenticationError',
|
||||
target: '#nhost.authentication.signedOut.failed.server'
|
||||
}
|
||||
]
|
||||
@@ -210,15 +290,37 @@ export const createNhostMachine = ({
|
||||
src: 'signInAnonymous',
|
||||
id: 'authenticateAnonymously',
|
||||
onDone: {
|
||||
actions: ['saveSession', 'persist'],
|
||||
actions: ['saveSession', 'persist', 'reportTokenChanged'],
|
||||
target: '#nhost.authentication.signedIn'
|
||||
},
|
||||
onError: '#nhost.authentication.signedOut.failed.server'
|
||||
onError: {
|
||||
actions: 'saveAuthenticationError',
|
||||
target: '#nhost.authentication.signedOut.failed.server'
|
||||
}
|
||||
}
|
||||
},
|
||||
mfa: {
|
||||
states: {
|
||||
totp: {
|
||||
invoke: {
|
||||
src: 'signInMfaTotp',
|
||||
id: 'signInMfaTotp',
|
||||
onDone: {
|
||||
actions: ['saveSession', 'persist', 'reportTokenChanged'],
|
||||
target: '#nhost.authentication.signedIn'
|
||||
},
|
||||
onError: {
|
||||
actions: ['saveAuthenticationError'],
|
||||
target: '#nhost.authentication.signedOut.failed.server'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
registering: {
|
||||
entry: 'resetSignUpError',
|
||||
invoke: {
|
||||
src: 'registerUser',
|
||||
id: 'registerUser',
|
||||
@@ -226,16 +328,16 @@ export const createNhostMachine = ({
|
||||
{
|
||||
cond: 'hasSession',
|
||||
target: '#nhost.authentication.signedIn',
|
||||
actions: ['saveSession', 'persist']
|
||||
actions: ['saveSession', 'persist', 'reportTokenChanged']
|
||||
},
|
||||
{
|
||||
target: '#nhost.authentication.signedOut.needsVerification'
|
||||
target: '#nhost.authentication.signedOut.needsEmailVerification'
|
||||
}
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
cond: 'unverified',
|
||||
target: '#nhost.authentication.signedOut.needsVerification'
|
||||
target: '#nhost.authentication.signedOut.needsEmailVerification'
|
||||
},
|
||||
{
|
||||
actions: 'saveRegisrationError',
|
||||
@@ -248,8 +350,13 @@ export const createNhostMachine = ({
|
||||
signedIn: {
|
||||
tags: ['ready'],
|
||||
type: 'parallel',
|
||||
entry: 'reportSignedIn',
|
||||
on: {
|
||||
SIGNOUT: '#nhost.authentication.signedOut.signingOut'
|
||||
SIGNOUT: '#nhost.authentication.signedOut.signingOut',
|
||||
DEANONYMIZE: {
|
||||
// TODO implement
|
||||
target: '.deanonymizing'
|
||||
}
|
||||
},
|
||||
states: {
|
||||
refreshTimer: {
|
||||
@@ -298,7 +405,7 @@ export const createNhostMachine = ({
|
||||
target: 'pending'
|
||||
},
|
||||
onError: [
|
||||
// TODO
|
||||
// TODO handle error
|
||||
// {
|
||||
// actions: 'retry',
|
||||
// cond: 'canRetry',
|
||||
@@ -314,6 +421,14 @@ export const createNhostMachine = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deanonymizing: {
|
||||
// TODO implement
|
||||
initial: 'error',
|
||||
states: {
|
||||
error: {},
|
||||
success: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,18 +437,27 @@ export const createNhostMachine = ({
|
||||
token: {
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {},
|
||||
idle: {
|
||||
on: {
|
||||
TRY_TOKEN: 'running'
|
||||
},
|
||||
initial: 'noErrors',
|
||||
states: { noErrors: {}, error: {} }
|
||||
},
|
||||
running: {
|
||||
invoke: {
|
||||
src: 'refreshToken',
|
||||
id: 'authenticateWithToken',
|
||||
onDone: {
|
||||
actions: ['saveSession', 'persist'],
|
||||
target: ['#nhost.authentication.signedIn', 'idle']
|
||||
actions: ['saveSession', 'persist', 'reportTokenChanged'],
|
||||
target: ['#nhost.authentication.signedIn', 'idle.noErrors']
|
||||
},
|
||||
onError: {
|
||||
target: ['#nhost.authentication.signedOut', 'idle']
|
||||
}
|
||||
onError: [
|
||||
{ cond: 'isSignedIn', target: 'idle.error' },
|
||||
{
|
||||
target: ['#nhost.authentication.signedOut', 'idle.error']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,16 +466,21 @@ export const createNhostMachine = ({
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
// TODO better naming
|
||||
reportSignedIn: send('SIGNED_IN'),
|
||||
reportSignedOut: send('SIGNED_OUT'),
|
||||
reportTokenChanged: send('TOKEN_CHANGED'),
|
||||
clearContext: assign(() => INITIAL_MACHINE_CONTEXT),
|
||||
|
||||
saveSession: assign({
|
||||
// TODO type
|
||||
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 }),
|
||||
mfa: (_, e) => e.data?.mfa ?? false
|
||||
refreshToken: (_, e) => ({ value: e.data?.session?.refreshToken })
|
||||
}),
|
||||
saveMfaTicket: assign({
|
||||
mfa: (_, e: any) => e.data?.mfa ?? null
|
||||
}),
|
||||
|
||||
resetTimer: assign({
|
||||
@@ -374,7 +503,6 @@ export const createNhostMachine = ({
|
||||
|
||||
// * Authenticaiton errors
|
||||
saveAuthenticationError: assign({
|
||||
// TODO type
|
||||
errors: ({ errors }, { data: { error } }: any) => ({ ...errors, authentication: error })
|
||||
}),
|
||||
resetAuthenticationError: assign({
|
||||
@@ -386,26 +514,43 @@ export const createNhostMachine = ({
|
||||
saveInvalidPassword: assign({
|
||||
errors: ({ errors }) => ({ ...errors, authentication: INVALID_PASSWORD_ERROR })
|
||||
}),
|
||||
|
||||
saveInvalidPhoneNumber: assign({
|
||||
errors: ({ errors }) => ({ ...errors, authentication: INVALID_PHONE_NUMBER_ERROR })
|
||||
}),
|
||||
saveRegisrationError: assign({
|
||||
// TODO type
|
||||
errors: ({ errors }, { data: { error } }: any) => ({ ...errors, registration: error })
|
||||
}),
|
||||
resetSignUpError: assign({
|
||||
errors: ({ errors: { registration, ...errors } }) => errors
|
||||
}),
|
||||
saveInvalidSignUpPassword: assign({
|
||||
errors: ({ errors }) => ({ ...errors, registration: INVALID_EMAIL_ERROR })
|
||||
}),
|
||||
saveInvalidSignUpEmail: assign({
|
||||
errors: ({ errors }) => ({ ...errors, registration: INVALID_PASSWORD_ERROR })
|
||||
}),
|
||||
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)
|
||||
clientStorageSetter(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)
|
||||
clientStorageSetter(NHOST_JWT_EXPIRES_AT_KEY, nextRefresh)
|
||||
} else {
|
||||
storageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
||||
clientStorageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
||||
}
|
||||
},
|
||||
destroyToken: () => {
|
||||
storageSetter(NHOST_REFRESH_TOKEN_KEY, null)
|
||||
storageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
||||
clientStorageSetter(NHOST_REFRESH_TOKEN_KEY, null)
|
||||
clientStorageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -414,6 +559,7 @@ export const createNhostMachine = ({
|
||||
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,
|
||||
isAutoRefreshDisabled: () => !autoRefreshToken,
|
||||
isAutoSignInDisabled: () => !autoSignIn,
|
||||
@@ -421,61 +567,71 @@ export const createNhostMachine = ({
|
||||
ctx.refreshTimer.elapsed >
|
||||
Math.max(
|
||||
(Date.now() - ctx.accessToken.expiresAt.getTime()) / 1_000 - TOKEN_REFRESH_MARGIN,
|
||||
MIN_TOKEN_REFRESH_INTERVAL
|
||||
refreshIntervalTime
|
||||
),
|
||||
|
||||
// * Authentication errors
|
||||
// TODO type
|
||||
unverified: (ctx, { data: { error } }: any) =>
|
||||
unverified: (_, { data: { error } }: any) =>
|
||||
error.status === 401 && error.message === 'Email is not verified',
|
||||
|
||||
// * Event guards
|
||||
// TODO type
|
||||
hasSession: (_, e: any) => !!e.data?.session,
|
||||
hasMfaTicket: (_, e: any) => !!e.data?.mfa,
|
||||
invalidEmail: (_, { email }) => !isValidEmail(email),
|
||||
invalidPassword: (_, { password }) => !isValidPassword(password)
|
||||
invalidPassword: (_, { password }) => !isValidPassword(password),
|
||||
invalidPhoneNumber: (_, { phoneNumber }) => !isValidPhoneNumber(phoneNumber)
|
||||
},
|
||||
|
||||
services: {
|
||||
signInPassword: (_, { email, password }) =>
|
||||
postRequest('/v1/auth/signin/email-password', {
|
||||
postRequest('/signin/email-password', {
|
||||
email,
|
||||
password
|
||||
}),
|
||||
signInPasswordlessEmail: (_, { email, options }) =>
|
||||
postRequest('/v1/auth/signin/passwordless/email', {
|
||||
email,
|
||||
options: {
|
||||
...options,
|
||||
redirectTo: options?.redirectTo?.startsWith('/')
|
||||
? clientUrl + options.redirectTo
|
||||
: options?.redirectTo
|
||||
}
|
||||
signInPasswordlessSms: (_, { phoneNumber, options }) =>
|
||||
postRequest('/signin/passwordless/sms', {
|
||||
phoneNumber,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
}),
|
||||
signInAnonymous: (_) => postRequest('/v1/auth/signin/anonymous'),
|
||||
signInPasswordlessSmsOtp: (_, { phoneNumber, otp }) =>
|
||||
postRequest('/signin/passwordless/sms/otp', {
|
||||
phoneNumber,
|
||||
otp
|
||||
}),
|
||||
|
||||
signInPasswordlessEmail: (_, { email, options }) =>
|
||||
postRequest('/signin/passwordless/email', {
|
||||
email,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
}),
|
||||
signInAnonymous: (_) => postRequest('/signin/anonymous'),
|
||||
signInMfaTotp: (context, { ticket, otp }) =>
|
||||
postRequest<
|
||||
{ mfa: Mfa | null; session: NhostSession | null },
|
||||
{ mfa: Mfa | null; session: NhostSession | null }
|
||||
>('/signin/mfa/totp', {
|
||||
ticket: ticket || context.mfa?.ticket,
|
||||
otp
|
||||
}),
|
||||
|
||||
refreshToken: async (ctx, event) => {
|
||||
const refreshToken = event.type === 'TRY_TOKEN' ? event.token : ctx.refreshToken.value
|
||||
const session = await postRequest('/v1/auth/token', {
|
||||
const session = await postRequest('/token', {
|
||||
refreshToken
|
||||
})
|
||||
return { session }
|
||||
},
|
||||
signout: (ctx, e) =>
|
||||
postRequest('/v1/auth/signout', {
|
||||
postRequest('/signout', {
|
||||
refreshToken: ctx.refreshToken.value,
|
||||
all: !!e.all
|
||||
}),
|
||||
|
||||
registerUser: (_, { email, password, options }) =>
|
||||
postRequest('/v1/auth/signup/email-password', {
|
||||
postRequest('/signup/email-password', {
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
...options,
|
||||
redirectTo: options?.redirectTo?.startsWith('/')
|
||||
? clientUrl + options.redirectTo
|
||||
: options?.redirectTo
|
||||
}
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
}),
|
||||
|
||||
autoSignIn: async () => {
|
||||
@@ -485,19 +641,27 @@ export const createNhostMachine = ({
|
||||
const params = new URLSearchParams(location.hash.slice(1))
|
||||
const refreshToken = params.get('refreshToken')
|
||||
if (refreshToken) {
|
||||
const session = await postRequest('/v1/auth/token', {
|
||||
const session = await postRequest('/token', {
|
||||
refreshToken
|
||||
})
|
||||
// * remove hash from the current url after consumming the token
|
||||
window.history.pushState({}, '', location.pathname)
|
||||
// TODO remove the hash. For the moment, it is kept to avoid regression from the current SDK.
|
||||
// * Then, only `refreshToken` will be in the hash, while `type` will be sent by hasura-auth as a query parameter
|
||||
// window.history.pushState({}, '', location.pathname)
|
||||
const channel = new BroadcastChannel('nhost')
|
||||
// TODO broadcat session instead of token
|
||||
channel.postMessage(refreshToken)
|
||||
return { session }
|
||||
}
|
||||
}
|
||||
throw Error()
|
||||
}
|
||||
throw Error()
|
||||
},
|
||||
importRefreshToken: async () => {
|
||||
const stringExpiresAt = await clientStorageGetter(NHOST_JWT_EXPIRES_AT_KEY)
|
||||
const expiresAt = stringExpiresAt ? new Date(stringExpiresAt) : null
|
||||
const refreshToken = await clientStorageGetter(NHOST_REFRESH_TOKEN_KEY)
|
||||
return { refreshToken, expiresAt }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,32 +6,65 @@ export interface Typegen0 {
|
||||
saveSession:
|
||||
| 'SESSION_UPDATE'
|
||||
| 'done.invoke.autoSignIn'
|
||||
| 'done.invoke.authenticatePasswordlessSmsOtp'
|
||||
| 'done.invoke.authenticateUserWithPassword'
|
||||
| 'done.invoke.authenticateAnonymously'
|
||||
| 'done.invoke.signInMfaTotp'
|
||||
| 'done.invoke.registerUser'
|
||||
| 'done.invoke.refreshToken'
|
||||
| 'done.invoke.authenticateWithToken'
|
||||
persist:
|
||||
| 'SESSION_UPDATE'
|
||||
| 'done.invoke.autoSignIn'
|
||||
| 'done.invoke.authenticatePasswordlessSmsOtp'
|
||||
| 'done.invoke.authenticateUserWithPassword'
|
||||
| 'done.invoke.authenticateAnonymously'
|
||||
| 'done.invoke.signInMfaTotp'
|
||||
| 'done.invoke.registerUser'
|
||||
| 'done.invoke.refreshToken'
|
||||
| 'done.invoke.authenticateWithToken'
|
||||
resetTimer: 'SESSION_UPDATE' | 'done.invoke.refreshToken' | ''
|
||||
saveRegisrationError: 'error.platform.registerUser'
|
||||
tickRefreshTimer: 'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending'
|
||||
resetAuthenticationError: 'xstate.init'
|
||||
reportTokenChanged:
|
||||
| 'SESSION_UPDATE'
|
||||
| 'done.invoke.autoSignIn'
|
||||
| 'done.invoke.authenticatePasswordlessSmsOtp'
|
||||
| 'done.invoke.authenticateUserWithPassword'
|
||||
| 'done.invoke.authenticateAnonymously'
|
||||
| 'done.invoke.signInMfaTotp'
|
||||
| 'done.invoke.registerUser'
|
||||
| 'done.invoke.authenticateWithToken'
|
||||
saveRefreshToken: 'done.invoke.importRefreshToken'
|
||||
saveInvalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL'
|
||||
saveInvalidPassword: 'SIGNIN_PASSWORD'
|
||||
saveInvalidPhoneNumber: 'SIGNIN_PASSWORDLESS_SMS' | 'SIGNIN_PASSWORDLESS_SMS_OTP'
|
||||
saveInvalidSignUpEmail: 'SIGNUP_EMAIL_PASSWORD'
|
||||
saveInvalidSignUpPassword: 'SIGNUP_EMAIL_PASSWORD'
|
||||
saveNoMfaTicketError: 'SIGNIN_MFA_TOTP'
|
||||
saveAuthenticationError:
|
||||
| 'error.platform.signingOut'
|
||||
| 'error.platform.authenticatePasswordlessEmail'
|
||||
| 'error.platform.authenticatePasswordlessSms'
|
||||
| 'error.platform.authenticatePasswordlessSmsOtp'
|
||||
| 'error.platform.authenticateUserWithPassword'
|
||||
| 'error.platform.authenticateAnonymously'
|
||||
| 'error.platform.registerUser'
|
||||
saveInvalidPassword: 'SIGNIN_PASSWORD' | 'SIGNUP_EMAIL_PASSWORD'
|
||||
saveInvalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL' | 'SIGNUP_EMAIL_PASSWORD'
|
||||
| 'error.platform.signInMfaTotp'
|
||||
saveMfaTicket: 'done.invoke.authenticateUserWithPassword'
|
||||
saveRegisrationError: 'error.platform.registerUser'
|
||||
tickRefreshTimer: 'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending'
|
||||
reportSignedOut: '' | 'error.platform.authenticateWithToken'
|
||||
resetAuthenticationError: 'xstate.init'
|
||||
clearContext: 'xstate.init'
|
||||
destroyToken: 'SIGNOUT'
|
||||
resetSignUpError: 'SIGNUP_EMAIL_PASSWORD'
|
||||
reportSignedIn:
|
||||
| 'SESSION_UPDATE'
|
||||
| 'done.invoke.autoSignIn'
|
||||
| ''
|
||||
| 'done.invoke.authenticatePasswordlessSmsOtp'
|
||||
| 'done.invoke.authenticateUserWithPassword'
|
||||
| 'done.invoke.authenticateAnonymously'
|
||||
| 'done.invoke.signInMfaTotp'
|
||||
| 'done.invoke.registerUser'
|
||||
| 'done.invoke.authenticateWithToken'
|
||||
}
|
||||
internalEvents: {
|
||||
'done.invoke.autoSignIn': {
|
||||
@@ -39,6 +72,11 @@ export interface Typegen0 {
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'done.invoke.authenticatePasswordlessSmsOtp': {
|
||||
type: 'done.invoke.authenticatePasswordlessSmsOtp'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'done.invoke.authenticateUserWithPassword': {
|
||||
type: 'done.invoke.authenticateUserWithPassword'
|
||||
data: unknown
|
||||
@@ -49,6 +87,11 @@ export interface Typegen0 {
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'done.invoke.signInMfaTotp': {
|
||||
type: 'done.invoke.signInMfaTotp'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'done.invoke.registerUser': {
|
||||
type: 'done.invoke.registerUser'
|
||||
data: unknown
|
||||
@@ -65,15 +108,23 @@ export interface Typegen0 {
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'': { type: '' }
|
||||
'error.platform.registerUser': { type: 'error.platform.registerUser'; data: unknown }
|
||||
'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending': {
|
||||
type: 'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending'
|
||||
'done.invoke.importRefreshToken': {
|
||||
type: 'done.invoke.importRefreshToken'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.signingOut': { type: 'error.platform.signingOut'; data: unknown }
|
||||
'error.platform.authenticatePasswordlessEmail': {
|
||||
type: 'error.platform.authenticatePasswordlessEmail'
|
||||
data: unknown
|
||||
}
|
||||
'error.platform.authenticatePasswordlessSms': {
|
||||
type: 'error.platform.authenticatePasswordlessSms'
|
||||
data: unknown
|
||||
}
|
||||
'error.platform.authenticatePasswordlessSmsOtp': {
|
||||
type: 'error.platform.authenticatePasswordlessSmsOtp'
|
||||
data: unknown
|
||||
}
|
||||
'error.platform.authenticateUserWithPassword': {
|
||||
type: 'error.platform.authenticateUserWithPassword'
|
||||
data: unknown
|
||||
@@ -82,30 +133,49 @@ export interface Typegen0 {
|
||||
type: 'error.platform.authenticateAnonymously'
|
||||
data: unknown
|
||||
}
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
'error.platform.signInMfaTotp': { type: 'error.platform.signInMfaTotp'; data: unknown }
|
||||
'error.platform.registerUser': { type: 'error.platform.registerUser'; data: unknown }
|
||||
'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending': {
|
||||
type: 'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending'
|
||||
}
|
||||
'error.platform.authenticateWithToken': {
|
||||
type: 'error.platform.authenticateWithToken'
|
||||
data: unknown
|
||||
}
|
||||
'error.platform.autoSignIn': { type: 'error.platform.autoSignIn'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
'error.platform.importRefreshToken': {
|
||||
type: 'error.platform.importRefreshToken'
|
||||
data: unknown
|
||||
}
|
||||
'done.invoke.signingOut': {
|
||||
type: 'done.invoke.signingOut'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.signingOut': { type: 'error.platform.signingOut'; data: unknown }
|
||||
'done.invoke.authenticatePasswordlessEmail': {
|
||||
type: 'done.invoke.authenticatePasswordlessEmail'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.refreshToken': { type: 'error.platform.refreshToken'; data: unknown }
|
||||
'error.platform.authenticateWithToken': {
|
||||
type: 'error.platform.authenticateWithToken'
|
||||
'done.invoke.authenticatePasswordlessSms': {
|
||||
type: 'done.invoke.authenticatePasswordlessSms'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.refreshToken': { type: 'error.platform.refreshToken'; data: unknown }
|
||||
}
|
||||
invokeSrcNameMap: {
|
||||
autoSignIn: 'done.invoke.autoSignIn'
|
||||
importRefreshToken: 'done.invoke.importRefreshToken'
|
||||
signout: 'done.invoke.signingOut'
|
||||
signInPasswordlessEmail: 'done.invoke.authenticatePasswordlessEmail'
|
||||
signInPasswordlessSms: 'done.invoke.authenticatePasswordlessSms'
|
||||
signInPasswordlessSmsOtp: 'done.invoke.authenticatePasswordlessSmsOtp'
|
||||
signInPassword: 'done.invoke.authenticateUserWithPassword'
|
||||
signInAnonymous: 'done.invoke.authenticateAnonymously'
|
||||
signInMfaTotp: 'done.invoke.signInMfaTotp'
|
||||
registerUser: 'done.invoke.registerUser'
|
||||
refreshToken: 'done.invoke.refreshToken' | 'done.invoke.authenticateWithToken'
|
||||
}
|
||||
@@ -116,21 +186,28 @@ export interface Typegen0 {
|
||||
delays: never
|
||||
}
|
||||
eventsCausingServices: {
|
||||
refreshToken: 'TRY_TOKEN' | ''
|
||||
autoSignIn: 'xstate.init'
|
||||
importRefreshToken: 'error.platform.autoSignIn' | ''
|
||||
refreshToken: '' | 'TRY_TOKEN'
|
||||
signInPassword: 'SIGNIN_PASSWORD'
|
||||
signInPasswordlessEmail: 'SIGNIN_PASSWORDLESS_EMAIL'
|
||||
signInPasswordlessSms: 'SIGNIN_PASSWORDLESS_SMS'
|
||||
signInPasswordlessSmsOtp: 'SIGNIN_PASSWORDLESS_SMS_OTP'
|
||||
registerUser: 'SIGNUP_EMAIL_PASSWORD'
|
||||
signInAnonymous: 'SIGNIN_ANONYMOUS'
|
||||
signInMfaTotp: 'SIGNIN_MFA_TOTP'
|
||||
signout: 'SIGNOUT'
|
||||
}
|
||||
eventsCausingGuards: {
|
||||
hasSession: 'SESSION_UPDATE' | 'done.invoke.registerUser'
|
||||
isAutoSignInDisabled: ''
|
||||
isSignedIn: ''
|
||||
isSignedIn: '' | 'error.platform.authenticateWithToken'
|
||||
hasRefreshTokenWithoutSession: ''
|
||||
invalidEmail: 'SIGNIN_PASSWORD' | 'SIGNIN_PASSWORDLESS_EMAIL' | 'SIGNUP_EMAIL_PASSWORD'
|
||||
invalidPassword: 'SIGNIN_PASSWORD' | 'SIGNUP_EMAIL_PASSWORD'
|
||||
invalidPhoneNumber: 'SIGNIN_PASSWORDLESS_SMS' | 'SIGNIN_PASSWORDLESS_SMS_OTP'
|
||||
noMfaTicket: 'SIGNIN_MFA_TOTP'
|
||||
hasMfaTicket: 'done.invoke.authenticateUserWithPassword'
|
||||
unverified: 'error.platform.authenticateUserWithPassword' | 'error.platform.registerUser'
|
||||
noToken: ''
|
||||
isAutoRefreshDisabled: ''
|
||||
@@ -141,22 +218,30 @@ export interface Typegen0 {
|
||||
matchesStates:
|
||||
| 'authentication'
|
||||
| 'authentication.checkAutoSignIn'
|
||||
| 'authentication.importingRefreshToken'
|
||||
| 'authentication.starting'
|
||||
| 'authentication.signedOut'
|
||||
| 'authentication.signedOut.noErrors'
|
||||
| 'authentication.signedOut.success'
|
||||
| 'authentication.signedOut.needsVerification'
|
||||
| 'authentication.signedOut.needsEmailVerification'
|
||||
| 'authentication.signedOut.needsSmsOtp'
|
||||
| 'authentication.signedOut.needsMfa'
|
||||
| 'authentication.signedOut.failed'
|
||||
| 'authentication.signedOut.failed.server'
|
||||
| 'authentication.signedOut.failed.validation'
|
||||
| 'authentication.signedOut.failed.validation.password'
|
||||
| 'authentication.signedOut.failed.validation.email'
|
||||
| 'authentication.signedOut.failed.validation.phoneNumber'
|
||||
| 'authentication.signedOut.signingOut'
|
||||
| 'authentication.authenticating'
|
||||
| 'authentication.authenticating.passwordlessEmail'
|
||||
| 'authentication.authenticating.passwordlessSms'
|
||||
| 'authentication.authenticating.passwordlessSmsOtp'
|
||||
| 'authentication.authenticating.password'
|
||||
| 'authentication.authenticating.token'
|
||||
| 'authentication.authenticating.anonymous'
|
||||
| 'authentication.authenticating.mfa'
|
||||
| 'authentication.authenticating.mfa.totp'
|
||||
| 'authentication.registering'
|
||||
| 'authentication.signedIn'
|
||||
| 'authentication.signedIn.refreshTimer'
|
||||
@@ -166,12 +251,18 @@ export interface Typegen0 {
|
||||
| 'authentication.signedIn.refreshTimer.running'
|
||||
| 'authentication.signedIn.refreshTimer.running.pending'
|
||||
| 'authentication.signedIn.refreshTimer.running.refreshing'
|
||||
| 'authentication.signedIn.deanonymizing'
|
||||
| 'authentication.signedIn.deanonymizing.error'
|
||||
| 'authentication.signedIn.deanonymizing.success'
|
||||
| 'token'
|
||||
| 'token.idle'
|
||||
| 'token.idle.noErrors'
|
||||
| 'token.idle.error'
|
||||
| 'token.running'
|
||||
| {
|
||||
authentication?:
|
||||
| 'checkAutoSignIn'
|
||||
| 'importingRefreshToken'
|
||||
| 'starting'
|
||||
| 'signedOut'
|
||||
| 'authenticating'
|
||||
@@ -181,13 +272,29 @@ export interface Typegen0 {
|
||||
signedOut?:
|
||||
| 'noErrors'
|
||||
| 'success'
|
||||
| 'needsVerification'
|
||||
| 'needsEmailVerification'
|
||||
| 'needsSmsOtp'
|
||||
| 'needsMfa'
|
||||
| 'failed'
|
||||
| 'signingOut'
|
||||
| { failed?: 'server' | 'validation' | { validation?: 'password' | 'email' } }
|
||||
authenticating?: 'passwordlessEmail' | 'password' | 'token' | 'anonymous'
|
||||
| {
|
||||
failed?:
|
||||
| 'server'
|
||||
| 'validation'
|
||||
| { validation?: 'password' | 'email' | 'phoneNumber' }
|
||||
}
|
||||
authenticating?:
|
||||
| 'passwordlessEmail'
|
||||
| 'passwordlessSms'
|
||||
| 'passwordlessSmsOtp'
|
||||
| 'password'
|
||||
| 'token'
|
||||
| 'anonymous'
|
||||
| 'mfa'
|
||||
| { mfa?: 'totp' }
|
||||
signedIn?:
|
||||
| 'refreshTimer'
|
||||
| 'deanonymizing'
|
||||
| {
|
||||
refreshTimer?:
|
||||
| 'disabled'
|
||||
@@ -195,9 +302,10 @@ export interface Typegen0 {
|
||||
| 'idle'
|
||||
| 'running'
|
||||
| { running?: 'pending' | 'refreshing' }
|
||||
deanonymizing?: 'error' | 'success'
|
||||
}
|
||||
}
|
||||
token?: 'idle' | 'running'
|
||||
token?: 'idle' | 'running' | { idle?: 'noErrors' | 'error' }
|
||||
}
|
||||
tags: 'ready'
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { Nhost } from '../client'
|
||||
import { AuthClient } from '../client'
|
||||
import { ErrorPayload } from '../errors'
|
||||
import { nhostApiClient } from '../hasura-auth'
|
||||
import { ResetPasswordOptions } from '../types'
|
||||
import { rewriteRedirectTo } from '../utils'
|
||||
|
||||
export type ResetPasswordContext = {
|
||||
error: ErrorPayload | null
|
||||
}
|
||||
export type ResetPasswordEvents = {
|
||||
type: 'REQUEST_CHANGE'
|
||||
email?: string
|
||||
options?: ResetPasswordOptions
|
||||
}
|
||||
export type ResetPasswordEvents =
|
||||
| {
|
||||
type: 'REQUEST'
|
||||
email?: string
|
||||
options?: ResetPasswordOptions
|
||||
}
|
||||
| { type: 'SUCCESS' }
|
||||
| { type: 'ERROR'; error: ErrorPayload | null }
|
||||
|
||||
export const createResetPasswordMachine = ({ backendUrl, clientUrl }: Nhost) => {
|
||||
export const createResetPasswordMachine = ({ backendUrl, clientUrl }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
@@ -23,13 +27,14 @@ export const createResetPasswordMachine = ({ backendUrl, clientUrl }: Nhost) =>
|
||||
events: {} as ResetPasswordEvents
|
||||
},
|
||||
tsTypes: {} as import('./reset-password.typegen').Typegen0,
|
||||
preserveActionOrder: true,
|
||||
id: 'changePassword',
|
||||
initial: 'idle',
|
||||
context: { error: null },
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
REQUEST_CHANGE: 'requesting'
|
||||
REQUEST: 'requesting'
|
||||
},
|
||||
initial: 'initial',
|
||||
states: {
|
||||
@@ -42,8 +47,8 @@ export const createResetPasswordMachine = ({ backendUrl, clientUrl }: Nhost) =>
|
||||
invoke: {
|
||||
src: 'requestChange',
|
||||
id: 'requestChange',
|
||||
onDone: 'idle.success',
|
||||
onError: { actions: 'saveRequestError', target: 'idle.error' }
|
||||
onDone: { target: 'idle.success', actions: 'reportSuccess' },
|
||||
onError: { actions: ['saveRequestError', 'reportError'], target: 'idle.error' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,22 +56,19 @@ export const createResetPasswordMachine = ({ backendUrl, clientUrl }: Nhost) =>
|
||||
{
|
||||
actions: {
|
||||
saveRequestError: assign({
|
||||
// TODO type
|
||||
error: (_, { data: { error } }: any) => {
|
||||
console.log(error)
|
||||
return error
|
||||
}
|
||||
})
|
||||
}),
|
||||
reportError: send((ctx) => ({ type: 'ERROR', error: ctx.error })),
|
||||
reportSuccess: send('SUCCESS')
|
||||
},
|
||||
services: {
|
||||
requestChange: (_, { email, options }) =>
|
||||
api.post<string, { data: { error?: ErrorPayload } }>('/v1/auth/user/password/reset', {
|
||||
api.post<string, { data: { error?: ErrorPayload } }>('/user/password/reset', {
|
||||
email,
|
||||
options: {
|
||||
redirectTo: options?.redirectTo?.startsWith('/')
|
||||
? clientUrl + options.redirectTo
|
||||
: options?.redirectTo
|
||||
}
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,18 @@
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true
|
||||
eventsCausingActions: {
|
||||
reportSuccess: 'done.invoke.requestChange'
|
||||
saveRequestError: 'error.platform.requestChange'
|
||||
reportError: 'error.platform.requestChange'
|
||||
}
|
||||
internalEvents: {
|
||||
'error.platform.requestChange': { type: 'error.platform.requestChange'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
'done.invoke.requestChange': {
|
||||
type: 'done.invoke.requestChange'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.requestChange': { type: 'error.platform.requestChange'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
}
|
||||
invokeSrcNameMap: {
|
||||
requestChange: 'done.invoke.requestChange'
|
||||
@@ -24,7 +26,7 @@ export interface Typegen0 {
|
||||
delays: never
|
||||
}
|
||||
eventsCausingServices: {
|
||||
requestChange: 'REQUEST_CHANGE'
|
||||
requestChange: 'REQUEST'
|
||||
}
|
||||
eventsCausingGuards: {}
|
||||
eventsCausingDelays: {}
|
||||
94
packages/core/src/machines/send-verification-email.ts
Normal file
94
packages/core/src/machines/send-verification-email.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { AuthClient } from '../client'
|
||||
import { ErrorPayload, INVALID_EMAIL_ERROR } from '../errors'
|
||||
import { nhostApiClient } from '../hasura-auth'
|
||||
import { SendVerificationEmailOptions } from '../types'
|
||||
import { rewriteRedirectTo } from '../utils'
|
||||
import { isValidEmail } from '../validators'
|
||||
|
||||
export type SendVerificationEmailContext = {
|
||||
error: ErrorPayload | null
|
||||
}
|
||||
|
||||
export type SendVerificationEmailEvents =
|
||||
| {
|
||||
type: 'REQUEST'
|
||||
email?: string
|
||||
options?: SendVerificationEmailOptions
|
||||
}
|
||||
| { type: 'SUCCESS' }
|
||||
| { type: 'ERROR'; error: ErrorPayload | null }
|
||||
|
||||
export const createSendVerificationEmailMachine = ({
|
||||
backendUrl,
|
||||
clientUrl,
|
||||
interpreter
|
||||
}: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
context: {} as SendVerificationEmailContext,
|
||||
events: {} as SendVerificationEmailEvents
|
||||
},
|
||||
tsTypes: {} as import('./send-verification-email.typegen').Typegen0,
|
||||
preserveActionOrder: true,
|
||||
id: 'sendVerificationEmail',
|
||||
initial: 'idle',
|
||||
context: { error: null },
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
REQUEST: [
|
||||
{
|
||||
cond: 'invalidEmail',
|
||||
actions: 'saveInvalidEmailError',
|
||||
target: '.error'
|
||||
},
|
||||
{
|
||||
target: 'requesting'
|
||||
}
|
||||
]
|
||||
},
|
||||
initial: 'initial',
|
||||
states: {
|
||||
initial: {},
|
||||
success: {},
|
||||
error: {}
|
||||
}
|
||||
},
|
||||
requesting: {
|
||||
invoke: {
|
||||
src: 'request',
|
||||
id: 'request',
|
||||
onDone: { target: 'idle.success', actions: 'reportSuccess' },
|
||||
onError: { actions: ['saveRequestError', 'reportError'], target: 'idle.error' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
saveInvalidEmailError: assign({ error: (_) => INVALID_EMAIL_ERROR }),
|
||||
saveRequestError: assign({
|
||||
error: (_, { data: { error } }: any) => error
|
||||
}),
|
||||
reportError: send((ctx) => ({ type: 'ERROR', error: ctx.error })),
|
||||
reportSuccess: send('SUCCESS')
|
||||
},
|
||||
guards: {
|
||||
invalidEmail: (_, { email }) => !isValidEmail(email)
|
||||
},
|
||||
services: {
|
||||
request: async (_, { email, options }) => {
|
||||
const res = await api.post('/user/email/send-verification-email', {
|
||||
email,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true
|
||||
eventsCausingActions: {
|
||||
saveInvalidEmailError: 'REQUEST'
|
||||
reportSuccess: 'done.invoke.request'
|
||||
saveRequestError: 'error.platform.request'
|
||||
reportError: 'error.platform.request'
|
||||
}
|
||||
internalEvents: {
|
||||
'done.invoke.request': {
|
||||
type: 'done.invoke.request'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.request': { type: 'error.platform.request'; data: unknown }
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
}
|
||||
invokeSrcNameMap: {
|
||||
request: 'done.invoke.request'
|
||||
}
|
||||
missingImplementations: {
|
||||
actions: never
|
||||
services: never
|
||||
guards: never
|
||||
delays: never
|
||||
}
|
||||
eventsCausingServices: {
|
||||
request: 'REQUEST'
|
||||
}
|
||||
eventsCausingGuards: {
|
||||
invalidEmail: 'REQUEST'
|
||||
}
|
||||
eventsCausingDelays: {}
|
||||
matchesStates:
|
||||
| 'idle'
|
||||
| 'idle.initial'
|
||||
| 'idle.success'
|
||||
| 'idle.error'
|
||||
| 'requesting'
|
||||
| { idle?: 'initial' | 'success' | 'error' }
|
||||
tags: never
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
export type StorageGetter = (key: string) => string | null
|
||||
export type StorageSetter = (key: string, value: string | null) => void
|
||||
export type StorageGetter = (key: string) => string | null | Promise<string | null>
|
||||
export type StorageSetter = (key: string, value: string | null) => void | Promise<void>
|
||||
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
// TODO rename to 'refreshTokenGetter' and 'refreshTokenSetter'
|
||||
export const defaultStorageGetter: StorageGetter = (key) => {
|
||||
const inMemoryLocalStorage: Map<string, string | null> = new Map()
|
||||
|
||||
export const defaultClientStorageGetter: StorageGetter = (key) => {
|
||||
if (isBrowser && localStorage) return localStorage.getItem(key)
|
||||
else {
|
||||
console.warn('no defaultStorageGetter')
|
||||
return null
|
||||
}
|
||||
else return inMemoryLocalStorage.get(key) ?? null
|
||||
}
|
||||
|
||||
export const defaultStorageSetter: StorageSetter = (key, value) => {
|
||||
export const defaultClientStorageSetter: StorageSetter = (key, value) => {
|
||||
if (isBrowser && localStorage) {
|
||||
if (value) {
|
||||
localStorage.setItem(key, value)
|
||||
@@ -21,10 +20,11 @@ export const defaultStorageSetter: StorageSetter = (key, value) => {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
} else {
|
||||
console.warn('no defaultStorageSetter')
|
||||
// throw Error(
|
||||
// 'localStorage is not available and no custom storageSetter has been set as an option'
|
||||
// )}
|
||||
if (value) {
|
||||
inMemoryLocalStorage.set(key, value)
|
||||
} else if (inMemoryLocalStorage.has(key)) {
|
||||
inMemoryLocalStorage.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// TODO create a dedicated package for types
|
||||
// TODO import generated typings from 'hasura-auth'
|
||||
|
||||
import { InterpreterFrom } from 'xstate'
|
||||
|
||||
import { AuthMachine } from './machines'
|
||||
|
||||
// TODO import generated typings from 'hasura-auth'
|
||||
export type AuthInterpreter = InterpreterFrom<AuthMachine>
|
||||
type RegistrationOptions = {
|
||||
locale?: string
|
||||
allowedRoles?: string[]
|
||||
@@ -17,6 +22,9 @@ export type PasswordlessOptions = RegistrationOptions & RedirectOption
|
||||
export type SignUpOptions = RegistrationOptions & RedirectOption
|
||||
export type ChangeEmailOptions = RedirectOption
|
||||
export type ResetPasswordOptions = RedirectOption
|
||||
export type SendVerificationEmailOptions = RedirectOption
|
||||
export type DeanonymizeOptions = { email?: string; password?: string } & RegistrationOptions
|
||||
export type ProviderOptions = RegistrationOptions & RedirectOption
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
@@ -39,6 +47,10 @@ export type NhostSession = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export type Mfa = {
|
||||
ticket: string
|
||||
}
|
||||
|
||||
export type Provider =
|
||||
| 'apple'
|
||||
| 'facebook'
|
||||
29
packages/core/src/utils.ts
Normal file
29
packages/core/src/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export const encodeQueryParameters = (baseUrl: string, parameters?: Record<string, unknown>) => {
|
||||
const encodedParameters =
|
||||
parameters &&
|
||||
Object.entries(parameters)
|
||||
.map(([key, value]) => {
|
||||
const stringValue = Array.isArray(value)
|
||||
? value.join(',')
|
||||
: typeof value === 'object'
|
||||
? JSON.stringify(value)
|
||||
: (value as string)
|
||||
return `${key}=${encodeURIComponent(stringValue)}`
|
||||
})
|
||||
.join('&')
|
||||
if (encodedParameters) return `${baseUrl}?${encodedParameters}`
|
||||
else return baseUrl
|
||||
}
|
||||
|
||||
export const rewriteRedirectTo = (
|
||||
clientUrl: string,
|
||||
options?: Record<string, unknown> & { redirectTo?: string }
|
||||
) =>
|
||||
options?.redirectTo
|
||||
? {
|
||||
...options,
|
||||
redirectTo: options?.redirectTo?.startsWith('/')
|
||||
? clientUrl + options.redirectTo
|
||||
: options?.redirectTo
|
||||
}
|
||||
: options
|
||||
@@ -3,7 +3,7 @@ import { MIN_PASSWORD_LENGTH } from './constants'
|
||||
export const isValidEmail = (email?: string | null) =>
|
||||
!!email &&
|
||||
typeof email === 'string' &&
|
||||
String(email)
|
||||
!!String(email)
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
@@ -11,3 +11,7 @@ export const isValidEmail = (email?: string | null) =>
|
||||
|
||||
export const isValidPassword = (password?: string | null) =>
|
||||
!!password && typeof password === 'string' && password.length >= MIN_PASSWORD_LENGTH
|
||||
|
||||
// TODO improve validation
|
||||
export const isValidPhoneNumber = (phoneNumber?: string | null) =>
|
||||
!!phoneNumber && typeof phoneNumber === 'string'
|
||||
@@ -1,5 +1,40 @@
|
||||
# @nhost/hasura-auth-js
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ab36f90: Correct access to user/session information through getUser/getSession/isReady function when authentication state is not ready yet
|
||||
In some cases e.g. NextJS build, `auth.getUser()`, `auth.getSession()` or `auth.isReady()` should be accessible without raising an error.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 744fd69: Use `@nhost/core` and its state machine
|
||||
|
||||
`@nhost/nhost-js` and `@nhost/hasura-auth-js` now use the xstate-based state management system from `@nhost/core`.
|
||||
|
||||
The client initiation remains the same, although the `clientStorage` and `clientStorageType` are deprecated in favor of `clientStorageGetter (key:string) => string | null | Promise<string | null>` and `clientStorageSetter: (key: string, value: string | null) => void | Promise<void>`.
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 744fd69: Unify vanilla, react and next APIs so they can work together
|
||||
React and NextJS libraries now works together with `@nhost/nhost-js`. It also means the Nhost client needs to be initiated before passing it to the React provider.
|
||||
See the [React](https://docs.nhost.io/reference/react#configuration) and [NextJS](https://docs.nhost.io/reference/nextjs/configuration) configuration documentation for additional information.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 744fd69: remove `nhost.auth.verifyEmail`
|
||||
Theres's a /verify endpoint in hasura-auth, but the sdk is not even using it as
|
||||
1. the user follows the /verify link in the email
|
||||
2. hasura-auth validates the link, attaches the token and redirects to the frontend
|
||||
3. the sdk gets the refresh token from the url
|
||||
4. the sdk consumes the refresh token
|
||||
- Updated dependencies [744fd69]
|
||||
- Updated dependencies [744fd69]
|
||||
- @nhost/core@0.3.0
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/hasura-auth-js",
|
||||
"version": "0.1.15",
|
||||
"version": "1.0.1",
|
||||
"description": "Hasura-auth client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -51,14 +51,14 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"axios": "^0.25.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"query-string": "^7.1.0"
|
||||
"@nhost/core": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/faker": "5",
|
||||
"axios": "^0.26.0",
|
||||
"faker": "5",
|
||||
"html-urls": "^2.4.27",
|
||||
"mailhog": "^4.16.0"
|
||||
"mailhog": "^4.16.0",
|
||||
"xstate": "^4.30.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import axios, { AxiosError, AxiosInstance } from 'axios'
|
||||
|
||||
import {
|
||||
ApiChangeEmailResponse,
|
||||
ApiChangePasswordResponse,
|
||||
ApiDeanonymizeResponse,
|
||||
ApiError,
|
||||
ApiRefreshTokenResponse,
|
||||
ApiResetPasswordResponse,
|
||||
ApiSendVerificationEmailResponse,
|
||||
ApiSignInData,
|
||||
ApiSignInResponse,
|
||||
ApiSignOutResponse,
|
||||
ChangeEmailParams,
|
||||
ChangePasswordParams,
|
||||
DeanonymizeParams,
|
||||
ResetPasswordParams,
|
||||
SendVerificationEmailParams,
|
||||
Session,
|
||||
SignInEmailPasswordParams,
|
||||
SignInPasswordlessEmailParams,
|
||||
SignInPasswordlessSmsOtpParams,
|
||||
SignInPasswordlessSmsParams,
|
||||
SignUpEmailPasswordParams
|
||||
} from './utils/types'
|
||||
|
||||
const SERVER_ERROR_CODE = 500
|
||||
export class HasuraAuthApi {
|
||||
private url: string
|
||||
private httpClient: AxiosInstance
|
||||
private accessToken: string | undefined
|
||||
|
||||
constructor({ url = '' }) {
|
||||
this.url = url
|
||||
|
||||
this.httpClient = axios.create({ baseURL: this.url })
|
||||
|
||||
// convert axios error to custom ApiError
|
||||
this.httpClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError<{ message: string }>) =>
|
||||
Promise.reject({
|
||||
message: error.response?.data?.message ?? error.message ?? JSON.stringify(error),
|
||||
status: error.response?.status ?? SERVER_ERROR_CODE
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `signUpWithEmailAndPassword` to sign up a new user using email and password.
|
||||
*/
|
||||
async signUpEmailPassword(params: SignUpEmailPasswordParams): Promise<ApiSignInResponse> {
|
||||
try {
|
||||
const res = await this.httpClient.post<ApiSignInData>('/signup/email-password', params)
|
||||
return { data: res.data, error: null }
|
||||
} catch (error) {
|
||||
return { data: null, error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async signInEmailPassword(params: SignInEmailPasswordParams): Promise<ApiSignInResponse> {
|
||||
try {
|
||||
const res = await this.httpClient.post<ApiSignInData>('/signin/email-password', params)
|
||||
return { data: res.data, error: null }
|
||||
} catch (error) {
|
||||
return { data: null, error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async signInPasswordlessEmail(params: SignInPasswordlessEmailParams): Promise<ApiSignInResponse> {
|
||||
try {
|
||||
const res = await this.httpClient.post<ApiSignInData>('/signin/passwordless/email', params)
|
||||
return { data: res.data, error: null }
|
||||
} catch (error) {
|
||||
return { data: null, error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async signInPasswordlessSms(params: SignInPasswordlessSmsParams): Promise<ApiSignInResponse> {
|
||||
try {
|
||||
const res = await this.httpClient.post<ApiSignInData>('/signin/passwordless/sms', params)
|
||||
return { data: res.data, error: null }
|
||||
} catch (error) {
|
||||
return { data: null, error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async signInPasswordlessSmsOtp(
|
||||
params: SignInPasswordlessSmsOtpParams
|
||||
): Promise<ApiSignInResponse> {
|
||||
try {
|
||||
const res = await this.httpClient.post<ApiSignInData>('/signin/passwordless/sms/otp', params)
|
||||
return { data: res.data, error: null }
|
||||
} catch (error) {
|
||||
return { data: null, error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async signOut(params: { refreshToken: string; all?: boolean }): Promise<ApiSignOutResponse> {
|
||||
try {
|
||||
await this.httpClient.post('/signout', params)
|
||||
|
||||
return { error: null }
|
||||
} catch (error) {
|
||||
return { error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async refreshToken(params: { refreshToken: string }): Promise<ApiRefreshTokenResponse> {
|
||||
try {
|
||||
const res = await this.httpClient.post<Session>('/token', params)
|
||||
|
||||
return { error: null, session: res.data }
|
||||
} catch (error) {
|
||||
return { error: error as ApiError, session: null }
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(params: ResetPasswordParams): Promise<ApiResetPasswordResponse> {
|
||||
try {
|
||||
await this.httpClient.post('/user/password/reset', params)
|
||||
|
||||
return { error: null }
|
||||
} catch (error) {
|
||||
return { error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async changePassword(params: ChangePasswordParams): Promise<ApiChangePasswordResponse> {
|
||||
try {
|
||||
await this.httpClient.post('/user/password', params, {
|
||||
headers: {
|
||||
...this.generateAuthHeaders()
|
||||
}
|
||||
})
|
||||
|
||||
return { error: null }
|
||||
} catch (error) {
|
||||
return { error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async sendVerificationEmail(
|
||||
params: SendVerificationEmailParams
|
||||
): Promise<ApiSendVerificationEmailResponse> {
|
||||
try {
|
||||
await this.httpClient.post('/user/email/send-verification-email', params)
|
||||
|
||||
return { error: null }
|
||||
} catch (error) {
|
||||
return { error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async changeEmail(params: ChangeEmailParams): Promise<ApiChangeEmailResponse> {
|
||||
try {
|
||||
await this.httpClient.post('/user/email/change', params, {
|
||||
headers: {
|
||||
...this.generateAuthHeaders()
|
||||
}
|
||||
})
|
||||
|
||||
return { error: null }
|
||||
} catch (error) {
|
||||
return { error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
async deanonymize(params: DeanonymizeParams): Promise<ApiDeanonymizeResponse> {
|
||||
try {
|
||||
await this.httpClient.post('/user/deanonymize', params)
|
||||
|
||||
return { error: null }
|
||||
} catch (error) {
|
||||
return { error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
// deanonymize
|
||||
|
||||
async verifyEmail(params: { email: string; ticket: string }): Promise<ApiSignInResponse> {
|
||||
try {
|
||||
const res = await this.httpClient.post<ApiSignInData>('/user/email/verify', params)
|
||||
|
||||
return { data: res.data, error: null }
|
||||
} catch (error) {
|
||||
return { data: null, error: error as ApiError }
|
||||
}
|
||||
}
|
||||
|
||||
setAccessToken(accessToken: string | undefined) {
|
||||
this.accessToken = accessToken
|
||||
}
|
||||
|
||||
private generateAuthHeaders() {
|
||||
if (!this.accessToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${this.accessToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,2 @@
|
||||
export * from './hasura-auth-api'
|
||||
export * from './hasura-auth-client'
|
||||
export * from './utils/types'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const NHOST_REFRESH_TOKEN = 'nhostRefreshToken'
|
||||
@@ -1,19 +1,81 @@
|
||||
import {
|
||||
AuthContext,
|
||||
defaultClientStorageGetter,
|
||||
defaultClientStorageSetter,
|
||||
StorageGetter,
|
||||
StorageSetter
|
||||
} from '@nhost/core'
|
||||
|
||||
import { ClientStorage, ClientStorageType, Session } from './types'
|
||||
|
||||
export const isBrowser = () => typeof window !== 'undefined'
|
||||
|
||||
export class inMemoryLocalStorage {
|
||||
private memory: Record<string, string | null>
|
||||
|
||||
constructor() {
|
||||
this.memory = {}
|
||||
}
|
||||
|
||||
setItem(key: string, value: string | null): void {
|
||||
this.memory[key] = value
|
||||
}
|
||||
getItem(key: string): string | null {
|
||||
return this.memory[key]
|
||||
}
|
||||
removeItem(key: string): void {
|
||||
delete this.memory[key]
|
||||
export const getSession = (context?: AuthContext): Session | null => {
|
||||
if (!context || !context.accessToken.value || !context.refreshToken.value) return null
|
||||
return {
|
||||
accessToken: context.accessToken.value,
|
||||
accessTokenExpiresIn: (context.accessToken.expiresAt.getTime() - Date.now()) / 1000,
|
||||
refreshToken: context.refreshToken.value,
|
||||
user: context.user
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (clientStorageType === 'react-native' || clientStorageType === 'custom') {
|
||||
checkStorageAccessors(clientStorage, ['getItem'])
|
||||
return (key) => clientStorage.getItem?.(key)
|
||||
} else if (clientStorageType === 'capacitor') {
|
||||
checkStorageAccessors(clientStorage, ['get'])
|
||||
return (key) => clientStorage.get?.({ key })
|
||||
} else if (clientStorageType === 'expo-secure-storage') {
|
||||
checkStorageAccessors(clientStorage, ['getItemAsync'])
|
||||
return (key) => clientStorage.getItemAsync?.(key)
|
||||
}
|
||||
}
|
||||
return defaultClientStorageGetter
|
||||
}
|
||||
|
||||
export const localStorageSetter = (
|
||||
clientStorageType: ClientStorageType,
|
||||
clientStorage?: ClientStorage
|
||||
): StorageSetter => {
|
||||
if (clientStorage) {
|
||||
if (clientStorageType === 'react-native' || clientStorageType === 'custom') {
|
||||
checkStorageAccessors(clientStorage, ['setItem', 'removeItem'])
|
||||
|
||||
return (key, value) => {
|
||||
if (value) clientStorage.setItem?.(key, value)
|
||||
else clientStorage.removeItem?.(key)
|
||||
}
|
||||
} else if (clientStorageType === 'capacitor') {
|
||||
checkStorageAccessors(clientStorage, ['set', 'remove'])
|
||||
return (key, value) => {
|
||||
if (value) clientStorage.set?.({ key, value })
|
||||
else clientStorage.remove?.({ key })
|
||||
}
|
||||
} else if (clientStorageType === 'expo-secure-storage') {
|
||||
checkStorageAccessors(clientStorage, ['setItemAsync', 'deleteItemAsync'])
|
||||
return async (key, value) => {
|
||||
if (value) await clientStorage.setItemAsync?.(key, value)
|
||||
else clientStorage.deleteItemAsync?.(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultClientStorageSetter
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
export interface User {
|
||||
id: string
|
||||
createdAt: string
|
||||
displayName: string
|
||||
avatarUrl: string
|
||||
locale: string
|
||||
email?: string
|
||||
isAnonymous: boolean
|
||||
defaultRole: string
|
||||
roles: string[]
|
||||
import { AuthClient, StorageGetter, StorageSetter, User } from '@nhost/core'
|
||||
|
||||
export interface NhostAuthConstructorParams {
|
||||
url: string
|
||||
refreshIntervalTime?: number
|
||||
/** @deprecated Use clientStorageGetter and clientStorageSetter options instead */
|
||||
clientStorage?: ClientStorage
|
||||
/** @deprecated Use clientStorageGetter and clientStorageSetter options instead */
|
||||
clientStorageType?: ClientStorageType
|
||||
clientStorageGetter?: StorageGetter
|
||||
clientStorageSetter?: StorageSetter
|
||||
autoRefreshToken?: boolean
|
||||
autoLogin?: boolean
|
||||
start?: boolean
|
||||
Client?: typeof AuthClient
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
@@ -16,7 +21,6 @@ export interface Session {
|
||||
refreshToken: string
|
||||
user: User | null
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
message: string
|
||||
status: number
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Tests
|
||||
|
||||
1. Run the Nhost backend for tests: `npm run test:backend:start`
|
||||
2. Wait for the Nhost backend to start. Should not take more than 90-180 seconds.
|
||||
3. In another terminal, run `npm test`
|
||||
1. Run the Nhost backend for tests:
|
||||
|
||||
```sh
|
||||
cd examples/testing-project
|
||||
nhost -d
|
||||
```
|
||||
|
||||
2. Wait for the Nhost backend to start
|
||||
3. In another terminal, run `pnpm run test`
|
||||
|
||||
@@ -27,8 +27,7 @@ describe('emails', () => {
|
||||
|
||||
// get verify email link
|
||||
const verifyEmailLink = htmlUrls({ html: message.html }).find(
|
||||
(href: { value: string; url: string; uri: string }) =>
|
||||
href.url.includes('verifyEmail')
|
||||
(href: { value: string; url: string; uri: string }) => href.url.includes('verifyEmail')
|
||||
)
|
||||
|
||||
// verify email
|
||||
@@ -61,8 +60,7 @@ describe('emails', () => {
|
||||
|
||||
// get verify email link
|
||||
const changeEmailLink = htmlUrls({ html: changeEmailEmail.html }).find(
|
||||
(href: { value: string; url: string; uri: string }) =>
|
||||
href.url.includes('emailConfirmChange')
|
||||
(href: { value: string; url: string; uri: string }) => href.url.includes('emailConfirmChange')
|
||||
)
|
||||
|
||||
// verify email
|
||||
@@ -114,8 +112,7 @@ describe('emails', () => {
|
||||
// test email link
|
||||
// get verify email link
|
||||
const verifyEmailLink = htmlUrls({ html: verifyEmailEmail.html }).find(
|
||||
(href: { value: string; url: string; uri: string }) =>
|
||||
href.url.includes('verifyEmail')
|
||||
(href: { value: string; url: string; uri: string }) => href.url.includes('verifyEmail')
|
||||
)
|
||||
|
||||
// verify email
|
||||
|
||||
@@ -32,8 +32,7 @@ export const signUpAndVerifyUser = async (params: SignUpParams) => {
|
||||
|
||||
// get verify email link
|
||||
const verifyEmailLink = htmlUrls({ html: message.html }).find(
|
||||
(href: { value: string; url: string; uri: string }) =>
|
||||
href.url.includes('verifyEmail')
|
||||
(href: { value: string; url: string; uri: string }) => href.url.includes('verifyEmail')
|
||||
)
|
||||
|
||||
// verify email
|
||||
@@ -58,8 +57,7 @@ export const signUpAndInUser = async (params: SignUpParams) => {
|
||||
|
||||
// get verify email link
|
||||
const verifyEmailLink = htmlUrls({ html: message.html }).find(
|
||||
(href: { value: string; url: string; uri: string }) =>
|
||||
href.url.includes('verifyEmail')
|
||||
(href: { value: string; url: string; uri: string }) => href.url.includes('verifyEmail')
|
||||
)
|
||||
|
||||
// verify email
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user