Compare commits
79 Commits
@nhost/rea
...
@nhost/cor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fec74e501 | ||
|
|
e94b28b3bc | ||
|
|
f591f76256 | ||
|
|
58fb955dc6 | ||
|
|
f472d42ae9 | ||
|
|
9993bea7ab | ||
|
|
6f33fc6ce6 | ||
|
|
b6858c5638 | ||
|
|
9b01c3ba93 | ||
|
|
fcfd6a9c13 | ||
|
|
e4daefe637 | ||
|
|
a0bcbb6269 | ||
|
|
1ec1004507 | ||
|
|
756d996096 | ||
|
|
c0f1d03c3c | ||
|
|
af55789d07 | ||
|
|
cb28676895 | ||
|
|
939a3d1090 | ||
|
|
0163d0588b | ||
|
|
c4124f22b0 | ||
|
|
7188b0971c | ||
|
|
6ac969320c | ||
|
|
414bc2e75b | ||
|
|
6862e1e24d | ||
|
|
4b9deaa2f7 | ||
|
|
cf366cef35 | ||
|
|
00d041f6b4 | ||
|
|
da06fef64e | ||
|
|
09be9582f8 | ||
|
|
6b26fed8ae | ||
|
|
5a62c66fc4 | ||
|
|
17e0e6d116 | ||
|
|
938000e61b | ||
|
|
d700107222 | ||
|
|
69d9e40187 | ||
|
|
fc5b18fdf0 | ||
|
|
fa3eb980a0 | ||
|
|
efad3a2b08 | ||
|
|
563fa4fe9b | ||
|
|
6f0a30059a | ||
|
|
47cda5d716 | ||
|
|
3f625ce9e1 | ||
|
|
38d2609249 | ||
|
|
030243cd45 | ||
|
|
c1905243d0 | ||
|
|
37627cc50e | ||
|
|
009f68d500 | ||
|
|
b752cc2be8 | ||
|
|
72fc7d4e44 | ||
|
|
80ef14e50a | ||
|
|
543ea2a0e7 | ||
|
|
6764d476fd | ||
|
|
7bed0eadc9 | ||
|
|
c7644ace34 | ||
|
|
49cdb2843e | ||
|
|
6f45856c46 | ||
|
|
61e719eea0 | ||
|
|
208bdbba2d | ||
|
|
cd62e1e833 | ||
|
|
1dfb11d7e8 | ||
|
|
8b5c4ed443 | ||
|
|
6bd5c96ed5 | ||
|
|
6b8762a62e | ||
|
|
ddeff7cbd6 | ||
|
|
ed952c1251 | ||
|
|
34e73f18bd | ||
|
|
84262a24f1 | ||
|
|
ec2a88d69c | ||
|
|
fe1049df6b | ||
|
|
a924d21815 | ||
|
|
4405535d4a | ||
|
|
af15771517 | ||
|
|
c066ea5b75 | ||
|
|
80e42b939b | ||
|
|
3ea6f685e2 | ||
|
|
ac77f427c3 | ||
|
|
0f95ee5bb4 | ||
|
|
47406d3617 | ||
|
|
125bc9a749 |
8
.github/workflows/tests.yaml
vendored
8
.github/workflows/tests.yaml
vendored
@@ -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)}}
|
||||
|
||||
32
README.md
32
README.md
@@ -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"/>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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**.
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
51
examples/react-apollo/cypress/e2e/2-sign-up/anonymous.cy.ts
Normal file
51
examples/react-apollo/cypress/e2e/2-sign-up/anonymous.cy.ts
Normal 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
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
22
examples/react-apollo/cypress/e2e/3-sign-in/token.cy.ts
Normal file
22
examples/react-apollo/cypress/e2e/3-sign-in/token.cy.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
15
examples/react-apollo/graphql.config.yaml
Normal file
15
examples/react-apollo/graphql.config.yaml
Normal 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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS "public"."user_id";
|
||||
DROP TABLE "public"."todos_user_id";
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
DROP table "public"."books";
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
356
examples/react-apollo/src/generated.ts
Normal file
356
examples/react-apollo/src/generated.ts
Normal 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 };
|
||||
@@ -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 (
|
||||
|
||||
@@ -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‘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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()]
|
||||
})
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
extends: '../../config/.eslintrc.vue.js'
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
1
examples/vue-apollo/src/env.d.ts
vendored
1
examples/vue-apollo/src/env.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "0.5.15",
|
||||
"version": "0.5.16",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/core",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"description": "Nhost core client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -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'
|
||||
|
||||
44
packages/core/src/promises/signInMfaTotp.ts
Normal file
44
packages/core/src/promises/signInMfaTotp.ts
Normal 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
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user