Compare commits

...

46 Commits

Author SHA1 Message Date
github-actions[bot]
ccaa4c4bba chore: update versions (#300)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-03-28 10:11:31 +02:00
Pilou
ab36f90cec fix: correct access to user/session information through getUser/getSession/isReady (#298)
* fix: correct access to user/session information through getUser/getSession/isReady

* chore: use carret instead of star
2022-03-28 10:09:38 +02:00
Johan Eliasson
cfbe2db430 fix: make it clear the @nhost/react-auth package is depricated (#297)
* fix: make it clear this package is depricated

* Update README.md

Co-authored-by: Pilou <24897252+plmercereau@users.noreply.github.com>
2022-03-28 09:57:55 +02:00
Pilou
6838ac6201 docs: fix deadlinks in README (#256) 2022-03-25 15:38:19 +00:00
Johan Eliasson
0caf43037d fix: updated react apollo crm package versions (#296)
* update

* update
2022-03-25 16:33:30 +01:00
Pilou
4ed626d5b5 chore: bump fixed versions in examples (#257)
* chore: bump fixed versions in examples

* chore: bump to latest sdk version

* chore: bump to latest version

* chore: bump example version
2022-03-25 14:49:35 +00:00
github-actions[bot]
9ff9abee6a chore: update versions (#293)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-03-25 11:50:45 +00:00
Pilou
9d3f0521a5 fix: keep location.hash in email redirections (#294)
In the next cycle, hasura-auth will only send the refresh token in the hash, and will add the
redirection type as a query parameter. We will then be able to remove/hide the hash from the url as
soon as the refresh token has been used
2022-03-25 11:34:41 +00:00
Pilou
744fd6929f Unify @nhost/react and @nhost/nhost-js (#273)
* chore: rename `@nhost/client` to `@nhost/core`

* chore: refactor hasura-auth-js to use @nhost/core

all existing tests pass

* refactor: adapt syntax to react, and rename machine to authMachine

* refactor: rename to authmachine and remove useless license files

pnpm uses the root license file when publishing packages

* feat: totp login+password

* refactor: passwordless sms, mfa, deanonymize

* refactor: state/value mfa code

* refactor: rename

* refactor: auth status and token change events

* refactor: adjust apollo to original syntax, and sync auth session accross all nhost-js sub-clients

* refactor: revert changelog and adjust documentation

* refactor: adjust to the shape-up signatures

* refactor: make nextjs work with the new system

* refactor: allow async storage getters and setters

* refactor: implement refreshSession

* chore: fix pnpm lock file

* docs: change NhostReactProvider to NhostNextProvider

* chore: changesets

* refactor: change signup/signin hooks signatures as per Johan's request

* refactor: remove `nhost.auth.verifyEmail`

* chore: add changeset

* docs: add mfa to the react example, and adjust examples to the new conventions

* feat: allow oauth providers to get options

* fix: sync sdk client with the machine state

* fix: don't use state.matches and state.hasTag in useSelector, and improve useSelectors

* refactor: set oauth options everywhere, document, and rewrite relative redirectTo

* chore: update pnpm lock file

* fix: correct paths to cjs dist file, and reactivate warning suppression about useLayoutEffect

* chore: keep the same parameters in the React Apollo providers

* refactor: use the new system in @nhost/react-auth, and mark it as deprecated

* chore: @nhost/react-auth changeset

* chore: update pnpm lock file

* docs: remove todos and point to the documenation when it exists

* docs: name files `.tsx` instead of `.jsx`

* refactor: retro-compatible clientStorageType and clientStorage options

* refactor: improve hooks names consistency

See https://github.com/nhost/nhost/pull/273#discussion_r829058348

* chore: don't override changelog

* refactor: bump hasura-auth to version 0.4.2

* refactor: move @apollo/client to a peer dependency

This was the case in the origination @nhost/react-apollo package

* refactor: prettier

* refactor: rename useConfigMfa properties

* chore: ellaborate changesets

* refactor: rename `anonymousSignIn` to `signInAnonymous`

* docs: correct typo

* refactor: rename anonymous signin hook

* refactor: use @nhost/nextjs instead of @nhost/react in Nextjs documentation

* chore: explain renaming from @nhost/client to @nhost/core

* chore: changeset

* chore: complete sentence in changeset
2022-03-24 20:37:56 +00:00
Szilárd Dóró
f43f52e766 updated react-apollo changelog (#292) 2022-03-24 12:33:31 +00:00
github-actions[bot]
fd4c54ee91 chore: update versions (#290)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-03-24 09:58:40 +00:00
Szilárd Dóró
b30ff6f507 fix: Auth header condition check (#291)
* fixed auth header conditions in storage functions

* fixed lint errors in hasura-storage-js package

* removed patch changeset file
2022-03-24 09:56:25 +00:00
Pilou
ff7ae21a87 feat: admin secret for storage (#289)
* added Hasura Admin Secret support to storage functions

* @nhost/hasura-storage-js changelog

Co-authored-by: Szilárd Dóró <szilard.doro@bishop.hu>
2022-03-23 16:55:30 +00:00
Jerry Jäppinen
6d2c7b26c0 Formatting and paths (#285) 2022-03-21 15:23:09 +00:00
github-actions[bot]
d2d3ba6eb7 chore: update versions (#272)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-03-15 14:38:45 +01:00
Johan Eliasson
e688600ea2 fix: current options when sign in with a provider (#274)
* fix: current options when sign in with a provider

* Create eighty-pianos-try.md

* Create chilled-swans-repeat.md

* send sign-up options

* doesn't hurt to do encodeURIComponent everywhere

Co-authored-by: Pilou <24897252+plmercereau@users.noreply.github.com>
2022-03-15 14:32:09 +01:00
Hoang Do
d9aec711c4 Fix(nhost-js): Fix typo in README (#283) 2022-03-15 07:03:41 +01:00
Dominic Garms
9bd01e756f export refresh function (#281)
* export refresh utilitly function

For being able to receive a new accessToken on client side it would be ideal to expose the `refresh` function.

* Create loud-planets-impress.md

Co-authored-by: Pilou <24897252+plmercereau@users.noreply.github.com>
2022-03-14 18:33:52 +01:00
Pilou
8f7643a90e fix: change target ES module build target to es2019 (#282) 2022-03-14 18:32:27 +01:00
Johan Eliasson
50b9d763ae feat: correct available providers (Discord & Twitch added) (#266)
* feat: correct available providers (Discord & Twitch added)

* Create hip-eels-unite.md

* putting back proviers

Co-authored-by: Pierre-Louis Mercereau <24897252+plmercereau@users.noreply.github.com>
2022-03-11 08:11:57 +01:00
Quentin Decré
63cb1f0ce6 doc(hasura-auth-client): fixed onAuthStateChanged @example (#268) 2022-03-10 18:42:38 +01:00
Pilou
7c70b1823d fix broken ci pipeline (#267)
* ci: clean ci

* ci: matrix

* ci: install dependencies

* ci: debug

* ci: updated packages

* ci: remove scope

* ci: add src to files

* ci: deactivate turbo cache

* ci: debug

* ci: downgrade turborepo

* ci: reverse version

* ci: installl again

* ci: pwd

* ci: increase depth

* ci: decrease pnpm version

* ci: another turbo version

* ci: 1.1.5

* ci: clean

* ci: frozen lockfile

* ci: add options

* ci: deactivate cache

* ci: single ci command

* ci: correct

* ci: desesperant

* ci: again

* ci: again

* ci: remove testing project

* ci: remove testing project

* ci: exec nhost in tmp

* ci: deactivate nhost

* ci: manually create nhost project

* ci: again

* ci: copy existing nhost project

* ci: correct cp arg

* ci: correct cp

* ci: cd, not cp

* ci: wrap up

* ci: scope ci script
2022-03-10 13:16:48 +01:00
Pierre-Louis Mercereau
d2aae774a0 ci: wait-for at the end 2022-03-10 11:10:33 +01:00
Pierre-Louis Mercereau
322e8a1b07 ci: recreate pnpm lock 2022-03-10 11:07:24 +01:00
Pierre-Louis Mercereau
d154f8d71b ci: checkout twice 2022-03-10 11:00:53 +01:00
Pierre-Louis Mercereau
0931afd84c ci: nhost cli at the top 2022-03-10 10:48:17 +01:00
Pierre-Louis Mercereau
837548cfd5 ci: wrap wait-on in an action 2022-03-10 10:43:40 +01:00
Pierre-Louis Mercereau
a6cabbca79 ci: deactivate wait-on 2022-03-10 10:37:55 +01:00
Pierre-Louis Mercereau
82f19fe717 ci: internal wait-on script 2022-03-10 10:35:19 +01:00
Pierre-Louis Mercereau
82be281153 ci: use npx 2022-03-10 10:28:31 +01:00
Pierre-Louis Mercereau
0112ca775f ci: start nhost cli 2022-03-10 10:22:34 +01:00
Pierre-Louis Mercereau
b30b812b93 ci: install nhost cli 2022-03-10 10:20:52 +01:00
Pierre-Louis Mercereau
bace64c306 ci: without nhost 2022-03-10 10:19:40 +01:00
Pierre-Louis Mercereau
be49b641e3 ci: deactivate all cache 2022-03-10 10:16:56 +01:00
Pierre-Louis Mercereau
5ac8c2f516 ci: deactivate turborepo cache 2022-03-10 10:12:38 +01:00
Pierre-Louis Mercereau
168ae1d82b ci: reactivate all ci steps 2022-03-10 10:08:46 +01:00
Pierre-Louis Mercereau
b4a2e28fc3 ci: manually install node packages 2022-03-10 10:05:31 +01:00
Pierre-Louis Mercereau
ca3ae21286 ci: try explicit pnpm install params 2022-03-10 10:01:54 +01:00
Pierre-Louis Mercereau
9f30c1af09 ci: debug 2022-03-10 09:51:16 +01:00
Pierre-Louis Mercereau
e1f9f64910 ci: deactivate all cache 2022-03-10 09:48:36 +01:00
Pierre-Louis Mercereau
2634dd8335 ci: deactivate turborepo cache 2022-03-10 09:46:30 +01:00
Pierre-Louis Mercereau
42b4c78d4c ci: correct gh action 2022-03-10 09:44:01 +01:00
Pierre-Louis Mercereau
54174c1b0f ci: change gh action 2022-03-10 09:41:06 +01:00
Johan Eliasson
9fad359ae9 docs: Added docs for permission variables (#263)
* added docs for permissions

* update

* wording update

* typos
2022-03-09 14:15:10 +01:00
Johan Eliasson
1cbf460223 added Nhost diagram (#265) 2022-03-09 13:37:06 +01:00
Pilou
8a3aa007b8 Update README.md (#258) 2022-03-08 21:55:28 +01:00
149 changed files with 16468 additions and 51040 deletions

View File

@@ -25,6 +25,6 @@ esbuild
platform: 'browser',
format: 'esm',
sourcemap: true,
target: 'esnext'
target: 'es2019'
})
.catch(() => process.exit(1))

View File

@@ -28,7 +28,7 @@ jobs:
- uses: pnpm/action-setup@v2.1.0
with:
version: 6.30.1
version: 6.32.3
run_install: true
- name: Create PR or Publish release

View File

@@ -27,26 +27,30 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [14, 16]
steps:
- uses: actions/checkout@v2
- name: Cache pnpm modules
uses: actions/cache@v2
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- name: Install nhost CLI
run: curl -L https://raw.githubusercontent.com/nhost/cli/main/get.sh | bash
- uses: pnpm/action-setup@v2.0.1
- name: Start Nhost Backend
run: |
cp -R examples/testing-project /tmp/
cd /tmp/testing-project
nhost dev &
- uses: pnpm/action-setup@v2.2.1
with:
version: 6.30.1
run_install: true
version: 6.32.3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Cache turbo
uses: actions/cache@v2
@@ -56,16 +60,11 @@ jobs:
restore-keys: |
turbo-${{ github.job }}-${{ github.ref_name }}-
- name: Install nhost CLI
run: curl -L https://raw.githubusercontent.com/nhost/cli/main/get.sh | bash
- name: Install dependencies
run: pnpm install
- name: Start Nhost Backend
run: |
cd examples/testing-project
- name: Wait for Nhost
run: pnpm run wait
nhost dev -d --no-browser &
- name: Wait for Nhost Backend to start
run: |
pnpm dlx wait-on http://localhost:1337/v1/auth/healthz -i 500 -t 120000
- run: pnpm run ci
- name: Build, tests and lint
run: pnpm run ci

View File

@@ -36,6 +36,13 @@ Nhost consists of open source software:
- Serverless Functions: Node.js (JavaScript and TypeScript)
- [Nhost CLI](https://docs.nhost.io/reference/cli) for local development
<div align="center">
<br />
<img src="assets/nhost-diagram.png"/>
<br />
<br />
</div>
Visit [https://docs.nhost.io](http://docs.nhost.io) for the complete documentation.
# How to get started
@@ -92,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 ❤️

BIN
assets/nhost-diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -2,12 +2,15 @@
## Get started
1. Install dependencies: `yarn`
2. Start dev server: `yarn dev`
From the **root** of the `nhost/nhost` repository:
## NOTES;
```bash
pnpm run clean:all
pnpm i
cd docs
pnpm run dev
```
The content is copied from the main `nhost/nhost` repo. This repo is only to modify styles/react components.
## Structure

View File

@@ -65,7 +65,7 @@ const CustomLink = ({
const components = {
img: (props: DetailedHTMLProps<HTMLProps<HTMLImageElement>, HTMLImageElement>) => {
return (
<span className="block mx-10 mt-5 ">
<span className="block mx-10 my-10 ">
<img src={props.src} alt={props.alt} className="mx-auto mt-2" />
</span>
)

View File

@@ -20,6 +20,8 @@ Access token data is included as headers with every API request. By default, eve
The default role for users is `user`.
> You can also [add custom permission](#add-permission-variables) varaibles if you need to.
---
## Select permissions
@@ -60,3 +62,23 @@ In our example, we only select `name`, because we want all other other columns t
We also want every new record's `user_id` value to be set to the ID of the user making the request. We can tell Hasura to do this with **column presets**.
1. Under column presets, set `user_id` to `x-hasura-user-id`.
## Add Permission Variables
You can add extra permission variables in the Nhost console under **Users** and then **Roles and permissions**. These permission variables are then available when creating permissions for your GraphQL API in the Hasura console.
![Permission Variables](/images/platform/permission-variables-preview.svg)
As an example, let's say you add a new permission variable `x-hasura-organisation-id` with path `user.profile.organisation.id`. This means that Nhost Auth will get the value for `x-hasura-organisation-id` by internally generating the following GraphQL query:
```graphql
query {
user(id: "<user-id>") {
profile {
organisation {
id
}
}
}
}
```

View File

@@ -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:

View File

@@ -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,7 +54,7 @@ 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'
@@ -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'

View File

@@ -6,7 +6,7 @@ Create a `auth-protected.js` file:
```jsx
import { useRouter } from 'next/router'
import { useAuthLoading, useAuthenticated } from '@nhost/react'
import { useAuthLoading, useAuthenticated } from '@nhost/nextjs'
export function authProtected(Comp) {
return function AuthProtected(props) {

View File

@@ -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')
)

View File

@@ -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,6 +214,8 @@ const Component = () => {
}
```
---
## Authentication status
### `useAuthLoading`
@@ -216,29 +240,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 +274,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 +357,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()

View File

@@ -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` | | |

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -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

View File

@@ -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:

View File

@@ -4,7 +4,6 @@ table:
configuration:
custom_column_names:
id: id
redirect_url: redirectUrl
custom_name: authProviderRequests
custom_root_fields:
delete: deleteAuthProviderRequests

View File

@@ -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",

View File

@@ -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>
)
}

View File

@@ -6,9 +6,9 @@ import {
useAuthenticated,
useChangeEmail,
useChangePassword,
useEmailPasswordlessSignIn,
useEmailPasswordSignIn,
useEmailPasswordSignUp,
useSignInEmailPasswordless,
useSignInEmailPassword,
useSignUpEmailPassword,
useSignOut
} from '@nhost/react'
import { useAuthQuery } from '@nhost/react-apollo'
@@ -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>
</>
)}

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -1,7 +0,0 @@
module.exports = {
style: {
postcss: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
},
};

View File

@@ -0,0 +1,7 @@
import { NhostClient } from '@nhost/nhost-js'
const nhost = new NhostClient({
backendUrl: process.env.NHOST_BACKEND_URL!
})
export { nhost }

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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"
>
&#8203;
</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"
>
&#8203;
</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>
)
}

View File

@@ -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
}

View File

@@ -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>
</>
);
)
}

View File

@@ -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>
</>
);
)
}

View File

@@ -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>
</>
);
)
}

View File

@@ -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>
</>
);
)
}

View File

@@ -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 }

File diff suppressed because it is too large Load Diff

View File

@@ -7,5 +7,5 @@ Once in the example's directory, run the two following commands in parallel:
nhost -d
# Start this project
pnpm run dev
yarn run dev
```

View File

@@ -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

View File

@@ -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",

View File

@@ -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={

View File

@@ -25,8 +25,7 @@ export const PublicGate: React.FC = ({ children }) => {
}
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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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')

View File

@@ -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])

View File

@@ -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)}

View 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>
)
}

View File

@@ -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">
&#8592; 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">
&#8592; 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>
)
}

View File

@@ -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> */}

View File

@@ -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

View File

@@ -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:

View File

@@ -1,11 +1,11 @@
{
"name": "@nhost-examples/testing-project",
"private": true,
"version": "1.0.1",
"description": "Wrapper to run the Nhost CLI for development and testing",
"scripts": {
"start": "nhost -d"
},
"author": "Pierre-Louis Mercereau",
"license": "MIT"
}
"name": "@nhost-examples/testing-project",
"private": true,
"version": "1.0.1",
"description": "Wrapper to run the Nhost CLI for development and testing",
"scripts": {
"start": "nhost -d"
},
"author": "Pierre-Louis Mercereau",
"license": "MIT"
}

View File

@@ -32,7 +32,8 @@
"prerelease": "pnpm clean && pnpm install && pnpm build",
"release": "pnpm run prerelease && changeset publish && git push --follow-tags && git status && pnpm -r publish",
"changeset": "changeset",
"snapshot": "pnpm prerelease && changeset version --snapshot preview && pnpm install && changeset publish --tag preview"
"snapshot": "pnpm prerelease && changeset version --snapshot preview && pnpm install && changeset publish --tag preview",
"wait": "wait-on http://localhost:1337/v1/auth/healthz -i 500 -t 120000"
},
"workspaces": [
"packages/*",
@@ -43,14 +44,14 @@
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-syntax-flow": "^7.16.7",
"@babel/plugin-transform-react-jsx": "^7.17.3",
"@changesets/cli": "^2.21.0",
"@faker-js/faker": "^6.0.0-alpha.7",
"@changesets/cli": "^2.21.1",
"@faker-js/faker": "^6.0.0-beta.0",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"@vitejs/plugin-react": "^1.2.0",
"esbuild": "^0.14.23",
"esbuild": "^0.14.25",
"esbuild-node-externals": "^1.4.1",
"eslint": "^8.10.0",
"eslint-config-react-app": "^7.0.0",
@@ -59,7 +60,7 @@
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"husky": "^7.0.4",
@@ -68,11 +69,12 @@
"prettier": "^2.5.1",
"ts-jest": "^27.1.3",
"tsconfig-paths-jest": "^0.0.1",
"turbo": "^1.1.4",
"turbo": "1.1.6",
"typescript": "4.5.5",
"vite": "^2.8.5",
"vite": "^2.8.6",
"vite-plugin-dts": "^0.9.9",
"vite-tsconfig-paths": "^3.4.1"
"vite-tsconfig-paths": "^3.4.1",
"wait-on": "^6.0.1"
},
"resolutions": {
"graphql": "15.7.2"

View File

@@ -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

View File

@@ -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.

View File

@@ -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:^"
}
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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 })
}
})
}
}
}

View File

@@ -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
})
}
}

View File

@@ -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 }

View File

@@ -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

View File

@@ -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"
}

View 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)
}
}

View 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
})
}
}

View File

@@ -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'
}

View File

@@ -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'

View File

@@ -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: {

View File

@@ -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:

View File

@@ -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: {

View File

@@ -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:

View File

@@ -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()

View 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}`
}
}
)
}
}
)
}

View 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
}

View 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' }

View File

@@ -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 }
}
}
}

View File

@@ -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'
}

View File

@@ -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)
})
}
}

View File

@@ -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: {}

View 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
}
}
}
)
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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'

View 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

View File

@@ -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'

View File

@@ -1,5 +1,52 @@
# @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
- e688600: fix: current options when sign in with a provider
We currently only support setting the redirectTo option for providers.
This patch removes the options that do not work and adds the redirectTo option correctly to the provider sign-in URL.
- 8f7643a: Change target ES module build target to es2019
Some systems based on older versions of Webpack or Babel don't support the current esbuild configuration e.g, [this issue](https://github.com/nhost/nhost/issues/275).
- e688600: fix: Correct available options for provider sign-in.
- 50b9d76: feat: correct available providers (Discord & Twitch added)
## 0.1.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/hasura-auth-js",
"version": "0.1.14",
"version": "1.0.1",
"description": "Hasura-auth client",
"license": "MIT",
"keywords": [
@@ -32,16 +32,16 @@
"verify:fix": "run-p prettier:fix lint:fix"
},
"main": "src/index.ts",
"exports": {
".": {
"import": {
"node": "./dist/index.cjs.js",
"default": "./dist/index.es.js"
},
"require": "./dist/index.cjs.js"
}
},
"publishConfig": {
"exports": {
".": {
"import": {
"node": "./dist/index.cjs.js",
"default": "./dist/index.es.js"
},
"require": "./dist/index.cjs.js"
}
},
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
@@ -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"
}
}
}

View File

@@ -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

View File

@@ -1,3 +1,2 @@
export * from './hasura-auth-api'
export * from './hasura-auth-client'
export * from './utils/types'

View File

@@ -1 +0,0 @@
export const NHOST_REFRESH_TOKEN = 'nhostRefreshToken'

View File

@@ -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
}

Some files were not shown because too many files have changed in this diff Show More