Compare commits

...

79 Commits

Author SHA1 Message Date
Pilou
2fec74e501 Merge pull request #717 from nhost/changeset-release/main
chore: update versions
2022-06-21 14:10:10 +02:00
Pierre-Louis Mercereau
e94b28b3bc chore: avoid major bumps of peer dependencies 2022-06-21 13:54:06 +02:00
github-actions[bot]
f591f76256 chore: update versions 2022-06-21 11:42:44 +00:00
Pilou
58fb955dc6 Merge pull request #720 from nhost/docs/vue-use-reset-password
docs(vue): correct useResetPassword inline example
2022-06-21 13:41:33 +02:00
Johan Eliasson
f472d42ae9 Merge pull request #732 from nhost/contributors-readme-action-RQTOlQZRpc
contributors readme action update
2022-06-21 07:39:06 +02:00
github-actions[bot]
9993bea7ab contrib-readme-action has updated readme 2022-06-21 05:22:34 +00:00
Johan Eliasson
6f33fc6ce6 Merge pull request #728 from nhost/contributors-readme-action-0KCGh0rhTk
contributors readme action update
2022-06-21 07:22:16 +02:00
Johan Eliasson
b6858c5638 Merge pull request #731 from nhost/contributors-readme-action-yVNIRBZEXT
contributors readme action update
2022-06-21 07:20:27 +02:00
github-actions[bot]
9b01c3ba93 contrib-readme-action has updated readme 2022-06-21 05:19:56 +00:00
Johan Eliasson
fcfd6a9c13 Merge pull request #729 from kylehayes/patch-1
Clarifying how to open Hasura Console
2022-06-21 07:19:41 +02:00
Kyle Hayes
e4daefe637 Clarifying how to open Hasura Console
Updating the documentation to reflect the new location of the button to open Hasura Console.
2022-06-20 17:45:07 -07:00
github-actions[bot]
a0bcbb6269 contrib-readme-action has updated readme 2022-06-20 13:16:03 +00:00
Pilou
1ec1004507 Merge pull request #727 from muttenzer/docs/permission-variables
Correct permission variables section
2022-06-20 15:15:40 +02:00
Pierre-Louis Mercereau
756d996096 chore: changeset 2022-06-20 12:16:30 +02:00
Timo
c0f1d03c3c Correct permission variables section
Mention how Nhost prefixes the custom JWT claims locally and correct example path.
2022-06-19 16:52:43 +02:00
Pilou
af55789d07 Merge pull request #723 from nhost/contributors-readme-action-IeghAq7Be4
contributors readme action update
2022-06-18 08:11:22 +02:00
Pilou
cb28676895 Merge pull request #722 from nhost/contributors-readme-action-8WdPB9amrq
contributors readme action update
2022-06-18 08:10:11 +02:00
github-actions[bot]
939a3d1090 contrib-readme-action has updated readme 2022-06-18 06:08:16 +00:00
Pilou
0163d0588b Merge pull request #721 from nhost/contributors-readme-action--AZAZ22Nlk
contributors readme action update
2022-06-18 08:08:02 +02:00
github-actions[bot]
c4124f22b0 contrib-readme-action has updated readme 2022-06-18 06:06:29 +00:00
Pilou
7188b0971c Merge pull request #718 from nhost/remove-build-script-dashes
chore: remove build script dashes
2022-06-18 08:06:16 +02:00
github-actions[bot]
6ac969320c contrib-readme-action has updated readme 2022-06-18 05:56:16 +00:00
Johan Eliasson
414bc2e75b Merge pull request #719 from muttenzer/docs/permission-variables
Update permission variables section
2022-06-18 07:56:00 +02:00
Pierre-Louis Mercereau
6862e1e24d docs(vue): correct useResetPassword inline example 2022-06-18 07:04:33 +02:00
muttenzer
4b9deaa2f7 Update permission variables section
Further instructions of how to handle custom permission variables in development.
2022-06-17 23:53:05 +02:00
Pierre-Louis Mercereau
cf366cef35 chore: remove build script dashes 2022-06-17 21:02:44 +02:00
Pilou
00d041f6b4 Merge pull request #714 from nhost/test/anonymous
test: anonymous sign-in and deanonymisation
2022-06-17 19:53:50 +02:00
Pierre-Louis Mercereau
da06fef64e refactor: use aria-label 2022-06-17 15:07:51 +02:00
Pierre-Louis Mercereau
09be9582f8 Merge branch 'main' into test/anonymous 2022-06-17 15:02:50 +02:00
Pilou
6b26fed8ae Merge pull request #713 from nhost/complete-mfa
Complete email+password sign-in with MFA
2022-06-17 15:02:08 +02:00
Pierre-Louis Mercereau
5a62c66fc4 Merge branch 'main' into test/anonymous 2022-06-17 14:26:17 +02:00
Pilou
17e0e6d116 Merge pull request #711 from nhost/test/change-email-password
test: change email and password
2022-06-17 14:22:02 +02:00
Pierre-Louis Mercereau
938000e61b test: don't accept an invalid email/password 2022-06-17 11:53:47 +02:00
Pierre-Louis Mercereau
d700107222 test: add apollo tests 2022-06-17 11:12:13 +02:00
Pierre-Louis Mercereau
69d9e40187 chore: chore 2022-06-16 15:40:55 +02:00
Pierre-Louis Mercereau
fc5b18fdf0 test: add item to todo list when anonymous 2022-06-15 20:55:37 +02:00
Pierre-Louis Mercereau
fa3eb980a0 feat: allow anonymous users to use the todo list 2022-06-15 19:03:47 +02:00
Pierre-Louis Mercereau
efad3a2b08 chore: add comment 2022-06-15 17:20:42 +02:00
Pierre-Louis Mercereau
563fa4fe9b test: anonymous sign-in and deanonymisation 2022-06-15 17:19:09 +02:00
Pierre-Louis Mercereau
6f0a30059a feat: complete email+password sign-in with MFA 2022-06-15 13:37:52 +02:00
Pierre-Louis Mercereau
47cda5d716 chore: order 2022-06-15 10:08:09 +02:00
Pierre-Louis Mercereau
3f625ce9e1 test: change email and password 2022-06-15 10:03:23 +02:00
Pilou
38d2609249 Merge pull request #710 from nhost/roles-docs
Info about allowed roles
2022-06-15 09:10:56 +02:00
Pilou
030243cd45 Merge pull request #691 from nhost/e2e-react-tests
test: passwordless email, sign-in with token, sign-out
2022-06-15 09:08:32 +02:00
Johan Eliasson
c1905243d0 roles info 2022-06-14 22:40:15 +02:00
Johan Eliasson
37627cc50e info about allowed roles 2022-06-14 21:44:14 +02:00
Pierre-Louis Mercereau
009f68d500 test: should get a session from localStorage 2022-06-14 17:44:35 +02:00
Pierre-Louis Mercereau
b752cc2be8 test: add assertions 2022-06-13 18:35:43 +02:00
Pierre-Louis Mercereau
72fc7d4e44 Merge branch 'main' into e2e-react-tests 2022-06-13 09:31:26 +02:00
Pilou
80ef14e50a Merge pull request #700 from nhost/react-example-todo-list
docs: replace the `books` table by a `todos` table
2022-06-13 08:23:55 +02:00
Pilou
543ea2a0e7 Merge pull request #706 from nhost/changeset-release/main
chore: update versions
2022-06-12 21:58:05 +02:00
github-actions[bot]
6764d476fd chore: update versions 2022-06-12 19:42:37 +00:00
Pilou
7bed0eadc9 Merge pull request #704 from nhost/fix/vue-nested-unref
Correct use of ref values in action options
2022-06-12 21:41:41 +02:00
Pierre-Louis Mercereau
c7644ace34 test: should return the same value when not a ref 2022-06-11 22:05:41 +02:00
Pierre-Louis Mercereau
49cdb2843e test: add one test 2022-06-11 21:58:03 +02:00
Pierre-Louis Mercereau
6f45856c46 fix: nestedUnref 2022-06-11 21:54:21 +02:00
Pierre-Louis Mercereau
61e719eea0 refactor: use findByRole 2022-06-10 18:37:13 +02:00
Pilou
208bdbba2d Merge pull request #703 from nhost/docs/fix-dependency
Use internal `*` dependencies in examples
2022-06-10 17:31:19 +02:00
Pierre-Louis Mercereau
cd62e1e833 docs: use internal * dependencies in examples 2022-06-10 16:47:32 +02:00
Pierre-Louis Mercereau
1dfb11d7e8 Merge branch 'e2e-react-tests' into react-example-todo-list 2022-06-10 14:30:07 +02:00
Pierre-Louis Mercereau
8b5c4ed443 refactor: make tests independent from each other 2022-06-10 14:28:03 +02:00
Pierre-Louis Mercereau
6bd5c96ed5 chore: modify autogenerated down migration 2022-06-10 11:58:52 +02:00
Pierre-Louis Mercereau
6b8762a62e chore: add missing change 2022-06-10 11:56:37 +02:00
Pierre-Louis Mercereau
ddeff7cbd6 refactor: rename index name 2022-06-10 11:55:17 +02:00
Pierre-Louis Mercereau
ed952c1251 perf: add index 2022-06-10 11:53:30 +02:00
Pierre-Louis Mercereau
34e73f18bd chore: remove .vscode directory 2022-06-10 11:44:12 +02:00
Pierre-Louis Mercereau
84262a24f1 docs: replace the books table by a todos table
adapt the Apollo page to add a todo item, add permissions so the connected user only sees its own
todos, add cypress test, use GraphQL codegen
2022-06-10 11:33:12 +02:00
Pierre-Louis Mercereau
ec2a88d69c chore: remove useless line 2022-06-09 11:40:22 +02:00
Pierre-Louis Mercereau
fe1049df6b test: token should be refresh on time 2022-06-09 11:39:18 +02:00
Pierre-Louis Mercereau
a924d21815 Merge remote-tracking branch 'origin/main' into e2e-react-tests 2022-06-09 09:09:03 +02:00
Pierre-Louis Mercereau
4405535d4a chore: adjustments 2022-06-09 08:43:08 +02:00
Pierre-Louis Mercereau
af15771517 test: simulate network errors on sign-un and sign-in 2022-06-08 16:01:09 +02:00
Pierre-Louis Mercereau
c066ea5b75 ci: tranform package name into a valid file name 2022-06-08 14:05:11 +02:00
Pierre-Louis Mercereau
80e42b939b make it fail 2022-06-08 13:36:19 +02:00
Pierre-Louis Mercereau
3ea6f685e2 Merge remote-tracking branch 'origin/main' into e2e-react-tests 2022-06-08 13:27:57 +02:00
Pierre-Louis Mercereau
ac77f427c3 Merge remote-tracking branch 'origin/main' into e2e-react-tests 2022-06-08 13:01:46 +02:00
Pierre-Louis Mercereau
0f95ee5bb4 chore: rename 2022-06-08 11:26:41 +02:00
Pierre-Louis Mercereau
47406d3617 test: passwordless email, sign-in with token, sign-out 2022-06-08 11:24:18 +02:00
Pierre-Louis Mercereau
125bc9a749 test: passwordless email, sign-in with token, sign-out 2022-06-08 11:14:50 +02:00
120 changed files with 1805 additions and 374 deletions

View File

@@ -67,12 +67,18 @@ jobs:
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
- name: Run e2e test
run: pnpm run e2e -- --filter="${{ matrix.package.name }}"
- id: file-name
if: ${{ failure() }}
name: Tranform package name into a valid file name
run: |
PACKAGE_FILE_NAME=$(echo "${{ matrix.package.name }}" | sed 's/@//g; s/\//-/g')
echo "::set-output name=fileName::$PACKAGE_FILE_NAME"
# * Run this step only if the previous step failed, and some Cypress screenshots/videos exist
- name: Upload Cypress videos and screenshots
if: ${{ failure() && hashFiles(format('{0}/cypress/screenshots/**', matrix.package.path), format('{0}/cypress/videos/**', matrix.package.path)) != ''}}
uses: actions/upload-artifact@v3
with:
name: cypress-${{ matrix.package.name }}
name: cypress-${{ steps.file-name.outputs.fileName }}
path: |
${{format('{0}/cypress/screenshots/**', matrix.package.path)}}
${{format('{0}/cypress/videos/**', matrix.package.path)}}

View File

@@ -285,21 +285,28 @@ Here are some ways of contributing to making Nhost better:
<sub><b>Savin Vadim</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/muttenzer">
<img src="https://avatars.githubusercontent.com/u/49474412?v=4" width="100;" alt="muttenzer"/>
<br />
<sub><b>Muttenzer</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ahmic">
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
<br />
<sub><b>Amir Ahmic</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/akd-io">
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
<br />
<sub><b>Anders Kjær Damgaard</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Sonichigo">
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
@@ -334,15 +341,15 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Helio Alves</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/nkhdo">
<img src="https://avatars.githubusercontent.com/u/26102306?v=4" width="100;" alt="nkhdo"/>
<br />
<sub><b>Hoang Do</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/MelodicCrypter">
<img src="https://avatars.githubusercontent.com/u/18341500?v=4" width="100;" alt="MelodicCrypter"/>
@@ -357,6 +364,13 @@ Here are some ways of contributing to making Nhost better:
<sub><b>Jacob Duval</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/kylehayes">
<img src="https://avatars.githubusercontent.com/u/509932?v=4" width="100;" alt="kylehayes"/>
<br />
<sub><b>Kyle Hayes</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/leothorp">
<img src="https://avatars.githubusercontent.com/u/12928449?v=4" width="100;" alt="leothorp"/>
@@ -370,7 +384,8 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Max Reynolds</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/ghoshnirmalya">
<img src="https://avatars.githubusercontent.com/u/6391763?v=4" width="100;" alt="ghoshnirmalya"/>
@@ -384,8 +399,7 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Quentin Decré</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/atapas">
<img src="https://avatars.githubusercontent.com/u/3633137?v=4" width="100;" alt="atapas"/>

View File

@@ -20,7 +20,8 @@ module.exports = {
'tests/**/*.ts',
'tests/**/*.d.ts'
],
plugins: ['@typescript-eslint', 'simple-import-sort'],
plugins: ['@typescript-eslint', 'simple-import-sort', 'cypress'],
extends: ['plugin:cypress/recommended'],
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
@@ -60,13 +61,5 @@ module.exports = {
allowAnonymousFunction: true
}
]
},
overrides: [
{
files: ['*.test.js', '*.spec.js', '*.test.ts', '*.spec.ts', '*.cy.js', '*.cy.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'off'
}
}
]
}
}

View File

@@ -1,6 +1,12 @@
const base = require('./.eslint.base')
module.exports = {
...base,
extends: ['react-app', 'plugin:react/recommended', 'plugin:react-hooks/recommended'],
extends: [
...base.extends,
'react-app',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:react/jsx-runtime'
],
plugins: [...base.plugins, 'react', 'react-hooks']
}

View File

@@ -1,7 +1,7 @@
const base = require('./.eslint.base')
module.exports = {
...base,
extends: ['plugin:import/recommended', 'plugin:import/typescript'],
extends: [...base.extends, 'plugin:import/recommended', 'plugin:import/typescript'],
parser: 'vue-eslint-parser',
parserOptions: {
...base.parserOptions,

View File

@@ -60,11 +60,35 @@ The default role is used when no role is specified in the GraphQL request. By de
### Allowed Roles
Allowed roles are roles the user is allowed to use when making a GraphQL request. Usually you would change the role from `user` (the default role) to some other role because you want Hasura to use a different role to resolve permissions for a particular GraphQL request.
By default, users have two allowed roles:
- `user`
- `me`
You can manage what allowed roles users should get when they sign up under **Users** -> **Roles & Permissions**.
:::info
You must also add the roles manually to the `auth.roles` table.
:::
It's also possible to give users a subset of allowed roles during signup.
**Example:** Only give the `user` role (without the `me` role) for the user's allowed roles:
```js
await nhost.auth.signUp({
email: 'joe@example.com',
password: 'secret-password'
options: {
allowedRoles: ['user']
}
})
```
### Public Role
The `public` role is used to resolve GraphQL permissions for unauthenticated users.

View File

@@ -21,7 +21,10 @@ The database is managed via the Hasura Console where you can manage the database
Hasura Console is where you manage your database. This is where you create and manage tables, schemas, and data.
Open the Hasura Console by clicking on **Data** in the top menu in the Nhost Dashboard, copy the **admin secret**, and click **Open Hasura**. Use the **admin secret** to sign in.
1) Open the Hasura Console by clicking on **GraphQL** in the top menu in the Nhost Dashboard.
2) Click **Open Hasura Console** at the top right of the page.
3) Copy the **admin secret**, and click **Open Hasura**.
4) Use the **admin secret** to sign in.
<video width="99%" autoPlay muted loop controls="true">
<source src="/videos/open-hasura-console.mp4" type="video/mp4" />

View File

@@ -54,6 +54,18 @@ query {
}
```
### Local Custom Permission Variables
To use custom permission variables locally, add your claims to the `config.yml` as following:
```
auth:
jwt:
custom_claims: '{"organisation-id":"profile.organisation.id"}'
```
Your custom claim will be automatically prefixed with `x-hasura-`, therefore, the example above results in a custom permission variable named `x-hasura-organisation-id`.
## Roles
Every GraphQL request is resolved based on a **single role**. Roles are added in the Hasura Console when selecting a table and clicking **Permisisons**.

View File

@@ -4,7 +4,7 @@ title: signUp()
sidebar_label: signUp()
slug: /reference/javascript/auth/sign-up
description: Use `nhost.auth.signUp` to sign up a user using email and password. If you want to sign up a user using passwordless email (Magic Link), SMS, or an OAuth provider, use the `signIn` function instead.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L101
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L102
---
# `signUp()`

View File

@@ -4,7 +4,7 @@ title: signIn()
sidebar_label: signIn()
slug: /reference/javascript/auth/sign-in
description: Use `nhost.auth.signIn` to sign in a user using email and password, passwordless (email or sms) or an external provider. `signIn` can be used to sign in a user in various ways depending on the parameters.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L144
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L145
---
# `signIn()`

View File

@@ -4,7 +4,7 @@ title: signOut()
sidebar_label: signOut()
slug: /reference/javascript/auth/sign-out
description: Use `nhost.auth.signOut` to sign out the user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L222
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L230
---
# `signOut()`

View File

@@ -4,7 +4,7 @@ title: resetPassword()
sidebar_label: resetPassword()
slug: /reference/javascript/auth/reset-password
description: Use `nhost.auth.resetPassword` to reset the password for a user. This will send a reset-password link in an email to the user. When the user clicks the reset-password link the user is automatically signed-in. Once signed-in, the user can change their password using `nhost.auth.changePassword()`.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L238
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L246
---
# `resetPassword()`

View File

@@ -4,7 +4,7 @@ title: changePassword()
sidebar_label: changePassword()
slug: /reference/javascript/auth/change-password
description: Use `nhost.auth.changePassword` to change the password for the user. The old password is not needed.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L254
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L262
---
# `changePassword()`

View File

@@ -4,7 +4,7 @@ title: sendVerificationEmail()
sidebar_label: sendVerificationEmail()
slug: /reference/javascript/auth/send-verification-email
description: Use `nhost.auth.sendVerificationEmail` to send a verification email to the specified email. The email contains a verification-email link. When the user clicks the verification-email link their email is verified.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L270
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L278
---
# `sendVerificationEmail()`

View File

@@ -4,7 +4,7 @@ title: changeEmail()
sidebar_label: changeEmail()
slug: /reference/javascript/auth/change-email
description: Use `nhost.auth.changeEmail` to change a user's email. This will send a confirm-email-change link in an email to the new email. Once the user clicks on the confirm-email-change link the email will be change to the new email.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L289
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L297
---
# `changeEmail()`

View File

@@ -4,7 +4,7 @@ title: deanonymize()
sidebar_label: deanonymize()
slug: /reference/javascript/auth/deanonymize
description: Use `nhost.auth.deanonymize` to deanonymize a user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L305
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L313
---
# `deanonymize()`

View File

@@ -4,7 +4,7 @@ title: onTokenChanged()
sidebar_label: onTokenChanged()
slug: /reference/javascript/auth/on-token-changed
description: Use `nhost.auth.onTokenChanged` to add a custom function that runs every time the access or refresh token is changed.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L348
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L356
---
# `onTokenChanged()`

View File

@@ -4,7 +4,7 @@ title: onAuthStateChanged()
sidebar_label: onAuthStateChanged()
slug: /reference/javascript/auth/on-auth-state-changed
description: Use `nhost.auth.onAuthStateChanged` to add a custom function that runs every time the authentication status of the user changes. E.g. add a custom function that runs every time the authentication status changes from signed-in to signed-out.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L383
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L391
---
# `onAuthStateChanged()`

View File

@@ -4,7 +4,7 @@ title: isAuthenticated()
sidebar_label: isAuthenticated()
slug: /reference/javascript/auth/is-authenticated
description: Use `nhost.auth.isAuthenticated` to check if the user is authenticated or not.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L425
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L433
---
# `isAuthenticated()`

View File

@@ -4,7 +4,7 @@ title: isAuthenticatedAsync()
sidebar_label: isAuthenticatedAsync()
slug: /reference/javascript/auth/is-authenticated-async
description: Use `nhost.auth.isAuthenticatedAsync` to wait (await) for any internal authentication network requests to finish and then return the authentication status.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L443
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L451
---
# `isAuthenticatedAsync()`

View File

@@ -4,7 +4,7 @@ title: getAuthenticationStatus()
sidebar_label: getAuthenticationStatus()
slug: /reference/javascript/auth/get-authentication-status
description: Use `nhost.auth.getAuthenticationStatus` to get the authentication status of the user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L469
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L477
---
# `getAuthenticationStatus()`

View File

@@ -4,7 +4,7 @@ title: getAccessToken()
sidebar_label: getAccessToken()
slug: /reference/javascript/auth/get-access-token
description: Use `nhost.auth.getAccessToken` to get the access token of the user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L499
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L507
---
# `getAccessToken()`

View File

@@ -4,7 +4,7 @@ title: getDecodedAccessToken()
sidebar_label: getDecodedAccessToken()
slug: /reference/javascript/auth/get-decoded-access-token
description: Use `nhost.auth.getDecodedAccessToken` to get the decoded access token of the user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L514
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L522
---
# `getDecodedAccessToken()`

View File

@@ -4,7 +4,7 @@ title: getHasuraClaims()
sidebar_label: getHasuraClaims()
slug: /reference/javascript/auth/get-hasura-claims
description: Use `nhost.auth.getHasuraClaims` to get the Hasura claims of the user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L531
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L539
---
# `getHasuraClaims()`

View File

@@ -4,7 +4,7 @@ title: getHasuraClaim()
sidebar_label: getHasuraClaim()
slug: /reference/javascript/auth/get-hasura-claim
description: Use `nhost.auth.getHasuraClaim` to get the value of a specific Hasura claim of the user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L549
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L557
---
# `getHasuraClaim()`

View File

@@ -4,7 +4,7 @@ title: refreshSession()
sidebar_label: refreshSession()
slug: /reference/javascript/auth/refresh-session
description: Use `nhost.auth.refreshSession` to refresh the session with either the current internal refresh token or an external refresh token.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L572
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L580
---
# `refreshSession()`

View File

@@ -4,7 +4,7 @@ title: getSession()
sidebar_label: getSession()
slug: /reference/javascript/auth/get-session
description: Use `nhost.auth.getSession()` to get the session of the user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L616
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L624
---
# `getSession()`

View File

@@ -4,7 +4,7 @@ title: getUser()
sidebar_label: getUser()
slug: /reference/javascript/auth/get-user
description: Use `nhost.auth.getUser()` to get the signed-in user.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L631
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L639
---
# `getUser()`

View File

@@ -4,7 +4,7 @@ title: HasuraAuthClient
sidebar_label: Auth
description: No description provided.
slug: /reference/javascript/auth
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L59
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/hasura-auth-client.ts#L60
---
# `HasuraAuthClient`

View File

@@ -4,7 +4,7 @@ title: AuthChangeEvent
sidebar_label: AuthChangeEvent
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L128
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L134
---
# `AuthChangeEvent`

View File

@@ -4,7 +4,7 @@ title: AuthChangedFunction
sidebar_label: AuthChangedFunction
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L130
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L136
---
# `AuthChangedFunction`

View File

@@ -4,7 +4,7 @@ title: ChangeEmailParams
sidebar_label: ChangeEmailParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L99
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L105
---
# `ChangeEmailParams`

View File

@@ -4,7 +4,7 @@ title: ChangePasswordParams
sidebar_label: ChangePasswordParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L90
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L96
---
# `ChangePasswordParams`

View File

@@ -4,7 +4,7 @@ title: DeanonymizeParams
sidebar_label: DeanonymizeParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L104
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L110
---
# `DeanonymizeParams`

View File

@@ -4,7 +4,7 @@ title: OnTokenChangedFunction
sidebar_label: OnTokenChangedFunction
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L132
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L138
---
# `OnTokenChangedFunction`

View File

@@ -4,7 +4,7 @@ title: ResetPasswordParams
sidebar_label: ResetPasswordParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L85
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L91
---
# `ResetPasswordParams`

View File

@@ -4,7 +4,7 @@ title: SendVerificationEmailParams
sidebar_label: SendVerificationEmailParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L94
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L100
---
# `SendVerificationEmailParams`

View File

@@ -1,19 +1,19 @@
---
# ⚠️ AUTO-GENERATED CONTENT. DO NOT EDIT THIS FILE DIRECTLY! ⚠️
title: LoginData
sidebar_label: LoginData
title: SignInEmailPasswordOtpParams
sidebar_label: SignInEmailPasswordOtpParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L134
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L59
---
# `LoginData`
# `SignInEmailPasswordOtpParams`
## Parameters
---
**<span className="parameter-name">mfa</span>** <span className="optional-status">optional</span> `boolean`
**<span className="parameter-name">otp</span>** <span className="optional-status">required</span> `string`
---

View File

@@ -4,7 +4,7 @@ title: SignInParams
sidebar_label: SignInParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L78
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L83
---
# `SignInParams`
@@ -12,6 +12,7 @@ custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-j
```ts
type SignInParams =
| SignInEmailPasswordParams
| SignInEmailPasswordOtpParams
| SignInPasswordlessEmailParams
| SignInPasswordlessSmsOtpParams
| SignInPasswordlessSmsParams

View File

@@ -4,7 +4,7 @@ title: SignInPasswordlessEmailParams
sidebar_label: SignInPasswordlessEmailParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L59
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L64
---
# `SignInPasswordlessEmailParams`

View File

@@ -4,7 +4,7 @@ title: SignInPasswordlessSmsOtpParams
sidebar_label: SignInPasswordlessSmsOtpParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L69
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L74
---
# `SignInPasswordlessSmsOtpParams`

View File

@@ -4,7 +4,7 @@ title: SignInPasswordlessSmsParams
sidebar_label: SignInPasswordlessSmsParams
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L64
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L69
---
# `SignInPasswordlessSmsParams`

View File

@@ -4,7 +4,7 @@ title: SignInReponse
sidebar_label: SignInReponse
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L117
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L123
---
# `SignInReponse`

View File

@@ -4,7 +4,7 @@ title: SignInWithProviderOptions
sidebar_label: SignInWithProviderOptions
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L73
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/hasura-auth-js/src/utils/types.ts#L78
---
# `SignInWithProviderOptions`

View File

@@ -4,7 +4,7 @@ title: useSignInEmailPassword()
sidebar_label: useSignInEmailPassword()
slug: /reference/nextjs/use-sign-in-email-password
description: Use the hook `useSignInEmailPassword` to sign in a user using email and password.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L49
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L54
---
# `useSignInEmailPassword()`

View File

@@ -4,7 +4,7 @@ title: SignInEmailPasswordHookResult
sidebar_label: SignInEmailPasswordHookResult
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L19
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L24
---
# `SignInEmailPasswordHookResult`

View File

@@ -4,7 +4,7 @@ title: useSignInEmailPassword()
sidebar_label: useSignInEmailPassword()
slug: /reference/react/use-sign-in-email-password
description: Use the hook `useSignInEmailPassword` to sign in a user using email and password.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L49
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L54
---
# `useSignInEmailPassword()`

View File

@@ -4,7 +4,7 @@ title: SignInEmailPasswordHookResult
sidebar_label: SignInEmailPasswordHookResult
description: No description provided.
displayed_sidebar: referenceSidebar
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L19
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/react/src/useSignInEmailPassword.ts#L24
---
# `SignInEmailPasswordHookResult`

View File

@@ -12,7 +12,9 @@ custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/vue/src/useRe
Use the composable `useResetPassword` to reset the password for a user. This will send a reset password link in an email to the user. When the user clicks on the reset-password link the user is automatically signed in and can change their password using the composable `useChangePassword`.
```tsx
const { resetPassword, isLoading, isSent, isError, error } = useResetPassword()
const { resetPassword, isLoading, isSent, isError, error } = useResetPassword({
redirectTo: 'http://localhost:3000/settings/change-password'
})
watchEffect(() => {
console.log(isLoading.value, isSent.value, isError.value, error.value)
@@ -21,9 +23,7 @@ watchEffect(() => {
const handleFormSubmit = async (e) => {
e.preventDefault()
await resetPassword('joe@example.com', {
redirectTo: 'http://localhost:3000/settings/change-password'
})
await resetPassword('joe@example.com')
}
```

View File

@@ -4,7 +4,7 @@ title: useSignInEmailPassword()
sidebar_label: useSignInEmailPassword()
slug: /reference/vue/use-sign-in-email-password
description: Use the composable `useSignInEmailPassword` to sign in a user using email and password.
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/vue/src/useSignInEmailPassword.ts#L44
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/vue/src/useSignInEmailPassword.ts#L46
---
# `useSignInEmailPassword()`

View File

@@ -20,9 +20,9 @@
"@mantine/hooks": "^4.2.2",
"@mantine/next": "^4.2.2",
"@mantine/notifications": "^4.2.2",
"@nhost/nextjs": "workspace:*",
"@nhost/react": "workspace:*",
"@nhost/react-apollo": "workspace:*",
"@nhost/nextjs": "*",
"@nhost/react": "*",
"@nhost/react-apollo": "*",
"graphql": "^16.3.0",
"next": "12.1.6",
"react": "18.1.0",

View File

@@ -6,8 +6,8 @@
"@apollo/client": "^3.5.10",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.6",
"@nhost/react": "workspace:*",
"@nhost/react-apollo": "workspace:*",
"@nhost/react": "*",
"@nhost/react-apollo": "*",
"@tailwindcss/forms": "^0.5.0",
"classnames": "^2.3.1",
"date-fns": "^2.28.0",

View File

@@ -6,6 +6,9 @@ export default defineConfig({
chromeWebSecurity: false,
// * for some reason, the mailhog API is not systematically available
// * when using `localhost` instead of `127.0.0.1`
mailHogUrl: 'http://127.0.0.1:8025'
mailHogUrl: 'http://127.0.0.1:8025',
env: {
backendUrl: 'http://localhost:1337'
}
}
} as Cypress.ConfigOptions)

View File

@@ -1,12 +0,0 @@
import { faker } from '@faker-js/faker'
context('Failed attempt to sign up with an email already present in the database', () => {
it('shoud raise an error when trying to sign up with an existing email', () => {
const email = faker.internet.email()
const password = faker.internet.password(10)
cy.signUpEmailPassword(email, password)
cy.contains('Verification email sent').should('be.visible')
cy.signUpEmailPassword(email, password)
cy.contains('Email already in use').should('be.visible')
})
})

View File

@@ -1,16 +0,0 @@
import { faker } from '@faker-js/faker'
context('Successful email+password sign-up', () => {
it('should redirect to /sign-in when not authenticated', () => {
cy.visit('/')
cy.location('pathname').should('equal', '/sign-in')
})
it('should sign-up with email and password', () => {
const email = faker.internet.email()
cy.signUpEmailPassword(email, faker.internet.password())
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
cy.contains('You are authenticated')
})
})

View File

@@ -0,0 +1,8 @@
context('Authentication guards', () => {
it('should redirect to /sign-in when not authenticated', () => {
cy.visit('/')
cy.location('pathname').should('equal', '/sign-in')
cy.visit('/apollo')
cy.location('pathname').should('equal', '/sign-in')
})
})

View File

@@ -0,0 +1,51 @@
import { faker } from '@faker-js/faker'
context('Anonymous users', () => {
beforeEach(() => {
cy.signInAnonymous()
})
it('should sign-up anonymously', () => {
cy.contains('You signed in anonymously')
})
it('should deanonymise with email+password', () => {
cy.fetchUserData()
.its('id')
.then((id) => {
const email = faker.internet.email()
const password = faker.internet.password()
cy.signUpEmailPassword(email, password)
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
cy.contains('You signed in anonymously').should('not.exist')
cy.fetchUserData().then((user) => {
cy.wrap(user).its('id').should('equal', id)
cy.wrap(user).its('email').should('equal', email)
})
})
})
it('should deanonymise with passwordless email', () => {
cy.fetchUserData()
.its('id')
.then((id) => {
const email = faker.internet.email()
cy.signUpEmailPasswordless(email)
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
cy.goToHomePage()
cy.contains('You signed in anonymously').should('not.exist')
cy.fetchUserData().then((user) => {
cy.wrap(user).its('id').should('equal', id)
cy.wrap(user).its('email').should('equal', email)
})
})
})
// TODO implement deanonymisation with Oauth?
// TODO forbid email/password change, MFA activation, and password reset when the following PR is released
// * https://github.com/nhost/hasura-auth/pull/190
})

View File

@@ -0,0 +1,28 @@
import { faker } from '@faker-js/faker'
context('Sign up with email+password', () => {
it('should sign-up with email and password', () => {
const email = faker.internet.email()
const password = faker.internet.password()
cy.signUpEmailPassword(email, password)
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
cy.contains('You are authenticated')
})
it('shoud raise an error when trying to sign up with an existing email', () => {
const email = faker.internet.email()
const password = faker.internet.password(10)
cy.signUpEmailPassword(email, password)
cy.contains('Verification email sent').should('be.visible')
cy.signUpEmailPassword(email, password)
cy.contains('Email already in use').should('be.visible')
})
// TODO implement in the UI
it.skip('should fail when network is not available', () => {
cy.disconnectBackend()
cy.signUpEmailPassword(faker.internet.email(), faker.internet.password())
cy.contains('Error').should('be.visible')
})
})

View File

@@ -0,0 +1,17 @@
import { faker } from '@faker-js/faker'
context('Sign up with passwordless email', () => {
it('should sign-up with passwordless email', () => {
const email = faker.internet.email()
cy.signUpEmailPasswordless(email)
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
cy.contains('Profile page')
})
it('should fail when network is not available', () => {
cy.disconnectBackend()
cy.signUpEmailPasswordless(faker.internet.email())
cy.contains('Error').should('be.visible')
})
})

View File

@@ -0,0 +1,78 @@
import totp from 'totp-generator'
import faker from '@faker-js/faker'
import { Decoder } from '@nuintun/qrcode'
context('Sign in with email+password', () => {
it('should sign-in with email and password', () => {
const email = faker.internet.email()
const password = faker.internet.password()
cy.signUpEmailPassword(email, password)
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
cy.signOut()
cy.contains('Log in to the Application').should('be.visible')
cy.signInEmailPassword(email, password)
cy.contains('You are authenticated')
})
// TODO implement in the UI
it.skip('should fail when network is not available', () => {
const email = faker.internet.email()
const password = faker.internet.password()
cy.disconnectBackend()
cy.signInEmailPassword(email, password)
cy.contains('Error').should('be.visible')
})
it('should activate and sign-in with MFA', () => {
// * Sign-up with email+password
const email = faker.internet.email()
const password = faker.internet.email()
cy.signUpEmailPassword(email, password)
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
cy.getNavBar()
.findByRole('button', { name: /Profile/i })
.click()
cy.findByText(/Activate 2-step verification/i)
.parent()
.findByRole('button')
.click()
cy.findByText(/Activate 2-step verification/i)
.get('img')
.then(async (img) => {
// * Activate MFA
const result = await new Decoder().scan(img.prop('src'))
const [, params] = result.data.split('?')
const { secret, algorithm, digits, period } = Object.fromEntries(
new URLSearchParams(params)
)
const code = totp(secret, {
algorithm: algorithm.replace('SHA1', 'SHA-1'),
digits: parseInt(digits),
period: parseInt(period)
})
cy.findByPlaceholderText('Enter activation code').type(code)
cy.findByRole('button', { name: /Activate/i }).click()
cy.contains('MFA has been activated!!!')
cy.signOut()
// * Sign-in with MFA
cy.visit('/sign-in')
cy.findByRole('button', { name: /Continue with email \+ password/i }).click()
cy.findByPlaceholderText('Email Address').type(email)
cy.findByPlaceholderText('Password').type(password)
cy.findByRole('button', { name: /Sign in/i }).click()
cy.contains('Send 2-step verification code')
const newCode = totp(secret, { timestamp: Date.now() })
cy.findByPlaceholderText('One-time password').type(newCode)
cy.findByRole('button', { name: /Send 2-step verification code/i }).click()
cy.contains('You are authenticated')
})
})
})

View File

@@ -0,0 +1,22 @@
context('Sign in with a refresh token', () => {
it('should sign-in with a refresh token', () => {
cy.signUpAndConfirmEmail()
cy.contains('Profile page')
cy.clearLocalStorage()
cy.reload()
cy.contains('Log in to the Application')
cy.visitPathWithRefreshToken('/profile')
cy.contains('Profile page')
})
it('should fail authentication when network is not available', () => {
cy.signUpAndConfirmEmail()
cy.contains('Profile page')
cy.disconnectBackend()
cy.clearLocalStorage()
cy.reload()
cy.contains('Log in to the Application')
cy.visitPathWithRefreshToken('/profile')
cy.location('pathname').should('equal', '/sign-in')
})
})

View File

@@ -0,0 +1,45 @@
import faker from '@faker-js/faker'
context('Apollo', () => {
const addItemTest = (sentence: string) => {
cy.getNavBar()
.findByRole('button', { name: /Apollo/i })
.click()
cy.contains('Todo list')
cy.focused().type(sentence)
cy.findByRole('button', { name: /Add/i }).click()
}
it('should add an item to the todo list when normally authenticated', () => {
cy.signUpAndConfirmEmail()
const sentence = faker.lorem.sentence()
addItemTest(sentence)
cy.get('li').contains(sentence)
})
it('should add an item to the todo list when anonymous', () => {
cy.signInAnonymous()
const sentence = faker.lorem.sentence()
addItemTest(sentence)
cy.get('li').contains(sentence)
})
it('should add an item to the todo list after a token refresh', () => {
// * This test has a limitation: Hasura's clock is not changing, so the previous JWT will still be valid.
cy.signUpAndConfirmEmail()
const now = Date.now()
cy.clock(now)
cy.tick(4 * 7 * 24 * 60 * 60 * 1000)
const sentence = faker.lorem.sentence()
addItemTest(sentence)
cy.get('li').contains(sentence)
})
it('should not add an item when backend is disconnected', () => {
cy.signUpAndConfirmEmail()
cy.disconnectBackend()
addItemTest(faker.lorem.sentence())
cy.contains('Network error')
cy.get('ul').should('be.empty')
})
})

View File

@@ -0,0 +1,30 @@
import faker from '@faker-js/faker'
context('Change email', () => {
it('should change email', () => {
const newEmail = faker.internet.email()
cy.signUpAndConfirmEmail()
cy.findByPlaceholderText('New email').type(newEmail)
cy.findByText(/Change Email/i)
.parent()
.findByRole('button')
.click()
cy.contains('Please check your inbox and follow the link to confirm the email change').should(
'be.visible'
)
cy.signOut()
cy.confirmEmail(newEmail)
cy.contains('Profile page')
})
it('should not accept an invalid email', () => {
const newEmail = faker.random.alphaNumeric()
cy.signUpAndConfirmEmail()
cy.findByPlaceholderText('New email').type(newEmail)
cy.findByText(/Change Email/i)
.parent()
.findByRole('button')
.click()
cy.contains('Email is incorrectly formatted').should('be.visible')
})
})

View File

@@ -0,0 +1,29 @@
import faker from '@faker-js/faker'
context('Change password', () => {
it('should change password', () => {
const email = faker.internet.email()
const newPassword = faker.internet.password()
cy.signUpAndConfirmEmail(email)
cy.findByPlaceholderText('New password').type(newPassword)
cy.findByText(/Change Password/i)
.parent()
.findByRole('button')
.click()
cy.contains('Password changed successfully').should('be.visible')
cy.signOut()
cy.signInEmailPassword(email, newPassword)
cy.contains('You are authenticated')
})
it('should not accept an invalid password', () => {
const newPassword = faker.random.alphaNumeric(2)
cy.signUpAndConfirmEmail()
cy.findByPlaceholderText('New password').type(newPassword)
cy.findByText(/Change Password/i)
.parent()
.findByRole('button')
.click()
cy.contains('Password is incorrectly formatted').should('be.visible')
})
})

View File

@@ -0,0 +1,13 @@
context('Sign out', () => {
beforeEach(() => {
cy.signUpAndConfirmEmail()
})
it('should sign out', () => {
cy.visitPathWithRefreshToken()
cy.goToProfilePage()
cy.contains('Profile page')
cy.signOut()
cy.contains('Log in to the Application')
})
})

View File

@@ -0,0 +1,30 @@
import faker from '@faker-js/faker'
context('Token refresh', () => {
it('should refresh token one minute before it expires', () => {
const email = faker.internet.email()
cy.signUpEmailPasswordless(email)
cy.contains('Verification email sent').should('be.visible')
const now = Date.now()
cy.clock(now)
cy.confirmEmail(email)
cy.intercept(Cypress.env('backendUrl') + '/v1/auth/token').as('tokenRequest')
cy.tick(14 * 60 * 1000)
cy.wait('@tokenRequest').its('response.statusCode').should('eq', 200)
})
it('should refresh session from localStorage after 4 weeks of inactivity', () => {
const email = faker.internet.email()
cy.signUpEmailPasswordless(email)
cy.contains('Verification email sent').should('be.visible')
const now = Date.now()
cy.clock(now)
cy.confirmEmail(email)
cy.contains('Profile page')
cy.tick(4 * 7 * 24 * 60 * 60 * 1000)
cy.reload()
cy.contains('Profile page')
})
})

View File

@@ -1,25 +1,85 @@
import { faker } from '@faker-js/faker'
import { User } from '@nhost/core'
import '@testing-library/cypress/add-commands'
import 'cypress-mailhog'
declare module 'mocha' {
export interface Context {
refreshToken?: string
}
}
declare global {
namespace Cypress {
interface Chainable {
signUpEmailPassword(email: string, password: string): Chainable<Element>
signUpEmailPasswordless(email: string): Chainable<Element>
signInEmailPassword(email: string, password: string): Chainable<Element>
signInAnonymous(): Chainable<Element>
/** Sign in from the refresh token stored in the global state */
visitPathWithRefreshToken(path?: string): Chainable<Element>
/** Click on the 'Sign Out' item of the left side menu to sign out the current user */
signOut(): Chainable<Element>
/** Run a sign-up + authentication sequence with passwordless to use an authenticated user in other tests */
signUpAndConfirmEmail(email?: string): Chainable<Element>
/** Gets a confirmation email and click on the link */
confirmEmail(email: string): Chainable<Element>
/** Save the refresh token in the global state so it can be reused with `this.refreshToken` */
saveRefreshToken(): Chainable<Element>
/** Make the Nhost backend unavailable */
disconnectBackend(): Chainable<Element>
/** Get the left side navigation bar */
getNavBar(): Chainable<Element>
/** Go to the profile page */
goToProfilePage(): Chainable<Element>
/** Go to the home page */
goToHomePage(): Chainable<Element>
/** Go getch the user ID in the profile page*/
fetchUserData(): Chainable<User>
}
}
}
Cypress.Commands.add('signUpEmailPassword', (email, password) => {
cy.visit('/sign-up')
cy.contains('Continue with email + password').click()
cy.findByRole('button', { name: /Continue with email \+ password/i }).click()
cy.findByPlaceholderText('First name').type(faker.name.firstName())
cy.findByPlaceholderText('Last name').type(faker.name.lastName())
cy.findByPlaceholderText('Email Address').type(email)
cy.findByPlaceholderText('Password').type(password)
cy.findByPlaceholderText('Confirm Password').type(password)
cy.contains('Continue with email + password').click()
cy.findByRole('button', { name: /Continue with email \+ password/i }).click()
})
Cypress.Commands.add('signUpEmailPasswordless', (email) => {
cy.visit('/sign-up')
cy.findByRole('button', { name: /Continue with passwordless email/i }).click()
cy.findByPlaceholderText('Email Address').type(email)
cy.findByRole('button', { name: /Continue with email/i }).click()
})
Cypress.Commands.add('signInEmailPassword', (email, password) => {
cy.visit('/sign-in')
cy.findByRole('button', { name: /Continue with email \+ password/i }).click()
cy.findByPlaceholderText('Email Address').type(email)
cy.findByPlaceholderText('Password').type(password)
cy.findByRole('button', { name: /Sign in/i }).click()
cy.saveRefreshToken()
})
Cypress.Commands.add('signInAnonymous', () => {
cy.visit('/sign-in')
cy.findByRole('link', { name: /sign in anonymously/i }).click()
cy.saveRefreshToken()
})
Cypress.Commands.add('visitPathWithRefreshToken', function (path = '/') {
cy.visit(path + '#refreshToken=' + this.refreshToken)
})
Cypress.Commands.add('signOut', () => {
cy.getNavBar()
.findByRole('button', { name: /Sign Out/i })
.click()
})
Cypress.Commands.add('confirmEmail', (email) => {
@@ -27,5 +87,53 @@ Cypress.Commands.add('confirmEmail', (email) => {
.should('have.length', 1)
.then(([message]) => {
cy.visit(message.Content.Headers['X-Link'][0])
cy.saveRefreshToken()
})
})
Cypress.Commands.add('signUpAndConfirmEmail', (givenEmail) => {
const email = givenEmail || faker.internet.email()
cy.signUpEmailPasswordless(email)
cy.contains('Verification email sent').should('be.visible')
cy.confirmEmail(email)
})
Cypress.Commands.add('saveRefreshToken', () => {
cy.getNavBar()
.findByRole('button', { name: /Sign Out/i })
.then(() => localStorage.getItem('nhostRefreshToken'))
.as('refreshToken')
})
Cypress.Commands.add('disconnectBackend', () => {
cy.intercept(Cypress.env('backendUrl') + '/**', {
forceNetworkError: true
})
})
Cypress.Commands.add('getNavBar', () => {
cy.findByRole(`navigation`, { name: /main navigation/i })
})
Cypress.Commands.add('goToProfilePage', () => {
cy.getNavBar()
.findByRole('button', { name: /Profile/i })
.click()
})
Cypress.Commands.add('goToHomePage', () => {
cy.getNavBar().findByRole('button', { name: /Home/i }).click()
})
Cypress.Commands.add('fetchUserData', () => {
cy.goToProfilePage()
cy.findByText('User information')
.parent()
.within(() => {
cy.get('pre')
.invoke('text')
.then((text) => JSON.parse(text))
.as('user')
})
return cy.get<User>('@user')
})

View File

@@ -0,0 +1,15 @@
schema:
- http://localhost:1337/v1/graphql:
headers:
x-hasura-admin-secret: nhost-admin-secret
x-hasura-role: user
documents: 'src/**/!(*.d).{ts,tsx}'
generates:
./src/generated.ts:
config:
namingConvention:
typeNames: change-case-all#pascalCase
transformUnderscore: true
plugins:
- typescript
- typescript-operations

View File

@@ -0,0 +1,104 @@
table:
name: todos
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
updated_at:
custom_name: updatedAt
user_id:
custom_name: userId
custom_column_names:
created_at: createdAt
updated_at: updatedAt
user_id: userId
custom_root_fields:
delete: deleteTodos
delete_by_pk: deleteTodo
insert: insertTodos
insert_one: insertTodo
select_aggregate: todosAggregate
select_by_pk: todo
update: updateTodos
update_by_pk: updateTodo
object_relationships:
- name: user
using:
foreign_key_constraint_on: user_id
insert_permissions:
- permission:
backend_only: false
check: {}
columns:
- contents
- id
set:
user_id: x-hasura-user-id
role: anonymous
- permission:
backend_only: false
check: {}
columns:
- contents
- id
set:
user_id: x-hasura-user-id
role: user
select_permissions:
- permission:
allow_aggregations: true
columns:
- contents
- created_at
- updated_at
- id
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
role: anonymous
- permission:
allow_aggregations: true
columns:
- contents
- created_at
- updated_at
- id
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
role: user
update_permissions:
- permission:
check:
user_id:
_eq: X-Hasura-User-Id
columns:
- contents
filter:
user_id:
_eq: X-Hasura-User-Id
role: anonymous
- permission:
check:
user_id:
_eq: X-Hasura-User-Id
columns:
- contents
filter:
user_id:
_eq: X-Hasura-User-Id
role: user
delete_permissions:
- permission:
filter:
user_id:
_eq: X-Hasura-User-Id
role: anonymous
- permission:
filter:
user_id:
_eq: X-Hasura-User-Id
role: user

View File

@@ -5,6 +5,6 @@
- "!include auth_user_providers.yaml"
- "!include auth_user_roles.yaml"
- "!include auth_users.yaml"
- "!include public_books.yaml"
- "!include public_todos.yaml"
- "!include storage_buckets.yaml"
- "!include storage_files.yaml"

View File

@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS "public"."user_id";
DROP TABLE "public"."todos_user_id";

View File

@@ -0,0 +1,20 @@
CREATE TABLE "public"."todos" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "user_id" uuid NOT NULL, "contents" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON UPDATE cascade ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_todos_updated_at"
BEFORE UPDATE ON "public"."todos"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_todos_updated_at" ON "public"."todos"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE INDEX "todos_user_id" on
"public"."todos" using btree ("user_id");

View File

@@ -0,0 +1,2 @@
CREATE TABLE "public"."books" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "title" text NOT NULL, PRIMARY KEY ("id") );
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
DROP table "public"."books";

View File

@@ -19,6 +19,7 @@
},
"scripts": {
"dev": "vite",
"generate": "graphql-codegen --config graphql.config.yaml",
"cypress": "cypress open",
"test": "cypress run",
"e2e": "start-test e2e:backend :1337/v1/auth/healthz e2e:frontend 3000 test",
@@ -33,11 +34,6 @@
"verify": "run-p prettier lint",
"verify:fix": "run-p prettier:fix lint:fix"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
@@ -52,14 +48,18 @@
},
"devDependencies": {
"@faker-js/faker": "^6.3.1",
"@graphql-codegen/cli": "^2.6.2",
"@nuintun/qrcode": "^3.3.0",
"@testing-library/cypress": "^8.0.3",
"@types/react": "^18.0.8",
"@types/react-dom": "^18.0.3",
"@types/totp-generator": "^0.0.4",
"@vitejs/plugin-react": "^1.3.2",
"@xstate/inspect": "^0.6.2",
"cypress": "^10.0.1",
"cypress-mailhog": "^1.4.0",
"start-server-and-test": "^1.14.0",
"totp-generator": "^0.0.13",
"typescript": "^4.6.3",
"vite": "^2.9.7",
"ws": "^8.7.0",

View File

@@ -1,10 +1,23 @@
import { Container, Title } from '@mantine/core'
import { Link } from 'react-router-dom'
import { Anchor, Container, Title } from '@mantine/core'
import { useUserIsAnonymous } from '@nhost/react'
const HomePage: React.FC = () => {
const isAnonymous = useUserIsAnonymous()
return (
<Container>
<Title>Home page</Title>
You are authenticated. You have now access to the authorised part of the application.
{isAnonymous && (
<p>
You signed in anonymously.{' '}
<Anchor role="link" component={Link} to="/sign-up">
Sign up
</Anchor>{' '}
to complete your registration
</p>
)}
</Container>
)
}

View File

@@ -1,32 +1,106 @@
import { gql } from '@apollo/client'
import { Container, Loader, Title } from '@mantine/core'
import { AddItemMutation, TodoListQuery } from 'src/generated'
import { gql, useMutation } from '@apollo/client'
import { Button, Card, Container, Grid, Loader, TextInput, Title } from '@mantine/core'
import { useInputState } from '@mantine/hooks'
import { showNotification } from '@mantine/notifications'
import { useAuthQuery } from '@nhost/react-apollo'
const GET_BOOKS = gql`
query BooksQuery {
books {
const TODO_LIST = gql`
query TodoList {
todos {
id
title
contents
}
}
`
const ADD_ITEM = gql`
mutation AddItem($contents: String!) {
insertTodo(object: { contents: $contents }) {
id
contents
}
}
`
export const ApolloPage: React.FC = () => {
const { loading, data } = useAuthQuery(GET_BOOKS, {
const { loading, data } = useAuthQuery<TodoListQuery>(TODO_LIST, {
pollInterval: 5000,
fetchPolicy: 'cache-and-network'
})
const [contents, setContents] = useInputState('')
const [mutate] = useMutation<AddItemMutation>(ADD_ITEM, {
variables: { contents },
onCompleted: () => {
setContents('')
},
onError: (error) => {
console.log(error)
showNotification({
color: 'red',
title: error.networkError ? 'Network error' : 'Error',
message: error.message
})
},
update: (cache, { data }) => {
cache.modify({
fields: {
todos(existingTodos = []) {
const newTodoRef = cache.writeFragment({
data: data?.insertTodo,
fragment: gql`
fragment NewTodo on todos {
id
contents
}
`
})
return [...existingTodos, newTodoRef]
}
}
})
}
})
const add = () => {
if (contents) {
mutate()
}
}
return (
<Container>
<Title>Apollo GraphQL</Title>
{loading && <Loader />}
{data?.books && (
<Card shadow="sm" p="lg" m="sm">
<Title>Todo list</Title>
<Grid>
<Grid.Col span={9}>
<TextInput
value={contents}
onChange={setContents}
autoFocus
onKeyDown={(e) => e.code === 'Enter' && add()}
/>
</Grid.Col>
<Grid.Col span={3}>
<Button
onClick={(e: React.MouseEvent) => {
e.preventDefault()
add()
}}
>
Add
</Button>
</Grid.Col>
</Grid>
<ul>
{data.books.map((book) => (
<li key={book.id}>{book.title}</li>
{data?.todos.map((item) => (
<li key={item.id}>{item.contents}</li>
))}
</ul>
)}
</Card>
</Container>
)
}

View File

@@ -10,8 +10,8 @@ const AuthLink: React.FC<{
variant?: ButtonVariant
}> = ({ icon, color, link, variant, children }) => {
return (
// <Link to={link}>
<Button
role="button"
component={Link}
fullWidth
radius="sm"
@@ -33,7 +33,6 @@ const AuthLink: React.FC<{
>
{children}
</Button>
// </Link>
)
}

View File

@@ -1,6 +1,6 @@
import { FaHouseUser, FaQuestion, FaSignOutAlt } from 'react-icons/fa'
import { SiApollographql } from 'react-icons/si'
import { useLocation,useNavigate } from 'react-router'
import { useLocation, useNavigate } from 'react-router'
import { Link } from 'react-router-dom'
import { Group, MantineColor, Navbar, Text, ThemeIcon, UnstyledButton } from '@mantine/core'
@@ -62,7 +62,7 @@ export default function NavBar() {
const navigate = useNavigate()
const links = data.map((link) => <MenuItem {...link} key={link.label} />)
return (
<Navbar width={{ sm: 300, lg: 400, base: 100 }}>
<Navbar width={{ sm: 300, lg: 400, base: 100 }} aria-label="main navigation">
<Navbar.Section grow mt="md">
{links}
{authenticated && (

View File

@@ -0,0 +1,356 @@
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
timestamptz: any;
uuid: any;
};
/** Boolean expression to compare columns of type "String". All fields are combined with logical 'AND'. */
export type StringComparisonExp = {
_eq?: InputMaybe<Scalars['String']>;
_gt?: InputMaybe<Scalars['String']>;
_gte?: InputMaybe<Scalars['String']>;
/** does the column match the given case-insensitive pattern */
_ilike?: InputMaybe<Scalars['String']>;
_in?: InputMaybe<Array<Scalars['String']>>;
/** does the column match the given POSIX regular expression, case insensitive */
_iregex?: InputMaybe<Scalars['String']>;
_is_null?: InputMaybe<Scalars['Boolean']>;
/** does the column match the given pattern */
_like?: InputMaybe<Scalars['String']>;
_lt?: InputMaybe<Scalars['String']>;
_lte?: InputMaybe<Scalars['String']>;
_neq?: InputMaybe<Scalars['String']>;
/** does the column NOT match the given case-insensitive pattern */
_nilike?: InputMaybe<Scalars['String']>;
_nin?: InputMaybe<Array<Scalars['String']>>;
/** does the column NOT match the given POSIX regular expression, case insensitive */
_niregex?: InputMaybe<Scalars['String']>;
/** does the column NOT match the given pattern */
_nlike?: InputMaybe<Scalars['String']>;
/** does the column NOT match the given POSIX regular expression, case sensitive */
_nregex?: InputMaybe<Scalars['String']>;
/** does the column NOT match the given SQL regular expression */
_nsimilar?: InputMaybe<Scalars['String']>;
/** does the column match the given POSIX regular expression, case sensitive */
_regex?: InputMaybe<Scalars['String']>;
/** does the column match the given SQL regular expression */
_similar?: InputMaybe<Scalars['String']>;
};
/** mutation root */
export type MutationRoot = {
__typename?: 'mutation_root';
/** delete single row from the table: "todos" */
deleteTodo?: Maybe<Todos>;
/** delete data from the table: "todos" */
deleteTodos?: Maybe<TodosMutationResponse>;
/** insert a single row into the table: "todos" */
insertTodo?: Maybe<Todos>;
/** insert data into the table: "todos" */
insertTodos?: Maybe<TodosMutationResponse>;
/** update single row of the table: "todos" */
updateTodo?: Maybe<Todos>;
/** update data of the table: "todos" */
updateTodos?: Maybe<TodosMutationResponse>;
};
/** mutation root */
export type MutationRootDeleteTodoArgs = {
id: Scalars['uuid'];
};
/** mutation root */
export type MutationRootDeleteTodosArgs = {
where: TodosBoolExp;
};
/** mutation root */
export type MutationRootInsertTodoArgs = {
object: TodosInsertInput;
on_conflict?: InputMaybe<TodosOnConflict>;
};
/** mutation root */
export type MutationRootInsertTodosArgs = {
objects: Array<TodosInsertInput>;
on_conflict?: InputMaybe<TodosOnConflict>;
};
/** mutation root */
export type MutationRootUpdateTodoArgs = {
_set?: InputMaybe<TodosSetInput>;
pk_columns: TodosPkColumnsInput;
};
/** mutation root */
export type MutationRootUpdateTodosArgs = {
_set?: InputMaybe<TodosSetInput>;
where: TodosBoolExp;
};
/** column ordering options */
export enum OrderBy {
/** in ascending order, nulls last */
Asc = 'asc',
/** in ascending order, nulls first */
AscNullsFirst = 'asc_nulls_first',
/** in ascending order, nulls last */
AscNullsLast = 'asc_nulls_last',
/** in descending order, nulls first */
Desc = 'desc',
/** in descending order, nulls first */
DescNullsFirst = 'desc_nulls_first',
/** in descending order, nulls last */
DescNullsLast = 'desc_nulls_last'
}
export type QueryRoot = {
__typename?: 'query_root';
/** fetch data from the table: "todos" using primary key columns */
todo?: Maybe<Todos>;
/** fetch data from the table: "todos" */
todos: Array<Todos>;
/** fetch aggregated fields from the table: "todos" */
todosAggregate: TodosAggregate;
};
export type QueryRootTodoArgs = {
id: Scalars['uuid'];
};
export type QueryRootTodosArgs = {
distinct_on?: InputMaybe<Array<TodosSelectColumn>>;
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
order_by?: InputMaybe<Array<TodosOrderBy>>;
where?: InputMaybe<TodosBoolExp>;
};
export type QueryRootTodosAggregateArgs = {
distinct_on?: InputMaybe<Array<TodosSelectColumn>>;
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
order_by?: InputMaybe<Array<TodosOrderBy>>;
where?: InputMaybe<TodosBoolExp>;
};
export type SubscriptionRoot = {
__typename?: 'subscription_root';
/** fetch data from the table: "todos" using primary key columns */
todo?: Maybe<Todos>;
/** fetch data from the table: "todos" */
todos: Array<Todos>;
/** fetch aggregated fields from the table: "todos" */
todosAggregate: TodosAggregate;
};
export type SubscriptionRootTodoArgs = {
id: Scalars['uuid'];
};
export type SubscriptionRootTodosArgs = {
distinct_on?: InputMaybe<Array<TodosSelectColumn>>;
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
order_by?: InputMaybe<Array<TodosOrderBy>>;
where?: InputMaybe<TodosBoolExp>;
};
export type SubscriptionRootTodosAggregateArgs = {
distinct_on?: InputMaybe<Array<TodosSelectColumn>>;
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
order_by?: InputMaybe<Array<TodosOrderBy>>;
where?: InputMaybe<TodosBoolExp>;
};
/** Boolean expression to compare columns of type "timestamptz". All fields are combined with logical 'AND'. */
export type TimestamptzComparisonExp = {
_eq?: InputMaybe<Scalars['timestamptz']>;
_gt?: InputMaybe<Scalars['timestamptz']>;
_gte?: InputMaybe<Scalars['timestamptz']>;
_in?: InputMaybe<Array<Scalars['timestamptz']>>;
_is_null?: InputMaybe<Scalars['Boolean']>;
_lt?: InputMaybe<Scalars['timestamptz']>;
_lte?: InputMaybe<Scalars['timestamptz']>;
_neq?: InputMaybe<Scalars['timestamptz']>;
_nin?: InputMaybe<Array<Scalars['timestamptz']>>;
};
/** columns and relationships of "todos" */
export type Todos = {
__typename?: 'todos';
contents: Scalars['String'];
createdAt: Scalars['timestamptz'];
id: Scalars['uuid'];
updatedAt: Scalars['timestamptz'];
userId: Scalars['uuid'];
};
/** aggregated selection of "todos" */
export type TodosAggregate = {
__typename?: 'todos_aggregate';
aggregate?: Maybe<TodosAggregateFields>;
nodes: Array<Todos>;
};
/** aggregate fields of "todos" */
export type TodosAggregateFields = {
__typename?: 'todos_aggregate_fields';
count: Scalars['Int'];
max?: Maybe<TodosMaxFields>;
min?: Maybe<TodosMinFields>;
};
/** aggregate fields of "todos" */
export type TodosAggregateFieldsCountArgs = {
columns?: InputMaybe<Array<TodosSelectColumn>>;
distinct?: InputMaybe<Scalars['Boolean']>;
};
/** Boolean expression to filter rows from the table "todos". All fields are combined with a logical 'AND'. */
export type TodosBoolExp = {
_and?: InputMaybe<Array<TodosBoolExp>>;
_not?: InputMaybe<TodosBoolExp>;
_or?: InputMaybe<Array<TodosBoolExp>>;
contents?: InputMaybe<StringComparisonExp>;
createdAt?: InputMaybe<TimestamptzComparisonExp>;
id?: InputMaybe<UuidComparisonExp>;
updatedAt?: InputMaybe<TimestamptzComparisonExp>;
userId?: InputMaybe<UuidComparisonExp>;
};
/** unique or primary key constraints on table "todos" */
export enum TodosConstraint {
/** unique or primary key constraint */
TodosPkey = 'todos_pkey'
}
/** input type for inserting data into table "todos" */
export type TodosInsertInput = {
contents?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['uuid']>;
};
/** aggregate max on columns */
export type TodosMaxFields = {
__typename?: 'todos_max_fields';
contents?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['timestamptz']>;
id?: Maybe<Scalars['uuid']>;
updatedAt?: Maybe<Scalars['timestamptz']>;
userId?: Maybe<Scalars['uuid']>;
};
/** aggregate min on columns */
export type TodosMinFields = {
__typename?: 'todos_min_fields';
contents?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['timestamptz']>;
id?: Maybe<Scalars['uuid']>;
updatedAt?: Maybe<Scalars['timestamptz']>;
userId?: Maybe<Scalars['uuid']>;
};
/** response of any mutation on the table "todos" */
export type TodosMutationResponse = {
__typename?: 'todos_mutation_response';
/** number of rows affected by the mutation */
affected_rows: Scalars['Int'];
/** data from the rows affected by the mutation */
returning: Array<Todos>;
};
/** on_conflict condition type for table "todos" */
export type TodosOnConflict = {
constraint: TodosConstraint;
update_columns?: Array<TodosUpdateColumn>;
where?: InputMaybe<TodosBoolExp>;
};
/** Ordering options when selecting data from "todos". */
export type TodosOrderBy = {
contents?: InputMaybe<OrderBy>;
createdAt?: InputMaybe<OrderBy>;
id?: InputMaybe<OrderBy>;
updatedAt?: InputMaybe<OrderBy>;
userId?: InputMaybe<OrderBy>;
};
/** primary key columns input for table: todos */
export type TodosPkColumnsInput = {
id: Scalars['uuid'];
};
/** select columns of table "todos" */
export enum TodosSelectColumn {
/** column name */
Contents = 'contents',
/** column name */
CreatedAt = 'createdAt',
/** column name */
Id = 'id',
/** column name */
UpdatedAt = 'updatedAt',
/** column name */
UserId = 'userId'
}
/** input type for updating data in table "todos" */
export type TodosSetInput = {
contents?: InputMaybe<Scalars['String']>;
};
/** update columns of table "todos" */
export enum TodosUpdateColumn {
/** column name */
Contents = 'contents'
}
/** Boolean expression to compare columns of type "uuid". All fields are combined with logical 'AND'. */
export type UuidComparisonExp = {
_eq?: InputMaybe<Scalars['uuid']>;
_gt?: InputMaybe<Scalars['uuid']>;
_gte?: InputMaybe<Scalars['uuid']>;
_in?: InputMaybe<Array<Scalars['uuid']>>;
_is_null?: InputMaybe<Scalars['Boolean']>;
_lt?: InputMaybe<Scalars['uuid']>;
_lte?: InputMaybe<Scalars['uuid']>;
_neq?: InputMaybe<Scalars['uuid']>;
_nin?: InputMaybe<Array<Scalars['uuid']>>;
};
export type TodoListQueryVariables = Exact<{ [key: string]: never; }>;
export type TodoListQuery = { __typename?: 'query_root', todos: Array<{ __typename?: 'todos', id: any, contents: string }> };
export type AddItemMutationVariables = Exact<{
contents: Scalars['String'];
}>;
export type AddItemMutation = { __typename?: 'mutation_root', insertTodo?: { __typename?: 'todos', id: any } | null };

View File

@@ -13,7 +13,6 @@ export const EmailPassword: React.FC = () => {
const [otp, setOtp] = useState('')
const { signInEmailPassword, needsMfaOtp, sendMfaOtp } = useSignInEmailPassword()
const navigate = useNavigate()
const [emailVerificationToggle, setEmailVerificationToggle] = useState(false)
const signIn = async () => {
@@ -26,14 +25,22 @@ export const EmailPassword: React.FC = () => {
})
} else if (result.needsEmailVerification) {
setEmailVerificationToggle(true)
} else if (!result.needsEmailVerification) {
} else if (result.isSuccess) {
navigate('/', { replace: true })
}
}
const sendOtp = async () => {
sendMfaOtp(otp)
console.log('TODO')
const result = await sendMfaOtp(otp)
if (result.isError) {
showNotification({
color: 'red',
title: 'Error',
message: result.error?.message
})
} else {
navigate('/', { replace: true })
}
}
if (needsMfaOtp)
return (

View File

@@ -2,6 +2,7 @@ import { FaLock } from 'react-icons/fa'
import { Link, Route, Routes, useNavigate } from 'react-router-dom'
import { Anchor, Center, Divider, Text } from '@mantine/core'
import { useSignInAnonymous } from '@nhost/react'
import AuthLayout from '../components/AuthLayout'
import AuthLink from '../components/AuthLink'
@@ -10,7 +11,6 @@ import OAuthLinks from '../components/OauthLinks'
import { EmailPassword } from './email-password'
import { EmailPasswordless } from './email-passwordless'
import { ForgotPassword } from './forgot-password'
import { useSignInAnonymous } from '@nhost/react'
const Index: React.FC = () => (
<>
@@ -41,10 +41,13 @@ export const SignInPage: React.FC = () => {
<Center>
<Text>
Don&lsquo;t have an account?{' '}
<Anchor component={Link} to="/sign-up">
<Anchor role="link" component={Link} to="/sign-up">
Sign up
</Anchor>{' '}
or <Anchor onClick={anonymousHandler}>sign in anonymously</Anchor>
or{' '}
<Anchor role="link" onClick={anonymousHandler}>
sign in anonymously
</Anchor>
</Text>
</Center>
}

View File

@@ -2,6 +2,7 @@ import { FaLock } from 'react-icons/fa'
import { Link, Route, Routes } from 'react-router-dom'
import { Anchor, Center, Divider, Text } from '@mantine/core'
import { useUserIsAnonymous } from '@nhost/react'
import AuthLayout from '../components/AuthLayout'
import AuthLink from '../components/AuthLink'
@@ -10,18 +11,25 @@ import OAuthLinks from '../components/OauthLinks'
import { EmailPassword } from './email-password'
import { EmailPasswordless } from './email-passwordless'
const Index: React.FC = () => (
<>
<OAuthLinks />
<Divider my="sm" />
<AuthLink icon={<FaLock />} variant="outline" link="/sign-up/email-passwordless">
Continue with passwordless email
</AuthLink>
<AuthLink variant="subtle" link="/sign-up/email-password">
Continue with email + password
</AuthLink>
</>
)
const Index: React.FC = () => {
const isAnonymous = useUserIsAnonymous()
return (
<>
{!isAnonymous && (
<>
<OAuthLinks />
<Divider my="sm" />
</>
)}
<AuthLink icon={<FaLock />} variant="outline" link="/sign-up/email-passwordless">
Continue with passwordless email
</AuthLink>
<AuthLink variant="subtle" link="/sign-up/email-password">
Continue with email + password
</AuthLink>
</>
)
}
export const SignUpPage: React.FC = () => {
return (
<AuthLayout

View File

@@ -5,7 +5,8 @@ import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
optimizeDeps: {
include: ['react/jsx-runtime']
include: ['react/jsx-runtime'],
exclude: ['@nhost/react']
},
plugins: [react()]
})

View File

@@ -1,3 +0,0 @@
module.exports = {
extends: '../../config/.eslintrc.vue.js'
}

View File

@@ -37,5 +37,9 @@
"typescript": "^4.5.4",
"vite": "^2.9.0",
"vue-tsc": "^0.29.8"
},
"eslintConfig": {
"root": true,
"extends": "../../config/.eslintrc.vue.js"
}
}

View File

@@ -2,7 +2,6 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -1,3 +1,11 @@
module.exports = {
extends: ['../../config/.eslintrc.vue']
root: true,
extends: ['../../config/.eslintrc.vue.js', '@antfu'],
rules: {
'@typescript-eslint/comma-dangle': 'off',
curly: 'off',
'quote-props': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off'
}
}

View File

@@ -11,7 +11,7 @@
},
"dependencies": {
"@apollo/client": "^3.6.2",
"@nhost/apollo": "^0.5.4",
"@nhost/apollo": "*",
"@nhost/vue": "*",
"@vue/apollo-composable": "^4.0.0-alpha.17",
"@vueuse/core": "^8.4.2",
@@ -38,8 +38,5 @@
"vite-plugin-pages": "^0.23.0",
"vitest": "^0.12.4",
"vue-tsc": "^0.34.12"
},
"eslintConfig": {
"extends": "@antfu"
}
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/comma-dangle,curly */
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from 'virtual:generated-pages'

View File

@@ -16,8 +16,8 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"prepare": "husky install",
"build": "pnpm run build:all -- --filter=!@nhost/docs --filter=!@nhost-examples/*",
"build:docs": "pnpm run build:all -- --filter=@nhost/docs",
"build": "pnpm run build:all --filter=!@nhost/docs --filter=!@nhost-examples/*",
"build:docs": "pnpm run build:all --filter=@nhost/docs",
"build:all": "turbo run build --include-dependencies",
"dev": "turbo run dev --filter=!@nhost/docs --filter=!@nhost-examples/* --filter=!@nhost/docgen --no-deps --include-dependencies",
"clean:all": "pnpm clean && rm -rf ./{{packages,examples}/*,docs}/{.nhost,node_modules} node_modules",
@@ -56,6 +56,7 @@
"c8": "^7.11.2",
"eslint": "^8.14.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
@@ -85,5 +86,8 @@
"packageManager": "pnpm@6.24.0",
"engines": {
"node": ">=14"
},
"eslintConfig": {
"extends": "./config/.eslintrc.js"
}
}

View File

@@ -1,5 +1,12 @@
# @nhost/apollo
## 0.5.16
### Patch Changes
- Updated dependencies [6f0a3005]
- @nhost/nhost-js@1.4.0
## 0.5.15
### Patch Changes

View File

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

View File

@@ -1,5 +1,12 @@
# @nhost/core
## 0.7.1
### Patch Changes
- 6f0a3005: `sendMfaOtp` now returns a promise
When using `useSignInEmailPassword`, the `sendMfaOtp` was `void`. It now returns a promise that resolves when the server returned the result of the OTP code submission, and returns `isSuccess`, `isError`, and `error`.
## 0.7.0
### Minor Changes

View File

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

View File

@@ -6,6 +6,7 @@ export * from './sendVerificationEmail'
export * from './signInAnonymous'
export * from './signInEmailPassword'
export * from './signInEmailPasswordless'
export * from './signInMfaTotp'
export * from './signInSmsPasswordless'
export * from './signInSmsPasswordlessOtp'
export * from './signOut'

View File

@@ -0,0 +1,44 @@
import { USER_ALREADY_SIGNED_IN } from '../errors'
import { AuthInterpreter } from '../types'
import { ActionLoadingState, SessionActionHandlerResult } from './types'
export interface SignInMfaTotpHandlerResult extends SessionActionHandlerResult {}
export interface SignInMfaTotpState extends SignInMfaTotpHandlerResult, ActionLoadingState {}
export const signInMfaTotpPromise = (interpreter: AuthInterpreter, otp: string, ticket?: string) =>
new Promise<SignInMfaTotpHandlerResult>((resolve) => {
const { changed, context } = interpreter.send('SIGNIN_MFA_TOTP', {
otp,
ticket
})
if (!changed) {
return resolve({
accessToken: context.accessToken.value,
error: USER_ALREADY_SIGNED_IN,
isError: true,
isSuccess: false,
user: context.user
})
}
interpreter.onTransition((state) => {
if (state.matches({ authentication: { signedOut: 'failed' } })) {
resolve({
accessToken: null,
error: state.context.errors.authentication || null,
isError: true,
isSuccess: false,
user: null
})
} else if (state.matches({ authentication: 'signedIn' })) {
resolve({
accessToken: state.context.accessToken.value,
error: null,
isError: false,
isSuccess: true,
user: state.context.user
})
}
})
})

View File

@@ -1,4 +1,4 @@
import { ErrorPayload, USER_UNAUTHENTICATED } from '../errors'
import { USER_UNAUTHENTICATED } from '../errors'
import { AuthInterpreter } from '../types'
import { ActionErrorState, ActionLoadingState, ActionSuccessState } from './types'
@@ -10,7 +10,7 @@ export const signOutPromise = async (
interpreter: AuthInterpreter,
all?: boolean
): Promise<SignOutlessHandlerResult> =>
new Promise<{ isSuccess: boolean; error: ErrorPayload | null; isError: boolean }>((resolve) => {
new Promise<SignOutlessHandlerResult>((resolve) => {
const { event } = interpreter.send('SIGNOUT', { all })
if (event.type !== 'SIGNED_OUT') {
return resolve({ isSuccess: false, isError: true, error: USER_UNAUTHENTICATED })

View File

@@ -1,5 +1,23 @@
# @nhost/hasura-auth-js
## 1.3.0
### Minor Changes
- 6f0a3005: Complete sign-in when email+password MFA is activated
It was not possible to complete authentication with `nhost.auth.signIn` in sending the TOTP code when email+password MFA was activated.
An user that activated MFA can now sign in with the two following steps:
```js
await nhost.auth.signIn({ email: 'email@domain.com', password: 'not-my-birthday' })
// Get the one-time password with an OTP application e.g. Google Authenticator
await nhost.auth.signIn({ otp: '123456' })
```
### Patch Changes
- Updated dependencies [6f0a3005]
- @nhost/core@0.7.1
## 1.2.0
### Minor Changes

View File

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

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