Compare commits

...

34 Commits

Author SHA1 Message Date
Pilou
bebf9e1f2b Merge pull request #443 from nhost/changeset-release/main
chore: update versions
2022-04-22 17:35:44 +02:00
github-actions[bot]
2413c10283 chore: update versions 2022-04-22 15:32:40 +00:00
Pilou
0f7fbdab97 Merge pull request #440 from nhost/fix/token-refresher
fix: improve reliability of the token refresher
2022-04-22 17:31:47 +02:00
Pilou
14e5fd63a6 Merge branch 'main' into fix/token-refresher 2022-04-22 17:27:11 +02:00
Pilou
2446913836 Merge pull request #439 from nhost/fix/refresh-session
fix: fix and improve `nhost.auth.refreshSession`
2022-04-22 17:22:47 +02:00
Pierre-Louis Mercereau
1f88a9f47a style: improve readability 2022-04-22 17:20:05 +02:00
Pierre-Louis Mercereau
261e37cda4 fix: fix nullable value miss 2022-04-22 16:50:30 +02:00
Pierre-Louis Mercereau
5ee395ea8e fix: ensure the session is destroyed when signout is done 2022-04-22 16:44:52 +02:00
Pilou
828633ffc9 Merge pull request #435 from nhost/chore/include-examples-in-monorepo
Chore/include examples in monorepo
2022-04-22 13:28:02 +02:00
Pierre-Louis Mercereau
7b7527a5e6 fix: improve reliability of the token refresher 2022-04-22 11:32:25 +02:00
Pilou
620566fa4d Merge pull request #438 from nhost/changeset-release/main
chore: update versions
2022-04-21 21:58:07 +02:00
github-actions[bot]
4ce8b88d27 chore: update versions 2022-04-21 15:45:25 +00:00
Pilou
28d25e46de Merge pull request #436 from nhost/fix/mixed-email-password-errors
fix: invalid password and email errors on sign up
2022-04-21 17:44:35 +02:00
Pierre-Louis Mercereau
e0cfcafead fix: fix and improve nhost.auth.refreshSession 2022-04-21 16:57:55 +02:00
Pilou
12bc30daa3 Merge pull request #437 from nhost/fix/broadcast-channel-react-native
fix: avoid error when BroadcastChannell is not available
2022-04-21 16:10:49 +02:00
Pierre-Louis Mercereau
7b5f00d10e fix: avoid error when BroadcastChannell is not available 2022-04-21 16:00:12 +02:00
Pierre-Louis Mercereau
58e1485c13 fix: invalid password and email errors on sign up 2022-04-21 14:46:30 +02:00
Johan Eliasson
a64f1c4396 typo 2022-04-21 13:54:09 +02:00
Pierre-Louis Mercereau
75a1428114 chore(examples): fix resolution error with nextjs 2022-04-21 13:42:23 +02:00
Pierre-Louis Mercereau
d82d830849 chore: update pnpm lock file 2022-04-21 13:23:13 +02:00
Pierre-Louis Mercereau
2def59fc6c chore: merge main 2022-04-21 13:21:36 +02:00
Pierre-Louis Mercereau
64ceb2c6bf docs: update examples readme 2022-04-21 12:46:05 +02:00
Pierre-Louis Mercereau
3ee007620c chore: use vite to build react-apollo-crm, and prettier examples 2022-04-21 12:40:19 +02:00
Pierre-Louis Mercereau
b9cf8172a0 chore(examples): include react-apollo and nextjs examples back in the monorepo workspace 2022-04-21 11:24:32 +02:00
Pilou
32edfb4a9f Merge pull request #432 from nhost/contributors-readme-action-GEao4TGXIq
contributors readme action update
2022-04-21 11:20:17 +02:00
Pilou
848db9b672 Merge pull request #433 from nhost/contributors-readme-action--71qTzGJKI
contributors readme action update
2022-04-21 11:19:43 +02:00
github-actions[bot]
3766921bcc contrib-readme-action has updated readme 2022-04-21 09:19:09 +00:00
Pilou
5546052b2c Merge pull request #430 from nhost/docs/update-example
Docs/update example
2022-04-21 11:18:54 +02:00
github-actions[bot]
c569b56d3d contrib-readme-action has updated readme 2022-04-21 09:18:43 +00:00
Pilou
52ffa84adb Merge pull request #431 from timpratim/patch-3
Changed the wording for better understanding
2022-04-21 11:18:26 +02:00
Pratim
b5ae438a8e Changed the wording for better understanding
Changed the wording for a better understanding of the permissions doc
2022-04-21 14:17:13 +05:30
Pierre-Louis Mercereau
fae05f7af2 docs: bump to latest version of hasura-auth 2022-04-21 10:38:15 +02:00
Pierre-Louis Mercereau
380d7fc8ce docs: bump versions 2022-04-21 10:36:53 +02:00
Pierre-Louis Mercereau
94132bbc7f docs: move allowed_redirect_urls option to the right place, and prettier files 2022-04-21 10:36:28 +02:00
133 changed files with 8437 additions and 22102 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
REACT_APP_BACKEND_URL=http://localhost:1337 VITE_NHOST_URL=http://localhost:1337

View File

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

View File

@@ -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"], }
}, }
};

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

View File

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

View File

@@ -1,6 +0,0 @@
import React from 'react'
import App from './App'
test('single noop test', () => {
expect(true).toBeTruthy()
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_NHOST_URL: string
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -1,6 +1,4 @@
mutation insertCustomerComment( mutation insertCustomerComment($customerComment: customerComments_insert_input!) {
$customerComment: customerComments_insert_input!
) {
insertCustomerComment(object: $customerComment) { insertCustomerComment(object: $customerComment) {
id id
} }

View File

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

View File

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

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&eacute;rifiez votre courriel</h2> <h2>V&eacute;rifiez votre courriel</h2>
<p>Utilisez ce lien pour v&eacute;rifier votre courriel:</p> <p>Utilisez ce lien pour v&eacute;rifier votre courriel:</p>
<p> <p>
<a href="${link}"> <a href="${link}"> V&eacute;rifier courriel </a>
V&eacute;rifier courriel
</a>
</p> </p>
</body> </body>
</html> </html>

View File

@@ -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&eacute;initializer votre mot de passe</h2> <h2>R&eacute;initializer votre mot de passe</h2>
<p>Utilisez ce lien pour r&eacute;initializer votre mot de passe:</p> <p>Utilisez ce lien pour r&eacute;initializer votre mot de passe:</p>
<p> <p>
<a href="${link}"> <a href="${link}"> R&eacute;initializer mot de passe </a>
R&eacute;initializer mot de passe
</a>
</p> </p>
</body> </body>
</html> </html>

View File

@@ -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&ccedil;on s&eacute;curitaire:</p> <p>Utilisez ce lien pour vous connecter de fa&ccedil;on s&eacute;curitaire:</p>
<p> <p>
<a href="${link}"> <a href="${link}"> Connexion </a>
Connexion
</a>
</p> </p>
</body> </body>
</html> </html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.`),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,3 +2,4 @@ packages:
- 'packages/**' - 'packages/**'
- 'docs' - 'docs'
- '!**/test/**' - '!**/test/**'
- 'examples/**'