Compare commits
34 Commits
@nhost/rea
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bebf9e1f2b | ||
|
|
2413c10283 | ||
|
|
0f7fbdab97 | ||
|
|
14e5fd63a6 | ||
|
|
2446913836 | ||
|
|
1f88a9f47a | ||
|
|
261e37cda4 | ||
|
|
5ee395ea8e | ||
|
|
828633ffc9 | ||
|
|
7b7527a5e6 | ||
|
|
620566fa4d | ||
|
|
4ce8b88d27 | ||
|
|
28d25e46de | ||
|
|
e0cfcafead | ||
|
|
12bc30daa3 | ||
|
|
7b5f00d10e | ||
|
|
58e1485c13 | ||
|
|
a64f1c4396 | ||
|
|
75a1428114 | ||
|
|
d82d830849 | ||
|
|
2def59fc6c | ||
|
|
64ceb2c6bf | ||
|
|
3ee007620c | ||
|
|
b9cf8172a0 | ||
|
|
32edfb4a9f | ||
|
|
848db9b672 | ||
|
|
3766921bcc | ||
|
|
5546052b2c | ||
|
|
c569b56d3d | ||
|
|
52ffa84adb | ||
|
|
b5ae438a8e | ||
|
|
fae05f7af2 | ||
|
|
380d7fc8ce | ||
|
|
94132bbc7f |
26
README.md
26
README.md
@@ -20,7 +20,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Nhost is a open-source GraphQL backend,** built with the following things in mind:
|
**Nhost is an open-source GraphQL backend,** built with the following things in mind:
|
||||||
|
|
||||||
- Open-Source
|
- Open-Source
|
||||||
- Developer Productivity
|
- Developer Productivity
|
||||||
@@ -234,6 +234,13 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<sub><b>Mustafa Hanif</b></sub>
|
<sub><b>Mustafa Hanif</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/timpratim">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/32492961?v=4" width="100;" alt="timpratim"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Pratim</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/Savinvadim1312">
|
<a href="https://github.com/Savinvadim1312">
|
||||||
<img src="https://avatars.githubusercontent.com/u/16936043?v=4" width="100;" alt="Savinvadim1312"/>
|
<img src="https://avatars.githubusercontent.com/u/16936043?v=4" width="100;" alt="Savinvadim1312"/>
|
||||||
@@ -247,15 +254,15 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Amir Ahmic</b></sub>
|
<sub><b>Amir Ahmic</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/akd-io">
|
<a href="https://github.com/akd-io">
|
||||||
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>Anders Kjær Damgaard</b></sub>
|
<sub><b>Anders Kjær Damgaard</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr>
|
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/rustyb">
|
<a href="https://github.com/rustyb">
|
||||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||||
@@ -290,21 +297,14 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Hoang Do</b></sub>
|
<sub><b>Hoang Do</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/ghoshnirmalya">
|
<a href="https://github.com/ghoshnirmalya">
|
||||||
<img src="https://avatars.githubusercontent.com/u/6391763?v=4" width="100;" alt="ghoshnirmalya"/>
|
<img src="https://avatars.githubusercontent.com/u/6391763?v=4" width="100;" alt="ghoshnirmalya"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>Nirmalya Ghosh</b></sub>
|
<sub><b>Nirmalya Ghosh</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/timpratim">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/32492961?v=4" width="100;" alt="timpratim"/>
|
|
||||||
<br />
|
|
||||||
<sub><b>Pratim</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/quentin-decre">
|
<a href="https://github.com/quentin-decre">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: 'Set permissions'
|
title: 'Set permissions'
|
||||||
---
|
---
|
||||||
|
|
||||||
In the previous section, you could fetch the todos because the **admin** role is enabled by default when using Hasura Console. When building your applications, you want to define permissions using **roles** that your users can assume when making requests.
|
While using the Hasura Console, you could fetch the todos because the **admin** role is enabled by default but when building your applications with a client, you want to define permissions using **roles** that your users can assume when making requests.
|
||||||
|
|
||||||
Hasura supports role-based access control. You create rules for each role, table, and operation (select, insert, update and delete) that can check dynamic session variables, like user ID.
|
Hasura supports role-based access control. You create rules for each role, table, and operation (select, insert, update and delete) that can check dynamic session variables, like user ID.
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ services:
|
|||||||
auth:
|
auth:
|
||||||
access_control:
|
access_control:
|
||||||
email:
|
email:
|
||||||
allowed_email_domains: ""
|
allowed_email_domains: ''
|
||||||
allowed_emails: ""
|
allowed_emails: ''
|
||||||
blocked_email_domains: ""
|
blocked_email_domains: ''
|
||||||
blocked_emails: ""
|
blocked_emails: ''
|
||||||
url:
|
allowed_redirect_urls: ''
|
||||||
allowed_redirect_urls: ""
|
|
||||||
anonymous_users_enabled: false
|
anonymous_users_enabled: false
|
||||||
client_url: http://localhost:3000
|
client_url: http://localhost:3000
|
||||||
disable_new_users: false
|
disable_new_users: false
|
||||||
@@ -28,11 +27,11 @@ auth:
|
|||||||
passwordless:
|
passwordless:
|
||||||
enabled: false
|
enabled: false
|
||||||
signin_email_verified_required: true
|
signin_email_verified_required: true
|
||||||
template_fetch_url: ""
|
template_fetch_url: ''
|
||||||
gravatar:
|
gravatar:
|
||||||
default: ""
|
default: ''
|
||||||
enabled: true
|
enabled: true
|
||||||
rating: ""
|
rating: ''
|
||||||
locale:
|
locale:
|
||||||
allowed: en
|
allowed: en
|
||||||
default: en
|
default: en
|
||||||
@@ -41,65 +40,65 @@ auth:
|
|||||||
min_length: 3
|
min_length: 3
|
||||||
provider:
|
provider:
|
||||||
apple:
|
apple:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
key_id: ""
|
key_id: ''
|
||||||
private_key: ""
|
private_key: ''
|
||||||
scope: name,email
|
scope: name,email
|
||||||
team_id: ""
|
team_id: ''
|
||||||
bitbucket:
|
bitbucket:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
facebook:
|
facebook:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: email,photos,displayName
|
scope: email,photos,displayName
|
||||||
github:
|
github:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: user:email
|
scope: user:email
|
||||||
token_url: ""
|
token_url: ''
|
||||||
user_profile_url: ""
|
user_profile_url: ''
|
||||||
gitlab:
|
gitlab:
|
||||||
base_url: ""
|
base_url: ''
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: read_user
|
scope: read_user
|
||||||
google:
|
google:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: email,profile
|
scope: email,profile
|
||||||
linkedin:
|
linkedin:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: r_emailaddress,r_liteprofile
|
scope: r_emailaddress,r_liteprofile
|
||||||
spotify:
|
spotify:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: user-read-email,user-read-private
|
scope: user-read-email,user-read-private
|
||||||
strava:
|
strava:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
twilio:
|
twilio:
|
||||||
account_sid: ""
|
account_sid: ''
|
||||||
auth_token: ""
|
auth_token: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
messaging_service_id: ""
|
messaging_service_id: ''
|
||||||
twitter:
|
twitter:
|
||||||
consumer_key: ""
|
consumer_key: ''
|
||||||
consumer_secret: ""
|
consumer_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
windows_live:
|
windows_live:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: wl.basic,wl.emails,wl.contacts_emails
|
scope: wl.basic,wl.emails,wl.contacts_emails
|
||||||
sms:
|
sms:
|
||||||
@@ -108,13 +107,13 @@ auth:
|
|||||||
enabled: false
|
enabled: false
|
||||||
provider:
|
provider:
|
||||||
twilio:
|
twilio:
|
||||||
account_sid: ""
|
account_sid: ''
|
||||||
auth_token: ""
|
auth_token: ''
|
||||||
from: ""
|
from: ''
|
||||||
messaging_service_id: ""
|
messaging_service_id: ''
|
||||||
smtp:
|
smtp:
|
||||||
host: nhost_mailhog
|
host: nhost_mailhog
|
||||||
method: ""
|
method: ''
|
||||||
pass: password
|
pass: password
|
||||||
port: 1765
|
port: 1765
|
||||||
secure: false
|
secure: false
|
||||||
|
|||||||
@@ -2,14 +2,32 @@
|
|||||||
|
|
||||||
This demo is a work in progress, further improvements are to come
|
This demo is a work in progress, further improvements are to come
|
||||||
|
|
||||||
### Installation
|
## Get started
|
||||||
|
|
||||||
First, clone this repo. Then run the commands:
|
1. Clone the repository
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/nhost/nhost
|
||||||
|
cd nhost
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd examples/nextjs
|
cd examples/nextjs
|
||||||
yarn
|
pnpm install
|
||||||
yarn dev
|
```
|
||||||
|
|
||||||
|
3. Terminal 1: Start Nhost
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nhost dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Terminal 2: Start React App
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to use this demo with your own cloud instance:
|
If you want to use this demo with your own cloud instance:
|
||||||
@@ -18,8 +36,3 @@ If you want to use this demo with your own cloud instance:
|
|||||||
- don't forget to change the client URL in the Nhost console so email verification will work: `Users -> Login Settings -> Client login URLs`: `http://localhost:4000`
|
- don't forget to change the client URL in the Nhost console so email verification will work: `Users -> Login Settings -> Client login URLs`: `http://localhost:4000`
|
||||||
|
|
||||||
If you want to use a local Nhost instance, start the CLI in parallel to Nextjs:
|
If you want to use a local Nhost instance, start the CLI in parallel to Nextjs:
|
||||||
|
|
||||||
```sh
|
|
||||||
# Inside examples/nextjs
|
|
||||||
nhost -d
|
|
||||||
```
|
|
||||||
|
|||||||
13
examples/nextjs/next.config.js
Normal file
13
examples/nextjs/next.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {}
|
||||||
|
|
||||||
|
const pkg = require('./package.json')
|
||||||
|
// * Only required to make it work with the monorepo. Is not required otherwise
|
||||||
|
const withTM = require('next-transpile-modules')(
|
||||||
|
// * All references to workspace packages are transpiled
|
||||||
|
Object.entries(pkg.dependencies)
|
||||||
|
.filter(([name, version]) => version.startsWith('workspace'))
|
||||||
|
.map(([name]) => name)
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = withTM(nextConfig)
|
||||||
@@ -15,7 +15,6 @@ auth:
|
|||||||
allowed_emails: ''
|
allowed_emails: ''
|
||||||
blocked_email_domains: ''
|
blocked_email_domains: ''
|
||||||
blocked_emails: ''
|
blocked_emails: ''
|
||||||
url:
|
|
||||||
allowed_redirect_urls: ''
|
allowed_redirect_urls: ''
|
||||||
anonymous_users_enabled: false
|
anonymous_users_enabled: false
|
||||||
client_url: http://localhost:3000
|
client_url: http://localhost:3000
|
||||||
|
|||||||
@@ -11,4 +11,4 @@
|
|||||||
max_connections: 50
|
max_connections: 50
|
||||||
retries: 20
|
retries: 20
|
||||||
use_prepared_statements: true
|
use_prepared_statements: true
|
||||||
tables: "!include default/tables/tables.yaml"
|
tables: '!include default/tables/tables.yaml'
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
- "!include auth_provider_requests.yaml"
|
- '!include auth_provider_requests.yaml'
|
||||||
- "!include auth_providers.yaml"
|
- '!include auth_providers.yaml'
|
||||||
- "!include auth_refresh_tokens.yaml"
|
- '!include auth_refresh_tokens.yaml'
|
||||||
- "!include auth_roles.yaml"
|
- '!include auth_roles.yaml'
|
||||||
- "!include auth_user_providers.yaml"
|
- '!include auth_user_providers.yaml'
|
||||||
- "!include auth_user_roles.yaml"
|
- '!include auth_user_roles.yaml'
|
||||||
- "!include auth_users.yaml"
|
- '!include auth_users.yaml'
|
||||||
- "!include public_books.yaml"
|
- '!include public_books.yaml'
|
||||||
- "!include storage_buckets.yaml"
|
- '!include storage_buckets.yaml'
|
||||||
- "!include storage_files.yaml"
|
- '!include storage_files.yaml'
|
||||||
|
|||||||
@@ -6,13 +6,23 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"prettier": "prettier --check .",
|
||||||
|
"prettier:fix": "prettier --write .",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||||
|
"verify": "run-p prettier lint",
|
||||||
|
"verify:fix": "run-p prettier:fix lint:fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.5.10",
|
"@apollo/client": "^3.5.10",
|
||||||
"@nhost/nextjs": "^1.0.10",
|
"@nhost/apollo": "workspace:*",
|
||||||
"@nhost/react": "^0.5.0",
|
"@nhost/core": "workspace:*",
|
||||||
"@nhost/react-apollo": "^4.0.10",
|
"@nhost/nextjs": "workspace:*",
|
||||||
|
"@nhost/react": "workspace:*",
|
||||||
|
"@nhost/react-apollo": "workspace:*",
|
||||||
|
"@nhost/nhost-js": "workspace:*",
|
||||||
|
"@nhost/hasura-auth-js": "workspace:*",
|
||||||
|
"@nhost/hasura-storage-js": "workspace:*",
|
||||||
"graphql": "^16.3.0",
|
"graphql": "^16.3.0",
|
||||||
"next": "12.1.0",
|
"next": "12.1.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
@@ -22,9 +32,10 @@
|
|||||||
"@types/node": "17.0.23",
|
"@types/node": "17.0.23",
|
||||||
"@types/react": "17.0.43",
|
"@types/react": "17.0.43",
|
||||||
"@xstate/inspect": "^0.6.2",
|
"@xstate/inspect": "^0.6.2",
|
||||||
"eslint": "8.8.0",
|
|
||||||
"eslint-config-next": "12.0.10",
|
"eslint-config-next": "12.0.10",
|
||||||
|
"next-transpile-modules": "^9.0.0",
|
||||||
"typescript": "4.5.5",
|
"typescript": "4.5.5",
|
||||||
"ws": "^8.5.0"
|
"ws": "^8.5.0",
|
||||||
|
"xstate": "^4.30.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_DEBUG) {
|
|||||||
const nhost = new NhostClient({ backendUrl: BACKEND_URL })
|
const nhost = new NhostClient({ backendUrl: BACKEND_URL })
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
|
// * Monorepo-related. See: https://stackoverflow.com/questions/71843247/react-nextjs-type-error-component-cannot-be-used-as-a-jsx-component
|
||||||
|
const AnyComponent = Component as any
|
||||||
return (
|
return (
|
||||||
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
|
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||||
<NhostApolloProvider nhost={nhost}>
|
<NhostApolloProvider nhost={nhost}>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Header />
|
<Header />
|
||||||
<Component {...pageProps} />
|
<AnyComponent {...pageProps} />
|
||||||
</div>
|
</div>
|
||||||
</NhostApolloProvider>
|
</NhostApolloProvider>
|
||||||
</NhostNextProvider>
|
</NhostNextProvider>
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
@@ -19,11 +15,6 @@
|
|||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
REACT_APP_BACKEND_URL=http://localhost:1337
|
VITE_NHOST_URL=http://localhost:1337
|
||||||
@@ -72,25 +72,33 @@ This example app has some work in progress:
|
|||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
1. Install dependencies
|
1. Clone the repository
|
||||||
|
|
||||||
```
|
```sh
|
||||||
npm install
|
git clone https://github.com/nhost/nhost
|
||||||
|
cd nhost
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Terminal 1: Start Nhost
|
2. Install dependencies
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd examples/react-apollo-crm
|
||||||
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. Terminal 1: Start Nhost
|
||||||
|
|
||||||
|
```sh
|
||||||
nhost dev
|
nhost dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Terminal 2: Start React App
|
4. Terminal 2: Start React App
|
||||||
|
|
||||||
```
|
```sh
|
||||||
npm run start
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Terminal 3: Start GraphQL Codegens
|
5. Terminal 3: Start GraphQL Codegens
|
||||||
|
|
||||||
> Make sure that the Nhost backend in step 2 has started and is available before you run this command
|
> Make sure that the Nhost backend in step 2 has started and is available before you run this command
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
client: {
|
client: {
|
||||||
service: {
|
service: {
|
||||||
name: "backend",
|
name: 'backend',
|
||||||
url: "http://localhost:1337/v1/graphql",
|
url: 'http://localhost:1337/v1/graphql',
|
||||||
headers: {
|
headers: {
|
||||||
"x-hasura-admin-secret": "nhost-admin-secret",
|
'x-hasura-admin-secret': 'nhost-admin-secret'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
includes: ['src/**/*.graphql', 'src/**/*.gql']
|
||||||
includes: ["src/**/*.graphql", "src/**/*.gql"],
|
}
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ schema: http://localhost:1337/v1/graphql
|
|||||||
headers:
|
headers:
|
||||||
x-hasura-admin-secret: nhost-admin-secret
|
x-hasura-admin-secret: nhost-admin-secret
|
||||||
documents:
|
documents:
|
||||||
- "src/**/*.graphql"
|
- 'src/**/*.graphql'
|
||||||
- "src/**/*.gql"
|
- 'src/**/*.gql'
|
||||||
generates:
|
generates:
|
||||||
src/utils/__generated__/graphql.ts:
|
src/utils/__generated__/graphql.ts:
|
||||||
plugins:
|
plugins:
|
||||||
- "typescript"
|
- 'typescript'
|
||||||
- "typescript-operations"
|
- 'typescript-operations'
|
||||||
- "typescript-react-apollo"
|
- 'typescript-react-apollo'
|
||||||
config:
|
config:
|
||||||
withRefetchFn: true
|
withRefetchFn: true
|
||||||
|
|||||||
18
examples/react-apollo-crm/index.html
Normal file
18
examples/react-apollo-crm/index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="Web site created using create-react-app" />
|
||||||
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -6,12 +6,11 @@ services:
|
|||||||
auth:
|
auth:
|
||||||
access_control:
|
access_control:
|
||||||
email:
|
email:
|
||||||
allowed_email_domains: ""
|
allowed_email_domains: ''
|
||||||
allowed_emails: ""
|
allowed_emails: ''
|
||||||
blocked_email_domains: ""
|
blocked_email_domains: ''
|
||||||
blocked_emails: ""
|
blocked_emails: ''
|
||||||
url:
|
allowed_redirect_urls: ''
|
||||||
allowed_redirect_urls: ""
|
|
||||||
anonymous_users_enabled: false
|
anonymous_users_enabled: false
|
||||||
client_url: http://localhost:3000
|
client_url: http://localhost:3000
|
||||||
disable_new_users: false
|
disable_new_users: false
|
||||||
@@ -19,12 +18,12 @@ auth:
|
|||||||
enabled: false
|
enabled: false
|
||||||
passwordless:
|
passwordless:
|
||||||
enabled: false
|
enabled: false
|
||||||
template_fetch_url: ""
|
template_fetch_url: ''
|
||||||
signin_email_verified_required: false
|
signin_email_verified_required: false
|
||||||
gravatar:
|
gravatar:
|
||||||
default: ""
|
default: ''
|
||||||
enabled: true
|
enabled: true
|
||||||
rating: ""
|
rating: ''
|
||||||
locale:
|
locale:
|
||||||
allowed: en
|
allowed: en
|
||||||
default: en
|
default: en
|
||||||
@@ -33,65 +32,65 @@ auth:
|
|||||||
min_length: 3
|
min_length: 3
|
||||||
provider:
|
provider:
|
||||||
apple:
|
apple:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
key_id: ""
|
key_id: ''
|
||||||
private_key: ""
|
private_key: ''
|
||||||
scope: name,email
|
scope: name,email
|
||||||
team_id: ""
|
team_id: ''
|
||||||
bitbucket:
|
bitbucket:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
facebook:
|
facebook:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: email,photos,displayName
|
scope: email,photos,displayName
|
||||||
github:
|
github:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: user:email
|
scope: user:email
|
||||||
token_url: ""
|
token_url: ''
|
||||||
user_profile_url: ""
|
user_profile_url: ''
|
||||||
gitlab:
|
gitlab:
|
||||||
base_url: ""
|
base_url: ''
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: read_user
|
scope: read_user
|
||||||
google:
|
google:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: email,profile
|
scope: email,profile
|
||||||
linkedin:
|
linkedin:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: r_emailaddress,r_liteprofile
|
scope: r_emailaddress,r_liteprofile
|
||||||
spotify:
|
spotify:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: user-read-email,user-read-private
|
scope: user-read-email,user-read-private
|
||||||
strava:
|
strava:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
twilio:
|
twilio:
|
||||||
account_sid: ""
|
account_sid: ''
|
||||||
auth_token: ""
|
auth_token: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
messaging_service_id: ""
|
messaging_service_id: ''
|
||||||
twitter:
|
twitter:
|
||||||
consumer_key: ""
|
consumer_key: ''
|
||||||
consumer_secret: ""
|
consumer_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
windows_live:
|
windows_live:
|
||||||
client_id: ""
|
client_id: ''
|
||||||
client_secret: ""
|
client_secret: ''
|
||||||
enabled: false
|
enabled: false
|
||||||
scope: wl.basic,wl.emails,wl.contacts_emails
|
scope: wl.basic,wl.emails,wl.contacts_emails
|
||||||
sms:
|
sms:
|
||||||
@@ -100,13 +99,13 @@ auth:
|
|||||||
enabled: false
|
enabled: false
|
||||||
provider:
|
provider:
|
||||||
twilio:
|
twilio:
|
||||||
account_sid: ""
|
account_sid: ''
|
||||||
auth_token: ""
|
auth_token: ''
|
||||||
from: ""
|
from: ''
|
||||||
messaging_service_id: ""
|
messaging_service_id: ''
|
||||||
smtp:
|
smtp:
|
||||||
host: nhost_mailhog
|
host: nhost_mailhog
|
||||||
method: ""
|
method: ''
|
||||||
pass: password
|
pass: password
|
||||||
port: 1586
|
port: 1586
|
||||||
secure: false
|
secure: false
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
<h2>Verify Email</h2>
|
<h2>Verify Email</h2>
|
||||||
<p>Use this link to verify your email:</p>
|
<p>Use this link to verify your email:</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a href="${serverUrl}/verify?&ticket=${ticket}&type=emailVerify&redirectTo=${redirectTo}">
|
||||||
href="${serverUrl}/verify?&ticket=${ticket}&type=emailVerify&redirectTo=${redirectTo}"
|
|
||||||
>
|
|
||||||
Verify Email
|
Verify Email
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
<h2>Reset Password</h2>
|
<h2>Reset Password</h2>
|
||||||
<p>Use this link to reset your password:</p>
|
<p>Use this link to reset your password:</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a href="${serverUrl}/verify?&ticket=${ticket}&type=passwordReset&redirectTo=${redirectTo}">
|
||||||
href="${serverUrl}/verify?&ticket=${ticket}&type=passwordReset&redirectTo=${redirectTo}"
|
|
||||||
>
|
|
||||||
Reset password
|
Reset password
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
type Mutation {
|
type Mutation {
|
||||||
actionName(
|
actionName(arg1: SampleInput!): SampleOutput
|
||||||
arg1: SampleInput!
|
|
||||||
): SampleOutput
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input SampleInput {
|
input SampleInput {
|
||||||
@@ -12,4 +10,3 @@ input SampleInput {
|
|||||||
type SampleOutput {
|
type SampleOutput {
|
||||||
accessToken: String!
|
accessToken: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,4 @@
|
|||||||
max_connections: 50
|
max_connections: 50
|
||||||
retries: 20
|
retries: 20
|
||||||
use_prepared_statements: true
|
use_prepared_statements: true
|
||||||
tables: "!include default/tables/tables.yaml"
|
tables: '!include default/tables/tables.yaml'
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ event_triggers:
|
|||||||
- definition:
|
- definition:
|
||||||
enable_manual: false
|
enable_manual: false
|
||||||
insert:
|
insert:
|
||||||
columns: "*"
|
columns: '*'
|
||||||
headers:
|
headers:
|
||||||
- name: nhost-webhook-secret
|
- name: nhost-webhook-secret
|
||||||
value_from_env: NHOST_WEBHOOK_SECRET
|
value_from_env: NHOST_WEBHOOK_SECRET
|
||||||
@@ -113,4 +113,4 @@ event_triggers:
|
|||||||
interval_sec: 10
|
interval_sec: 10
|
||||||
num_retries: 0
|
num_retries: 0
|
||||||
timeout_sec: 60
|
timeout_sec: 60
|
||||||
webhook: "{{NHOST_BACKEND_URL}}/v1/functions/users/insert/create-company-connection"
|
webhook: '{{NHOST_BACKEND_URL}}/v1/functions/users/insert/create-company-connection'
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
- "!include auth_provider_requests.yaml"
|
- '!include auth_provider_requests.yaml'
|
||||||
- "!include auth_providers.yaml"
|
- '!include auth_providers.yaml'
|
||||||
- "!include auth_refresh_tokens.yaml"
|
- '!include auth_refresh_tokens.yaml'
|
||||||
- "!include auth_roles.yaml"
|
- '!include auth_roles.yaml'
|
||||||
- "!include auth_user_providers.yaml"
|
- '!include auth_user_providers.yaml'
|
||||||
- "!include auth_user_roles.yaml"
|
- '!include auth_user_roles.yaml'
|
||||||
- "!include auth_users.yaml"
|
- '!include auth_users.yaml'
|
||||||
- "!include public_companies.yaml"
|
- '!include public_companies.yaml'
|
||||||
- "!include public_company_users.yaml"
|
- '!include public_company_users.yaml'
|
||||||
- "!include public_customer_comments.yaml"
|
- '!include public_customer_comments.yaml'
|
||||||
- "!include public_customers.yaml"
|
- '!include public_customers.yaml'
|
||||||
- "!include storage_buckets.yaml"
|
- '!include storage_buckets.yaml'
|
||||||
- "!include storage_files.yaml"
|
- '!include storage_files.yaml'
|
||||||
|
|||||||
@@ -3,38 +3,34 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.4.16",
|
"@apollo/client": "^3.5.10",
|
||||||
"@headlessui/react": "^1.4.2",
|
"@headlessui/react": "^1.5.0",
|
||||||
"@heroicons/react": "^1.0.5",
|
"@heroicons/react": "^1.0.6",
|
||||||
"@nhost/nhost-js": "^1.0.0",
|
"@nhost/nhost-js": "workspace:*",
|
||||||
"@nhost/react": "^0.3.0",
|
"@nhost/react": "workspace:*",
|
||||||
"@nhost/react-apollo": "^4.0.0",
|
"@nhost/react-apollo": "workspace:*",
|
||||||
"@tailwindcss/forms": "^0.3.4",
|
"@tailwindcss/forms": "^0.5.0",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
|
||||||
"@testing-library/react": "^11.1.0",
|
|
||||||
"@testing-library/user-event": "^12.1.10",
|
|
||||||
"@types/jest": "^26.0.15",
|
|
||||||
"@types/node": "^12.0.0",
|
|
||||||
"@types/react": "^17.0.0",
|
|
||||||
"@types/react-dom": "^17.0.0",
|
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"graphql": "^15.8.0",
|
"graphql": "15.7.2",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^6.0.2",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-scripts": "^5.0.0",
|
"tailwindcss": "^3.0.24"
|
||||||
"typescript": "^4.1.2",
|
|
||||||
"web-vitals": "^1.0.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"dev": "vite",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
"test": "react-scripts test",
|
"preview": "vite preview",
|
||||||
"eject": "react-scripts eject",
|
"codegen": "graphql-codegen --config codegen.yaml --errors-only",
|
||||||
"codegen": "graphql-codegen --config codegen.yaml --errors-only"
|
"prettier": "prettier --check .",
|
||||||
|
"prettier:fix": "prettier --write .",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||||
|
"verify": "run-p prettier lint",
|
||||||
|
"verify:fix": "run-p prettier:fix lint:fix"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -55,15 +51,21 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^2.2.1",
|
"@graphql-codegen/cli": "^2.6.2",
|
||||||
"@graphql-codegen/introspection": "^2.1.0",
|
"@graphql-codegen/introspection": "^2.1.1",
|
||||||
"@graphql-codegen/typescript": "^2.2.4",
|
"@graphql-codegen/typescript": "^2.4.8",
|
||||||
"@graphql-codegen/typescript-operations": "^2.1.8",
|
"@graphql-codegen/typescript-operations": "^2.3.5",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^3.1.6",
|
"@graphql-codegen/typescript-react-apollo": "^3.2.11",
|
||||||
|
"@types/jest": "^26.0.24",
|
||||||
|
"@types/node": "^12.20.48",
|
||||||
|
"@types/react": "^17.0.44",
|
||||||
|
"@types/react-dom": "^17.0.15",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"autoprefixer": "^9.8.8",
|
"@vitejs/plugin-react": "^1.3.1",
|
||||||
"express": "^4.17.1",
|
"autoprefixer": "^10.4.4",
|
||||||
"postcss": "^7.0.39",
|
"express": "^4.17.3",
|
||||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17"
|
"postcss": "^8.4.12",
|
||||||
|
"typescript": "^4.6.3",
|
||||||
|
"vite": "^2.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
examples/react-apollo-crm/postcss.config.js
Normal file
6
examples/react-apollo-crm/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import App from './App'
|
|
||||||
|
|
||||||
test('single noop test', () => {
|
|
||||||
expect(true).toBeTruthy()
|
|
||||||
})
|
|
||||||
@@ -2,7 +2,7 @@ import './App.css'
|
|||||||
import { NhostReactProvider } from '@nhost/react'
|
import { NhostReactProvider } from '@nhost/react'
|
||||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||||
import { nhost } from './utils/nhost'
|
import { nhost } from './utils/nhost'
|
||||||
import { Route, Routes } from 'react-router'
|
import { Route, Routes } from 'react-router-dom'
|
||||||
import { Layout } from './components/ui/Layout'
|
import { Layout } from './components/ui/Layout'
|
||||||
import { Customers } from './components/Customers'
|
import { Customers } from './components/Customers'
|
||||||
import { Dashboard } from './components/Dashboard'
|
import { Dashboard } from './components/Dashboard'
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export function ChangePasswordModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={open} as={Fragment}>
|
<Transition.Root show={open} as={Fragment}>
|
||||||
<Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" onClose={setOpen}>
|
<Dialog as="div" className="fixed inset-0 z-10 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">
|
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@@ -31,14 +31,11 @@ export function ChangePasswordModal() {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<Dialog.Overlay className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
{/* This element is to trick the browser into centering the modal contents. */}
|
{/* This element is to trick the browser into centering the modal contents. */}
|
||||||
<span
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
​
|
​
|
||||||
</span>
|
</span>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
@@ -50,14 +47,11 @@ export function ChangePasswordModal() {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
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">
|
<div className="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||||
as="h3"
|
|
||||||
className="text-lg leading-6 font-medium text-gray-900"
|
|
||||||
>
|
|
||||||
Change Password
|
Change Password
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -78,7 +72,7 @@ export function ChangePasswordModal() {
|
|||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
className="inline-flex justify-center w-full px-4 py-2 text-base font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm"
|
||||||
>
|
>
|
||||||
Set new password
|
Set new password
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
import { Main } from "./ui/Main";
|
import { Main } from './ui/Main'
|
||||||
import { Breadcrumbs } from "./ui/Breadcrumbs";
|
import { Breadcrumbs } from './ui/Breadcrumbs'
|
||||||
import { HeaderSection } from "./ui/HeaderSection";
|
import { HeaderSection } from './ui/HeaderSection'
|
||||||
import { PageHeader } from "./ui/PageHeader";
|
import { PageHeader } from './ui/PageHeader'
|
||||||
|
|
||||||
import { useParams } from "react-router";
|
import { useParams } from 'react-router-dom'
|
||||||
import { useCustomerQuery } from "../utils/__generated__/graphql";
|
import { useCustomerQuery } from '../utils/__generated__/graphql'
|
||||||
import { NavLink, Outlet } from "react-router-dom";
|
import { NavLink, Outlet } from 'react-router-dom'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import { CustomerActivities } from "./CustomerActivities";
|
import { CustomerActivities } from './CustomerActivities'
|
||||||
import { CustomerAddComment } from "./CustomerAddComment";
|
import { CustomerAddComment } from './CustomerAddComment'
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "Overview", href: "" },
|
{ name: 'Overview', href: '' },
|
||||||
{ name: "Orders", href: "orders" },
|
{ name: 'Orders', href: 'orders' },
|
||||||
{ name: "Files", href: "files" },
|
{ name: 'Files', href: 'files' }
|
||||||
];
|
]
|
||||||
|
|
||||||
export function Customer() {
|
export function Customer() {
|
||||||
const { customerId } = useParams();
|
const { customerId } = useParams()
|
||||||
|
|
||||||
const { data, loading } = useCustomerQuery({
|
const { data, loading } = useCustomerQuery({
|
||||||
variables: {
|
variables: {
|
||||||
customerId,
|
customerId
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div>Loading..</div>;
|
return <div>Loading..</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !data.customer) {
|
if (!data || !data.customer) {
|
||||||
return <div>No customer..</div>;
|
return <div>No customer..</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { customer } = data;
|
const { customer } = data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Main>
|
<Main>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
backLink={""}
|
backLink={''}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ link: "/customers", text: "Customers" },
|
{ link: '/customers', text: 'Customers' },
|
||||||
{ link: `customers/${customerId}`, text: customer.name },
|
{ link: `customers/${customerId}`, text: customer.name }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<HeaderSection>
|
<HeaderSection>
|
||||||
@@ -57,7 +57,7 @@ export function Customer() {
|
|||||||
id="current-tab"
|
id="current-tab"
|
||||||
name="current-tab"
|
name="current-tab"
|
||||||
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||||
defaultValue={"ok"}
|
defaultValue={'ok'}
|
||||||
>
|
>
|
||||||
<option>1</option>
|
<option>1</option>
|
||||||
<option>2</option>
|
<option>2</option>
|
||||||
@@ -71,10 +71,10 @@ export function Customer() {
|
|||||||
className={({ isActive }) => {
|
className={({ isActive }) => {
|
||||||
return classNames(
|
return classNames(
|
||||||
isActive
|
isActive
|
||||||
? "border-blue-500 text-blue-600"
|
? 'border-blue-500 text-blue-600'
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
|
||||||
"whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm"
|
'whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm'
|
||||||
);
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.name}
|
{tab.name}
|
||||||
@@ -95,5 +95,5 @@ export function Customer() {
|
|||||||
<CustomerAddComment />
|
<CustomerAddComment />
|
||||||
</div>
|
</div>
|
||||||
</Main>
|
</Main>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
import { ChatAltIcon } from "@heroicons/react/solid";
|
import { ChatAltIcon } from '@heroicons/react/solid'
|
||||||
import { useGetCustomerCommentsSubscription } from "../utils/__generated__/graphql";
|
import { useGetCustomerCommentsSubscription } from '../utils/__generated__/graphql'
|
||||||
import { useParams } from "react-router";
|
import { useParams } from 'react-router-dom'
|
||||||
import { nhost } from "../utils/nhost";
|
import { nhost } from '../utils/nhost'
|
||||||
import { PhotographIcon } from "@heroicons/react/outline";
|
import { PhotographIcon } from '@heroicons/react/outline'
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { formatDistanceToNow, parseISO } from "date-fns";
|
import { formatDistanceToNow, parseISO } from 'date-fns'
|
||||||
|
|
||||||
export function CustomerActivities() {
|
export function CustomerActivities() {
|
||||||
const { customerId } = useParams();
|
const { customerId } = useParams<{ customerId: string }>()
|
||||||
|
|
||||||
const { data, loading } = useGetCustomerCommentsSubscription({
|
const { data, loading } = useGetCustomerCommentsSubscription({
|
||||||
variables: {
|
variables: {
|
||||||
where: {
|
where: {
|
||||||
customerId: {
|
customerId: {
|
||||||
_eq: customerId,
|
_eq: customerId
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
console.log({ data });
|
console.log({ data })
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !data.customerComments) {
|
if (!data || !data.customerComments) {
|
||||||
return <div>no comments</div>;
|
return <div>no comments</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { customerComments } = data;
|
const { customerComments } = data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-root">
|
<div className="flow-root">
|
||||||
@@ -54,10 +54,7 @@ export function CustomerActivities() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="absolute -bottom-0.5 -right-1 bg-white rounded-tl px-0.5 py-px">
|
<span className="absolute -bottom-0.5 -right-1 bg-white rounded-tl px-0.5 py-px">
|
||||||
<ChatAltIcon
|
<ChatAltIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||||
className="w-5 h-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -69,7 +66,7 @@ export function CustomerActivities() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-sm text-gray-500">
|
<p className="mt-0.5 text-sm text-gray-500">
|
||||||
{formatDistanceToNow(parseISO(comment.createdAt), {
|
{formatDistanceToNow(parseISO(comment.createdAt), {
|
||||||
addSuffix: true,
|
addSuffix: true
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,24 +77,22 @@ export function CustomerActivities() {
|
|||||||
<div
|
<div
|
||||||
className="flex items-center mt-3 text-sm text-gray-700 cursor-pointer"
|
className="flex items-center mt-3 text-sm text-gray-700 cursor-pointer"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const { presignedUrl, error } =
|
const { presignedUrl, error } = await nhost.storage.getPresignedUrl({
|
||||||
await nhost.storage.getPresignedUrl({
|
fileId: comment.file!.id
|
||||||
fileId: comment.file!.id,
|
})
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return alert(error.message);
|
return alert(error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(presignedUrl?.url, "_blank");
|
window.open(presignedUrl?.url, '_blank')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<PhotographIcon className="w-5 mr-1 text-gray-500" />
|
<PhotographIcon className="w-5 mr-1 text-gray-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{comment.file.name},{" "}
|
{comment.file.name}, {prettyBytes(comment.file.size as number)}
|
||||||
{prettyBytes(comment.file.size as number)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -106,9 +101,9 @@ export function CustomerActivities() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
import { useState } from "react";
|
import { useState } from 'react'
|
||||||
import { useParams } from "react-router";
|
import { useParams } from 'react-router-dom'
|
||||||
import { nhost } from "../utils/nhost";
|
import { nhost } from '../utils/nhost'
|
||||||
import { useInsertCustomerCommentMutation } from "../utils/__generated__/graphql";
|
import { useInsertCustomerCommentMutation } from '../utils/__generated__/graphql'
|
||||||
|
|
||||||
export function CustomerAddComment() {
|
export function CustomerAddComment() {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState('')
|
||||||
const [file, setFile] = useState<null | File>(null);
|
const [file, setFile] = useState<null | File>(null)
|
||||||
|
|
||||||
const { customerId } = useParams();
|
const { customerId } = useParams<{ customerId: string }>()
|
||||||
const [insertCustomerComment, { loading }] =
|
const [insertCustomerComment, { loading }] = useInsertCustomerCommentMutation()
|
||||||
useInsertCustomerCommentMutation();
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
let fileMetadata;
|
let fileMetadata
|
||||||
if (file) {
|
if (file) {
|
||||||
const fileUploadRes = await nhost.storage.upload({
|
const fileUploadRes = await nhost.storage.upload({
|
||||||
file,
|
file,
|
||||||
bucketId: "customerComments",
|
bucketId: 'customerComments'
|
||||||
});
|
})
|
||||||
|
|
||||||
if (fileUploadRes.error) {
|
if (fileUploadRes.error) {
|
||||||
alert(`error: ${fileUploadRes.error}`);
|
alert(`error: ${fileUploadRes.error}`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileMetadata = fileUploadRes.fileMetadata;
|
fileMetadata = fileUploadRes.fileMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
await insertCustomerComment({
|
await insertCustomerComment({
|
||||||
@@ -34,21 +33,18 @@ export function CustomerAddComment() {
|
|||||||
customerComment: {
|
customerComment: {
|
||||||
text,
|
text,
|
||||||
customerId,
|
customerId,
|
||||||
fileId: fileMetadata ? fileMetadata.id : null,
|
fileId: fileMetadata ? fileMetadata.id : null
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
setText("");
|
setText('')
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-lg mx-auto">
|
<div className="max-w-lg mx-auto">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<label
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||||
htmlFor="email"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Comment
|
Comment
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -57,7 +53,7 @@ export function CustomerAddComment() {
|
|||||||
name="about"
|
name="about"
|
||||||
rows={3}
|
rows={3}
|
||||||
className="block w-full max-w-lg border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
className="block w-full max-w-lg border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
defaultValue={""}
|
defaultValue={''}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -94,7 +90,7 @@ export function CustomerAddComment() {
|
|||||||
className="sr-only"
|
className="sr-only"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
setFile(e.target.files[0]);
|
setFile(e.target.files[0])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -104,9 +100,7 @@ export function CustomerAddComment() {
|
|||||||
{file ? (
|
{file ? (
|
||||||
<div>{file.name}</div>
|
<div>{file.name}</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">PNG, JPG, GIF up to 10MB</p>
|
||||||
PNG, JPG, GIF up to 10MB
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,5 +118,5 @@ export function CustomerAddComment() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import { Main } from "./ui/Main";
|
import { Main } from './ui/Main'
|
||||||
import { Breadcrumbs } from "./ui/Breadcrumbs";
|
import { Breadcrumbs } from './ui/Breadcrumbs'
|
||||||
import { HeaderSection } from "./ui/HeaderSection";
|
import { HeaderSection } from './ui/HeaderSection'
|
||||||
import { PageHeader } from "./ui/PageHeader";
|
import { PageHeader } from './ui/PageHeader'
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom'
|
||||||
import { useGetCustomersSubscription } from "../utils/__generated__/graphql";
|
import { useGetCustomersSubscription } from '../utils/__generated__/graphql'
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
export function Customers() {
|
export function Customers() {
|
||||||
return (
|
return (
|
||||||
<Main>
|
<Main>
|
||||||
<Breadcrumbs
|
<Breadcrumbs backLink={''} breadcrumbs={[{ link: '/customers', text: 'Customers' }]} />
|
||||||
backLink={""}
|
|
||||||
breadcrumbs={[{ link: "/customers", text: "Customers" }]}
|
|
||||||
/>
|
|
||||||
<HeaderSection>
|
<HeaderSection>
|
||||||
<PageHeader>Customers</PageHeader>
|
<PageHeader>Customers</PageHeader>
|
||||||
<div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
|
<div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
|
||||||
@@ -31,11 +28,11 @@ export function Customers() {
|
|||||||
<CustomersList />
|
<CustomersList />
|
||||||
</div>
|
</div>
|
||||||
</Main>
|
</Main>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomersList() {
|
function CustomersList() {
|
||||||
const { data } = useGetCustomersSubscription();
|
const { data } = useGetCustomersSubscription()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -57,17 +54,14 @@ function CustomersList() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{data?.customers.map((customer, i) => {
|
{data?.customers.map((customer, i) => {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr key={customer.id} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||||
key={customer.id}
|
|
||||||
className={i % 2 === 0 ? "bg-white" : "bg-gray-50"}
|
|
||||||
>
|
|
||||||
<td className="text-sm font-medium text-gray-900 whitespace-nowrap">
|
<td className="text-sm font-medium text-gray-900 whitespace-nowrap">
|
||||||
<Link to={customer.id} className="block px-6 py-4">
|
<Link to={customer.id} className="block px-6 py-4">
|
||||||
{customer.name}
|
{customer.name}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -88,9 +82,9 @@ function CustomersList() {
|
|||||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-gray-700">
|
||||||
Showing <span className="font-medium">1</span> to{" "}
|
Showing <span className="font-medium">1</span> to{' '}
|
||||||
<span className="font-medium">10</span> of{" "}
|
<span className="font-medium">10</span> of <span className="font-medium">97</span>{' '}
|
||||||
<span className="font-medium">97</span> results
|
results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -136,5 +130,5 @@ function CustomersList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Main } from "./ui/Main";
|
import { Main } from './ui/Main'
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
return <Main>Dashboard</Main>;
|
return <Main>Dashboard</Main>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,64 @@
|
|||||||
import { useState } from "react";
|
import { useState } from 'react'
|
||||||
import { Main } from "./ui/Main";
|
import { Main } from './ui/Main'
|
||||||
import { Breadcrumbs } from "./ui/Breadcrumbs";
|
import { Breadcrumbs } from './ui/Breadcrumbs'
|
||||||
import { HeaderSection } from "./ui/HeaderSection";
|
import { HeaderSection } from './ui/HeaderSection'
|
||||||
import { PageHeader } from "./ui/PageHeader";
|
import { PageHeader } from './ui/PageHeader'
|
||||||
import {
|
import { useGetCompanyWhereQuery, useInsertCustomerMutation } from '../utils/__generated__/graphql'
|
||||||
useGetCompanyWhereQuery,
|
import { nhost } from '../utils/nhost'
|
||||||
useInsertCustomerMutation,
|
import { useNavigate } from 'react-router-dom'
|
||||||
} from "../utils/__generated__/graphql";
|
|
||||||
import { nhost } from "../utils/nhost";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
|
|
||||||
export function NewCustomer() {
|
export function NewCustomer() {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState('')
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState('')
|
||||||
const [addressLine1, setAddressLine1] = useState("");
|
const [addressLine1, setAddressLine1] = useState('')
|
||||||
|
|
||||||
const user = nhost.auth.getUser();
|
const user = nhost.auth.getUser()
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate()
|
||||||
|
|
||||||
const { data } = useGetCompanyWhereQuery({
|
const { data } = useGetCompanyWhereQuery({
|
||||||
variables: {
|
variables: {
|
||||||
where: {
|
where: {
|
||||||
companyUsers: {
|
companyUsers: {
|
||||||
userId: {
|
userId: {
|
||||||
_eq: user?.id,
|
_eq: user?.id
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const [insertCustomer, { loading }] = useInsertCustomerMutation();
|
const [insertCustomer, { loading }] = useInsertCustomerMutation()
|
||||||
|
|
||||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
console.log("handle submit");
|
console.log('handle submit')
|
||||||
|
|
||||||
let res;
|
let res
|
||||||
try {
|
try {
|
||||||
res = await insertCustomer({
|
res = await insertCustomer({
|
||||||
variables: {
|
variables: {
|
||||||
customer: {
|
customer: {
|
||||||
name,
|
name,
|
||||||
addressLine1,
|
addressLine1,
|
||||||
companyId: data?.companies[0].id,
|
companyId: data?.companies[0].id
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return alert(`error: ${error}`);
|
return alert(`error: ${error}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(`/customers/${res.data?.insertCustomer?.id}`);
|
navigate(`/customers/${res.data?.insertCustomer?.id}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Main>
|
<Main>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
backLink={""}
|
backLink={''}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ link: "/customers", text: "Customers" },
|
{ link: '/customers', text: 'Customers' },
|
||||||
{ link: "/new-customer", text: "New Customer" },
|
{ link: '/new-customer', text: 'New Customer' }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<HeaderSection>
|
<HeaderSection>
|
||||||
@@ -73,10 +70,7 @@ export function NewCustomer() {
|
|||||||
<div className="pt-12">
|
<div className="pt-12">
|
||||||
<div className="grid grid-cols-1 mt-6 gap-y-6 gap-x-4 sm:grid-cols-6">
|
<div className="grid grid-cols-1 mt-6 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||||
<div className="sm:col-span-3">
|
<div className="sm:col-span-3">
|
||||||
<label
|
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
|
||||||
htmlFor="first-name"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -93,10 +87,7 @@ export function NewCustomer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-3">
|
<div className="sm:col-span-3">
|
||||||
<label
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||||
htmlFor="email"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Email address
|
Email address
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -113,10 +104,7 @@ export function NewCustomer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-6">
|
<div className="sm:col-span-6">
|
||||||
<label
|
<label htmlFor="street-address" className="block text-sm font-medium text-gray-700">
|
||||||
htmlFor="street-address"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Street address
|
Street address
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -147,5 +135,5 @@ export function NewCustomer() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Main>
|
</Main>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useNhostAuth } from '@nhost/react'
|
import { useNhostAuth } from '@nhost/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Navigate, useLocation } from 'react-router'
|
import { Navigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
export function RequireAuth({ children }: { children: JSX.Element }) {
|
export function RequireAuth({ children }: { children: JSX.Element }) {
|
||||||
const { isAuthenticated, isLoading } = useNhostAuth()
|
const { isAuthenticated, isLoading } = useNhostAuth()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useNhostAuth } from '@nhost/react'
|
import { useNhostAuth } from '@nhost/react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { nhost } from '../utils/nhost'
|
import { nhost } from '../utils/nhost'
|
||||||
|
|
||||||
export function ResetPassword() {
|
export function ResetPassword() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useNhostAuth } from '@nhost/react'
|
import { useNhostAuth } from '@nhost/react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { nhost } from '../utils/nhost'
|
import { nhost } from '../utils/nhost'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useNhostAuth } from '@nhost/react'
|
import { useNhostAuth } from '@nhost/react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { nhost } from '../utils/nhost'
|
import { nhost } from '../utils/nhost'
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export function SignUp() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center py-4">
|
<div className="py-4 text-center">
|
||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
<Link to="/sign-in" className="text-blue-600 hover:text-blue-500">
|
<Link to="/sign-in" className="text-blue-600 hover:text-blue-500">
|
||||||
Sign In
|
Sign In
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
type BreadcrumbsProps = {
|
type BreadcrumbsProps = {
|
||||||
backLink: string;
|
backLink: string
|
||||||
breadcrumbs: Breadcrumb[];
|
breadcrumbs: Breadcrumb[]
|
||||||
};
|
}
|
||||||
|
|
||||||
type Breadcrumb = {
|
type Breadcrumb = {
|
||||||
link: string;
|
link: string
|
||||||
text: string;
|
text: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export function Breadcrumbs(props: BreadcrumbsProps) {
|
export function Breadcrumbs(props: BreadcrumbsProps) {
|
||||||
const { backLink, breadcrumbs } = props;
|
const { backLink, breadcrumbs } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -32,13 +32,10 @@ export function Breadcrumbs(props: BreadcrumbsProps) {
|
|||||||
<nav className="hidden sm:flex" aria-label="Breadcrumb">
|
<nav className="hidden sm:flex" aria-label="Breadcrumb">
|
||||||
<ol className="flex items-center space-x-4">
|
<ol className="flex items-center space-x-4">
|
||||||
{breadcrumbs.map((breadcrumb, i) => {
|
{breadcrumbs.map((breadcrumb, i) => {
|
||||||
const isFirstItem = i === 0;
|
const isFirstItem = i === 0
|
||||||
const classes = classNames(
|
const classes = classNames('text-sm font-medium text-gray-500 hover:text-gray-700', {
|
||||||
"text-sm font-medium text-gray-500 hover:text-gray-700",
|
'ml-4': !isFirstItem
|
||||||
{
|
})
|
||||||
"ml-4": !isFirstItem,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
@@ -54,10 +51,10 @@ export function Breadcrumbs(props: BreadcrumbsProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
export function HeaderSection({ children }: { children: React.ReactNode }) {
|
export function HeaderSection({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <div className="mt-2 md:flex md:items-center md:justify-between">{children}</div>
|
||||||
<div className="mt-2 md:flex md:items-center md:justify-between">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
export function Main({ children }: { children: React.ReactNode }) {
|
export function Main({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">{children}</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
export function PageHeader({ children }: { children: React.ReactNode }) {
|
export function PageHeader({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -7,5 +7,5 @@ export function PageHeader({ children }: { children: React.ReactNode }) {
|
|||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
10
examples/react-apollo-crm/src/env.d.ts
vendored
Normal file
10
examples/react-apollo-crm/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_NHOST_URL: string
|
||||||
|
// more env variables...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
mutation insertCustomerComment(
|
mutation insertCustomerComment($customerComment: customerComments_insert_input!) {
|
||||||
$customerComment: customerComments_insert_input!
|
|
||||||
) {
|
|
||||||
insertCustomerComment(object: $customerComment) {
|
insertCustomerComment(object: $customerComment) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from 'react-dom'
|
||||||
import "./index.css";
|
import './index.css'
|
||||||
import App from "./App";
|
import App from './App'
|
||||||
import reportWebVitals from "./reportWebVitals";
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
@@ -11,10 +10,5 @@ ReactDOM.render(
|
|||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById("root")
|
document.getElementById('root')
|
||||||
);
|
)
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/// <reference types="react-scripts" />
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { ReportHandler } from 'web-vitals';
|
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
// allows you to do things like:
|
// allows you to do things like:
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom'
|
||||||
|
|||||||
5392
examples/react-apollo-crm/src/utils/__generated__/graphql.ts
generated
5392
examples/react-apollo-crm/src/utils/__generated__/graphql.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import { NhostClient } from '@nhost/react'
|
import { NhostClient } from '@nhost/react'
|
||||||
|
|
||||||
const nhost = new NhostClient({
|
const nhost = new NhostClient({
|
||||||
backendUrl: process.env.REACT_APP_BACKEND_URL!
|
backendUrl: import.meta.env.VITE_NHOST_URL || 'http://localhost:1337'
|
||||||
})
|
})
|
||||||
|
|
||||||
export { nhost }
|
export { nhost }
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
|
content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
|
||||||
darkMode: false, // or 'media' or 'class'
|
darkMode: 'media',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {}
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {},
|
extend: {}
|
||||||
},
|
},
|
||||||
plugins: [require("@tailwindcss/forms")],
|
plugins: [require('@tailwindcss/forms')]
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"outDir": "dist",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"composite": true,
|
||||||
"allowJs": true,
|
"module": "esnext"
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src/**/*", "types/**/*", "../../types/**/*", "tests/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
9
examples/react-apollo-crm/vite.config.js
Normal file
9
examples/react-apollo-crm/vite.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tsconfigPaths(), react()]
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,29 @@
|
|||||||
# React-Apollo example
|
# React-Apollo example
|
||||||
|
|
||||||
Once in the example's directory, run the two following commands in parallel:
|
## Get started
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Start the Nhost CLI in the background
|
git clone https://github.com/nhost/nhost
|
||||||
nhost -d
|
cd nhost
|
||||||
|
```
|
||||||
# Start this project
|
|
||||||
yarn run dev
|
2. Install dependencies
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd examples/react-apollo
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Terminal 1: Start Nhost
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nhost dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Terminal 2: Start React App
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
hasura_graphql_enable_remote_schema_permissions: false
|
hasura_graphql_enable_remote_schema_permissions: false
|
||||||
auth:
|
auth:
|
||||||
version: 0.4.2
|
version: 0.6.3
|
||||||
auth:
|
auth:
|
||||||
access_control:
|
access_control:
|
||||||
email:
|
email:
|
||||||
@@ -15,7 +15,6 @@ auth:
|
|||||||
allowed_emails: ''
|
allowed_emails: ''
|
||||||
blocked_email_domains: ''
|
blocked_email_domains: ''
|
||||||
blocked_emails: ''
|
blocked_emails: ''
|
||||||
url:
|
|
||||||
allowed_redirect_urls: ''
|
allowed_redirect_urls: ''
|
||||||
anonymous_users_enabled: false
|
anonymous_users_enabled: false
|
||||||
client_url: http://localhost:3000
|
client_url: http://localhost:3000
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Confirm Email Change</h2>
|
<h2>Confirm Email Change</h2>
|
||||||
<p>Use this link to confirm changing email:</p>
|
<p>Use this link to confirm changing email:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Change email </a>
|
||||||
Change email
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Verify Email</h2>
|
<h2>Verify Email</h2>
|
||||||
<p>Use this link to verify your email:</p>
|
<p>Use this link to verify your email:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Verify Email </a>
|
||||||
Verify Email
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Reset Password</h2>
|
<h2>Reset Password</h2>
|
||||||
<p>Use this link to reset your password:</p>
|
<p>Use this link to reset your password:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Reset password </a>
|
||||||
Reset password
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Magic Link</h2>
|
<h2>Magic Link</h2>
|
||||||
<p>Use this link to securely sign in:</p>
|
<p>Use this link to securely sign in:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Sign In </a>
|
||||||
Sign In
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Confirmer changement de courriel</h2>
|
<h2>Confirmer changement de courriel</h2>
|
||||||
<p>Utilisez ce lien pour confirmer le changement de courriel:</p>
|
<p>Utilisez ce lien pour confirmer le changement de courriel:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Changer courriel </a>
|
||||||
Changer courriel
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Vérifiez votre courriel</h2>
|
<h2>Vérifiez votre courriel</h2>
|
||||||
<p>Utilisez ce lien pour vérifier votre courriel:</p>
|
<p>Utilisez ce lien pour vérifier votre courriel:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Vérifier courriel </a>
|
||||||
Vérifier courriel
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Réinitializer votre mot de passe</h2>
|
<h2>Réinitializer votre mot de passe</h2>
|
||||||
<p>Utilisez ce lien pour réinitializer votre mot de passe:</p>
|
<p>Utilisez ce lien pour réinitializer votre mot de passe:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Réinitializer mot de passe </a>
|
||||||
Réinitializer mot de passe
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
<h2>Lien magique</h2>
|
<h2>Lien magique</h2>
|
||||||
<p>Utilisez ce lien pour vous connecter de façon sécuritaire:</p>
|
<p>Utilisez ce lien pour vous connecter de façon sécuritaire:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="${link}">
|
<a href="${link}"> Connexion </a>
|
||||||
Connexion
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -11,4 +11,4 @@
|
|||||||
max_connections: 50
|
max_connections: 50
|
||||||
retries: 20
|
retries: 20
|
||||||
use_prepared_statements: true
|
use_prepared_statements: true
|
||||||
tables: "!include default/tables/tables.yaml"
|
tables: '!include default/tables/tables.yaml'
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
- "!include auth_provider_requests.yaml"
|
- '!include auth_provider_requests.yaml'
|
||||||
- "!include auth_providers.yaml"
|
- '!include auth_providers.yaml'
|
||||||
- "!include auth_refresh_tokens.yaml"
|
- '!include auth_refresh_tokens.yaml'
|
||||||
- "!include auth_roles.yaml"
|
- '!include auth_roles.yaml'
|
||||||
- "!include auth_user_providers.yaml"
|
- '!include auth_user_providers.yaml'
|
||||||
- "!include auth_user_roles.yaml"
|
- '!include auth_user_roles.yaml'
|
||||||
- "!include auth_users.yaml"
|
- '!include auth_users.yaml'
|
||||||
- "!include public_books.yaml"
|
- '!include public_books.yaml'
|
||||||
- "!include storage_buckets.yaml"
|
- '!include storage_buckets.yaml'
|
||||||
- "!include storage_files.yaml"
|
- '!include storage_files.yaml'
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.5.10",
|
"@apollo/client": "^3.5.10",
|
||||||
"@nhost/react": "^0.5.0",
|
"@nhost/core": "workspace:*",
|
||||||
"@nhost/react-apollo": "^4.0.10",
|
"@nhost/react": "workspace:*",
|
||||||
|
"@nhost/react-apollo": "workspace:*",
|
||||||
"@rsuite/icons": "^1.0.2",
|
"@rsuite/icons": "^1.0.2",
|
||||||
|
"graphql": "15.7.2",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@@ -15,12 +17,19 @@
|
|||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
"react-router": "^6.3.0",
|
"react-router": "^6.3.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"rsuite": "^5.7.1"
|
"rsuite": "^5.8.1"
|
||||||
},
|
},
|
||||||
|
"lib": "workspace:*",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"prettier": "prettier --check .",
|
||||||
|
"prettier:fix": "prettier --write .",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||||
|
"verify": "run-p prettier lint",
|
||||||
|
"verify:fix": "run-p prettier:fix lint:fix"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -42,8 +51,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^17.0.43",
|
"@types/react": "^17.0.43",
|
||||||
"@types/react-dom": "^17.0.14",
|
"@types/react-dom": "^17.0.14",
|
||||||
"@vitejs/plugin-react": "^1.3.0",
|
"@vitejs/plugin-react": "^1.3.1",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
"vite": "^2.9.1"
|
"vite": "^2.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"outDir": "dist",
|
||||||
"lib": [
|
"composite": true,
|
||||||
"dom",
|
"module": "esnext"
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"baseUrl": "./src",
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*", "types/**/*", "../../types/**/*", "tests/**/*"]
|
||||||
"src"
|
|
||||||
],
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()]
|
plugins: [tsconfigPaths(), react()]
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@ auth:
|
|||||||
allowed_emails: ''
|
allowed_emails: ''
|
||||||
blocked_email_domains: ''
|
blocked_email_domains: ''
|
||||||
blocked_emails: ''
|
blocked_emails: ''
|
||||||
url:
|
|
||||||
allowed_redirect_urls: ''
|
allowed_redirect_urls: ''
|
||||||
anonymous_users_enabled: false
|
anonymous_users_enabled: false
|
||||||
client_url: http://localhost:3000
|
client_url: http://localhost:3000
|
||||||
|
|||||||
34
package.json
34
package.json
@@ -40,39 +40,39 @@
|
|||||||
"docs"
|
"docs"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.5",
|
"@babel/core": "^7.17.9",
|
||||||
"@babel/eslint-parser": "^7.17.0",
|
"@babel/eslint-parser": "^7.17.0",
|
||||||
"@babel/plugin-syntax-flow": "^7.16.7",
|
"@babel/plugin-syntax-flow": "^7.16.7",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.17.3",
|
"@babel/plugin-transform-react-jsx": "^7.17.3",
|
||||||
"@changesets/cli": "^2.21.1",
|
"@changesets/cli": "^2.22.0",
|
||||||
"@faker-js/faker": "^6.0.0-beta.0",
|
"@faker-js/faker": "^6.1.2",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
"@typescript-eslint/parser": "^5.20.0",
|
||||||
"@vitejs/plugin-react": "^1.2.0",
|
"@vitejs/plugin-react": "^1.3.1",
|
||||||
"esbuild": "^0.14.25",
|
"esbuild": "^0.14.37",
|
||||||
"esbuild-node-externals": "^1.4.1",
|
"esbuild-node-externals": "^1.4.1",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.13.0",
|
||||||
"eslint-config-react-app": "^7.0.0",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-flowtype": "^8.0.3",
|
"eslint-plugin-flowtype": "^8.0.3",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-plugin-react": "^7.29.3",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.4.0",
|
||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.6.2",
|
||||||
"ts-jest": "^27.1.3",
|
"ts-jest": "^27.1.4",
|
||||||
"tsconfig-paths-jest": "^0.0.1",
|
"tsconfig-paths-jest": "^0.0.1",
|
||||||
"turbo": "1.1.6",
|
"turbo": "1.1.6",
|
||||||
"typescript": "4.5.5",
|
"typescript": "4.5.5",
|
||||||
"vite": "^2.8.6",
|
"vite": "^2.9.5",
|
||||||
"vite-plugin-dts": "^0.9.9",
|
"vite-plugin-dts": "^0.9.10",
|
||||||
"vite-tsconfig-paths": "^3.4.1",
|
"vite-tsconfig-paths": "^3.4.1",
|
||||||
"wait-on": "^6.0.1"
|
"wait-on": "^6.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# @nhost/apollo
|
# @nhost/apollo
|
||||||
|
|
||||||
|
## 0.4.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [5ee395e]
|
||||||
|
- Updated dependencies [e0cfcaf]
|
||||||
|
- Updated dependencies [7b7527a]
|
||||||
|
- @nhost/core@0.3.13
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [7b5f00d]
|
||||||
|
- Updated dependencies [58e1485]
|
||||||
|
- @nhost/core@0.3.12
|
||||||
|
|
||||||
## 0.4.1
|
## 0.4.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/apollo",
|
"name": "@nhost/apollo",
|
||||||
"version": "0.4.1",
|
"version": "0.4.3",
|
||||||
"description": "Nhost Apollo Client library",
|
"description": "Nhost Apollo Client library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
# @nhost/core
|
# @nhost/core
|
||||||
|
|
||||||
|
## 0.3.13
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 5ee395e: Ensure the session is destroyed when signout is done
|
||||||
|
The user session, in particular the access token (JWT), was still available after sign out.
|
||||||
|
Any information about user session is now removed from the auth state as soon as the sign out action is called.
|
||||||
|
- e0cfcaf: fix and improve `nhost.auth.refreshSession`
|
||||||
|
`nhost.auth.refreshSession` is now functional and returns possible errors, or the user session if the token has been sucessfully refreshed.
|
||||||
|
If the user was previously not authenticated, it will sign them in. See [#286](https://github.com/nhost/nhost/issues/286)
|
||||||
|
- 7b7527a: Improve reliability of the token refresher
|
||||||
|
The token refresher had an unreliable behaviour, leading to too many refreshes, or refreshes that are missed, leading to an expired access token (JWT).
|
||||||
|
|
||||||
|
The internal refresher rules have been made more explicit in the code. Every second, this runs:
|
||||||
|
|
||||||
|
- If the client defined a `refreshIntervalTime` and the interval between when the last access token has been created and now is more than this value, then it triggers a refresh
|
||||||
|
- If the access token expires in less than five minutes, then it triggers a refresh
|
||||||
|
|
||||||
|
If a refresh fails, then it switches to a specific rule: it will make an attempt to refresh the token every five seconds
|
||||||
|
|
||||||
|
## 0.3.12
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 7b5f00d: Avoid error when BroadcastChannell is not available
|
||||||
|
- 58e1485: Fix invalid password and email errors on sign up
|
||||||
|
When signin up, an invalid password was returning the `invalid-email` error, and an invalid email was returning `invalid-password`.
|
||||||
|
This is now in order.
|
||||||
|
|
||||||
## 0.3.11
|
## 0.3.11
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/core",
|
"name": "@nhost/core",
|
||||||
"version": "0.3.11",
|
"version": "0.3.13",
|
||||||
"description": "Nhost core client library",
|
"description": "Nhost core client library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { interpret } from 'xstate'
|
import { interpret } from 'xstate'
|
||||||
|
|
||||||
import { MIN_TOKEN_REFRESH_INTERVAL } from './constants'
|
|
||||||
import { AuthMachine, AuthMachineOptions, createAuthMachine } from './machines'
|
import { AuthMachine, AuthMachineOptions, createAuthMachine } from './machines'
|
||||||
import { defaultClientStorageGetter, defaultClientStorageSetter } from './storage'
|
import { defaultClientStorageGetter, defaultClientStorageSetter } from './storage'
|
||||||
import type { AuthInterpreter } from './types'
|
import type { AuthInterpreter } from './types'
|
||||||
@@ -20,7 +19,7 @@ export class AuthClient {
|
|||||||
clientUrl = (typeof window !== 'undefined' && window.location?.origin) || '',
|
clientUrl = (typeof window !== 'undefined' && window.location?.origin) || '',
|
||||||
clientStorageGetter = defaultClientStorageGetter,
|
clientStorageGetter = defaultClientStorageGetter,
|
||||||
clientStorageSetter = defaultClientStorageSetter,
|
clientStorageSetter = defaultClientStorageSetter,
|
||||||
refreshIntervalTime = MIN_TOKEN_REFRESH_INTERVAL,
|
refreshIntervalTime,
|
||||||
autoSignIn = true,
|
autoSignIn = true,
|
||||||
autoRefreshToken = true,
|
autoRefreshToken = true,
|
||||||
start = true
|
start = true
|
||||||
@@ -44,6 +43,7 @@ export class AuthClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && autoSignIn) {
|
if (typeof window !== 'undefined' && autoSignIn) {
|
||||||
|
try {
|
||||||
this._channel = new BroadcastChannel('nhost')
|
this._channel = new BroadcastChannel('nhost')
|
||||||
this._channel.addEventListener('message', (token) => {
|
this._channel.addEventListener('message', (token) => {
|
||||||
const existingToken = this.interpreter?.state.context.refreshToken
|
const existingToken = this.interpreter?.state.context.refreshToken
|
||||||
@@ -51,6 +51,9 @@ export class AuthClient {
|
|||||||
this.interpreter.send({ type: 'TRY_TOKEN', token: token.data })
|
this.interpreter.send({ type: 'TRY_TOKEN', token: token.data })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// * BroadcastChannel is not available e.g. react-native
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ export const NHOST_JWT_EXPIRES_AT_KEY = 'nhostRefreshTokenExpiresAt'
|
|||||||
|
|
||||||
export const MIN_PASSWORD_LENGTH = 3
|
export const MIN_PASSWORD_LENGTH = 3
|
||||||
|
|
||||||
// * Minimum time in seconds before the JWT expiration and the first refresh attempt
|
/**
|
||||||
export const TOKEN_REFRESH_MARGIN = 900
|
* Minimum time in seconds between now and the JWT expiration time before the JWT is refreshed
|
||||||
|
* For instance, if set to 60, the client will refresh the JWT one minute before it expires
|
||||||
|
*/
|
||||||
|
export const TOKEN_REFRESH_MARGIN = 300 // five minutes
|
||||||
|
|
||||||
// * Minimum time in seconds for a refresh regardless ot the JWT expiration
|
/** Number of seconds before retrying a token refresh after an error */
|
||||||
export const MIN_TOKEN_REFRESH_INTERVAL = 60
|
export const REFRESH_TOKEN_RETRY_INTERVAL = 5
|
||||||
|
|
||||||
// * Number of seconds before retrying a token refresh after an error
|
// TODO not implemented yet
|
||||||
export const REFRESH_TOKEN_RETRY_INTERVAL = 10
|
|
||||||
|
|
||||||
// * Maximum number of attempts to refresh a token before stopping the timer and logging out
|
|
||||||
// TODO try when offline for a long time: maybe we could keep state as 'signedIn'
|
// TODO try when offline for a long time: maybe we could keep state as 'signedIn'
|
||||||
|
/** Maximum number of attempts to refresh a token before stopping the timer and logging out */
|
||||||
export const REFRESH_TOKEN_RETRY_MAX_ATTEMPTS = 30
|
export const REFRESH_TOKEN_RETRY_MAX_ATTEMPTS = 30
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const NETWORK_ERROR_CODE = 0
|
export const NETWORK_ERROR_CODE = 0
|
||||||
export const VALIDATION_ERROR_CODE = 10
|
export const VALIDATION_ERROR_CODE = 10
|
||||||
|
export const STATE_ERROR_CODE = 20
|
||||||
|
|
||||||
export type ErrorPayload = {
|
export type ErrorPayload = {
|
||||||
error: string
|
error: string
|
||||||
@@ -38,3 +39,16 @@ export const NO_MFA_TICKET_ERROR: ValidationErrorPayload = {
|
|||||||
error: 'no-mfa-ticket',
|
error: 'no-mfa-ticket',
|
||||||
message: 'No MFA ticket has been provided'
|
message: 'No MFA ticket has been provided'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NO_REFRESH_TOKEN: ValidationErrorPayload = {
|
||||||
|
status: VALIDATION_ERROR_CODE,
|
||||||
|
error: 'no-refresh-token',
|
||||||
|
message: 'No refresh token has been provided'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOKEN_REFRESHER_RUNNING_ERROR: ErrorPayload = {
|
||||||
|
status: STATE_ERROR_CODE,
|
||||||
|
error: 'refresher-already-running',
|
||||||
|
message:
|
||||||
|
'The token refresher is already running. You must wait until is has finished before submitting a new token.'
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ export type AuthContext = {
|
|||||||
} | null
|
} | null
|
||||||
accessToken: {
|
accessToken: {
|
||||||
value: string | null
|
value: string | null
|
||||||
expiresAt: Date
|
expiresAt: Date | null
|
||||||
}
|
}
|
||||||
refreshTimer: {
|
refreshTimer: {
|
||||||
elapsed: number
|
startedAt: Date | null
|
||||||
attempts: number
|
attempts: number
|
||||||
|
lastAttempt: Date | null
|
||||||
}
|
}
|
||||||
refreshToken: {
|
refreshToken: {
|
||||||
value: string | null
|
value: string | null
|
||||||
@@ -25,11 +26,12 @@ export const INITIAL_MACHINE_CONTEXT: AuthContext = {
|
|||||||
mfa: null,
|
mfa: null,
|
||||||
accessToken: {
|
accessToken: {
|
||||||
value: null,
|
value: null,
|
||||||
expiresAt: new Date()
|
expiresAt: null
|
||||||
},
|
},
|
||||||
refreshTimer: {
|
refreshTimer: {
|
||||||
elapsed: 0,
|
startedAt: null,
|
||||||
attempts: 0
|
attempts: 0,
|
||||||
|
lastAttempt: null
|
||||||
},
|
},
|
||||||
refreshToken: {
|
refreshToken: {
|
||||||
value: null
|
value: null
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { assign, createMachine, send } from 'xstate'
|
|||||||
import {
|
import {
|
||||||
NHOST_JWT_EXPIRES_AT_KEY,
|
NHOST_JWT_EXPIRES_AT_KEY,
|
||||||
NHOST_REFRESH_TOKEN_KEY,
|
NHOST_REFRESH_TOKEN_KEY,
|
||||||
|
REFRESH_TOKEN_RETRY_INTERVAL,
|
||||||
TOKEN_REFRESH_MARGIN
|
TOKEN_REFRESH_MARGIN
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +32,10 @@ export * from './send-verification-email'
|
|||||||
export type AuthMachineOptions = {
|
export type AuthMachineOptions = {
|
||||||
backendUrl: string
|
backendUrl: string
|
||||||
clientUrl?: string
|
clientUrl?: string
|
||||||
|
/**
|
||||||
|
* Interval in seconds before refreshing the JWT regardless of its expiration.
|
||||||
|
* When undefined, the option is ignored and the refresh will start 3 minutes before the access token (JWT) expiration
|
||||||
|
*/
|
||||||
refreshIntervalTime?: number
|
refreshIntervalTime?: number
|
||||||
clientStorageGetter?: StorageGetter
|
clientStorageGetter?: StorageGetter
|
||||||
clientStorageSetter?: StorageSetter
|
clientStorageSetter?: StorageSetter
|
||||||
@@ -50,7 +55,8 @@ export const createAuthMachine = ({
|
|||||||
refreshIntervalTime,
|
refreshIntervalTime,
|
||||||
autoRefreshToken = true,
|
autoRefreshToken = true,
|
||||||
autoSignIn = true
|
autoSignIn = true
|
||||||
}: Required<AuthMachineOptions>) => {
|
}: Required<Omit<AuthMachineOptions, 'refreshIntervalTime'>> &
|
||||||
|
Pick<AuthMachineOptions, 'refreshIntervalTime'>) => {
|
||||||
const api = nhostApiClient(backendUrl)
|
const api = nhostApiClient(backendUrl)
|
||||||
const postRequest = async <T = any, R = AxiosResponse<T>, D = any>(
|
const postRequest = async <T = any, R = AxiosResponse<T>, D = any>(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -141,8 +147,8 @@ export const createAuthMachine = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
signingOut: {
|
signingOut: {
|
||||||
entry: 'destroyToken',
|
entry: ['clearContextExceptRefreshToken'],
|
||||||
exit: 'clearContext',
|
exit: ['destroyRefreshToken', 'reportTokenChanged'],
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'signout',
|
src: 'signout',
|
||||||
id: 'signingOut',
|
id: 'signingOut',
|
||||||
@@ -385,7 +391,6 @@ export const createAuthMachine = ({
|
|||||||
pending: {
|
pending: {
|
||||||
after: {
|
after: {
|
||||||
'1000': {
|
'1000': {
|
||||||
actions: 'tickRefreshTimer',
|
|
||||||
internal: false,
|
internal: false,
|
||||||
target: 'pending'
|
target: 'pending'
|
||||||
}
|
}
|
||||||
@@ -409,7 +414,8 @@ export const createAuthMachine = ({
|
|||||||
target: 'pending'
|
target: 'pending'
|
||||||
},
|
},
|
||||||
onError: [
|
onError: [
|
||||||
// TODO handle error
|
{ actions: 'saveRefreshAttempt', target: 'pending' }
|
||||||
|
// ? stop trying after x attempts?
|
||||||
// {
|
// {
|
||||||
// actions: 'retry',
|
// actions: 'retry',
|
||||||
// cond: 'canRetry',
|
// cond: 'canRetry',
|
||||||
@@ -457,6 +463,7 @@ export const createAuthMachine = ({
|
|||||||
target: ['#nhost.authentication.signedIn', 'idle.noErrors']
|
target: ['#nhost.authentication.signedIn', 'idle.noErrors']
|
||||||
},
|
},
|
||||||
onError: [
|
onError: [
|
||||||
|
// TODO save error
|
||||||
{ cond: 'isSignedIn', target: 'idle.error' },
|
{ cond: 'isSignedIn', target: 'idle.error' },
|
||||||
{
|
{
|
||||||
target: ['#nhost.authentication.signedOut', 'idle.error']
|
target: ['#nhost.authentication.signedOut', 'idle.error']
|
||||||
@@ -473,7 +480,13 @@ export const createAuthMachine = ({
|
|||||||
reportSignedIn: send('SIGNED_IN'),
|
reportSignedIn: send('SIGNED_IN'),
|
||||||
reportSignedOut: send('SIGNED_OUT'),
|
reportSignedOut: send('SIGNED_OUT'),
|
||||||
reportTokenChanged: send('TOKEN_CHANGED'),
|
reportTokenChanged: send('TOKEN_CHANGED'),
|
||||||
clearContext: assign(() => INITIAL_MACHINE_CONTEXT),
|
clearContextExceptRefreshToken: assign(({ refreshToken: { value } }) => {
|
||||||
|
clientStorageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
||||||
|
return {
|
||||||
|
...INITIAL_MACHINE_CONTEXT,
|
||||||
|
refreshToken: { value }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
saveSession: assign({
|
saveSession: assign({
|
||||||
user: (_, e: any) => e.data?.session?.user,
|
user: (_, e: any) => e.data?.session?.user,
|
||||||
@@ -490,17 +503,19 @@ export const createAuthMachine = ({
|
|||||||
resetTimer: assign({
|
resetTimer: assign({
|
||||||
refreshTimer: (ctx, e) => {
|
refreshTimer: (ctx, e) => {
|
||||||
return {
|
return {
|
||||||
elapsed: 0,
|
startedAt: new Date(),
|
||||||
attempts: 0
|
attempts: 0,
|
||||||
|
lastAttempt: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
tickRefreshTimer: assign({
|
saveRefreshAttempt: assign({
|
||||||
refreshTimer: (ctx, e) => {
|
refreshTimer: (ctx, e) => {
|
||||||
return {
|
return {
|
||||||
elapsed: ctx.refreshTimer.elapsed + 1,
|
startedAt: ctx.refreshTimer.startedAt,
|
||||||
attempts: ctx.refreshTimer.attempts
|
attempts: ctx.refreshTimer.attempts + 1,
|
||||||
|
lastAttempt: new Date()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -528,10 +543,10 @@ export const createAuthMachine = ({
|
|||||||
errors: ({ errors: { registration, ...errors } }) => errors
|
errors: ({ errors: { registration, ...errors } }) => errors
|
||||||
}),
|
}),
|
||||||
saveInvalidSignUpPassword: assign({
|
saveInvalidSignUpPassword: assign({
|
||||||
errors: ({ errors }) => ({ ...errors, registration: INVALID_EMAIL_ERROR })
|
errors: ({ errors }) => ({ ...errors, registration: INVALID_PASSWORD_ERROR })
|
||||||
}),
|
}),
|
||||||
saveInvalidSignUpEmail: assign({
|
saveInvalidSignUpEmail: assign({
|
||||||
errors: ({ errors }) => ({ ...errors, registration: INVALID_PASSWORD_ERROR })
|
errors: ({ errors }) => ({ ...errors, registration: INVALID_EMAIL_ERROR })
|
||||||
}),
|
}),
|
||||||
saveNoMfaTicketError: assign({
|
saveNoMfaTicketError: assign({
|
||||||
errors: ({ errors }) => ({ ...errors, registration: NO_MFA_TICKET_ERROR })
|
errors: ({ errors }) => ({ ...errors, registration: NO_MFA_TICKET_ERROR })
|
||||||
@@ -552,10 +567,12 @@ export const createAuthMachine = ({
|
|||||||
clientStorageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
clientStorageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyToken: () => {
|
destroyRefreshToken: assign({
|
||||||
|
refreshToken: (_) => {
|
||||||
clientStorageSetter(NHOST_REFRESH_TOKEN_KEY, null)
|
clientStorageSetter(NHOST_REFRESH_TOKEN_KEY, null)
|
||||||
clientStorageSetter(NHOST_JWT_EXPIRES_AT_KEY, null)
|
return { value: null }
|
||||||
}
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
guards: {
|
guards: {
|
||||||
@@ -567,13 +584,30 @@ export const createAuthMachine = ({
|
|||||||
hasRefreshToken: (ctx) => !!ctx.refreshToken.value,
|
hasRefreshToken: (ctx) => !!ctx.refreshToken.value,
|
||||||
isAutoRefreshDisabled: () => !autoRefreshToken,
|
isAutoRefreshDisabled: () => !autoRefreshToken,
|
||||||
isAutoSignInDisabled: () => !autoSignIn,
|
isAutoSignInDisabled: () => !autoSignIn,
|
||||||
refreshTimerShouldRefresh: (ctx) =>
|
refreshTimerShouldRefresh: (ctx) => {
|
||||||
ctx.refreshTimer.elapsed >
|
const { expiresAt } = ctx.accessToken
|
||||||
Math.max(
|
if (!expiresAt) {
|
||||||
(Date.now() - ctx.accessToken.expiresAt.getTime()) / 1_000 - TOKEN_REFRESH_MARGIN,
|
return false
|
||||||
refreshIntervalTime
|
}
|
||||||
),
|
if (ctx.refreshTimer.lastAttempt) {
|
||||||
|
// * If a refesh previously failed, only try to refresh every `REFRESH_TOKEN_RETRY_INTERVAL` seconds
|
||||||
|
const elapsed = Date.now() - ctx.refreshTimer.lastAttempt.getTime()
|
||||||
|
return elapsed > REFRESH_TOKEN_RETRY_INTERVAL * 1_000
|
||||||
|
}
|
||||||
|
if (refreshIntervalTime) {
|
||||||
|
// * If a refreshIntervalTime has been passed on as an option, it will notify
|
||||||
|
// * the token should be refershed when this interval is overdue
|
||||||
|
const elapsed = Date.now() - (ctx.refreshTimer.startedAt?.getTime() || 0)
|
||||||
|
if (elapsed > refreshIntervalTime * 1_000) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// * In any case, it's time to refresh when there's less than
|
||||||
|
// * TOKEN_REFRESH_MARGIN seconds before the JWT exprires
|
||||||
|
const expiresIn = expiresAt.getTime() - Date.now()
|
||||||
|
const remaining = expiresIn - 1_000 * TOKEN_REFRESH_MARGIN
|
||||||
|
return remaining <= 0
|
||||||
|
},
|
||||||
// * Authentication errors
|
// * Authentication errors
|
||||||
unverified: (_, { data: { error } }: any) =>
|
unverified: (_, { data: { error } }: any) =>
|
||||||
error.status === 401 && error.message === 'Email is not verified',
|
error.status === 401 && error.message === 'Email is not verified',
|
||||||
@@ -645,13 +679,18 @@ export const createAuthMachine = ({
|
|||||||
autoSignIn: async () => {
|
autoSignIn: async () => {
|
||||||
// TODO throwing errors is not really important as they are captured by the xstate invoker
|
// TODO throwing errors is not really important as they are captured by the xstate invoker
|
||||||
// * Still, keep them for the moment as it needs to be tested in every environemnt e.g. nodejs, expo, react-native...
|
// * Still, keep them for the moment as it needs to be tested in every environemnt e.g. nodejs, expo, react-native...
|
||||||
if (typeof window === 'undefined' || !window.location)
|
if (typeof window === 'undefined' || !window.location) {
|
||||||
throw Error('window is undefined or location does not exist')
|
throw Error('window is undefined or location does not exist')
|
||||||
|
}
|
||||||
const { hash } = window.location
|
const { hash } = window.location
|
||||||
if (!hash) throw Error('No hash in window.location')
|
if (!hash) {
|
||||||
|
throw Error('No hash in window.location')
|
||||||
|
}
|
||||||
const params = new URLSearchParams(hash.slice(1))
|
const params = new URLSearchParams(hash.slice(1))
|
||||||
const refreshToken = params.get('refreshToken')
|
const refreshToken = params.get('refreshToken')
|
||||||
if (!refreshToken) throw Error('No refresh token in the location hash')
|
if (!refreshToken) {
|
||||||
|
throw Error('No refresh token in the location hash')
|
||||||
|
}
|
||||||
const session = await postRequest('/token', {
|
const session = await postRequest('/token', {
|
||||||
refreshToken
|
refreshToken
|
||||||
})
|
})
|
||||||
@@ -659,9 +698,13 @@ export const createAuthMachine = ({
|
|||||||
// TODO remove the hash. For the moment, it is kept to avoid regression from the current SDK.
|
// 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
|
// * 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)
|
// window.history.pushState({}, '', location.pathname)
|
||||||
|
try {
|
||||||
const channel = new BroadcastChannel('nhost')
|
const channel = new BroadcastChannel('nhost')
|
||||||
// ? broadcat session instead of token ?
|
// ? broadcat session instead of token ?
|
||||||
channel.postMessage(refreshToken)
|
channel.postMessage(refreshToken)
|
||||||
|
} catch (error) {
|
||||||
|
// * BroadcastChannel is not available e.g. react-native
|
||||||
|
}
|
||||||
return { session }
|
return { session }
|
||||||
},
|
},
|
||||||
importRefreshToken: async () => {
|
importRefreshToken: async () => {
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ export interface Typegen0 {
|
|||||||
| 'error.platform.signInMfaTotp'
|
| 'error.platform.signInMfaTotp'
|
||||||
saveMfaTicket: 'done.invoke.authenticateUserWithPassword'
|
saveMfaTicket: 'done.invoke.authenticateUserWithPassword'
|
||||||
saveRegisrationError: 'error.platform.registerUser'
|
saveRegisrationError: 'error.platform.registerUser'
|
||||||
tickRefreshTimer: 'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending'
|
saveRefreshAttempt: 'error.platform.refreshToken'
|
||||||
reportSignedOut: '' | 'error.platform.authenticateWithToken'
|
reportSignedOut: '' | 'error.platform.authenticateWithToken'
|
||||||
resetAuthenticationError: 'xstate.init'
|
resetAuthenticationError: 'xstate.init'
|
||||||
clearContext: 'xstate.init'
|
destroyRefreshToken: 'xstate.init'
|
||||||
destroyToken: 'SIGNOUT'
|
clearContextExceptRefreshToken: 'SIGNOUT'
|
||||||
resetSignUpError: 'SIGNUP_EMAIL_PASSWORD'
|
resetSignUpError: 'SIGNUP_EMAIL_PASSWORD'
|
||||||
reportSignedIn:
|
reportSignedIn:
|
||||||
| 'SESSION_UPDATE'
|
| 'SESSION_UPDATE'
|
||||||
@@ -136,13 +136,14 @@ export interface Typegen0 {
|
|||||||
}
|
}
|
||||||
'error.platform.signInMfaTotp': { type: 'error.platform.signInMfaTotp'; data: unknown }
|
'error.platform.signInMfaTotp': { type: 'error.platform.signInMfaTotp'; data: unknown }
|
||||||
'error.platform.registerUser': { type: 'error.platform.registerUser'; data: unknown }
|
'error.platform.registerUser': { type: 'error.platform.registerUser'; data: unknown }
|
||||||
'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending': {
|
'error.platform.refreshToken': { type: 'error.platform.refreshToken'; data: unknown }
|
||||||
type: 'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending'
|
|
||||||
}
|
|
||||||
'error.platform.authenticateWithToken': {
|
'error.platform.authenticateWithToken': {
|
||||||
type: 'error.platform.authenticateWithToken'
|
type: 'error.platform.authenticateWithToken'
|
||||||
data: unknown
|
data: unknown
|
||||||
}
|
}
|
||||||
|
'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending': {
|
||||||
|
type: 'xstate.after(1000)#nhost.authentication.signedIn.refreshTimer.running.pending'
|
||||||
|
}
|
||||||
'error.platform.autoSignIn': { type: 'error.platform.autoSignIn'; data: unknown }
|
'error.platform.autoSignIn': { type: 'error.platform.autoSignIn'; data: unknown }
|
||||||
'xstate.init': { type: 'xstate.init' }
|
'xstate.init': { type: 'xstate.init' }
|
||||||
'error.platform.importRefreshToken': {
|
'error.platform.importRefreshToken': {
|
||||||
@@ -165,7 +166,6 @@ export interface Typegen0 {
|
|||||||
data: unknown
|
data: unknown
|
||||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
}
|
}
|
||||||
'error.platform.refreshToken': { type: 'error.platform.refreshToken'; data: unknown }
|
|
||||||
}
|
}
|
||||||
invokeSrcNameMap: {
|
invokeSrcNameMap: {
|
||||||
autoSignIn: 'done.invoke.autoSignIn'
|
autoSignIn: 'done.invoke.autoSignIn'
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
# @nhost/hasura-auth-js
|
# @nhost/hasura-auth-js
|
||||||
|
|
||||||
|
## 1.0.14
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 5ee395e: Ensure the session is destroyed when signout is done
|
||||||
|
The user session, in particular the access token (JWT), was still available after sign out.
|
||||||
|
Any information about user session is now removed from the auth state as soon as the sign out action is called.
|
||||||
|
- e0cfcaf: fix and improve `nhost.auth.refreshSession`
|
||||||
|
`nhost.auth.refreshSession` is now functional and returns possible errors, or the user session if the token has been sucessfully refreshed.
|
||||||
|
If the user was previously not authenticated, it will sign them in. See [#286](https://github.com/nhost/nhost/issues/286)
|
||||||
|
- Updated dependencies [5ee395e]
|
||||||
|
- Updated dependencies [e0cfcaf]
|
||||||
|
- Updated dependencies [7b7527a]
|
||||||
|
- @nhost/core@0.3.13
|
||||||
|
|
||||||
|
## 1.0.13
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [7b5f00d]
|
||||||
|
- Updated dependencies [58e1485]
|
||||||
|
- @nhost/core@0.3.12
|
||||||
|
|
||||||
## 1.0.12
|
## 1.0.12
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/hasura-auth-js",
|
"name": "@nhost/hasura-auth-js",
|
||||||
"version": "1.0.12",
|
"version": "1.0.14",
|
||||||
"description": "Hasura-auth client",
|
"description": "Hasura-auth client",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
createResetPasswordMachine,
|
createResetPasswordMachine,
|
||||||
createSendVerificationEmailMachine,
|
createSendVerificationEmailMachine,
|
||||||
encodeQueryParameters,
|
encodeQueryParameters,
|
||||||
rewriteRedirectTo
|
NO_REFRESH_TOKEN,
|
||||||
|
rewriteRedirectTo,
|
||||||
|
TOKEN_REFRESHER_RUNNING_ERROR
|
||||||
} from '@nhost/core'
|
} from '@nhost/core'
|
||||||
|
|
||||||
import { getSession, isBrowser, localStorageGetter, localStorageSetter } from './utils/helpers'
|
import { getSession, isBrowser, localStorageGetter, localStorageSetter } from './utils/helpers'
|
||||||
@@ -107,12 +109,13 @@ export class HasuraAuthClient {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
interpreter.send({ type: 'SIGNUP_EMAIL_PASSWORD', email, password, options })
|
interpreter.send({ type: 'SIGNUP_EMAIL_PASSWORD', email, password, options })
|
||||||
interpreter.onTransition((state) => {
|
interpreter.onTransition((state) => {
|
||||||
if (state.matches({ authentication: { signedOut: 'needsEmailVerification' } }))
|
if (state.matches({ authentication: { signedOut: 'needsEmailVerification' } })) {
|
||||||
return resolve({ session: null, error: null })
|
return resolve({ session: null, error: null })
|
||||||
else if (state.matches({ authentication: { signedOut: 'failed' } })) {
|
} else if (state.matches({ authentication: { signedOut: 'failed' } })) {
|
||||||
return resolve({ session: null, error: state.context.errors.registration || null })
|
return resolve({ session: null, error: state.context.errors.registration || null })
|
||||||
} else if (state.matches({ authentication: 'signedIn' }))
|
} else if (state.matches({ authentication: 'signedIn' })) {
|
||||||
return resolve({ session: getSession(state.context), error: null })
|
return resolve({ session: getSession(state.context), error: null })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -169,30 +172,31 @@ export class HasuraAuthClient {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
interpreter.send({ type: 'SIGNIN_PASSWORD', ...params })
|
interpreter.send({ type: 'SIGNIN_PASSWORD', ...params })
|
||||||
interpreter.onTransition((state) => {
|
interpreter.onTransition((state) => {
|
||||||
if (state.matches({ authentication: 'signedIn' }))
|
if (state.matches({ authentication: 'signedIn' })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: getSession(state.context),
|
session: getSession(state.context),
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
else if (state.matches({ authentication: { signedOut: 'needsEmailVerification' } }))
|
} else if (state.matches({ authentication: { signedOut: 'needsEmailVerification' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: EMAIL_NEEDS_VERIFICATION
|
error: EMAIL_NEEDS_VERIFICATION
|
||||||
})
|
})
|
||||||
else if (state.matches({ authentication: { signedOut: 'needsMfa' } }))
|
} else if (state.matches({ authentication: { signedOut: 'needsMfa' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: state.context.mfa,
|
mfa: state.context.mfa,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
else if (state.matches({ authentication: { signedOut: 'failed' } }))
|
} else if (state.matches({ authentication: { signedOut: 'failed' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: state.context.errors.authentication || null
|
error: state.context.errors.authentication || null
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -201,18 +205,19 @@ export class HasuraAuthClient {
|
|||||||
if ('email' in params && !('otp' in params)) {
|
if ('email' in params && !('otp' in params)) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
interpreter.onTransition((state) => {
|
interpreter.onTransition((state) => {
|
||||||
if (state.matches({ authentication: { signedOut: 'needsEmailVerification' } }))
|
if (state.matches({ authentication: { signedOut: 'needsEmailVerification' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
else if (state.matches({ authentication: { signedOut: 'failed' } }))
|
} else if (state.matches({ authentication: { signedOut: 'failed' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: state.context.errors.authentication || null
|
error: state.context.errors.authentication || null
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
interpreter.send({ type: 'SIGNIN_PASSWORDLESS_EMAIL', ...params })
|
interpreter.send({ type: 'SIGNIN_PASSWORDLESS_EMAIL', ...params })
|
||||||
@@ -223,18 +228,19 @@ export class HasuraAuthClient {
|
|||||||
if ('phoneNumber' in params && !('otp' in params)) {
|
if ('phoneNumber' in params && !('otp' in params)) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
interpreter.onTransition((state) => {
|
interpreter.onTransition((state) => {
|
||||||
if (state.matches({ authentication: { signedOut: 'needsSmsOtp' } }))
|
if (state.matches({ authentication: { signedOut: 'needsSmsOtp' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
else if (state.matches({ authentication: { signedOut: 'failed' } }))
|
} else if (state.matches({ authentication: { signedOut: 'failed' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: state.context.errors.authentication || null
|
error: state.context.errors.authentication || null
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
interpreter.send({ type: 'SIGNIN_PASSWORDLESS_SMS', ...params })
|
interpreter.send({ type: 'SIGNIN_PASSWORDLESS_SMS', ...params })
|
||||||
})
|
})
|
||||||
@@ -244,18 +250,19 @@ export class HasuraAuthClient {
|
|||||||
if ('otp' in params) {
|
if ('otp' in params) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
interpreter.onTransition((state) => {
|
interpreter.onTransition((state) => {
|
||||||
if (state.matches({ authentication: 'signedIn' }))
|
if (state.matches({ authentication: 'signedIn' })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: getSession(state.context),
|
session: getSession(state.context),
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
else if (state.matches({ authentication: { signedOut: 'failed' } }))
|
} else if (state.matches({ authentication: { signedOut: 'failed' } })) {
|
||||||
resolve({
|
resolve({
|
||||||
session: null,
|
session: null,
|
||||||
mfa: null,
|
mfa: null,
|
||||||
error: state.context.errors.authentication || null
|
error: state.context.errors.authentication || null
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
interpreter.send({ type: 'SIGNIN_PASSWORDLESS_SMS_OTP', ...params })
|
interpreter.send({ type: 'SIGNIN_PASSWORDLESS_SMS_OTP', ...params })
|
||||||
})
|
})
|
||||||
@@ -278,13 +285,17 @@ export class HasuraAuthClient {
|
|||||||
*/
|
*/
|
||||||
async signOut(params?: { all?: boolean }): Promise<ApiSignOutResponse> {
|
async signOut(params?: { all?: boolean }): Promise<ApiSignOutResponse> {
|
||||||
const interpreter = await this.waitUntilReady()
|
const interpreter = await this.waitUntilReady()
|
||||||
if (!this.isAuthenticated()) return { error: USER_UNAUTHENTICATED }
|
if (!this.isAuthenticated()) {
|
||||||
|
return { error: USER_UNAUTHENTICATED }
|
||||||
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
interpreter.send({ type: 'SIGNOUT', all: params?.all })
|
interpreter.send({ type: 'SIGNOUT', all: params?.all })
|
||||||
interpreter.onTransition((state) => {
|
interpreter.onTransition((state) => {
|
||||||
if (state.matches({ authentication: { signedOut: 'success' } })) resolve({ error: null })
|
if (state.matches({ authentication: { signedOut: 'success' } })) {
|
||||||
else if (state.matches({ authentication: { signedOut: 'failed' } }))
|
resolve({ error: null })
|
||||||
|
} else if (state.matches({ authentication: { signedOut: { failed: 'server' } } })) {
|
||||||
resolve({ error: state.context.errors.signout || null })
|
resolve({ error: state.context.errors.signout || null })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -301,8 +312,11 @@ export class HasuraAuthClient {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const service = interpret(createResetPasswordMachine(this._client))
|
const service = interpret(createResetPasswordMachine(this._client))
|
||||||
service.onTransition(({ event }) => {
|
service.onTransition(({ event }) => {
|
||||||
if (event.type === 'ERROR') return resolve({ error: event.error })
|
if (event.type === 'ERROR') {
|
||||||
else if (event.type === 'SUCCESS') return resolve({ error: null })
|
return resolve({ error: event.error })
|
||||||
|
} else if (event.type === 'SUCCESS') {
|
||||||
|
return resolve({ error: null })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
service.start()
|
service.start()
|
||||||
service.send({ type: 'REQUEST', email, options })
|
service.send({ type: 'REQUEST', email, options })
|
||||||
@@ -321,8 +335,11 @@ export class HasuraAuthClient {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const service = interpret(createChangePasswordMachine(this._client))
|
const service = interpret(createChangePasswordMachine(this._client))
|
||||||
service.onTransition(({ event }) => {
|
service.onTransition(({ event }) => {
|
||||||
if (event.type === 'ERROR') return resolve({ error: event.error })
|
if (event.type === 'ERROR') {
|
||||||
else if (event.type === 'SUCCESS') return resolve({ error: null })
|
return resolve({ error: event.error })
|
||||||
|
} else if (event.type === 'SUCCESS') {
|
||||||
|
return resolve({ error: null })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
service.start()
|
service.start()
|
||||||
service.send({ type: 'REQUEST', password: params.newPassword })
|
service.send({ type: 'REQUEST', password: params.newPassword })
|
||||||
@@ -344,8 +361,11 @@ export class HasuraAuthClient {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const service = interpret(createSendVerificationEmailMachine(this._client))
|
const service = interpret(createSendVerificationEmailMachine(this._client))
|
||||||
service.onTransition(({ event }) => {
|
service.onTransition(({ event }) => {
|
||||||
if (event.type === 'ERROR') return resolve({ error: event.error })
|
if (event.type === 'ERROR') {
|
||||||
else if (event.type === 'SUCCESS') return resolve({ error: null })
|
return resolve({ error: event.error })
|
||||||
|
} else if (event.type === 'SUCCESS') {
|
||||||
|
return resolve({ error: null })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
service.start()
|
service.start()
|
||||||
service.send({ type: 'REQUEST', email: params.email, options: params.options })
|
service.send({ type: 'REQUEST', email: params.email, options: params.options })
|
||||||
@@ -364,8 +384,11 @@ export class HasuraAuthClient {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const service = interpret(createChangeEmailMachine(this._client))
|
const service = interpret(createChangeEmailMachine(this._client))
|
||||||
service.onTransition(({ event }) => {
|
service.onTransition(({ event }) => {
|
||||||
if (event.type === 'ERROR') return resolve({ error: event.error })
|
if (event.type === 'ERROR') {
|
||||||
else if (event.type === 'SUCCESS') return resolve({ error: null })
|
return resolve({ error: event.error })
|
||||||
|
} else if (event.type === 'SUCCESS') {
|
||||||
|
return resolve({ error: null })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
service.start()
|
service.start()
|
||||||
service.send({ type: 'REQUEST', email: newEmail, options })
|
service.send({ type: 'REQUEST', email: newEmail, options })
|
||||||
@@ -383,13 +406,15 @@ export class HasuraAuthClient {
|
|||||||
async deanonymize(params: DeanonymizeParams): Promise<ApiDeanonymizeResponse> {
|
async deanonymize(params: DeanonymizeParams): Promise<ApiDeanonymizeResponse> {
|
||||||
const interpreter = await this.waitUntilReady()
|
const interpreter = await this.waitUntilReady()
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!this.isAuthenticated() || !interpreter.state.context.user?.isAnonymous)
|
if (!this.isAuthenticated() || !interpreter.state.context.user?.isAnonymous) {
|
||||||
return { error: USER_NOT_ANONYMOUS }
|
return { error: USER_NOT_ANONYMOUS }
|
||||||
|
}
|
||||||
interpreter.onTransition((state) => {
|
interpreter.onTransition((state) => {
|
||||||
if (state.matches({ authentication: { signedIn: { deanonymizing: 'success' } } }))
|
if (state.matches({ authentication: { signedIn: { deanonymizing: 'success' } } })) {
|
||||||
resolve({ error: null })
|
resolve({ error: null })
|
||||||
else if (state.matches({ authentication: { signedIn: { deanonymizing: 'error' } } }))
|
} else if (state.matches({ authentication: { signedIn: { deanonymizing: 'error' } } })) {
|
||||||
resolve({ error: state.context.errors.authentication || null })
|
resolve({ error: state.context.errors.authentication || null })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
interpreter.start()
|
interpreter.start()
|
||||||
const { signInMethod, connection, ...options } = params
|
const { signInMethod, connection, ...options } = params
|
||||||
@@ -414,7 +439,9 @@ export class HasuraAuthClient {
|
|||||||
onTokenChanged(fn: OnTokenChangedFunction): Function {
|
onTokenChanged(fn: OnTokenChangedFunction): Function {
|
||||||
const listen = (interpreter: AuthInterpreter) =>
|
const listen = (interpreter: AuthInterpreter) =>
|
||||||
interpreter.onTransition(({ event, context }) => {
|
interpreter.onTransition(({ event, context }) => {
|
||||||
if (event.type === 'TOKEN_CHANGED') fn(getSession(context))
|
if (event.type === 'TOKEN_CHANGED') {
|
||||||
|
fn(getSession(context))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this._client.interpreter) {
|
if (this._client.interpreter) {
|
||||||
@@ -447,8 +474,9 @@ export class HasuraAuthClient {
|
|||||||
onAuthStateChanged(fn: AuthChangedFunction): Function {
|
onAuthStateChanged(fn: AuthChangedFunction): Function {
|
||||||
const listen = (interpreter: AuthInterpreter) =>
|
const listen = (interpreter: AuthInterpreter) =>
|
||||||
interpreter.onTransition(({ event, context }) => {
|
interpreter.onTransition(({ event, context }) => {
|
||||||
if (event.type === 'SIGNED_IN' || event.type === 'SIGNED_OUT')
|
if (event.type === 'SIGNED_IN' || event.type === 'SIGNED_OUT') {
|
||||||
fn(event.type, getSession(context))
|
fn(event.type, getSession(context))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if (this._client.interpreter) {
|
if (this._client.interpreter) {
|
||||||
const subscription = listen(this._client.interpreter)
|
const subscription = listen(this._client.interpreter)
|
||||||
@@ -529,7 +557,9 @@ export class HasuraAuthClient {
|
|||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
} {
|
} {
|
||||||
if (!this.isReady()) return { isAuthenticated: false, isLoading: true }
|
if (!this.isReady()) {
|
||||||
|
return { isAuthenticated: false, isLoading: true }
|
||||||
|
}
|
||||||
return { isAuthenticated: this.isAuthenticated(), isLoading: false }
|
return { isAuthenticated: this.isAuthenticated(), isLoading: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,24 +597,34 @@ export class HasuraAuthClient {
|
|||||||
*
|
*
|
||||||
* @docs https://docs.nhost.io/TODO
|
* @docs https://docs.nhost.io/TODO
|
||||||
*/
|
*/
|
||||||
async refreshSession(refreshToken?: string): Promise<void> {
|
async refreshSession(refreshToken?: string): Promise<{
|
||||||
|
session: Session | null
|
||||||
|
error: ApiError | null
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
const interpreter = await this.waitUntilReady()
|
const interpreter = await this.waitUntilReady()
|
||||||
if (interpreter.state.matches({ token: 'idle' })) return
|
if (interpreter.state.matches({ token: 'idle' }))
|
||||||
|
return { session: null, error: TOKEN_REFRESHER_RUNNING_ERROR }
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const token = refreshToken || interpreter.state.context.refreshToken.value
|
const token = refreshToken || interpreter.state.context.refreshToken.value
|
||||||
if (!token) return resolve()
|
if (!token) return resolve({ session: null, error: NO_REFRESH_TOKEN })
|
||||||
interpreter?.onTransition((state) => {
|
interpreter?.onTransition((state) => {
|
||||||
if (state.matches({ token: { idle: 'error' } })) resolve()
|
if (state.matches({ token: { idle: 'error' } }))
|
||||||
else if (state.event.type === 'TOKEN_CHANGED') resolve()
|
resolve({
|
||||||
|
session: null,
|
||||||
|
// * TODO get the error from xstate once it is implemented
|
||||||
|
error: { status: 400, message: 'Invalid refresh token' }
|
||||||
|
})
|
||||||
|
else if (state.event.type === 'TOKEN_CHANGED')
|
||||||
|
resolve({ session: getSession(state.context), error: null })
|
||||||
})
|
})
|
||||||
interpreter.send({
|
interpreter.send({
|
||||||
type: 'TRY_TOKEN',
|
type: 'TRY_TOKEN',
|
||||||
token
|
token
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} catch {
|
} catch (error: any) {
|
||||||
return
|
return { session: null, error: error.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +666,9 @@ export class HasuraAuthClient {
|
|||||||
if (!interpreter) {
|
if (!interpreter) {
|
||||||
throw Error('Auth interpreter not set')
|
throw Error('Auth interpreter not set')
|
||||||
}
|
}
|
||||||
if (interpreter.state.hasTag('ready')) return Promise.resolve(interpreter)
|
if (interpreter.state.hasTag('ready')) {
|
||||||
|
return Promise.resolve(interpreter)
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let timer: ReturnType<typeof setTimeout> = setTimeout(
|
let timer: ReturnType<typeof setTimeout> = setTimeout(
|
||||||
() => reject(`The state machine is not yet ready after ${TIMEOUT_IN_SECONS} seconds.`),
|
() => reject(`The state machine is not yet ready after ${TIMEOUT_IN_SECONS} seconds.`),
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ import { ClientStorage, ClientStorageType, Session } from './types'
|
|||||||
export const isBrowser = () => typeof window !== 'undefined'
|
export const isBrowser = () => typeof window !== 'undefined'
|
||||||
|
|
||||||
export const getSession = (context?: AuthContext): Session | null => {
|
export const getSession = (context?: AuthContext): Session | null => {
|
||||||
if (!context || !context.accessToken.value || !context.refreshToken.value) return null
|
if (
|
||||||
|
!context ||
|
||||||
|
!context.accessToken.value ||
|
||||||
|
!context.refreshToken.value ||
|
||||||
|
!context.accessToken.expiresAt
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
accessToken: context.accessToken.value,
|
accessToken: context.accessToken.value,
|
||||||
accessTokenExpiresIn: (context.accessToken.expiresAt.getTime() - Date.now()) / 1000,
|
accessTokenExpiresIn: (context.accessToken.expiresAt.getTime() - Date.now()) / 1000,
|
||||||
@@ -59,20 +66,29 @@ export const localStorageSetter = (
|
|||||||
checkStorageAccessors(clientStorage, ['setItem', 'removeItem'])
|
checkStorageAccessors(clientStorage, ['setItem', 'removeItem'])
|
||||||
|
|
||||||
return (key, value) => {
|
return (key, value) => {
|
||||||
if (value) clientStorage.setItem?.(key, value)
|
if (value) {
|
||||||
else clientStorage.removeItem?.(key)
|
clientStorage.setItem?.(key, value)
|
||||||
|
} else {
|
||||||
|
clientStorage.removeItem?.(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (clientStorageType === 'capacitor') {
|
} else if (clientStorageType === 'capacitor') {
|
||||||
checkStorageAccessors(clientStorage, ['set', 'remove'])
|
checkStorageAccessors(clientStorage, ['set', 'remove'])
|
||||||
return (key, value) => {
|
return (key, value) => {
|
||||||
if (value) clientStorage.set?.({ key, value })
|
if (value) {
|
||||||
else clientStorage.remove?.({ key })
|
clientStorage.set?.({ key, value })
|
||||||
|
} else {
|
||||||
|
clientStorage.remove?.({ key })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (clientStorageType === 'expo-secure-storage') {
|
} else if (clientStorageType === 'expo-secure-storage') {
|
||||||
checkStorageAccessors(clientStorage, ['setItemAsync', 'deleteItemAsync'])
|
checkStorageAccessors(clientStorage, ['setItemAsync', 'deleteItemAsync'])
|
||||||
return async (key, value) => {
|
return async (key, value) => {
|
||||||
if (value) await clientStorage.setItemAsync?.(key, value)
|
if (value) {
|
||||||
else clientStorage.deleteItemAsync?.(key)
|
await clientStorage.setItemAsync?.(key, value)
|
||||||
|
} else {
|
||||||
|
clientStorage.deleteItemAsync?.(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# @nhost/nextjs
|
# @nhost/nextjs
|
||||||
|
|
||||||
|
## 1.0.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [5ee395e]
|
||||||
|
- @nhost/react@0.5.6
|
||||||
|
- @nhost/nhost-js@1.1.3
|
||||||
|
|
||||||
|
## 1.0.16
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/nhost-js@1.1.2
|
||||||
|
- @nhost/react@0.5.5
|
||||||
|
|
||||||
## 1.0.15
|
## 1.0.15
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/nextjs",
|
"name": "@nhost/nextjs",
|
||||||
"version": "1.0.15",
|
"version": "1.0.17",
|
||||||
"description": "Nhost NextJS library",
|
"description": "Nhost NextJS library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @nhost/nhost-js
|
# @nhost/nhost-js
|
||||||
|
|
||||||
|
## 1.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [5ee395e]
|
||||||
|
- Updated dependencies [e0cfcaf]
|
||||||
|
- @nhost/hasura-auth-js@1.0.14
|
||||||
|
|
||||||
|
## 1.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/hasura-auth-js@1.0.13
|
||||||
|
|
||||||
## 1.1.1
|
## 1.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/nhost-js",
|
"name": "@nhost/nhost-js",
|
||||||
"version": "1.1.1",
|
"version": "1.1.3",
|
||||||
"description": "Nhost JavaScript SDK",
|
"description": "Nhost JavaScript SDK",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# @nhost/react-apollo
|
# @nhost/react-apollo
|
||||||
|
|
||||||
|
## 4.0.16
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [5ee395e]
|
||||||
|
- @nhost/react@0.5.6
|
||||||
|
- @nhost/apollo@0.4.3
|
||||||
|
|
||||||
|
## 4.0.15
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/apollo@0.4.2
|
||||||
|
- @nhost/react@0.5.5
|
||||||
|
|
||||||
## 4.0.14
|
## 4.0.14
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/react-apollo",
|
"name": "@nhost/react-apollo",
|
||||||
"version": "4.0.14",
|
"version": "4.0.16",
|
||||||
"description": "Nhost React Apollo client",
|
"description": "Nhost React Apollo client",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @nhost/react
|
# @nhost/react
|
||||||
|
|
||||||
|
## 0.5.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 5ee395e: Ensure the session is destroyed when signout is done
|
||||||
|
In the `useSignOut` hook, `signOut` now returns a promise. We are now sure the user session is empty once the promise is resolved.
|
||||||
|
- @nhost/nhost-js@1.1.3
|
||||||
|
|
||||||
|
## 0.5.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/nhost-js@1.1.2
|
||||||
|
|
||||||
## 0.5.4
|
## 0.5.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/react",
|
"name": "@nhost/react",
|
||||||
"version": "0.5.4",
|
"version": "0.5.6",
|
||||||
"description": "Nhost React library",
|
"description": "Nhost React library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -74,8 +74,21 @@ export const useAccessToken = () => {
|
|||||||
export const useSignOut = (stateAll: boolean = false) => {
|
export const useSignOut = (stateAll: boolean = false) => {
|
||||||
const service = useAuthInterpreter()
|
const service = useAuthInterpreter()
|
||||||
const signOut = (valueAll?: boolean | unknown) =>
|
const signOut = (valueAll?: boolean | unknown) =>
|
||||||
|
new Promise<{ isSuccess: boolean }>((resolve) => {
|
||||||
service.send({ type: 'SIGNOUT', all: typeof valueAll === 'boolean' ? valueAll : stateAll })
|
service.send({ type: 'SIGNOUT', all: typeof valueAll === 'boolean' ? valueAll : stateAll })
|
||||||
const isSuccess =
|
service.onTransition((state) => {
|
||||||
!!service.status && service.state.matches({ authentication: { signedOut: 'success' } })
|
if (state.matches({ authentication: { signedOut: 'success' } })) {
|
||||||
|
resolve({ isSuccess: true })
|
||||||
|
} else if (state.matches({ authentication: { signedOut: { failed: 'server' } } }))
|
||||||
|
resolve({ isSuccess: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSuccess = useSelector(
|
||||||
|
service,
|
||||||
|
(state) => state.matches({ authentication: { signedOut: 'success' } }),
|
||||||
|
(a, b) => a === b
|
||||||
|
)
|
||||||
|
|
||||||
return { signOut, isSuccess }
|
return { signOut, isSuccess }
|
||||||
}
|
}
|
||||||
|
|||||||
6857
pnpm-lock.yaml
generated
6857
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,4 @@ packages:
|
|||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
- 'docs'
|
- 'docs'
|
||||||
- '!**/test/**'
|
- '!**/test/**'
|
||||||
|
- 'examples/**'
|
||||||
|
|||||||
Reference in New Issue
Block a user