Compare commits
237 Commits
@nhost/cli
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1446a8f13b | ||
|
|
ff75998e93 | ||
|
|
9cc044ca9f | ||
|
|
c65e44b8d6 | ||
|
|
9ec73b4c22 | ||
|
|
94b70e0ce9 | ||
|
|
d108dff4f4 | ||
|
|
7a8e771a72 | ||
|
|
f8fb4bbedd | ||
|
|
c6b1c33a8e | ||
|
|
65b36eff13 | ||
|
|
c192cb9503 | ||
|
|
94ff290264 | ||
|
|
78781ebfec | ||
|
|
63d6059981 | ||
|
|
022d49fb25 | ||
|
|
3bd1aa4d53 | ||
|
|
f3cca4997b | ||
|
|
0fd7a487d6 | ||
|
|
1bb032c1e7 | ||
|
|
2c97db68b5 | ||
|
|
714f2872ee | ||
|
|
65fc26a0e8 | ||
|
|
86a56f28c1 | ||
|
|
6e8abe28d6 | ||
|
|
39925ff5ca | ||
|
|
583a77ed0d | ||
|
|
e704831500 | ||
|
|
95948dd5b9 | ||
|
|
247b69c952 | ||
|
|
7d15b76402 | ||
|
|
b1ae65fd72 | ||
|
|
0063fd1840 | ||
|
|
743a7e6507 | ||
|
|
078652861f | ||
|
|
39840cfd95 | ||
|
|
940a36a68f | ||
|
|
77b109b3df | ||
|
|
15907d65e6 | ||
|
|
7d7d16fa71 | ||
|
|
3f39e48cbd | ||
|
|
97ade32869 | ||
|
|
8583af8290 | ||
|
|
a28193a6ba | ||
|
|
9d6c64430a | ||
|
|
519d1bf5cb | ||
|
|
5ffb0320b5 | ||
|
|
6607e73cc2 | ||
|
|
b4bac161a5 | ||
|
|
37d15377c8 | ||
|
|
8ee1df3be4 | ||
|
|
47ffca945e | ||
|
|
d60f5e623c | ||
|
|
6f80643ee0 | ||
|
|
8d5084725d | ||
|
|
693498dd09 | ||
|
|
4d36a966ea | ||
|
|
239a075f1d | ||
|
|
931194812e | ||
|
|
c8f80c58f3 | ||
|
|
7fdb5aee0a | ||
|
|
1710808fef | ||
|
|
696815d4a8 | ||
|
|
5cc9be00b6 | ||
|
|
28dae23a91 | ||
|
|
7819e20cf4 | ||
|
|
6be3758668 | ||
|
|
658c67faf4 | ||
|
|
e7f3a5f6e0 | ||
|
|
7135aee78b | ||
|
|
587eaff734 | ||
|
|
7cf875f4b8 | ||
|
|
657cfb91c5 | ||
|
|
103dd6e98e | ||
|
|
3c8caa680b | ||
|
|
1bcee357fe | ||
|
|
b729aa9290 | ||
|
|
57780ee645 | ||
|
|
aad8d22380 | ||
|
|
85d33c4de0 | ||
|
|
ab3e2dcee9 | ||
|
|
12f4504b61 | ||
|
|
71d7a11c96 | ||
|
|
16a6c5073e | ||
|
|
3fcc86792a | ||
|
|
27909128e4 | ||
|
|
72371c72a1 | ||
|
|
d878414b10 | ||
|
|
9b840f7c4a | ||
|
|
4fd09b4080 | ||
|
|
bdb786fa83 | ||
|
|
d42be972b4 | ||
|
|
5920c830b3 | ||
|
|
5fc16653c0 | ||
|
|
900ec48889 | ||
|
|
83d3c90f43 | ||
|
|
cf20ee5a8f | ||
|
|
9180154325 | ||
|
|
1ae025b745 | ||
|
|
ba538a4ad9 | ||
|
|
0e5e47b8f8 | ||
|
|
113beed447 | ||
|
|
6eeb9d2e65 | ||
|
|
3db2959bc2 | ||
|
|
16fcc08b0a | ||
|
|
5b098c8ef4 | ||
|
|
058956bdcb | ||
|
|
d3384614b4 | ||
|
|
0064fccb12 | ||
|
|
6efd45fcb7 | ||
|
|
4420c0e070 | ||
|
|
354b07947a | ||
|
|
2fa5c10e14 | ||
|
|
94124c7754 | ||
|
|
e405b738a6 | ||
|
|
947b7e037f | ||
|
|
cd6f37f2a6 | ||
|
|
39df4d5b9c | ||
|
|
63ee1d7659 | ||
|
|
eb33952760 | ||
|
|
e91215bbac | ||
|
|
ccaa4c4bba | ||
|
|
ab36f90cec | ||
|
|
cfbe2db430 | ||
|
|
6838ac6201 | ||
|
|
0caf43037d | ||
|
|
4ed626d5b5 | ||
|
|
9ff9abee6a | ||
|
|
9d3f0521a5 | ||
|
|
744fd6929f | ||
|
|
f43f52e766 | ||
|
|
fd4c54ee91 | ||
|
|
b30ff6f507 | ||
|
|
ff7ae21a87 | ||
|
|
b2c398df22 | ||
|
|
888192282f | ||
|
|
945b557dea | ||
|
|
4031d8a9e1 | ||
|
|
c77aa16181 | ||
|
|
ea2fb2e9a4 | ||
|
|
e147487e27 | ||
|
|
6f52652e10 | ||
|
|
1a8d9b5c28 | ||
|
|
9111299ddd | ||
|
|
1c7f520073 | ||
|
|
dff37a4cd0 | ||
|
|
96a572379e | ||
|
|
da3bbf2e10 | ||
|
|
a11fa372ff | ||
|
|
6d2c7b26c0 | ||
|
|
d2d3ba6eb7 | ||
|
|
e688600ea2 | ||
|
|
d9aec711c4 | ||
|
|
9bd01e756f | ||
|
|
8f7643a90e | ||
|
|
50b9d763ae | ||
|
|
63cb1f0ce6 | ||
|
|
7c70b1823d | ||
|
|
47c57ff665 | ||
|
|
1cb330016b | ||
|
|
497652d1b4 | ||
|
|
20eb7aa381 | ||
|
|
4a3c2f92b1 | ||
|
|
5647e64265 | ||
|
|
c113debf46 | ||
|
|
3f0ae4a58c | ||
|
|
3d5b8183e8 | ||
|
|
789ef8f783 | ||
|
|
94df175ca3 | ||
|
|
c8bcefb0e5 | ||
|
|
fc52f59eb8 | ||
|
|
a80389e5c7 | ||
|
|
923276422b | ||
|
|
7c9192f3a0 | ||
|
|
51d139b7aa | ||
|
|
8fe1bdb6f7 | ||
|
|
5b288bc0d1 | ||
|
|
27cd769c76 | ||
|
|
5bb370869d | ||
|
|
ec68f64db4 | ||
|
|
4cf8f146c9 | ||
|
|
61cf317541 | ||
|
|
a2066c9b41 | ||
|
|
817b152704 | ||
|
|
cc73494c91 | ||
|
|
64ed4083b9 | ||
|
|
77e8c58cc6 | ||
|
|
0cd2eab309 | ||
|
|
7d8c843c74 | ||
|
|
da1c2d6914 | ||
|
|
3a949301f9 | ||
|
|
585eebab49 | ||
|
|
45c3e4686e | ||
|
|
52f2e67952 | ||
|
|
929774aa5b | ||
|
|
e711e338e7 | ||
|
|
d2aae774a0 | ||
|
|
322e8a1b07 | ||
|
|
d154f8d71b | ||
|
|
0931afd84c | ||
|
|
837548cfd5 | ||
|
|
a6cabbca79 | ||
|
|
82f19fe717 | ||
|
|
82be281153 | ||
|
|
0112ca775f | ||
|
|
b30b812b93 | ||
|
|
bace64c306 | ||
|
|
be49b641e3 | ||
|
|
5ac8c2f516 | ||
|
|
168ae1d82b | ||
|
|
b4a2e28fc3 | ||
|
|
ca3ae21286 | ||
|
|
9f30c1af09 | ||
|
|
e1f9f64910 | ||
|
|
2634dd8335 | ||
|
|
42b4c78d4c | ||
|
|
54174c1b0f | ||
|
|
9fad359ae9 | ||
|
|
1cbf460223 | ||
|
|
8a3aa007b8 | ||
|
|
4235eb812e | ||
|
|
ed145234b2 | ||
|
|
7c2597ddc7 | ||
|
|
2b1f8182f2 | ||
|
|
ded9e7637a | ||
|
|
c42fb85bae | ||
|
|
33edc4291b | ||
|
|
df89d804c5 | ||
|
|
89da44d715 | ||
|
|
789faad645 | ||
|
|
8c7267cbee | ||
|
|
96c12ffff1 | ||
|
|
783729a6f6 | ||
|
|
60d4dbabdf | ||
|
|
a77ddcdbc2 | ||
|
|
f4c8a776a4 | ||
|
|
efbaf08483 |
@@ -25,6 +25,6 @@ esbuild
|
||||
platform: 'browser',
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
target: 'esnext'
|
||||
target: 'es2019'
|
||||
})
|
||||
.catch(() => process.exit(1))
|
||||
|
||||
30
.github/workflows/changesets.yaml
vendored
30
.github/workflows/changesets.yaml
vendored
@@ -16,29 +16,33 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Cache pnpm modules
|
||||
uses: actions/cache@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v2.1.0
|
||||
with:
|
||||
version: 6.30.1
|
||||
run_install: true
|
||||
|
||||
version: 6.32.3
|
||||
# run_install: true
|
||||
- name: Use Node.js 17
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '17.8.0'
|
||||
cache: 'pnpm'
|
||||
- name: Pick the right npm version
|
||||
# * See: https://github.com/pnpm/pnpm/issues/4348
|
||||
run: npm install --global npm@8.4
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Create PR or Publish release
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm ci:version
|
||||
version: pnpm run ci:version
|
||||
commit: 'chore: update versions'
|
||||
title: 'chore: update versions'
|
||||
publish: pnpm release
|
||||
publish: pnpm run release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
15
.github/workflows/contributors.yaml
vendored
Normal file
15
.github/workflows/contributors.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Add contributors
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
contrib-readme-job:
|
||||
runs-on: ubuntu-latest
|
||||
name: A job to automate contrib in readme
|
||||
steps:
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
47
.github/workflows/tests.yaml
vendored
47
.github/workflows/tests.yaml
vendored
@@ -27,26 +27,30 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
|
||||
node-version: [14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Cache pnpm modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-
|
||||
- name: Install nhost CLI
|
||||
run: curl -L https://raw.githubusercontent.com/nhost/cli/main/get.sh | bash
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
- name: Start Nhost Backend
|
||||
run: |
|
||||
cp -R examples/testing-project /tmp/
|
||||
cd /tmp/testing-project
|
||||
nhost dev &
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.1
|
||||
with:
|
||||
version: 6.30.1
|
||||
run_install: true
|
||||
version: 6.32.3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache turbo
|
||||
uses: actions/cache@v2
|
||||
@@ -56,16 +60,11 @@ jobs:
|
||||
restore-keys: |
|
||||
turbo-${{ github.job }}-${{ github.ref_name }}-
|
||||
|
||||
- name: Install nhost CLI
|
||||
run: curl -L https://raw.githubusercontent.com/nhost/cli/main/get.sh | bash
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Start Nhost Backend
|
||||
run: |
|
||||
cd examples/testing-project
|
||||
- name: Wait for Nhost
|
||||
run: pnpm run wait
|
||||
|
||||
nhost dev -d --no-browser &
|
||||
- name: Wait for Nhost Backend to start
|
||||
run: |
|
||||
pnpm dlx wait-on http://localhost:1337/v1/auth/healthz -i 500 -t 120000
|
||||
|
||||
- run: pnpm run ci
|
||||
- name: Build, tests and lint
|
||||
run: pnpm run ci
|
||||
|
||||
214
README.md
214
README.md
@@ -1,8 +1,8 @@
|
||||
<div align="center">
|
||||
<img width="237" src="https://raw.githubusercontent.com/nhost/nhost/main/assets/logo.png"/>
|
||||

|
||||
|
||||
<br />
|
||||
<br />
|
||||
<div align="center">
|
||||
|
||||
# Nhost
|
||||
|
||||
<a href="https://docs.nhost.io/get-started">Quickstart</a>
|
||||
<span> • </span>
|
||||
@@ -20,9 +20,9 @@
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
**Nhost is a serverless backend for web and mobile apps** built with the following things in mind:
|
||||
**Nhost is a open-source GraphQL backend,** built with the following things in mind:
|
||||
|
||||
- Open Source
|
||||
- Open-Source
|
||||
- Developer Productivity
|
||||
- SQL
|
||||
- GraphQL
|
||||
@@ -36,6 +36,15 @@ Nhost consists of open source software:
|
||||
- Serverless Functions: Node.js (JavaScript and TypeScript)
|
||||
- [Nhost CLI](https://docs.nhost.io/reference/cli) for local development
|
||||
|
||||
## Architecture of Nhost
|
||||
|
||||
<div align="center">
|
||||
<br />
|
||||
<img src="assets/nhost-diagram.png"/>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
Visit [https://docs.nhost.io](http://docs.nhost.io) for the complete documentation.
|
||||
|
||||
# How to get started
|
||||
@@ -92,8 +101,8 @@ Nhost libraries and tools
|
||||
- [JavaScript/TypeScript SDK](https://docs.nhost.io/reference/sdk)
|
||||
- [Dart and Flutter SDK](https://github.com/nhost/nhost-dart)
|
||||
- [Nhost CLI](https://docs.nhost.io/reference/cli)
|
||||
- [Nhost React Auth](https://docs.nhost.io/reference/supporting-libraries/react-auth)
|
||||
- [Nhost React Apollo](https://docs.nhost.io/reference/supporting-libraries/react-apollo)
|
||||
- [Nhost React](https://docs.nhost.io/reference/react)
|
||||
- [Nhost Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
|
||||
## Community ❤️
|
||||
|
||||
@@ -101,7 +110,7 @@ First and foremost: **Star and watch this repository** to stay up-to-date.
|
||||
|
||||
Also, follow Nhost on [GitHub Discussions](https://github.com/nhost/nhost/discussions), our [Blog](https://nhost.io/blog), and on [Twitter](https://twitter.com/nhostio). You can chat with the team and other members on [Discord](https://discord.com/invite/9V7Qb2U) and follow our tutorials and other video material at [YouTube](https://www.youtube.com/channel/UCJ7irtvV9Y0EQMxpabb6ntg?view_as=subscriber).
|
||||
|
||||
## Nhost is Open Source
|
||||
### Nhost is Open Source
|
||||
|
||||
This repository, and most of our other open source projects, are licensed under the MIT license.
|
||||
|
||||
@@ -113,6 +122,189 @@ Here are some ways of contributing to making Nhost better:
|
||||
- Join our [Discord](https://discord.com/invite/9V7Qb2U) and connect with other members to share and learn from.
|
||||
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) for more details about how to contribute. We're looking forward to your contribution!
|
||||
|
||||
## Security
|
||||
### Contributors
|
||||
|
||||
If you discover a security vulnerability within Nhost, please e-mail [security@nhost.io](mailto:security@nhost.io). All security vulnerabilities will be promptly addressed.
|
||||
<!-- readme: contributors -start -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/plmercereau">
|
||||
<img src="https://avatars.githubusercontent.com/u/24897252?v=4" width="100;" alt="plmercereau"/>
|
||||
<br />
|
||||
<sub><b>Pilou</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elitan">
|
||||
<img src="https://avatars.githubusercontent.com/u/331818?v=4" width="100;" alt="elitan"/>
|
||||
<br />
|
||||
<sub><b>Johan Eliasson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/szilarddoro">
|
||||
<img src="https://avatars.githubusercontent.com/u/310881?v=4" width="100;" alt="szilarddoro"/>
|
||||
<br />
|
||||
<sub><b>Szilárd Dóró</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nunopato">
|
||||
<img src="https://avatars.githubusercontent.com/u/1523504?v=4" width="100;" alt="nunopato"/>
|
||||
<br />
|
||||
<sub><b>Nuno Pato</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subatuba21">
|
||||
<img src="https://avatars.githubusercontent.com/u/34824571?v=4" width="100;" alt="subatuba21"/>
|
||||
<br />
|
||||
<sub><b>Subha Das</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/guicurcio">
|
||||
<img src="https://avatars.githubusercontent.com/u/20285232?v=4" width="100;" alt="guicurcio"/>
|
||||
<br />
|
||||
<sub><b>Guido Curcio</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/sebagudelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/43288271?v=4" width="100;" alt="sebagudelo"/>
|
||||
<br />
|
||||
<sub><b>Sebagudelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mrinalwahal">
|
||||
<img src="https://avatars.githubusercontent.com/u/9859731?v=4" width="100;" alt="mrinalwahal"/>
|
||||
<br />
|
||||
<sub><b>Mrinal Wahal</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/gdangelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/4352286?v=4" width="100;" alt="gdangelo"/>
|
||||
<br />
|
||||
<sub><b>Grégory D'Angelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/FuzzyReason">
|
||||
<img src="https://avatars.githubusercontent.com/u/62517920?v=4" width="100;" alt="FuzzyReason"/>
|
||||
<br />
|
||||
<sub><b>Vadim Smirnov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subhendukundu">
|
||||
<img src="https://avatars.githubusercontent.com/u/20059141?v=4" width="100;" alt="subhendukundu"/>
|
||||
<br />
|
||||
<sub><b>Subhendu Kundu</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/chrtze">
|
||||
<img src="https://avatars.githubusercontent.com/u/3797215?v=4" width="100;" alt="chrtze"/>
|
||||
<br />
|
||||
<sub><b>Christopher Möller</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jerryjappinen">
|
||||
<img src="https://avatars.githubusercontent.com/u/1101002?v=4" width="100;" alt="jerryjappinen"/>
|
||||
<br />
|
||||
<sub><b>Jerry Jäppinen</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mustafa-hanif">
|
||||
<img src="https://avatars.githubusercontent.com/u/30019262?v=4" width="100;" alt="mustafa-hanif"/>
|
||||
<br />
|
||||
<sub><b>Mustafa Hanif</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Savinvadim1312">
|
||||
<img src="https://avatars.githubusercontent.com/u/16936043?v=4" width="100;" alt="Savinvadim1312"/>
|
||||
<br />
|
||||
<sub><b>Savin Vadim</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 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>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rustyb">
|
||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||
<br />
|
||||
<sub><b>Colin Broderick</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dohomi">
|
||||
<img src="https://avatars.githubusercontent.com/u/489221?v=4" width="100;" alt="dohomi"/>
|
||||
<br />
|
||||
<sub><b>Dominic Garms</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alveshelio">
|
||||
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
|
||||
<br />
|
||||
<sub><b>Helio Alves</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<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>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ghoshnirmalya">
|
||||
<img src="https://avatars.githubusercontent.com/u/6391763?v=4" width="100;" alt="ghoshnirmalya"/>
|
||||
<br />
|
||||
<sub><b>Nirmalya Ghosh</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/timpratim">
|
||||
<img src="https://avatars.githubusercontent.com/u/32492961?v=4" width="100;" alt="timpratim"/>
|
||||
<br />
|
||||
<sub><b>Pratim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/quentin-decre">
|
||||
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
||||
<br />
|
||||
<sub><b>Quentin Decré</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/komninoschat">
|
||||
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>
|
||||
<br />
|
||||
<sub><b>Komninos</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<!-- readme: contributors -end -->
|
||||
|
||||
BIN
assets/nhost-diagram.png
Normal file
BIN
assets/nhost-diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -2,12 +2,15 @@
|
||||
|
||||
## Get started
|
||||
|
||||
1. Install dependencies: `yarn`
|
||||
2. Start dev server: `yarn dev`
|
||||
From the **root** of the `nhost/nhost` repository:
|
||||
|
||||
## NOTES;
|
||||
```bash
|
||||
pnpm run clean:all
|
||||
pnpm i
|
||||
cd docs
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
The content is copied from the main `nhost/nhost` repo. This repo is only to modify styles/react components.
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -30,8 +33,10 @@ export const orderTwo = {
|
||||
},
|
||||
reference: {
|
||||
sdk: ['index', 'graphql', 'authentication', 'storage', 'functions'],
|
||||
react: ['index', 'hooks', 'protecting-routes', 'apollo'],
|
||||
nextjs: ['index', 'configuration', 'protecting-routes', ],
|
||||
cli: ['index'],
|
||||
'supporting-libraries': ['react-apollo', 'react-auth']
|
||||
'hasura-auth': ['index', 'installation', 'configuration', 'environment-variables', 'schema', 'api-reference']
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -2,15 +2,15 @@ import { lightNhostTheme } from '@/data/lightTheme'
|
||||
import { useState } from 'react'
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript'
|
||||
import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript'
|
||||
|
||||
import Check from '../icons/Check'
|
||||
import Copy from '../icons/Copy'
|
||||
|
||||
// @ts-ignore -> add to types
|
||||
// @ts-ignore -> add to types
|
||||
SyntaxHighlighter.registerLanguage('js', js)
|
||||
SyntaxHighlighter.registerLanguage('language-js', js)
|
||||
SyntaxHighlighter.registerLanguage('language-ts', ts)
|
||||
// TODO highlight JSX
|
||||
SyntaxHighlighter.registerLanguage('jsx', js)
|
||||
SyntaxHighlighter.registerLanguage('language-jsx', js)
|
||||
|
||||
export interface CodeEditorProps {
|
||||
code: string
|
||||
@@ -24,7 +24,7 @@ export interface CodeEditorProps {
|
||||
}
|
||||
|
||||
const CodeEditor = (props: CodeEditorProps) => {
|
||||
const { children, url } = props
|
||||
const { children, className } = props
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
return (
|
||||
@@ -53,6 +53,7 @@ const CodeEditor = (props: CodeEditorProps) => {
|
||||
</button>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
language={className}
|
||||
style={lightNhostTheme}
|
||||
wrapLongLines={true}
|
||||
wrapLines={true}
|
||||
|
||||
@@ -8,6 +8,7 @@ import React, { DetailedHTMLProps, HTMLProps, PropsWithChildren } from 'react'
|
||||
|
||||
import Command from '../Command'
|
||||
import Divider from '../Divider'
|
||||
import { Swagger } from '../Swagger'
|
||||
|
||||
function Note({ children }: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
@@ -65,7 +66,7 @@ const CustomLink = ({
|
||||
const components = {
|
||||
img: (props: DetailedHTMLProps<HTMLProps<HTMLImageElement>, HTMLImageElement>) => {
|
||||
return (
|
||||
<span className="block mx-10 mt-5 ">
|
||||
<span className="block mx-10 my-10 ">
|
||||
<img src={props.src} alt={props.alt} className="mx-auto mt-2" />
|
||||
</span>
|
||||
)
|
||||
@@ -135,6 +136,7 @@ const components = {
|
||||
}: DetailedHTMLProps<HTMLProps<HTMLTableCellElement>, HTMLTableCellElement>) => {
|
||||
return <td className={clsx('font-display', className)} {...props} />
|
||||
},
|
||||
Swagger,
|
||||
Mermaid: ({ chart }) => {
|
||||
const [html, setHtml] = React.useState('')
|
||||
React.useEffect(() => {
|
||||
|
||||
29
docs/components/Swagger.tsx
Normal file
29
docs/components/Swagger.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import SwaggerUI from 'swagger-ui-react'
|
||||
import 'swagger-ui-react/swagger-ui.css'
|
||||
|
||||
const OperationsLayout = (props) => {
|
||||
const { getComponent } = props
|
||||
const Operations = getComponent('operations', true)
|
||||
let SvgAssets = getComponent('SvgAssets')
|
||||
return (
|
||||
<div className="swagger-ui">
|
||||
<SvgAssets />
|
||||
<Operations />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OperationsLayoutPlugin = () => ({
|
||||
components: {
|
||||
OperationsLayout
|
||||
}
|
||||
})
|
||||
|
||||
export const Swagger: React.FC<{ spec: string }> = ({ spec }) => (
|
||||
<SwaggerUI
|
||||
url={`/openapi/${spec}`}
|
||||
plugins={[OperationsLayoutPlugin]}
|
||||
layout="OperationsLayout"
|
||||
supportedSubmitMethods={[]}
|
||||
/>
|
||||
)
|
||||
@@ -18,9 +18,14 @@ In this guide, we'll keep the example simple. We're not using a frontend framewo
|
||||
|
||||
Create a new folder called `nhost-todos`, and initialize a new JavaScript app there:
|
||||
|
||||
Using npm package manager
|
||||
```sh
|
||||
npm init -y
|
||||
# or
|
||||
```
|
||||
or
|
||||
|
||||
Using Yarn package manager
|
||||
```sh
|
||||
yarn init -y
|
||||
```
|
||||
|
||||
@@ -28,9 +33,13 @@ yarn init -y
|
||||
|
||||
Install Nhost JavaScript SDK:
|
||||
|
||||
Using npm package manager
|
||||
```sh
|
||||
npm install @nhost/nhost-js
|
||||
# or
|
||||
```
|
||||
or
|
||||
Using Yarn package manager
|
||||
```sh
|
||||
yarn add @nhost/nhost-js
|
||||
```
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ Access token data is included as headers with every API request. By default, eve
|
||||
|
||||
The default role for users is `user`.
|
||||
|
||||
> You can also [add custom permission](#add-permission-variables) varaibles if you need to.
|
||||
|
||||
---
|
||||
|
||||
## Select permissions
|
||||
@@ -60,3 +62,23 @@ In our example, we only select `name`, because we want all other other columns t
|
||||
We also want every new record's `user_id` value to be set to the ID of the user making the request. We can tell Hasura to do this with **column presets**.
|
||||
|
||||
1. Under column presets, set `user_id` to `x-hasura-user-id`.
|
||||
|
||||
## Add Permission Variables
|
||||
|
||||
You can add extra permission variables in the Nhost console under **Users** and then **Roles and permissions**. These permission variables are then available when creating permissions for your GraphQL API in the Hasura console.
|
||||
|
||||

|
||||
|
||||
As an example, let's say you add a new permission variable `x-hasura-organisation-id` with path `user.profile.organisation.id`. This means that Nhost Auth will get the value for `x-hasura-organisation-id` by internally generating the following GraphQL query:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
user(id: "<user-id>") {
|
||||
profile {
|
||||
organisation {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,10 +39,10 @@ HTTP endpoints are automatically generated based on the file structure under `fu
|
||||
As such, given this file structure:
|
||||
|
||||
```js
|
||||
functions / index.js
|
||||
users / index.ts
|
||||
active.ts
|
||||
my - company.js
|
||||
functions/index.js
|
||||
functions/users/index.ts
|
||||
functions/active.ts
|
||||
functions/my-company.js
|
||||
```
|
||||
|
||||
The following endpoints will be available:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: 'API Reference'
|
||||
subtitle: 'Hasura Auth'
|
||||
---
|
||||
|
||||
<Swagger spec="hasura-auth.json" />
|
||||
221
docs/content/docs/reference/hasura-auth/configuration.mdx
Normal file
221
docs/content/docs/reference/hasura-auth/configuration.mdx
Normal file
@@ -0,0 +1,221 @@
|
||||
---
|
||||
title: Configuration
|
||||
---
|
||||
|
||||
## Email configuration
|
||||
|
||||
Hasura Auth automatically sends transactional emails to manage the following operations:
|
||||
|
||||
- Sign up
|
||||
- Password reset
|
||||
- Email change
|
||||
- Passwordless with emails
|
||||
|
||||
### SMTP settings
|
||||
|
||||
```bash
|
||||
AUTH_SMTP_HOST=smtp.example.com
|
||||
AUTH_SMTP_PORT=1025
|
||||
AUTH_SMTP_USER=user
|
||||
AUTH_SMTP_PASS=password
|
||||
AUTH_SMTP_SENDER=hasura-auth@example.com
|
||||
```
|
||||
|
||||
See the [environment variables](/reference/hasura-auth/environment-variables) for additional information about how to connnect to an SMTP server.
|
||||
|
||||
### Email templates
|
||||
|
||||
You can create your own templates to customize the emails that will be sent to the users. You can have a look at the [official email templates](https://github.com/nhost/hasura-auth/tree/main/email-templates) to understand how they are structured.
|
||||
|
||||
#### With Docker
|
||||
|
||||
When using Docker, you can mount your own email templates from the local file system. You can have a look at this [docker-compose example](https://github.com/nhost/hasura-auth/blob/16df3e84b6c9a4f888b2ff07bd85afc34f8ed051/docker-compose-example.yaml#L41) to see how to set it up.
|
||||
|
||||
#### Remote email templates
|
||||
|
||||
When running Hasura Auth in its own infrastructure, it is possible to mount a volume with custom `email-templates` directory. However, in some cases, we may want to fetch templates from an external HTTP endpoint. Hence the introduction of a new `AUTH_EMAIL_TEMPLATE_FETCH_URL` environment variable:
|
||||
|
||||
```bash
|
||||
AUTH_EMAIL_TEMPLATE_FETCH_URL=https://github.com/nhost/nhost/tree/custom-email-templates-example/examples/custom-email-templates
|
||||
```
|
||||
|
||||
In the above example, on every email creation, the server will use this URL to fetch its templates, depending on the locale, email type and field.
|
||||
|
||||
For instance, the template for english verification email body will the fetched in [https://raw.githubusercontent.com/nhost/nhost/main/examples/custom-email-templates/en/email-verify/body.html](https://raw.githubusercontent.com/nhost/nhost/main/examples/custom-email-templates/en/email-verify/body.html).
|
||||
|
||||
See the [example in the main nhost/nhost repository](https://github.com/nhost/nhost/tree/main/examples/custom-email-templates).
|
||||
|
||||
The context variables in email templates have been simplified: the `${link}` variable contains the entire redirection url the recipient needs to follow.
|
||||
|
||||
---
|
||||
|
||||
## Redirections
|
||||
|
||||
Some authentication operations redirects the users to the frontend application:
|
||||
|
||||
- After an OAuth provider completes or fails authentication, the user is redirected to the frontend
|
||||
- Every email sent to the user (passwordless with email, password/email change, password reset) contains a link, that redirects the user to the frontend
|
||||
|
||||
In order to achieve that, you need to set the `AUTH_CLIENT_URL` environment variable, for instance:
|
||||
|
||||
```bash
|
||||
AUTH_CLIENT_URL=https://my-app.vercel.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Email + password authentication
|
||||
|
||||
### Email checks
|
||||
|
||||
You can specify a list of allowed emails or domains with `AUTH_ACCESS_CONTROL_ALLOWED_EMAILS` and `AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS`.
|
||||
|
||||
As an example, the following environment variables will only allow `@nhost.io`, `@example.com` and `bob@smith.com` to register to the application:
|
||||
|
||||
```bash
|
||||
AUTH_ACCESS_CONTROL_ALLOWED_EMAILS=bob@smith.com
|
||||
AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS=nhost.io,example.com
|
||||
```
|
||||
|
||||
In the above example, users with the following emails would be able to register `bob@smith.com`, `emma@example.com`, `john@nhost.io`, whereas `mary@firebase.com` won't.
|
||||
|
||||
Similarly, it is possible to provide a list of forbidden emails or domains with `AUTH_ACCESS_CONTROL_BLOCKED_EMAILS` and `AUTH_ACCESS_CONTROL_BLOCKED_EMAIL_DOMAINS`.
|
||||
|
||||
### Password checks
|
||||
|
||||
Hasura auth does not accepts passwords with less than three characters. This limit can be changed in changing the `AUTH_PASSWORD_MIN_LENGTH` environment variable.
|
||||
|
||||
It is also possible to only allow [passwords that have not been pwned](https://haveibeenpwned.com/) in setting `AUTH_PASSWORD_HIBP_ENABLED` to `true`.
|
||||
|
||||
<!-- TODO ### Change -->
|
||||
<!-- TODO ### Reset email -->
|
||||
|
||||
<!-- TODO ### Reset password -->
|
||||
|
||||
<!-- ---
|
||||
TODO ## Anonymous users -->
|
||||
|
||||
---
|
||||
|
||||
## Multi-factor authentication
|
||||
|
||||
Hasura Auth supports different types of Multi-Factor Authentication (MFA): passwordless with emails (magic links), passwordless with SMS, and Time-based one-time passwords.
|
||||
|
||||
### Passwordless with emails (magic links)
|
||||
|
||||
Hasura Auth supports email [passwordless authentication](https://en.wikipedia.org/wiki/Passwordless_authentication). It requires [SMTP](#email-configuration) to be configured properly.
|
||||
|
||||
Set `AUTH_EMAIL_PASSWORDLESS_ENABLED` to `true` to enable passwordless authentication.
|
||||
|
||||
<!-- TODO ### Passwordless with SMS -->
|
||||
|
||||
### Time-based one-time password (TOTP)
|
||||
|
||||
It is possible to add a step to authentication with email and password authentication. Once users registered, they can activate MFA TOTP:
|
||||
|
||||
1. Users generate a QR Code, that is then scanned in an authentication app such as [Authy](https://authy.com/) or [Google Authenticator](https://en.wikipedia.org/wiki/Google_Authenticator).
|
||||
2. They then send the TOTP code to Hasura Auth. MFA is now activated
|
||||
3. Next time they authenticate, Hasura Auth will first expect their email and password, but then, instead of completing authentication, Hasura Auth will expect the TOTP in order to return the refresh and the access tokens.
|
||||
|
||||
In order for users to be able to activate MFA TOTP, `AUTH_MFA_ENABLED` must be set to `true`.
|
||||
|
||||
<!-- ---
|
||||
|
||||
TODO ## OAuth authentication -->
|
||||
|
||||
---
|
||||
|
||||
## Gravatar
|
||||
|
||||
Hasura Auth stores the avatar URL of users in `auth.users.avatar_url`. By default, it will look for the Gravatar linked to the email, and store it into this field.
|
||||
It is possible to deactivate the use of Gravatar in setting the `AUTH_GRAVATAR_ENABLED` environment variable to `false`.
|
||||
|
||||
---
|
||||
|
||||
## Extending user schema
|
||||
|
||||
Adding columns to the user tables may be tempting. However, all the tables and columns have a specific purpose, and changing the structure of the `auth` schema will very likely end in breaking the functionning of Hasura Auth. It's, therefore, **highly recommended** not to modify the database schema for any tables in the `auth` schema.
|
||||
|
||||
Instead, we recommend adding extra user information in the following ways:
|
||||
|
||||
- to store information in the `auth.users.metadata` column
|
||||
- to store information in a separate table located in the `public` PostgreSQL schema, and to point to `auth.users.id` through a foreign key.
|
||||
|
||||
### `metadata` user field
|
||||
|
||||
The `auth.users.metadata` field is a JSON column, that can be used as an option on registration:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "bob@bob.com",
|
||||
"passord": "12345678",
|
||||
"options": {
|
||||
"metadata": {
|
||||
"first_name": "Bob"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Additional user information in the `public` schema
|
||||
|
||||
As previously explained, the alteration of the `auth` schema may seriously hamper the functionning of Hasura Auth. The `metadata` field in the `auth.users` table may tackle some use cases, but in some other cases, we want to keep a certain level of structure in the way data is structured.
|
||||
|
||||
In that case, it is possible to create a dedicated table in the `public` schema, with a `user_id` foreign key column that would point to the `auth.users.id` column. It is then possible to add an Hasura object relationship that would join the two tables together.
|
||||
|
||||
<!-- TODO hooks on the metadata field -->
|
||||
|
||||
---
|
||||
|
||||
## Custom Hasura JWT claims
|
||||
|
||||
Hasura comes with a [powerful authorisation system](https://hasura.io/docs/latest/graphql/core/auth/authorization/index.html). Hasura Auth is already configured to add `x-hasura-user-id`, `x-hasura-allowed-roles`, and `x-hasura-user-isAnonymous` to the JSON Web Tokens it generates.
|
||||
|
||||
In Hasura Auth, it is possible to define custom claims to add to the JWT, so they can be used by Hasura to determine the permissions of the received GraphQL operation.
|
||||
|
||||
Each custom claim is defined by a pair of a key and a value:
|
||||
|
||||
- The key determines the name of the claim, prefixed by `x-hasura`. For instance, `organisation-id` will become `x-hasura-organisation-id`.
|
||||
- The value is a representation of the path to look at to determine the value of the claim. For instance `profile.organisation.id` will look for the `user.profile` Hasura relationship, and the `profile.organisation` Hasura relationship. Array values are transformed into Postgres syntax so Hasura can interpret them. See the official Hasura documentation to understand the [session variables format](https://hasura.io/docs/latest/graphql/core/auth/authorization/roles-variables.html#format-of-session-variables).
|
||||
|
||||
```bash
|
||||
AUTH_JWT_CUSTOM_CLAIMS={"organisation-id":"profile.organisation.id", "project-ids":"profile.contributesTo.project.id"}
|
||||
```
|
||||
|
||||
Will automatically generate and fetch the following GraphQL query:
|
||||
|
||||
```graphql
|
||||
{
|
||||
user(id: "<user-id>") {
|
||||
profile {
|
||||
organisation {
|
||||
id
|
||||
}
|
||||
contributesTo {
|
||||
project {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It will then use the same expressions e.g. `profile.contributesTo.project.id` to evaluate the result with [JSONata](https://jsonata.org/), and possibly transform arrays into Hasura-readable, PostgreSQL arrays.Finally, it adds the custom claims to the JWT in the `https://hasura.io/jwt/claims` namespace:
|
||||
|
||||
```json
|
||||
{
|
||||
"https://hasura.io/jwt/claims": {
|
||||
"x-hasura-organisation-id": "8bdc4f57-7d64-4146-a663-6bcb05ea2ac1",
|
||||
"x-hasura-project-ids": "{\"3af1b33f-fd0f-425e-92e2-0db09c8b2e29\",\"979cb94c-d873-4d5b-8ee0-74527428f58f\"}",
|
||||
"x-hasura-allowed-roles": [ "me", "user" ],
|
||||
"x-hasura-default-role": "user",
|
||||
"x-hasura-user-id": "121bbea4-908e-4540-ac5d-52c7f6f93bec",
|
||||
"x-hasura-user-isAnonymous": "false"
|
||||
}
|
||||
"sub": "f8776768-4bbd-46f8-bae1-3c40da4a89ff",
|
||||
"iss": "hasura-auth",
|
||||
"iat": 1643040189,
|
||||
"exp": 1643041089
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Environment Variables
|
||||
---
|
||||
|
||||
## General environment variables
|
||||
|
||||
| Name (a star**\*** means the variable is required) | Description | Default value |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
|
||||
| HASURA_GRAPHQL_JWT_SECRET**\*** | Key used for generating JWTs. Must be `HMAC-SHA`-based and the same as configured in Hasura. [More info](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html#running-with-jwt) | |
|
||||
| HASURA_GRAPHQL_DATABASE_URL**\*** | [PostgreSQL connection URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). Required to inject the `auth` schema into the database. | |
|
||||
| HASURA_GRAPHQL_GRAPHQL_URL**\*** | Hasura GraphQL endpoint. Required to manipulate account data. For instance: `https://graphql-engine:8080/v1/graphql` | |
|
||||
| HASURA_GRAPHQL_ADMIN_SECRET**\*** | Hasura GraphQL Admin Secret. Required to manipulate account data. | |
|
||||
| AUTH_HOST | Server host. [Docs](http://expressjs.com/en/5x/api.html#app.listen) | `0.0.0.0` |
|
||||
| AUTH_PORT | Server port. [Docs](http://expressjs.com/en/5x/api.html#app.listen) | `4000` |
|
||||
| AUTH_SERVER_URL | Server URL of where Hasura Backend Plus is running. This value is to used as a callback in email templates and for the OAuth authentication process. | |
|
||||
| AUTH_CLIENT_URL | URL of your frontend application. Used to redirect users to the right page once actions based on emails or OAuth succeed. | |
|
||||
| AUTH_SMTP_HOST | SMTP server hostname used for sending emails | |
|
||||
| AUTH_SMTP_PORT | SMTP port | `587` |
|
||||
| AUTH_SMTP_USER | Username to use to authenticate on the SMTP server | |
|
||||
| AUTH_SMTP_PASS | Password to use to authenticate on the SMTP server | |
|
||||
| AUTH_SMTP_SENDER | Email to use in the `From` field of the email | |
|
||||
| AUTH_SMTP_AUTH_METHOD | SMTP authentication method | `PLAIN` |
|
||||
| AUTH_SMTP_SECURE | Enables SSL. [More info](https://nodemailer.com/smtp/#tls-options). | `false` |
|
||||
| AUTH_GRAVATAR_ENABLED | | `true` |
|
||||
| AUTH_GRAVATAR_DEFAULT | | `blank` |
|
||||
| AUTH_GRAVATAR_RATING | | `g` |
|
||||
| AUTH_ANONYMOUS_USERS_ENABLED | Enables users to register as an anonymous user. | `false` |
|
||||
| AUTH_DISABLE_NEW_USERS | If set, new users will be disabled after finishing registration and won't be able to connect. | `false` |
|
||||
| AUTH_ACCESS_CONTROL_ALLOWED_EMAILS | Comma-separated list of emails that are allowed to register. | |
|
||||
| AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS | Comma-separated list of email domains that are allowed to register. If `ALLOWED_EMAIL_DOMAINS` is `tesla.com,ikea.se`, only emails from tesla.com and ikea.se would be allowed to register an account. | `` (allow all email domains) |
|
||||
| AUTH_ACCESS_CONTROL_BLOCKED_EMAILS | Comma-separated list of emails that cannot register. | |
|
||||
| AUTH_ACCESS_CONTROL_BLOCKED_EMAIL_DOMAINS | Comma-separated list of email domains that cannot register. | |
|
||||
| AUTH_PASSWORD_MIN_LENGTH | Minimum password length. | `3` |
|
||||
| AUTH_PASSWORD_HIBP_ENABLED | User's password is checked against [Pwned Passwords](https://haveibeenpwned.com/Passwords). | `false` |
|
||||
| AUTH_USER_DEFAULT_ROLE | Default user role for registered users. | `user` |
|
||||
| AUTH_USER_DEFAULT_ALLOWED_ROLES | Comma-separated list of default allowed user roles. | `me,$AUTH_USER_DEFAULT_ROLE` |
|
||||
| AUTH_LOCALE_DEFAULT | | `en` |
|
||||
| AUTH_LOCALE_ALLOWED_LOCALES | | `en` |
|
||||
| AUTH_EMAIL_PASSWORDLESS_ENABLED | Enables passwordless authentication by email. The SMTP server must then be configured. | `false` |
|
||||
| AUTH_SMS_PASSWORDLESS_ENABLED | Enables passwordless authentication by SMS. An SMS provider must then be configured. | `false` |
|
||||
| AUTH_SMS_PROVIDER | SMS provider name. Only `twilio` is possible as an option for now. | |
|
||||
| AUTH_SMS_TWILIO_ACCOUNT_SID | | |
|
||||
| AUTH_SMS_TWILIO_AUTH_TOKEN | | |
|
||||
| AUTH_SMS_TWILIO_MESSAGING_SERVICE_ID | | |
|
||||
| AUTH_SMS_TWILIO_FROM | | |
|
||||
| AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED | When enabled, any email-based authentication requires emails to be verified by a link sent to this email. | `true` |
|
||||
| AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS | | |
|
||||
| AUTH_MFA_ENABLED | Enables users to use Multi Factor Authentication. | `false` |
|
||||
| AUTH_MFA_TOTP_ISSUER | The name of the One Time Password (OTP) issuer. Probably your app's name. | `hasura-auth` |
|
||||
| AUTH_ACCESS_TOKEN_EXPIRES_IN | Number of seconds before the access token (JWT) expires. | `900`(15 minutes) |
|
||||
| AUTH_REFRESH_TOKEN_EXPIRES_IN | Number of seconds before the refresh token expires. | `2592000` (30 days) |
|
||||
| AUTH_EMAIL_TEMPLATE_FETCH_URL | | |
|
||||
| AUTH_JWT_CUSTOM_CLAIMS | | |
|
||||
|
||||
## OAuth environment variables
|
||||
|
||||
| Name (a star**\*** means the variable is required when the provider is enabled) | Default value |
|
||||
| ------------------------------------------------------------------------------- | ----------------------------------- |
|
||||
| AUTH_PROVIDER_GITHUB_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_GITHUB_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_GITHUB_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_GITHUB_AUTHORIZATION_URL | |
|
||||
| AUTH_PROVIDER_GITHUB_TOKEN_URL | |
|
||||
| AUTH_PROVIDER_GITHUB_USER_PROFILE_URL | |
|
||||
| AUTH_PROVIDER_GITHUB_SCOPE | `user:email ` |
|
||||
| AUTH_PROVIDER_GOOGLE_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_GOOGLE_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_GOOGLE_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_GOOGLE_SCOPE | `email,profile` |
|
||||
| AUTH_PROVIDER_FACEBOOK_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_FACEBOOK_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_FACEBOOK_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_FACEBOOK_PROFILE_FIELDS | `email,photos,displayName` |
|
||||
| AUTH_PROVIDER_FACEBOOK_SCOPE | `email` |
|
||||
| AUTH_PROVIDER_TWITTER_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_TWITTER_CONSUMER_KEY**\*** | |
|
||||
| AUTH_PROVIDER_TWITTER_CONSUMER_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_LINKEDIN_ENABLED | |
|
||||
| AUTH_PROVIDER_LINKEDIN_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_LINKEDIN_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_LINKEDIN_SCOPE | `r_emailaddress,r_liteprofile` |
|
||||
| AUTH_PROVIDER_APPLE_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_APPLE_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_APPLE_TEAM_ID**\*** | |
|
||||
| AUTH_PROVIDER_APPLE_KEY_ID**\*** | |
|
||||
| AUTH_PROVIDER_APPLE_PRIVATE_KEY**\*** | Base64 format |
|
||||
| AUTH_PROVIDER_APPLE_SCOPE | `name,email` |
|
||||
| AUTH_PROVIDER_WINDOWS_LIVE_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_WINDOWS_LIVE_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_WINDOWS_LIVE_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_WINDOWS_LIVE_SCOPE | `wl.basic,wl.emails` |
|
||||
| AUTH_PROVIDER_SPOTIFY_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_SPOTIFY_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_SPOTIFY_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_SPOTIFY_SCOPE | `user-read-email,user-read-private` |
|
||||
| AUTH_PROVIDER_GITLAB_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_GITLAB_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_GITLAB_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_GITLAB_BASE_URL | |
|
||||
| AUTH_PROVIDER_GITLAB_SCOPE | `read_user` |
|
||||
| AUTH_PROVIDER_BITBUCKET_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_BITBUCKET_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_BITBUCKET_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_STRAVA_ENABLED | `false` |
|
||||
| AUTH_PROVIDER_STRAVA_CLIENT_ID**\*** | |
|
||||
| AUTH_PROVIDER_STRAVA_CLIENT_SECRET**\*** | |
|
||||
| AUTH_PROVIDER_STRAVA_SCOPE | `profile:read_all` |
|
||||
41
docs/content/docs/reference/hasura-auth/index.mdx
Normal file
41
docs/content/docs/reference/hasura-auth/index.mdx
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: 'Overview'
|
||||
---
|
||||
|
||||
Hasura Auth handles **authentication** for [Hasura](https://github.com/hasura/graphql-engine).
|
||||
|
||||
Hasura Auth runs in a separate Docker container alongside Postgres and Hasura.
|
||||
|
||||
## Features
|
||||
|
||||
- 🧑🤝🧑 Users are stored in Postgres and accessed via GraphQL
|
||||
- 🔑 Multiple sign-in methods
|
||||
- ✨ Integrates with GraphQL and Hasura Permissions
|
||||
- 🔐 JWT tokens and Refresh Tokens.
|
||||
- ✉️ Emails sent on various operations
|
||||
<!-- - ✅ Optional checking for Pwned Passwords. -->
|
||||
- 🛡️ Two-factor authentication support.
|
||||
- 👨💻 Written 100% in TypeScript.
|
||||
|
||||
### Authentication methods
|
||||
|
||||
- **Email and Password**: simple email and password method.
|
||||
- **Email**, also called **passwordless email** or **magic link**.
|
||||
- **SMS**, also called **passwordless sms**.
|
||||
- **Anonymous**: sign in users without any method. Anonymous users can be
|
||||
converted to _regular_ users at a later stage.
|
||||
- **OAuth providers**: Facebook, Google, GitHub, Twitter, Apple, LinkedIn, Windows Live, Spotify, Strave, GitLab, BitBucket
|
||||
|
||||
## Integration with Hasura
|
||||
|
||||
Hasura Auth's final purpose is to securely provide a JSON Web Token that can be added as an authorization header to GraphQL operation sent to Hasura.
|
||||
Hasura auth automatically generates and manages two kinds of tokens:
|
||||
|
||||
- An access token (JWT), that will be used to authenticate the GraphQL operations in Hasura, and that has a limited expiration limit (15 minutes by default)
|
||||
- A refresh token, that is used to ask Hasura Auth for a new access token, and that can be consummed only once.
|
||||
|
||||
Access tokens generated by Hasura Auth contains information and user id, its default role, and the roles they actually have. In addition, it is possible since version `0.2.0` to extend JWT claims with custom information such as organisation or project ownership, so your application can leverage the capabilities of the [Hasura permissions layer](https://hasura.io/docs/latest/graphql/core/auth/authorization/index.html).
|
||||
|
||||
<!-- - Users and accounts are saved in the database. -->
|
||||
|
||||
You can read further information about JWT and Hasura in the [official Hasura documentation](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html).
|
||||
24
docs/content/docs/reference/hasura-auth/installation.mdx
Normal file
24
docs/content/docs/reference/hasura-auth/installation.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Installation
|
||||
---
|
||||
|
||||
Hasura Auth runs in a container alongside Postgres and Hasura.
|
||||
|
||||
## Nhost (recommended)
|
||||
|
||||
The recommended way to start using Hasura Auth is by using Nhost. With Nhost, you will get a complete backend ready in seconds with Hasura, authentication, storage and serverless functions.
|
||||
|
||||
Go to [Nhost](https://nhost.io) and start building your app now.
|
||||
|
||||
## Docker-compose
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nhost/hasura-auth.git
|
||||
cd hasura-auth
|
||||
cp .env.example .env
|
||||
docker-compose -f docker-compose-example.yaml up
|
||||
```
|
||||
|
||||
Hasura Auth comes with plenty of options. They are explained in the [configuration section](/reference/hasura-auth/configuration).
|
||||
|
||||
If you are already familiar with the application, you can also have a look at the [environment variables](/reference/hasura-auth/environment-variables) that can be passed on to your docker container.
|
||||
85
docs/content/docs/reference/hasura-auth/schema.mdx
Normal file
85
docs/content/docs/reference/hasura-auth/schema.mdx
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: 'Schema'
|
||||
---
|
||||
|
||||
Hasura Auth stores all its data in a dedicated `auth` PostgreSQL schema. When Hasura Auth starts, it checks if the `auth` schema exists, then automatically syncs the following tables and their corresponding Hasura metadata:
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
migrations {
|
||||
integer id PK
|
||||
varchar name
|
||||
varchar hash
|
||||
timestamp executed_at "CURRENT_TIMESTAMP"
|
||||
}
|
||||
|
||||
users ||--o{ user_roles : roles
|
||||
user_roles }o--|| roles: role
|
||||
users }o--|| roles: role
|
||||
users ||--o{ refresh_tokens: refreshTokens
|
||||
users ||--o{ user_providers: provider
|
||||
providers ||--o{ user_providers: user
|
||||
|
||||
provider_requests {
|
||||
uuid id PK "gen_random_uuid()"
|
||||
test redirect_url
|
||||
}
|
||||
|
||||
|
||||
refresh_tokens {
|
||||
uuid refresh_token PK
|
||||
uuid user_id FK
|
||||
timestamptz created_at "now()"
|
||||
timestamptz expires_at
|
||||
}
|
||||
|
||||
providers {
|
||||
text id PK
|
||||
}
|
||||
user_providers {
|
||||
uuid id PK "gen_random_uuid()"
|
||||
timestamptz created_at "now()"
|
||||
timestamptz updated_at "now()"
|
||||
uuid user_id FK
|
||||
text access_token
|
||||
text refresh_token
|
||||
text provider_id FK
|
||||
text provider_user_id
|
||||
}
|
||||
user_roles {
|
||||
uuid id PK "gen_random_uuid()"
|
||||
timestamptz created_at "now()"
|
||||
uuid user_id FK
|
||||
text role FK
|
||||
}
|
||||
users {
|
||||
uuid id PK "gen_random_uuid()"
|
||||
timestamptz created_at "now()"
|
||||
timestamptz updated_at "now()"
|
||||
timestamptz last_seen "nullable"
|
||||
boolean disabled "false"
|
||||
text display_name "''"
|
||||
text avatar_url "''"
|
||||
varchar locale
|
||||
email email "nullable"
|
||||
text phone_number "nullable"
|
||||
text password_hash "nullable"
|
||||
boolean email_verified "false"
|
||||
boolean phone_number_verified "false"
|
||||
email new_email "nullable"
|
||||
text otp_method_last_used "nullable"
|
||||
text otp_hash "nullable"
|
||||
timestamptz opt_hash_expires_at "now()"
|
||||
text default_role FK "user"
|
||||
boolean is_anonymous "false"
|
||||
text totp_secret "nullable"
|
||||
text active_mfa_type "nullable"
|
||||
text ticket "nullable"
|
||||
timestamptz ticket_expires_at "now()"
|
||||
jsonb metadata "nullable"
|
||||
}
|
||||
|
||||
roles {
|
||||
text roles PK
|
||||
}
|
||||
```
|
||||
@@ -28,3 +28,11 @@ In this section:
|
||||
### Nhost CLI
|
||||
|
||||
- [CLI overview](/reference/cli)
|
||||
### Hasura Auth
|
||||
|
||||
- [Overview](./reference/hasura-auth)
|
||||
- [Installation](./reference/hasura-auth/installation)
|
||||
- [Configuration](./reference/hasura-auth/configuration)
|
||||
- [Environment variables](./reference/hasura-auth/environment-variables)
|
||||
- [API](./reference/hasura-auth/api-reference)
|
||||
- [Schema](./reference/hasura-auth/api-reference)
|
||||
|
||||
@@ -20,27 +20,27 @@ npm install @nhost/react @nhost/nextjs
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuring Nhost with Next.js follows the same logic as React, except we are initializing with `NhostSSR` instead of `Nhost`.
|
||||
Under the hood, `NhostSSR` uses cookies to store the refresh token, and disables auto-refresh and auto-login when running on the server-side.
|
||||
Configuring Nhost with Next.js follows the same logic as React, except we are initializing with the `NhostClient` from the `@nhost/nextjs` package.
|
||||
Under the hood, `NhostClient` uses cookies to store the refresh token, and disables auto-refresh and auto-login when running on the server-side.
|
||||
|
||||
```jsx
|
||||
// {project-root}/pages/_app.tsx
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
import { NhostSSR, NhostProvider } from '@nhost/nextjs'
|
||||
import { NhostClient, NhostNextProvider } from '@nhost/nextjs'
|
||||
|
||||
import Header from '../components/Header'
|
||||
|
||||
const nhost = new NhostSSR({ backendUrl: 'my-app.nhost.run' })
|
||||
const nhost = new NhostClient({ backendUrl: 'my-app.nhost.run' })
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<NhostProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<div>
|
||||
<Header />
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</NhostProvider>
|
||||
</NhostNextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ export default MyApp
|
||||
The logic is the same as in a classic React application:
|
||||
|
||||
```jsx
|
||||
// {project-root}/pages/csr-page.jsx
|
||||
// {project-root}/pages/csr-page.tsx
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
import { useAccessToken, useAuthenticated, useUserData } from '@nhost/react'
|
||||
import { useAccessToken, useAuthenticated, useUserData } from '@nhost/nextjs'
|
||||
|
||||
const ClientSidePage: React.FC = () => {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
@@ -83,7 +83,7 @@ export default ClientSidePage
|
||||
You need to load the session from the server first from `getServerSideProps`. Once it is done, the `_app` component will make sure to load or update the session through `pageProps`.
|
||||
|
||||
```jsx
|
||||
// {project-root}/pages/ssr-page.jsx
|
||||
// {project-root}/pages/ssr-page.tsx
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
@@ -93,7 +93,7 @@ import {
|
||||
useAccessToken,
|
||||
useAuthenticated,
|
||||
useUserData
|
||||
} from '@nhost/react'
|
||||
} from '@nhost/nextjs'
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const nhostSession = await getNhostSession('my-app.nhost.run', context)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: 'Introduction'
|
||||
---
|
||||
|
||||
It is possible to use [`@nhost/react`](/reference/react) in any Next.js page that would be configured to render on the client-side.
|
||||
All the React hooks and helpers from [`@nhost/react`](/reference/react) are available in Next.js and are exported in the `@nhost/nextjs` package.
|
||||
|
||||
When rendering a page from the server-side, Next.js needs to get some information from the client to determine their authentication status. Such communication is only available from cookies, and the Nhost client is designed to enable such a mechanism.
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ Create a `auth-protected.js` file:
|
||||
|
||||
```jsx
|
||||
import { useRouter } from 'next/router'
|
||||
import { useAuthLoading, useAuthenticated } from '@nhost/react'
|
||||
import { useAuthenticationStatus } from '@nhost/nextjs'
|
||||
|
||||
export function authProtected(Comp) {
|
||||
return function AuthProtected(props) {
|
||||
const router = useRouter()
|
||||
const isLoading = useAuthLoading()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
|
||||
@@ -7,38 +7,37 @@ title: 'Apollo GraphQL'
|
||||
With Yarn:
|
||||
|
||||
```sh
|
||||
yarn add @nhost/react @nhost/react-apollo
|
||||
yarn add @nhost/react @nhost/react-apollo @apollo/client
|
||||
```
|
||||
|
||||
With Npm:
|
||||
|
||||
```sh
|
||||
npm install @nhost/react @nhost/react-apollo
|
||||
npm install @nhost/react @nhost/react-apollo @apollo/client
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Let's add a `NhostApolloProvider`. Make sure the Apollo Provider is nested into `NhostProvider`, as it will need the Nhost context to determine the authentication headers to be sent to the GraphQL endpoint.
|
||||
Let's add a `NhostApolloProvider`. Make sure the Apollo Provider is nested into `NhostReactProvider`, as it will need the Nhost context to determine the authentication headers to be sent to the GraphQL endpoint.
|
||||
|
||||
```jsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import { NhostClient, NhostReactProvider } from '@nhost/react'
|
||||
|
||||
const nhost = new Nhost({
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: 'http://localhost:1337'
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<NhostApolloProvider>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<App />
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</NhostReactProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
@@ -7,36 +7,38 @@ title: 'Hooks'
|
||||
### Email and Password Sign-Un
|
||||
|
||||
```js
|
||||
const { signUp, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
useEmailPasswordSignUp(email?: string, password?: string, options?: Options )
|
||||
const { signUpEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useSignUpEmailPassword(options?: Options)
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signUp` | (email?: string, password?: string) => void | Used for a new user to sign up. The email/password arguments will take precedence over the possible state values used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsVerification` | boolean | Returns `true` if the sign-up has been accepted, but a verificaiton email has been sent and is awaiting. |
|
||||
| `isSuccess` | boolean | Returns `true` if the sign-up suceeded. Returns `false` if the new email needs to be verified first, or if an error occurred. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined | |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signUpEmailPassword` | (email?: string, password?: string) => void | Used for a new user to sign up. Returns a promise with the current context |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsEmailVerification` | boolean | Returns `true` if the sign-up has been accepted, but a verificaiton email has been sent and is awaiting. |
|
||||
| `isSuccess` | boolean | Returns `true` if the sign-up suceeded. Returns `false` if the new email needs to be verified first, or if an error occurred. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| null | Provides details about the error. |
|
||||
| `user` | User \| null | User information |
|
||||
| `accessToken` | string \| null | Access token (JWT) |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined | |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useEmailPasswordSignUp } from '@nhost/react'
|
||||
import { useSignUpEmailPassword } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const { signUp, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
useEmailPasswordSignUp(email, password)
|
||||
const { signUpEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useSignUpEmailPassword()
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Email" />
|
||||
@@ -45,9 +47,9 @@ const Component = () => {
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<button onClick={signUp}>Register</button>
|
||||
<button onClick={() => signUpEmailPassword(email, password)}>Register</button>
|
||||
{isSuccess && <div>Your account have beed created! You are now authenticated</div>}
|
||||
{needsVerification && (
|
||||
{needsEmailVerification && (
|
||||
<div>Please check your mailbox and follow the verification link to verify your email</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -58,30 +60,43 @@ const Component = () => {
|
||||
### Email and Password Sign-In
|
||||
|
||||
```js
|
||||
const { signIn, isLoading, needsVerification, isSuccess, isError, error } =
|
||||
useEmailPasswordSignIn(email?: string, password?: string)
|
||||
const {
|
||||
signInEmailPassword,
|
||||
isLoading,
|
||||
needsEmailVerification,
|
||||
needsMfaOtp,
|
||||
sendMfaOtp,
|
||||
isSuccess,
|
||||
isError,
|
||||
error,
|
||||
user
|
||||
} = useSignInEmailPassword()
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signIn` | (email?: string, password?: string) => void | Will try to authenticate. The email/password arguments will take precedence over the possible state values used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsVerification` | boolean | Returns `true` if the user email is still pending verification. |
|
||||
| `isSuccess` | boolean | Returns `true` if the user has successfully authenticated. Returns `false` in case or error or if the new email needs to be verified first. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signInEmailPassword` | (email?: string, password?: string) | Will try to authenticate. Returns a promise with the current context |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsEmailVerification` | boolean | Returns `true` if the user email is still pending email verification. |
|
||||
| `needsMfaOtp` | boolean | Returns `true` if the server is awaiting an MFA one-time password to complete the authentication. |
|
||||
| `sendMfaOtp` | (otp: string) => void | Sends MFA One-time password. Will turn either `isSuccess` or `isError` to true, and store potential error in `error`. |
|
||||
| `isSuccess` | boolean | Returns `true` if the user has successfully authenticated. Returns `false` in case or error or if the new email needs to be verified first. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| null | Provides details about the error. |
|
||||
| `user` | User \| null | User information |
|
||||
| `accessToken` | string \| null | Access token (JWT) |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useEmailPasswordSignIn } from '@nhost/react'
|
||||
import { useSignInEmailPassword } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const { signIn, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
useEmailPasswordSignIn(email, password)
|
||||
const { signInEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useSignInEmailPassword()
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Email" />
|
||||
@@ -90,9 +105,9 @@ const Component = () => {
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<button onClick={signUp}>Register</button>
|
||||
<button onClick={() => signInEmailPassword(email, password)}>Register</button>
|
||||
{isSuccess && <div>Authentication suceeded</div>}
|
||||
{needsVerification && (
|
||||
{needsEmailVerification && (
|
||||
<div>
|
||||
You must verify your email to sign in. Check your mailbox and follow the instructions to
|
||||
verify your email.
|
||||
@@ -105,49 +120,69 @@ const Component = () => {
|
||||
|
||||
### Oauth Providers
|
||||
|
||||
```js
|
||||
const providerLink = useProviderLink(options?: Options)
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasua Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```js
|
||||
import { useProviderLink } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const { github } = useProviderLink()
|
||||
return <a href={github}>Authenticate with GitHub</a>
|
||||
const { facebook, github } = useProviderLink()
|
||||
return
|
||||
;<div>
|
||||
<a href={facebook}>Authenticate with Facebook</a>
|
||||
<a href={github}>Authenticate with GitHub</a>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Passwordless email authentication
|
||||
|
||||
```js
|
||||
const { signIn, isLoading, isSuccess, isError, error } =
|
||||
useEmailPasswordlessSignIn(email?: string, options?: Options)
|
||||
const { signInEmailPasswordless, isLoading, isSuccess, isError, error } =
|
||||
useSignInEmailPasswordless(options?: Options)
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signIn` | (email?: string) => void | Sends a magic link to the given email The email argument will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSuccess` | boolean | Returns `true` if the magic link email user has successfully send. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} | Provides details about the error. |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasua Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `signInEmailPasswordless` | (email?: string) => void | Sends a magic link to the given email. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSuccess` | boolean | Returns `true` if the magic link email user has successfully send. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} | Provides details about the error. |
|
||||
| `options.locale` | string \| undefined | Locale of the user, in two digits, for instance `en`. |
|
||||
| `options.allowedRoles` | string[] \| undefined | Allowed roles of the user. Must be a subset of the default allowed roles defined in Hasua Auth. |
|
||||
| `options.defaultRole` | string \| undefined | Default role of the user. Must be part of the default allowed roles defined in Hasura Auth. |
|
||||
| `options.displayName` | string \| undefined |
|
||||
| `options.metadata` | Record<string, unknown> \| undefined | Custom additional user information stored in the `metadata` column. Can be any JSON object. |
|
||||
| `options.redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useEmailPasswordlessSignIn } from '@nhost/react'
|
||||
import { useSignInEmailPasswordless } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const { signIn, isLoading, isSuccess, isError, error } = useEmailPasswordlessSignIn(email)
|
||||
const { signInEmailPasswordless, isLoading, isSuccess, isError, error } =
|
||||
useSignInEmailPasswordless()
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Email" />
|
||||
<button onClick={signUp}>Register</button>
|
||||
<button onClick={() => signInEmailPasswordless(email)}>Authenticate</button>
|
||||
{isSuccess && (
|
||||
<div>
|
||||
An email has been sent to {email}. Please check your mailbox and click on the
|
||||
@@ -192,22 +227,23 @@ const Component = () => {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication status
|
||||
|
||||
### `useAuthLoading`
|
||||
### `useAuthenticationStatus`
|
||||
|
||||
The Nhost client may need some initial steps to determine the authentication status during startup, like fetching a new JWT from an existing refresh token.
|
||||
|
||||
`useAuthLoading` will return `true` until the authentication status is known.
|
||||
`isLoading` will return `true` until the authentication status is known.
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useAuthLoading, useAuthenticated } from '@nhost/react'
|
||||
import { useAuthenticationStatus } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const isLoading = useAuthLoading()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
if (isLoading) return <div>Loading Nhost authentication status...</div>
|
||||
else if (isAuthenticated) return <div>User is authenticated</div>
|
||||
else return <div>Public section</div>
|
||||
@@ -216,29 +252,31 @@ const Component = () => {
|
||||
|
||||
### Get the JWT access token
|
||||
|
||||
<!-- TODO better documentation -->
|
||||
<!-- TODO ellaborate -->
|
||||
|
||||
```js
|
||||
const accessToken = useAccessToken()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User management
|
||||
|
||||
### Change email
|
||||
|
||||
```js
|
||||
const { changeEmail, isLoading, isSuccess, needsVerification, isError, error } =
|
||||
useChangeEmail(email?: string, options?: { redirectTo?: string })
|
||||
const { changeEmail, isLoading, isSuccess, needsEmailVerification, isError, error } =
|
||||
useChangeEmail(options?: { redirectTo?: string })
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `changeEmail` | (email?: string) => void | Rrequests the email change. The arguement password will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsVerification` | boolean | Returns `true` if the email change has been requested, but that a email has been sent to the user to verify the new email. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
| Name | Type | Notes |
|
||||
| ------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `changeEmail` | (email?: string) => void | Requests the email change. Returns a promise with the current context |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `needsEmailVerification` | boolean | Returns `true` if the email change has been requested, but that a email has been sent to the user to verify the new email. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| null | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
@@ -248,12 +286,12 @@ import { useChangeEmail } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const { changeEmail, isLoading, needsVerification, isError, error } = useChangeEmail(password)
|
||||
const { changeEmail, isLoading, needsEmailVerification, isError, error } = useChangeEmail()
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} />
|
||||
<button onClick={changeEmail}>Change password</button>
|
||||
{needsVerification && (
|
||||
<button onClick={() => changeEmail(email)}>Change email</button>
|
||||
{needsEmailVerification && (
|
||||
<div>
|
||||
Please check your mailbox and follow the verification link to confirm your new email
|
||||
</div>
|
||||
@@ -266,16 +304,16 @@ const Component = () => {
|
||||
### Change password
|
||||
|
||||
```js
|
||||
const { changePassword, isLoading, isSuccess, isError, error } = useChangePassword(password?: string)
|
||||
const { changePassword, isLoading, isSuccess, isError, error } = useChangePassword()
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `changePassword` | (password?: string) => void | Requests the password change. The arguement password will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSuccess` | boolean | Returns `true` if the password has beed successfully changed. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| Name | Type | Notes |
|
||||
| ---------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `changePassword` | (password?: string) | Requests the password change. Returns a promise with the current context |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSuccess` | boolean | Returns `true` if the password has beed successfully changed. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| null | Provides details about the error. |
|
||||
|
||||
#### Usage
|
||||
|
||||
@@ -285,11 +323,11 @@ import { useChangePassword } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [password, setPassword] = useState('')
|
||||
const { changePassword, isLoading, isSuccess, isError, error } = useChangePassword(password)
|
||||
const { changePassword, isLoading, isSuccess, isError, error } = useChangePassword()
|
||||
return (
|
||||
<div>
|
||||
<input value={password} onChange={(event) => setPassword(event.target.value)} />
|
||||
<button onClick={changePassword}>Change password</button>
|
||||
<button onClick={() => changePassword(password)}>Change password</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -300,17 +338,17 @@ const Component = () => {
|
||||
If a user loses their password, we can resend them an email to authenticate so that they can change it to a new one:
|
||||
|
||||
```js
|
||||
const { resetPassword, isLoading, isSent, isError, error } = useResetPassword(email?: string, options?: { redirectTo?: string })
|
||||
const { resetPassword, isLoading, isSent, isError, error } = useResetPassword(options?: { redirectTo?: string })
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| --------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `resetPassword` | (email?: string) => void | Sends an email with a temporary connection link. The arguement email will take precedence over the the possible state value used when creating the hook. |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSent` | boolean | Returns `true` when the email has been successfully sent. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| undefined | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
| Name | Type | Notes |
|
||||
| --------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `resetPassword` | (email?: string) | Sends an email with a temporary connection link. Returns a promise with the current context |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSent` | boolean | Returns `true` when the email has been successfully sent. |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| null | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
@@ -319,23 +357,93 @@ import { useState } from 'react'
|
||||
import { useResetPassword } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEamil] = useState('')
|
||||
const { resetPassword, isLoading, isSent, isError, error } = useResetPassword(email?: string)
|
||||
const [email, setEmail] = useState('')
|
||||
const { resetPassword, isLoading, isSent, isError, error } = useResetPassword()
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} />
|
||||
<button onClick={resetPassword}>Send reset link</button>
|
||||
<button onClick={() => resetPassword(email)}>Send reset link</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## User data
|
||||
|
||||
<!-- TODO document -->
|
||||
### Send email verification
|
||||
|
||||
```js
|
||||
const userData = useUserData()
|
||||
const { sendEmail, isLoading, isSent, isError, error } =
|
||||
useSendVerificationEmail(options?: { redirectTo?: string })
|
||||
```
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `sendEmail` | (email?: string) | Resend the verification email. Returns a promise with the current context |
|
||||
| `isLoading` | boolean | Returns `true` when the action is executing, `false` when it finished its execution. |
|
||||
| `isSent` | boolean | Returns `true` if the verification email has been sent |
|
||||
| `isError` | boolean | Returns `true` if an error occurred. |
|
||||
| `error` | {status: number, error: string, message: string} \| null | Provides details about the error. |
|
||||
| `redirectTo` | string \| undefined | Redirection path in the client application that will be used in the link in the verification email. For instance, if you want to redirect to `https://myapp.com/success`, the `redirectTo` value is `'/success'`. |
|
||||
|
||||
#### Usage
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { useSendVerificationEmail } from '@nhost/react'
|
||||
|
||||
const Component = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const { sendEmail, isLoading, isSent, isError, error } = useSendVerificationEmail()
|
||||
return (
|
||||
<div>
|
||||
<input value={email} onChange={(event) => setEmail(event.target.value)} />
|
||||
<button onClick={() => sendEmail(email)}>Send email verification</button>
|
||||
{isSent && (
|
||||
<div>Please check your mailbox and follow the verification link to confirm your email</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User data
|
||||
|
||||
```js
|
||||
const { id, email, displayName, avatarUrl, isAnonymous, locale, defaultRole, roles, metadata, createdAt } = useUserData()
|
||||
```
|
||||
|
||||
| Name | Type | Default | Notes |
|
||||
| ------------- | ------------- | ---------------- | ------------------------------------------------------ |
|
||||
| `id` | string | | User's unique identifier (uuid) |
|
||||
| `email` | string | | User's email address |
|
||||
| `displayName` | string | `""` | User's display name |
|
||||
| `avatarUrl` | string | `""` | The URL to the user's profile picture |
|
||||
| `isAnonymous` | boolean | `false` | Whether or not the user is anonymous |
|
||||
| `locale` | string | `"en"` | A two-characters locale |
|
||||
| `defaultRole` | string | `"user"` | The default role of the user |
|
||||
| `roles` | string[] | `["me", "user"]` | The roles assigned to the user |
|
||||
| `metadata` | JSON object | `null` | Additional attributes used for user information |
|
||||
| `createdAt` | string | | The date-time when the user has been created |
|
||||
|
||||
Example of an authenticated user:
|
||||
|
||||
```json
|
||||
{
|
||||
"avatarUrl": "https://s.gravatar.com/avatar/3020737ed9d932c6665111a5550454d2?r=g&default=blank",
|
||||
"createdAt": "2022-04-11T16:33:14.780439+00:00",
|
||||
"defaultRole": "user",
|
||||
"displayName": "Grégory D'Angelo",
|
||||
"email": "greg@nhost.io",
|
||||
"id": "05e054c7-a722-42e7-90a6-3f77a2f118c8",
|
||||
"isAnonymous": false,
|
||||
"locale": "en",
|
||||
"metadata": {
|
||||
"lastName": "D'Angelo",
|
||||
"firstName": "Grégory"
|
||||
},
|
||||
"roles": ["user", "me"]
|
||||
}
|
||||
```
|
||||
|
||||
### Avatar
|
||||
@@ -368,7 +476,7 @@ const Avatar = () => {
|
||||
### Display name
|
||||
|
||||
```jsx
|
||||
import { displayName } from '@nhost/react'
|
||||
import { useDisplayName } from '@nhost/react'
|
||||
|
||||
const Avatar = () => {
|
||||
const displayName = useDisplayName()
|
||||
|
||||
@@ -20,26 +20,25 @@ npm install @nhost/react
|
||||
|
||||
## Configuration
|
||||
|
||||
`@nhost/react` exports a React provider `NhostProvider` that makes the authentication state and the several hooks available in your application. Wrap this component around your whole App.
|
||||
`@nhost/react` exports a React provider `NhostReactProvider` that makes the authentication state and the several hooks available in your application. Wrap this component around your whole App.
|
||||
|
||||
```jsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import { NhostClient, NhostReactProvider } from '@nhost/react'
|
||||
|
||||
import App from './App'
|
||||
|
||||
const nhost = new Nhost({
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: 'http://localhost:1337'
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<App />
|
||||
</NhostProvider>
|
||||
</NhostReactProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
@@ -50,13 +49,20 @@ ReactDOM.render(
|
||||
### Options
|
||||
|
||||
```js
|
||||
const nhost = new Nhost({ backendUrl, autoSignIn, autoRefreshToken, storageGetter, storageSetter })
|
||||
const nhost = new NhostClient({
|
||||
backendUrl,
|
||||
autoLogin,
|
||||
autoRefreshToken,
|
||||
clientStorageGetter,
|
||||
clientStorageSetter
|
||||
})
|
||||
```
|
||||
|
||||
| Name | Type | Default | Notes |
|
||||
| ------------------ | ----------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `backendUrl` | string | | The Nhost app url, for instance `https://my-app.nhost.run`. When using the CLI, its value is `http://localhost:1337` |
|
||||
| `autoSignIn` | boolean | `true` | If set to `true`, the client will detect credentials in the current URL that could have been sent during an email verification or an Oauth authentication. It will also automatically authenticate all the active tabs in the current browser. |
|
||||
| `autoRefreshToken` | boolean | `true` | If set to `true`, the JWT (access token) will be automatically refreshed before it expires. |
|
||||
| `storageGetter` | (key:string) => string \| null | use localStorage | Nhost stores a refresh token in `localStorage` so the session can be restored when starting the browser. |
|
||||
| `storageSetter` | (key: string, value: string \| null | use localStorage | |
|
||||
| Name | Type | Default | Notes |
|
||||
| --------------------- | ----------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `backendUrl` | string | | The Nhost app url, for instance `https://my-app.nhost.run`. When using the CLI, its value is `http://localhost:1337` |
|
||||
| `autoLogin` | boolean | `true` | If set to `true`, the client will detect credentials in the current URL that could have been sent during an email verification or an Oauth authentication. It will also automatically authenticate all the active tabs in the current browser. |
|
||||
| `autoRefreshToken` | boolean | `true` | If set to `true`, the JWT (access token) will be automatically refreshed before it expires. |
|
||||
| `clientStorageGetter` | (key:string) => string \| null | use localStorage | Nhost stores a refresh token in `localStorage` so the session can be restored when starting the browser. |
|
||||
| `clientStorageSetter` | (key: string, value: string \| null | use localStorage | |
|
||||
| `refreshIntervalTime` | | |
|
||||
|
||||
@@ -4,41 +4,67 @@ title: 'Protecting routes'
|
||||
|
||||
## React Router
|
||||
|
||||
You can protect routes by creating an `AuthGate` component when using `@nhost/react` with [React Router](https://reactrouter.com/web/guides/quick-start).
|
||||
> This example uses the latest version of [React Router (v6)](https://reactrouter.com/docs/en/v6).
|
||||
|
||||
You can protect routes by creating a wrapper component, `ProtectedRoute`, to implement the authentication logic using `@nhost/react`.
|
||||
|
||||
```jsx
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import { useAuthLoading, useAuthenticated } from '@nhost/react'
|
||||
// src/components/ProtectedRoute.js
|
||||
|
||||
export function AuthGate(children) {
|
||||
const isLoading = useAuthLoading()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
import { useAuthenticationStatus } from '@nhost/react';
|
||||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
const ProtectedRoute = () => {
|
||||
const { isAuthenticated, isLoading } = useAuthenticationStatus();
|
||||
const location = useLocation();
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Redirect to="/login" />
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
```
|
||||
|
||||
Then, in your React Router, wrap the `AuthGate` component around the routes you want to protect:
|
||||
So, if the user is not authenticated, we redirect him to the `/login` route using the [`Navigate`](https://reactrouter.com/docs/en/v6/api#navigate) component from React Router. Otherwise, we render the [`Outlet`](https://reactrouter.com/docs/en/v6/api#outlet) component, also provided by React Router, to render the `ProtectedRoute` child route elements.
|
||||
|
||||
Then, in your `App.js` file, you can use a [layout route](https://reactrouter.com/docs/en/v6/getting-started/concepts#layout-route) to wrap the `ProtectedRoute` component around the routes you want to protect:
|
||||
|
||||
```jsx
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/login">
|
||||
<Login />
|
||||
</Route>
|
||||
<Route path="/" exact>
|
||||
<AuthGate> // <--- Use AuthGate component like this
|
||||
<div>My protected dashboard</div>
|
||||
</AuthGate>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
// src/App.js
|
||||
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { NhostReactProvider } from '@nhost/react';
|
||||
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Profile from './pages/Profile';
|
||||
|
||||
import { nhost } from './lib/nhost';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/dashboard" element={<ProtectedRoute />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</NhostReactProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,6 +33,7 @@ export const orderTwo = {
|
||||
sdk: ['index', 'graphql', 'authentication', 'storage', 'functions'],
|
||||
react: ['index', 'hooks', 'protecting-routes', 'apollo'],
|
||||
nextjs: ['index', 'configuration', 'protecting-routes', ],
|
||||
cli: ['index']
|
||||
cli: ['index'],
|
||||
'hasura-auth': ['index', 'installation', 'configuration', 'environment-variables', 'schema', 'api-reference']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "nhost-documentation",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build:next": "next build",
|
||||
@@ -34,9 +33,11 @@
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-syntax-highlighter": "^15.4.5"
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"swagger-ui-react": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/react": "^17.0.37",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"next-sitemap": "^1.6.203",
|
||||
|
||||
95
docs/public/images/platform/permission-variables-preview.svg
Normal file
95
docs/public/images/platform/permission-variables-preview.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 227 KiB |
1631
docs/public/openapi/hasura-auth.json
Normal file
1631
docs/public/openapi/hasura-auth.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||
<url><loc>https://docs.nhost.io</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/authentication</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/upgrade</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/database</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/serverless-functions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/storage</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/cli</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/nextjs</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/react</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/supporting-libraries</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/install-cli</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/local-changes</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/metadata-and-serverless-functions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/workflow-setup</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start/javascript-client</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start/permissions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start/schema</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/sign-in-methods</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/social-login</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/user-management</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/database/graphql</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/database/permissions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost/environment-variables</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost/github-integration</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost/local-development</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/serverless-functions/event-triggers</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/nextjs/server-side</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/react/apollo</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/react/hooks</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/authentication</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/functions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/graphql</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/storage</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/supporting-libraries/react-apollo</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/supporting-libraries/react-auth</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-02-21T10:17:26.089Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/authentication</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/upgrade</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/database</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/serverless-functions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/storage</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/cli</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/hasura-auth</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/nextjs</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.648Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/react</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/install-cli</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/local-changes</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/metadata-and-serverless-functions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/cli-workflow/workflow-setup</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start/javascript-client</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start/permissions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/get-started/quick-start/schema</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/email-templates</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/sign-in-methods</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/sign-in-with-facebook</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/sign-in-with-github</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/sign-in-with-google</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/sign-in-with-linkedin</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/sign-in-with-spotify</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/social-sign-in</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/authentication/user-management</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/database/graphql</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/database/permissions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost/environment-variables</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost/github-integration</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/nhost/local-development</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/platform/serverless-functions/event-triggers</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/hasura-auth/api-reference</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/hasura-auth/configuration</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/hasura-auth/environment-variables</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/hasura-auth/installation</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/hasura-auth/schema</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/nextjs/configuration</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/nextjs/protecting-routes</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/react/apollo</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/react/hooks</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/react/protecting-routes</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/authentication</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/functions</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/graphql</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
<url><loc>https://docs.nhost.io/reference/sdk/storage</loc><changefreq>daily</changefreq><priority>0.7</priority><lastmod>2022-04-08T13:10:11.649Z</lastmod></url>
|
||||
</urlset>
|
||||
@@ -4,12 +4,6 @@
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- Updated dependencies [207ae38]
|
||||
- @nhost/react-apollo@3.0.0
|
||||
- @nhost/apollo@0.2.0
|
||||
|
||||
@@ -7,7 +7,8 @@ export default function Header() {
|
||||
<nav>
|
||||
<Link href="/">Index</Link> <br />
|
||||
<Link href="/second">Second</Link> <br />
|
||||
<Link href="/third">Third</Link> <br />
|
||||
<Link href="/third">SSR auth-guarded page</Link> <br />
|
||||
<Link href="/client-side-auth-guard">CSR auth-guarded page</Link> <br />
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
|
||||
21
examples/nextjs/components/protected-route.tsx
Normal file
21
examples/nextjs/components/protected-route.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useAuthenticationStatus } from '@nhost/nextjs'
|
||||
|
||||
export function authProtected(Comp) {
|
||||
return function AuthProtected(props) {
|
||||
const router = useRouter()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
console.log('Authentication guard: check auth status', { isLoading, isAuthenticated })
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
router.push('/')
|
||||
return null
|
||||
}
|
||||
|
||||
return <Comp {...props} />
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
environment:
|
||||
hasura_graphql_enable_remote_schema_permissions: false
|
||||
auth:
|
||||
version: 0.2.1
|
||||
version: 0.4.2
|
||||
auth:
|
||||
access_control:
|
||||
email:
|
||||
|
||||
@@ -4,7 +4,6 @@ table:
|
||||
configuration:
|
||||
custom_column_names:
|
||||
id: id
|
||||
redirect_url: redirectUrl
|
||||
custom_name: authProviderRequests
|
||||
custom_root_fields:
|
||||
delete: deleteAuthProviderRequests
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -9,17 +9,18 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nhost/nextjs": "^0.2.0",
|
||||
"@nhost/react": "^0.2.0",
|
||||
"@nhost/react-apollo": "^3.0.0",
|
||||
"@apollo/client": "^3.5.10",
|
||||
"@nhost/nextjs": "^1.0.10",
|
||||
"@nhost/react": "^0.5.0",
|
||||
"@nhost/react-apollo": "^4.0.10",
|
||||
"graphql": "^16.3.0",
|
||||
"next": "12.1.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.17",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "17.0.43",
|
||||
"@xstate/inspect": "^0.6.2",
|
||||
"eslint": "8.8.0",
|
||||
"eslint-config-next": "12.0.10",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { AppProps } from 'next/app'
|
||||
import React from 'react'
|
||||
|
||||
import { NhostSSR } from '@nhost/client'
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { NhostClient, NhostNextProvider } from '@nhost/nextjs'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
import { inspect } from '@xstate/inspect'
|
||||
|
||||
@@ -17,18 +16,18 @@ if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_DEBUG) {
|
||||
iframe: false
|
||||
})
|
||||
}
|
||||
const nhost = new NhostSSR({ backendUrl: BACKEND_URL })
|
||||
const nhost = new NhostClient({ backendUrl: BACKEND_URL })
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<NhostProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<NhostApolloProvider>
|
||||
<NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<div className="App">
|
||||
<Header />
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</NhostNextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
17
examples/nextjs/pages/client-side-auth-guard.tsx
Normal file
17
examples/nextjs/pages/client-side-auth-guard.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
|
||||
import { useAccessToken } from '@nhost/nextjs'
|
||||
|
||||
import { authProtected } from '../components/protected-route'
|
||||
|
||||
const ClientSideAuthPage: React.FC = () => {
|
||||
const accessToken = useAccessToken()
|
||||
return (
|
||||
<div>
|
||||
<h1>Client-side rendered page only accessible to authenticated users</h1>
|
||||
<div>Access token: {accessToken}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default authProtected(ClientSideAuthPage)
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
useAuthenticated,
|
||||
useChangeEmail,
|
||||
useChangePassword,
|
||||
useEmailPasswordlessSignIn,
|
||||
useEmailPasswordSignIn,
|
||||
useEmailPasswordSignUp,
|
||||
useSignOut
|
||||
} from '@nhost/react'
|
||||
useSignInEmailPassword,
|
||||
useSignInEmailPasswordless,
|
||||
useSignOut,
|
||||
useSignUpEmailPassword
|
||||
} from '@nhost/nextjs'
|
||||
import { useAuthQuery } from '@nhost/react-apollo'
|
||||
|
||||
import { BOOKS_QUERY } from '../helpers'
|
||||
@@ -25,11 +25,11 @@ const Home: NextPage = () => {
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const accessToken = useAccessToken()
|
||||
const { signOut } = useSignOut()
|
||||
const { signUp, ...signUpResult } = useEmailPasswordSignUp(email, password)
|
||||
const { signIn } = useEmailPasswordSignIn(email, password)
|
||||
const { signIn: passwordlessSignIn } = useEmailPasswordlessSignIn(email)
|
||||
const { changeEmail, ...changeEmailResult } = useChangeEmail(newEmail)
|
||||
const { changePassword, ...changePasswordResult } = useChangePassword(newPassword)
|
||||
const { signUpEmailPassword, ...signUpResult } = useSignUpEmailPassword()
|
||||
const { signInEmailPassword } = useSignInEmailPassword()
|
||||
const { signInEmailPasswordless } = useSignInEmailPasswordless()
|
||||
const { changeEmail, ...changeEmailResult } = useChangeEmail()
|
||||
const { changePassword, ...changePasswordResult } = useChangePassword()
|
||||
const { loading, data, error } = useAuthQuery(BOOKS_QUERY)
|
||||
return (
|
||||
<div>
|
||||
@@ -37,20 +37,24 @@ const Home: NextPage = () => {
|
||||
<>
|
||||
<button onClick={signOut}>Logout</button>
|
||||
<input value={newEmail} onChange={(e) => setNewEmail(e.target.value)} />
|
||||
<button onClick={changeEmail}>Change email</button>
|
||||
<button onClick={() => changeEmail(email)}>Change email</button>
|
||||
<div>{JSON.stringify(changeEmailResult)}</div>
|
||||
<button onClick={changePassword}>Change password</button>
|
||||
<button onClick={() => changePassword(password)}>Change password</button>
|
||||
<input value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
||||
<div>{JSON.stringify(changePasswordResult)}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<input value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<button onClick={passwordlessSignIn}>Passwordless signin</button>
|
||||
<button onClick={() => signInEmailPasswordless(email)}>Passwordless signin</button>
|
||||
<div>{JSON.stringify(signUpResult)}</div>
|
||||
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
|
||||
<button onClick={signUp}>Email + password sign-up</button>
|
||||
<button onClick={signIn}>Email + password sign-in</button>
|
||||
<button onClick={() => signUpEmailPassword(email, password)}>
|
||||
Email + password sign-up
|
||||
</button>
|
||||
<button onClick={() => signInEmailPassword(email, password)}>
|
||||
Email + password sign-in
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
import { getNhostSession, NhostSession } from '@nhost/nextjs'
|
||||
import { useAccessToken, useAuthenticated, useUserData } from '@nhost/react'
|
||||
import { NhostSession } from '@nhost/core'
|
||||
import { getNhostSession, useAccessToken, useAuthenticated, useUserData } from '@nhost/nextjs'
|
||||
|
||||
import { BACKEND_URL } from '../helpers'
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NextPageContext } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
import { getNhostSession, NhostSession } from '@nhost/nextjs'
|
||||
import { useAccessToken, useAuthenticated } from '@nhost/react'
|
||||
import { NhostSession } from '@nhost/core'
|
||||
import { getNhostSession, useAccessToken } from '@nhost/nextjs'
|
||||
|
||||
import { authProtected } from '../components/protected-route'
|
||||
import { BACKEND_URL } from '../helpers'
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
@@ -17,15 +18,12 @@ export async function getServerSideProps(context: NextPageContext) {
|
||||
|
||||
const RefetchPage: React.FC<{ initial: NhostSession }> = () => {
|
||||
const accessToken = useAccessToken()
|
||||
const isAuthenticated = useAuthenticated()
|
||||
if (!isAuthenticated) return <div>User it not authenticated </div>
|
||||
return (
|
||||
<div>
|
||||
<h1>Third page</h1>
|
||||
User is authenticated: {isAuthenticated ? 'yes' : 'no'}
|
||||
<h1>SSR page only accessible to authenticated users</h1>
|
||||
<div>Access token: {accessToken}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefetchPage
|
||||
export default authProtected(RefetchPage)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
# @nhost-examples/react-apollo
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [207ae38]
|
||||
- @nhost/react-apollo@3.0.0
|
||||
- @nhost/nhost-js@0.3.11
|
||||
- @nhost/react-auth@2.0.9
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
style: {
|
||||
postcss: {
|
||||
plugins: [require("tailwindcss"), require("autoprefixer")],
|
||||
},
|
||||
},
|
||||
};
|
||||
7
examples/react-apollo-crm/functions/_utils/nhost.ts
Normal file
7
examples/react-apollo-crm/functions/_utils/nhost.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: process.env.NHOST_BACKEND_URL!
|
||||
})
|
||||
|
||||
export { nhost }
|
||||
@@ -1,107 +1,105 @@
|
||||
import { Request, Response } from "express";
|
||||
import { nhost } from "../../../src/utils/nhost";
|
||||
import { Request, Response } from 'express'
|
||||
import { nhost } from '../../_utils/nhost'
|
||||
|
||||
const handler = async (req: Request, res: Response) => {
|
||||
if (
|
||||
req.headers["nhsot-webhook-secret"] !== process.env.NHSOT_WEBHOOK_SECRET
|
||||
) {
|
||||
return res.status(401).send("Unauthorized");
|
||||
}
|
||||
if (req.headers['nhsot-webhook-secret'] !== process.env.NHSOT_WEBHOOK_SECRET) {
|
||||
return res.status(401).send('Unauthorized')
|
||||
}
|
||||
|
||||
// User who just signed up
|
||||
const user = req.body.event.data.new;
|
||||
// User who just signed up
|
||||
const user = req.body.event.data.new
|
||||
|
||||
// Get the user's email domain
|
||||
const emailDomain = user.email.split("@")[1];
|
||||
// Get the user's email domain
|
||||
const emailDomain = user.email.split('@')[1]
|
||||
|
||||
// Check if a company with the user's email domain already exists.
|
||||
const GET_COMPANY_WITH_EMAIL_DOMAIN = `
|
||||
// Check if a company with the user's email domain already exists.
|
||||
const GET_COMPANY_WITH_EMAIL_DOMAIN = `
|
||||
query getCompanyWithEmailDomain($emailDomain: String!) {
|
||||
companies(where: { emailDomain: { _eq: $emailDomain } }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
GET_COMPANY_WITH_EMAIL_DOMAIN,
|
||||
{
|
||||
emailDomain,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-hasura-admin-secret": process.env.NHOST_ADMIN_SECRET,
|
||||
`
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
GET_COMPANY_WITH_EMAIL_DOMAIN,
|
||||
{
|
||||
emailDomain
|
||||
},
|
||||
}
|
||||
);
|
||||
{
|
||||
headers: {
|
||||
'x-hasura-admin-secret': process.env.NHOST_ADMIN_SECRET
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return res.status(500).send(error);
|
||||
}
|
||||
if (error) {
|
||||
return res.status(500).send(error)
|
||||
}
|
||||
|
||||
const { companies } = data as any;
|
||||
const { companies } = data as any
|
||||
|
||||
let companyId;
|
||||
if (companies.length === 1) {
|
||||
// if a company already exists, use that company's id
|
||||
companyId = companies[0].id;
|
||||
} else {
|
||||
// else, create a new company for the newly created user with the same email domain as the user
|
||||
const CREATE_NEW_COMPANY = `
|
||||
let companyId
|
||||
if (companies.length === 1) {
|
||||
// if a company already exists, use that company's id
|
||||
companyId = companies[0].id
|
||||
} else {
|
||||
// else, create a new company for the newly created user with the same email domain as the user
|
||||
const CREATE_NEW_COMPANY = `
|
||||
mutation insertCompany($emailDomain: String!) {
|
||||
insertCompany(object: { name: $emailDomain, emailDomain: $emailDomain }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
CREATE_NEW_COMPANY,
|
||||
{
|
||||
emailDomain,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-hasura-admin-secret": process.env.NHOST_ADMIN_SECRET,
|
||||
},
|
||||
`
|
||||
const { data, error } = await nhost.graphql.request(
|
||||
CREATE_NEW_COMPANY,
|
||||
{
|
||||
emailDomain
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-hasura-admin-secret': process.env.NHOST_ADMIN_SECRET
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return res.status(500).send(error)
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return res.status(500).send(error);
|
||||
}
|
||||
const { insertCompany } = data as any
|
||||
|
||||
const { insertCompany } = data as any;
|
||||
companyId = insertCompany.id
|
||||
}
|
||||
|
||||
companyId = insertCompany.id;
|
||||
}
|
||||
// We now have the company id of an existing, or a newly created company.
|
||||
// Now let's add the user to the company.
|
||||
|
||||
// We now have the company id of an existing, or a newly created company.
|
||||
// Now let's add the user to the company.
|
||||
|
||||
const ADD_USER_TO_COMPANY = `
|
||||
const ADD_USER_TO_COMPANY = `
|
||||
mutation addUserToCompany($userId: uuid!, $companyId: uuid!) {
|
||||
insertCompanyUser(object: {userId: $userId, companyId: $companyId}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { error: addUserToCompanyError } = await nhost.graphql.request(
|
||||
ADD_USER_TO_COMPANY,
|
||||
{
|
||||
userId: user.id,
|
||||
companyId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-hasura-admin-secret": process.env.NHOST_ADMIN_SECRET,
|
||||
`
|
||||
const { error: addUserToCompanyError } = await nhost.graphql.request(
|
||||
ADD_USER_TO_COMPANY,
|
||||
{
|
||||
userId: user.id,
|
||||
companyId
|
||||
},
|
||||
}
|
||||
);
|
||||
{
|
||||
headers: {
|
||||
'x-hasura-admin-secret': process.env.NHOST_ADMIN_SECRET
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (addUserToCompanyError) {
|
||||
return res.status(500).send(error);
|
||||
}
|
||||
if (addUserToCompanyError) {
|
||||
return res.status(500).send(error)
|
||||
}
|
||||
|
||||
res.status(200).send(`OK`);
|
||||
};
|
||||
res.status(200).send(`OK`)
|
||||
}
|
||||
|
||||
export default handler;
|
||||
export default handler
|
||||
|
||||
46182
examples/react-apollo-crm/package-lock.json
generated
46182
examples/react-apollo-crm/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,11 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.4.16",
|
||||
"@craco/craco": "^6.4.0",
|
||||
"@headlessui/react": "^1.4.2",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@nhost/nhost-js": "^0.3.4",
|
||||
"@nhost/react-apollo": "^2.0.7-0",
|
||||
"@nhost/react-auth": "^2.0.3",
|
||||
"@saeris/apollo-server-vercel": "^1.0.1",
|
||||
"@nhost/nhost-js": "^1.0.0",
|
||||
"@nhost/react": "^0.3.0",
|
||||
"@nhost/react-apollo": "^4.0.0",
|
||||
"@tailwindcss/forms": "^0.3.4",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
@@ -27,14 +25,14 @@
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-scripts": "^5.0.0",
|
||||
"typescript": "^4.1.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"codegen": "graphql-codegen --config codegen.yaml --errors-only"
|
||||
},
|
||||
@@ -68,4 +66,4 @@
|
||||
"postcss": "^7.0.39",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import "./App.css";
|
||||
import { NhostAuthProvider } from "@nhost/react-auth";
|
||||
import { NhostApolloProvider } from "@nhost/react-apollo";
|
||||
import { nhost } from "./utils/nhost";
|
||||
import { Route, Routes } from "react-router";
|
||||
import { Layout } from "./components/ui/Layout";
|
||||
import { Customers } from "./components/Customers";
|
||||
import { Dashboard } from "./components/Dashboard";
|
||||
import { NewCustomer } from "./components/NewCustomer";
|
||||
import { RequireAuth } from "./components/RequireAuth";
|
||||
import { Customer } from "./components/Customer";
|
||||
import { SignUp } from "./components/SignUp";
|
||||
import { SignIn } from "./components/SignIn";
|
||||
import { ResetPassword } from "./components/ResetPassword";
|
||||
import './App.css'
|
||||
import { NhostReactProvider } from '@nhost/react'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
import { nhost } from './utils/nhost'
|
||||
import { Route, Routes } from 'react-router'
|
||||
import { Layout } from './components/ui/Layout'
|
||||
import { Customers } from './components/Customers'
|
||||
import { Dashboard } from './components/Dashboard'
|
||||
import { NewCustomer } from './components/NewCustomer'
|
||||
import { RequireAuth } from './components/RequireAuth'
|
||||
import { Customer } from './components/Customer'
|
||||
import { SignUp } from './components/SignUp'
|
||||
import { SignIn } from './components/SignIn'
|
||||
import { ResetPassword } from './components/ResetPassword'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<NhostAuthProvider nhost={nhost}>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<AppRouter />
|
||||
</NhostApolloProvider>
|
||||
</NhostAuthProvider>
|
||||
);
|
||||
</NhostReactProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function AppRouter() {
|
||||
@@ -55,7 +55,7 @@ function AppRouter() {
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
||||
@@ -1,98 +1,93 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { CheckIcon } from "@heroicons/react/outline";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function ChangePasswordModal() {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [open, setOpen] = useState(true)
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.changePassword({ newPassword });
|
||||
const { error } = await nhost.auth.changePassword({ newPassword })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
}
|
||||
if (error) {
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
onClose={setOpen}
|
||||
>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" onClose={setOpen}>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span
|
||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg leading-6 font-medium text-gray-900"
|
||||
>
|
||||
Change Password
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
tabIndex={2}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span
|
||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg leading-6 font-medium text-gray-900"
|
||||
>
|
||||
Change Password
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
tabIndex={2}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm"
|
||||
>
|
||||
Set new password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm"
|
||||
>
|
||||
Set new password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import React from "react";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import React from 'react'
|
||||
import { Navigate, useLocation } from 'react-router'
|
||||
|
||||
export function RequireAuth({ children }: { children: JSX.Element }) {
|
||||
const { isAuthenticated, isLoading } = useNhostAuth();
|
||||
const location = useLocation();
|
||||
const { isAuthenticated, isLoading } = useNhostAuth()
|
||||
const location = useLocation()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading user data...</div>;
|
||||
return <div>Loading user data...</div>
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/sign-in" state={{ from: location }} />;
|
||||
return <Navigate to="/sign-in" state={{ from: location }} />
|
||||
}
|
||||
|
||||
return children;
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function ResetPassword() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const { isAuthenticated } = useNhostAuth();
|
||||
const { isAuthenticated } = useNhostAuth()
|
||||
|
||||
let navigate = useNavigate();
|
||||
let navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.resetPassword({ email });
|
||||
const { error } = await nhost.auth.resetPassword({ email })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
alert("Check out email inbox");
|
||||
};
|
||||
alert('Check out email inbox')
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate("/");
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -33,19 +33,14 @@ export function ResetPassword() {
|
||||
<div className="flex justify-center">
|
||||
<div className="text-2xl font-bold text-blue-700">AquaSystem</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900">
|
||||
Reset Password
|
||||
</h2>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900">Reset Password</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -77,5 +72,5 @@ export function ResetPassword() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function SignIn() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const { isAuthenticated } = useNhostAuth();
|
||||
const { isAuthenticated } = useNhostAuth()
|
||||
|
||||
let navigate = useNavigate();
|
||||
let navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.signIn({ email, password });
|
||||
const { error } = await nhost.auth.signIn({ email, password })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
navigate("/", { replace: true });
|
||||
};
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate("/");
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -44,10 +44,7 @@ export function SignIn() {
|
||||
<div className="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -66,10 +63,7 @@ export function SignIn() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -110,7 +104,7 @@ export function SignIn() {
|
||||
</form>
|
||||
</div>
|
||||
<div className="text-center py-4">
|
||||
Don't have an account?{" "}
|
||||
Don't have an account?{' '}
|
||||
<Link to="/sign-up" className="text-blue-600 hover:text-blue-500">
|
||||
Sign Up
|
||||
</Link>
|
||||
@@ -118,5 +112,5 @@ export function SignIn() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { useNhostAuth } from "@nhost/react-auth";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { nhost } from "../utils/nhost";
|
||||
import { useNhostAuth } from '@nhost/react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { nhost } from '../utils/nhost'
|
||||
|
||||
export function SignUp() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const { isAuthenticated } = useNhostAuth();
|
||||
const { isAuthenticated } = useNhostAuth()
|
||||
|
||||
let navigate = useNavigate();
|
||||
let navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { error } = await nhost.auth.signUp({ email, password });
|
||||
const { error } = await nhost.auth.signUp({ email, password })
|
||||
|
||||
if (error) {
|
||||
return alert(error.message);
|
||||
return alert(error.message)
|
||||
}
|
||||
|
||||
navigate("/", { replace: true });
|
||||
};
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate("/");
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -44,10 +44,7 @@ export function SignUp() {
|
||||
<div className="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -66,10 +63,7 @@ export function SignUp() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -100,7 +94,7 @@ export function SignUp() {
|
||||
</div>
|
||||
|
||||
<div className="text-center py-4">
|
||||
Already have an account?{" "}
|
||||
Already have an account?{' '}
|
||||
<Link to="/sign-in" className="text-blue-600 hover:text-blue-500">
|
||||
Sign In
|
||||
</Link>
|
||||
@@ -108,5 +102,5 @@ export function SignUp() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import { Dialog, Menu, Transition } from '@headlessui/react'
|
||||
import {
|
||||
FolderIcon,
|
||||
HomeIcon,
|
||||
InboxIcon,
|
||||
MenuAlt2Icon,
|
||||
UsersIcon,
|
||||
XIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { SearchIcon } from "@heroicons/react/solid";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { nhost } from "../../utils/nhost";
|
||||
import { ChangePasswordModal } from "../ChangePasswordModal";
|
||||
XIcon
|
||||
} from '@heroicons/react/outline'
|
||||
import { SearchIcon } from '@heroicons/react/solid'
|
||||
import { NavLink, Outlet } from 'react-router-dom'
|
||||
import { nhost } from '../../utils/nhost'
|
||||
import { ChangePasswordModal } from '../ChangePasswordModal'
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/", icon: HomeIcon, current: true },
|
||||
{ name: "Orders", href: "/orders", icon: UsersIcon, current: false },
|
||||
{ name: "Customers", href: "/customers", icon: FolderIcon, current: false },
|
||||
{ name: "Settings", href: "/settings", icon: InboxIcon, current: false },
|
||||
];
|
||||
{ name: 'Dashboard', href: '/', icon: HomeIcon, current: true },
|
||||
{ name: 'Orders', href: '/orders', icon: UsersIcon, current: false },
|
||||
{ name: 'Customers', href: '/customers', icon: FolderIcon, current: false },
|
||||
{ name: 'Settings', href: '/settings', icon: InboxIcon, current: false }
|
||||
]
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export function Layout() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false)
|
||||
|
||||
console.log("Layout Reload");
|
||||
console.log('Layout Reload')
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useEffect RUN");
|
||||
console.log('useEffect RUN')
|
||||
|
||||
if (window.location.hash.search("type=passwordReset") !== -1) {
|
||||
console.log("FOUND!");
|
||||
if (window.location.hash.search('type=passwordReset') !== -1) {
|
||||
console.log('FOUND!')
|
||||
|
||||
setShowChangePasswordModal(true);
|
||||
setShowChangePasswordModal(true)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showChangePasswordModal && <ChangePasswordModal />}
|
||||
<div>
|
||||
<Transition.Root show={sidebarOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed inset-0 z-40 flex md:hidden"
|
||||
onClose={setSidebarOpen}
|
||||
>
|
||||
<Dialog as="div" className="fixed inset-0 z-40 flex md:hidden" onClose={setSidebarOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
@@ -87,10 +83,7 @@ export function Layout() {
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XIcon
|
||||
className="w-6 h-6 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon className="w-6 h-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
@@ -109,11 +102,9 @@ export function Layout() {
|
||||
to={item.href}
|
||||
className={({ isActive }) => {
|
||||
return classNames(
|
||||
isActive
|
||||
? "bg-blue-800 text-white"
|
||||
: "text-blue-100 hover:bg-blue-600",
|
||||
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
|
||||
);
|
||||
isActive ? 'bg-blue-800 text-white' : 'text-blue-100 hover:bg-blue-600',
|
||||
'group flex items-center px-2 py-2 text-base font-medium rounded-md'
|
||||
)
|
||||
}}
|
||||
>
|
||||
<item.icon
|
||||
@@ -138,9 +129,7 @@ export function Layout() {
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex flex-col flex-grow pt-5 overflow-y-auto bg-blue-700">
|
||||
<div className="flex items-center flex-shrink-0 px-4">
|
||||
<span className="text-lg font-semibold text-white">
|
||||
AquaSystem
|
||||
</span>
|
||||
<span className="text-lg font-semibold text-white">AquaSystem</span>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-5">
|
||||
<nav className="flex-1 px-2 pb-4 space-y-1">
|
||||
@@ -150,11 +139,9 @@ export function Layout() {
|
||||
to={item.href}
|
||||
className={({ isActive }) => {
|
||||
return classNames(
|
||||
isActive
|
||||
? "bg-blue-800 text-white"
|
||||
: "text-blue-100 hover:bg-blue-600",
|
||||
"group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||
);
|
||||
isActive ? 'bg-blue-800 text-white' : 'text-blue-100 hover:bg-blue-600',
|
||||
'group flex items-center px-2 py-2 text-sm font-medium rounded-md'
|
||||
)
|
||||
}}
|
||||
>
|
||||
<item.icon
|
||||
@@ -234,11 +221,11 @@ export function Layout() {
|
||||
<div
|
||||
// to={"/login"}
|
||||
onClick={async () => {
|
||||
await nhost.auth.signOut();
|
||||
await nhost.auth.signOut()
|
||||
}}
|
||||
className={classNames(
|
||||
active ? "bg-gray-100" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700"
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700'
|
||||
)}
|
||||
>
|
||||
Sign out
|
||||
@@ -260,5 +247,5 @@ export function Layout() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NhostClient } from "@nhost/nhost-js";
|
||||
import { NhostClient } from '@nhost/react'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: process.env.REACT_APP_BACKEND_URL!,
|
||||
});
|
||||
backendUrl: process.env.REACT_APP_BACKEND_URL!
|
||||
})
|
||||
|
||||
export { nhost };
|
||||
export { nhost }
|
||||
|
||||
11360
examples/react-apollo-crm/yarn.lock
Normal file
11360
examples/react-apollo-crm/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,5 +7,5 @@ Once in the example's directory, run the two following commands in parallel:
|
||||
nhost -d
|
||||
|
||||
# Start this project
|
||||
pnpm run dev
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nhost React+Apollo demo</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
environment:
|
||||
hasura_graphql_enable_remote_schema_permissions: false
|
||||
auth:
|
||||
version: 0.2.1
|
||||
version: 0.4.2
|
||||
auth:
|
||||
access_control:
|
||||
email:
|
||||
@@ -124,10 +124,10 @@ auth:
|
||||
allowed_roles: user,me
|
||||
default_allowed_roles: user,me
|
||||
default_role: user
|
||||
mfa:
|
||||
enabled: false
|
||||
issuer: nhost
|
||||
signin_email_verified_required: true
|
||||
mfa:
|
||||
enabled: true
|
||||
issuer: nhost
|
||||
storage:
|
||||
force_download_for_content_types: text/html,application/javascript
|
||||
version: 3
|
||||
|
||||
@@ -2,8 +2,6 @@ table:
|
||||
name: provider_requests
|
||||
schema: auth
|
||||
configuration:
|
||||
custom_column_names:
|
||||
redirect_url: redirectUrl
|
||||
custom_name: authProviderRequests
|
||||
custom_root_fields:
|
||||
delete: deleteAuthProviderRequests
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-apollo",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@nhost/react": "^0.2.0",
|
||||
"@nhost/react-apollo": "^3.0.0",
|
||||
"@apollo/client": "^3.5.10",
|
||||
"@nhost/react": "^0.5.0",
|
||||
"@nhost/react-apollo": "^4.0.10",
|
||||
"@rsuite/icons": "^1.0.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"less": "^4.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"rsuite": "^5.6.2"
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"rsuite": "^5.7.1"
|
||||
},
|
||||
"lib": "workspace:*",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
@@ -38,14 +40,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@vitejs/plugin-react": "^1.2.0",
|
||||
"@xstate/inspect": "^0.6.2",
|
||||
"graphql": "15.7.2",
|
||||
"less": "^4.1.2",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.8.5",
|
||||
"ws": "^8.5.0"
|
||||
"@types/react": "^17.0.43",
|
||||
"@types/react-dom": "^17.0.14",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^2.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Panel } from 'rsuite'
|
||||
|
||||
@@ -19,11 +20,11 @@ export const AboutPage: React.FC = () => (
|
||||
<li>React</li>
|
||||
<li>React-router</li>
|
||||
<li>RSuite</li>
|
||||
<li>...and of course, the Nhost React client</li>
|
||||
<li>and of course, the Nhost React client</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
Noew let's go to the <Link to="/">index page</Link>
|
||||
Noew let's go to the <Link to="/">index page</Link>
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import './App.css'
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { useEffect } from 'react'
|
||||
import { Link, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Container, Content, Header, Nav, Navbar } from 'rsuite'
|
||||
|
||||
import { useAuthenticated, useSignOut } from '@nhost/react'
|
||||
import ExitIcon from '@rsuite/icons/Exit'
|
||||
import { Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Container, Header, Navbar, Content, Nav } from 'rsuite'
|
||||
import { useEffect } from 'react'
|
||||
import { SignInPage } from './sign-in'
|
||||
import { AuthGate } from './components/auth-gates'
|
||||
|
||||
import { AuthGate, PublicGate } from './components/auth-gates'
|
||||
import { AboutPage } from './About'
|
||||
import { ApolloPage } from './apollo'
|
||||
import Home from './Home'
|
||||
import { ProfilePage } from './profile'
|
||||
import { ApolloPage } from './apollo'
|
||||
import { SignInPage } from './sign-in'
|
||||
import { SignUpPage } from './sign-up'
|
||||
import { AboutPage } from './About'
|
||||
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
@@ -57,8 +60,22 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/sign-in/*" element={<SignInPage />} />
|
||||
<Route path="/sign-up/*" element={<SignUpPage />} />
|
||||
<Route
|
||||
path="/sign-in/*"
|
||||
element={
|
||||
<PublicGate>
|
||||
<SignInPage />
|
||||
</PublicGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sign-up/*"
|
||||
element={
|
||||
<PublicGate>
|
||||
<SignUpPage />
|
||||
</PublicGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react'
|
||||
import { Panel } from 'rsuite'
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { gql } from '@apollo/client'
|
||||
import React from 'react'
|
||||
import { Panel, Table } from 'rsuite'
|
||||
|
||||
import { gql } from '@apollo/client'
|
||||
import { useAuthQuery } from '@nhost/react-apollo'
|
||||
|
||||
const GET_BOOKS = gql`
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthenticated, useAuthLoading } from '@nhost/react'
|
||||
|
||||
import { useAuthenticationStatus } from '@nhost/react'
|
||||
|
||||
export const AuthGate: React.FC = ({ children }) => {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const isLoading = useAuthLoading()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
const location = useLocation()
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
@@ -17,16 +18,14 @@ export const AuthGate: React.FC = ({ children }) => {
|
||||
}
|
||||
|
||||
export const PublicGate: React.FC = ({ children }) => {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const isLoading = useAuthLoading()
|
||||
const { isLoading, isAuthenticated } = useAuthenticationStatus()
|
||||
const location = useLocation()
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
// ? stay on the same route - is it the best way to do so?
|
||||
return <Navigate to={location} state={{ from: location }} replace />
|
||||
return <Navigate to={'/'} state={{ from: location }} replace />
|
||||
}
|
||||
|
||||
return <div>{children}</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Button, Input, Message } from 'rsuite'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useEmailPasswordlessSignIn } from '@nhost/react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Button, Input, Message } from 'rsuite'
|
||||
|
||||
import { useSignInEmailPasswordless } from '@nhost/react'
|
||||
|
||||
export const EmailPasswordlessForm: React.FC = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const navigate = useNavigate()
|
||||
const { signIn, isError, isSuccess, error } = useEmailPasswordlessSignIn(email, {
|
||||
const { signInEmailPasswordless, isError, isSuccess, error } = useSignInEmailPasswordless({
|
||||
redirectTo: '/profile'
|
||||
})
|
||||
const [showError, setShowError] = useState(true)
|
||||
@@ -42,7 +43,7 @@ export const EmailPasswordlessForm: React.FC = () => {
|
||||
style={{ marginTop: '0.5em' }}
|
||||
onClick={() => {
|
||||
setShowError(true)
|
||||
signIn()
|
||||
signInEmailPasswordless(email)
|
||||
}}
|
||||
>
|
||||
Continue with email
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { FaFacebook, FaGithub, FaGoogle } from 'react-icons/fa'
|
||||
import { IconButton } from 'rsuite'
|
||||
import { FaGithub, FaGoogle, FaFacebook } from 'react-icons/fa'
|
||||
import { Icon } from '@rsuite/icons'
|
||||
|
||||
import { useProviderLink } from '@nhost/react'
|
||||
import { Icon } from '@rsuite/icons'
|
||||
|
||||
export const OAuthLinks: React.FC = () => {
|
||||
// TODO show how to use options
|
||||
const { github, google, facebook } = useProviderLink()
|
||||
return (
|
||||
<div>
|
||||
|
||||
14
examples/react-apollo/src/env.d.ts
vendored
14
examples/react-apollo/src/env.d.ts
vendored
@@ -1,10 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_NHOST_URL: string
|
||||
// more env variables...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
readonly VITE_NHOST_URL: string
|
||||
// more env variables...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import { NhostProvider } from '@nhost/react'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import 'rsuite/styles/index.less' // or 'rsuite/dist/rsuite.min.css'
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
import { NhostClient, NhostReactProvider } from '@nhost/react'
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo'
|
||||
|
||||
const nhost = new Nhost({
|
||||
import 'rsuite/styles/index.less' // or 'rsuite/dist/rsuite.min.css'
|
||||
|
||||
import App from './App'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
backendUrl: import.meta.env.VITE_NHOST_URL || 'http://localhost:1337'
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<NhostApolloProvider>
|
||||
<NhostReactProvider nhost={nhost}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<App />
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</NhostReactProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { useChangeEmail, useEmail } from '@nhost/react'
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, FlexboxGrid, Input, Message, Panel, toaster, Notification } from 'rsuite'
|
||||
import { Button, FlexboxGrid, Input, Message, Notification, Panel, toaster } from 'rsuite'
|
||||
|
||||
import { useChangeEmail, useEmail } from '@nhost/react'
|
||||
|
||||
export const ChangeEmail: React.FC = () => {
|
||||
const [newEmail, setNewEmail] = useState('')
|
||||
const email = useEmail()
|
||||
const { changeEmail, error, needsVerification } = useChangeEmail(newEmail, {
|
||||
const { changeEmail, error, needsEmailVerification } = useChangeEmail({
|
||||
redirectTo: '/profile'
|
||||
})
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (needsVerification) {
|
||||
if (needsEmailVerification) {
|
||||
toaster.push(
|
||||
<Notification type="info" header="Info" closable>
|
||||
An email has been sent to {newEmail}. Please check your inbox and follow the link to
|
||||
@@ -21,7 +23,7 @@ export const ChangeEmail: React.FC = () => {
|
||||
setNewEmail('')
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [needsVerification])
|
||||
}, [needsEmailVerification])
|
||||
|
||||
// * Set error message from the registration hook errors
|
||||
useEffect(() => {
|
||||
@@ -33,7 +35,8 @@ export const ChangeEmail: React.FC = () => {
|
||||
}, [newEmail])
|
||||
// * Show an error message when passwords are different
|
||||
useEffect(() => {
|
||||
if (email === newEmail) setErrorMessage('You need to set a different email as the current one')
|
||||
if (newEmail && email === newEmail)
|
||||
setErrorMessage('You need to set a different email as the current one')
|
||||
else setErrorMessage('')
|
||||
}, [email, newEmail])
|
||||
|
||||
@@ -44,7 +47,7 @@ export const ChangeEmail: React.FC = () => {
|
||||
<Input value={newEmail} onChange={setNewEmail} placeholder="New email" />
|
||||
</FlexboxGrid.Item>
|
||||
<FlexboxGrid.Item colspan={12}>
|
||||
<Button onClick={changeEmail} block appearance="primary">
|
||||
<Button onClick={() => changeEmail(email)} block appearance="primary">
|
||||
Change
|
||||
</Button>
|
||||
</FlexboxGrid.Item>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useChangePassword } from '@nhost/react'
|
||||
import React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, FlexboxGrid, Input, Message, Panel, toaster, Notification } from 'rsuite'
|
||||
import { Button, FlexboxGrid, Input, Message, Notification, Panel, toaster } from 'rsuite'
|
||||
|
||||
import { useChangePassword } from '@nhost/react'
|
||||
|
||||
export const ChangePassword: React.FC = () => {
|
||||
const [password, setPassword] = useState('')
|
||||
const { changePassword, isSuccess, error } = useChangePassword(password)
|
||||
const { changePassword, isSuccess, error } = useChangePassword()
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
// * See https://github.com/rsuite/rsuite/issues/2336
|
||||
@@ -44,7 +46,7 @@ export const ChangePassword: React.FC = () => {
|
||||
/>
|
||||
</FlexboxGrid.Item>
|
||||
<FlexboxGrid.Item colspan={12}>
|
||||
<Button onClick={changePassword} block appearance="primary">
|
||||
<Button onClick={() => changePassword(password)} block appearance="primary">
|
||||
Change
|
||||
</Button>
|
||||
</FlexboxGrid.Item>
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import decode from 'jwt-decode'
|
||||
import React from 'react'
|
||||
import ReactJson from 'react-json-view'
|
||||
import { Col, Panel, Row } from 'rsuite'
|
||||
import { useAccessToken, useUserData } from '@nhost/react'
|
||||
import { Button, Col, Panel, Row } from 'rsuite'
|
||||
|
||||
import { useAccessToken, useNhostClient, useUserData } from '@nhost/react'
|
||||
|
||||
import { ChangeEmail } from './change-email'
|
||||
import { ChangePassword } from './change-password'
|
||||
import { Mfa } from './mfa'
|
||||
|
||||
export const ProfilePage: React.FC = () => {
|
||||
const accessToken = useAccessToken()
|
||||
const userData = useUserData()
|
||||
const nhost = useNhostClient()
|
||||
return (
|
||||
<Panel header="Profile page" bordered>
|
||||
<Row>
|
||||
<Col md={12} sm={24}>
|
||||
<Mfa />
|
||||
</Col>
|
||||
<Col md={12} sm={24}>
|
||||
<ChangeEmail />
|
||||
</Col>
|
||||
@@ -33,6 +40,9 @@ export const ProfilePage: React.FC = () => {
|
||||
</Col>
|
||||
<Col md={12} sm={24}>
|
||||
<Panel header="JWT" bordered>
|
||||
<Button block appearance="primary" onClick={() => nhost.auth.refreshSession()}>
|
||||
Refresh session
|
||||
</Button>
|
||||
{accessToken && (
|
||||
<ReactJson
|
||||
src={decode(accessToken)}
|
||||
|
||||
30
examples/react-apollo/src/profile/mfa.tsx
Normal file
30
examples/react-apollo/src/profile/mfa.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Button, Input, Panel } from 'rsuite'
|
||||
|
||||
import { useConfigMfa } from '@nhost/react'
|
||||
|
||||
export const Mfa: React.FC = () => {
|
||||
const [code, setCode] = useState('')
|
||||
const { generateQrCode, activateMfa, isActivated, isGenerated, qrCodeDataUrl } = useConfigMfa()
|
||||
|
||||
return (
|
||||
<Panel header="Activate 2-step verification" bordered>
|
||||
{!isGenerated && (
|
||||
<Button block appearance="primary" onClick={generateQrCode}>
|
||||
Generate
|
||||
</Button>
|
||||
)}
|
||||
{isGenerated && !isActivated && (
|
||||
<div>
|
||||
<img alt="qrcode" src={qrCodeDataUrl} />
|
||||
<Input value={code} onChange={setCode} placeholder="Enter activation code" />
|
||||
<Button block appearance="primary" onClick={() => activateMfa(code)}>
|
||||
Activate
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isActivated && <div>MFA has been activated!!!</div>}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,27 @@
|
||||
import { Button, Divider, Input, Message } from 'rsuite'
|
||||
import { useEmailPasswordSignIn } from '@nhost/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { Button, Divider, Input, Message } from 'rsuite'
|
||||
|
||||
import { useSignInEmailPassword } from '@nhost/react'
|
||||
|
||||
const Footer: React.FC = () => (
|
||||
<div>
|
||||
<Divider />
|
||||
<Button as={NavLink} to="/sign-in" block appearance="link">
|
||||
← Other Login Options
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const EmailPassword: React.FC = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const { signIn, error } = useEmailPasswordSignIn(email, password)
|
||||
const [otp, setOtp] = useState('')
|
||||
const { signInEmailPassword, error, needsMfaOtp, sendMfaOtp } = useSignInEmailPassword(
|
||||
email,
|
||||
password,
|
||||
otp
|
||||
)
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
// * Set error message from the authentication hook errors
|
||||
@@ -18,41 +33,68 @@ export const EmailPassword: React.FC = () => {
|
||||
setErrorMessage('')
|
||||
}, [email, password])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
placeholder="Email Address"
|
||||
size="lg"
|
||||
autoFocus
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
if (needsMfaOtp)
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={otp}
|
||||
onChange={setOtp}
|
||||
placeholder="One-time password"
|
||||
size="lg"
|
||||
autoFocus
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<Message showIcon type="error">
|
||||
{errorMessage}
|
||||
</Message>
|
||||
)}
|
||||
<Button appearance="primary" onClick={sendMfaOtp} block>
|
||||
Send 2-step verification code
|
||||
</Button>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
placeholder="Email Address"
|
||||
size="lg"
|
||||
autoFocus
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
/>
|
||||
|
||||
{errorMessage && (
|
||||
<Message showIcon type="error">
|
||||
{errorMessage}
|
||||
</Message>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<Message showIcon type="error">
|
||||
{errorMessage}
|
||||
</Message>
|
||||
)}
|
||||
|
||||
<Button appearance="primary" onClick={signIn} block>
|
||||
Sign in
|
||||
</Button>
|
||||
<Button as={NavLink} block to="/sign-in/forgot-password">
|
||||
Forgot password?
|
||||
</Button>
|
||||
<Divider />
|
||||
<Button as={NavLink} to="/sign-in" block appearance="link">
|
||||
← Other Login Options
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
<Button
|
||||
appearance="primary"
|
||||
onClick={async () => {
|
||||
const result = await signInEmailPassword(email, password)
|
||||
console.log(result)
|
||||
}}
|
||||
block
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
<Button as={NavLink} block to="/sign-in/forgot-password">
|
||||
Forgot password?
|
||||
</Button>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from 'rsuite'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import React from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { Button } from 'rsuite'
|
||||
|
||||
import { EmailPasswordlessForm } from '../components/email-passwordless-form'
|
||||
export const EmailPasswordless: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Button, Divider, Input, Message, Notification, toaster } from 'rsuite'
|
||||
import { useResetPassword } from '@nhost/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { Button, Divider, Input, Message, Notification, toaster } from 'rsuite'
|
||||
|
||||
import { useResetPassword } from '@nhost/react'
|
||||
|
||||
export const ForgotPassword: React.FC = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const { resetPassword, isSent, error } = useResetPassword(email, { redirectTo: '/profile' })
|
||||
const { resetPassword, isSent, error } = useResetPassword({ redirectTo: '/profile' })
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
// * Set error message from the authentication hook errors
|
||||
@@ -16,16 +17,12 @@ export const ForgotPassword: React.FC = () => {
|
||||
useEffect(() => {
|
||||
setErrorMessage('')
|
||||
}, [email])
|
||||
// * See https://github.com/rsuite/rsuite/issues/2336
|
||||
useEffect(() => {
|
||||
toaster.push(<div />)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSent) {
|
||||
toaster.push(
|
||||
<Notification type="info" header="Info" closable>
|
||||
An email has been sent with a passwordless authentication link, so you'll be able to
|
||||
An email has been sent with a passwordless authentication link, so you will be able to
|
||||
authenticate and change your password.
|
||||
</Notification>
|
||||
)
|
||||
@@ -48,7 +45,7 @@ export const ForgotPassword: React.FC = () => {
|
||||
</Message>
|
||||
)}
|
||||
|
||||
<Button appearance="primary" onClick={resetPassword} block>
|
||||
<Button appearance="primary" onClick={() => resetPassword(email)} block>
|
||||
Reset your password
|
||||
</Button>
|
||||
<Divider />
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import { NavLink, Route, Routes } from 'react-router-dom'
|
||||
import { Button, Divider, FlexboxGrid, IconButton, Panel } from 'rsuite'
|
||||
import { Icon } from '@rsuite/icons'
|
||||
import React from 'react'
|
||||
import { FaLock } from 'react-icons/fa'
|
||||
import { Link, NavLink, Route, Routes } from 'react-router-dom'
|
||||
import { Button, Divider, FlexboxGrid, IconButton, Panel } from 'rsuite'
|
||||
|
||||
import { Icon } from '@rsuite/icons'
|
||||
|
||||
import { OAuthLinks } from '../components'
|
||||
import { VerificationEmailSent } from '../verification-email-sent'
|
||||
|
||||
import { EmailPassword } from './email-password'
|
||||
import { ForgotPassword } from './forgot-password'
|
||||
import { EmailPasswordless } from './email-passwordless'
|
||||
// import { useAnonymousSignIn } from '@nhost/react'
|
||||
import { ForgotPassword } from './forgot-password'
|
||||
// import { useSignInAnonymous } from '@nhost/react'
|
||||
|
||||
const Index: React.FC = () => (
|
||||
<div>
|
||||
@@ -31,7 +33,7 @@ const Index: React.FC = () => (
|
||||
)
|
||||
|
||||
export const SignInPage: React.FC = () => {
|
||||
// const { signIn } = useAnonymousSignIn()
|
||||
// const { signIn } = useSignInAnonymous()
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<FlexboxGrid justify="center">
|
||||
@@ -48,7 +50,8 @@ export const SignInPage: React.FC = () => {
|
||||
</FlexboxGrid.Item>
|
||||
</FlexboxGrid>
|
||||
<Divider />
|
||||
{/* Don't have an account? <Link to="/sign-up">Sign up</Link> or{' '}
|
||||
Don‘t have an account? <Link to="/sign-up">Sign up</Link>
|
||||
{/* or{' '}
|
||||
<a href="#" onClick={signIn}>
|
||||
enter the app anonymously
|
||||
</a> */}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Button, Input, Message } from 'rsuite'
|
||||
import { useEmailPasswordSignUp } from '@nhost/react'
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { NavLink, useNavigate } from 'react-router-dom'
|
||||
import { Button, Input, Message } from 'rsuite'
|
||||
|
||||
import { useSignUpEmailPassword } from '@nhost/react'
|
||||
|
||||
export const EmailPassword: React.FC = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
@@ -14,18 +16,15 @@ export const EmailPassword: React.FC = () => {
|
||||
)
|
||||
const navigate = useNavigate()
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const { signUp, error, needsVerification, isSuccess } = useEmailPasswordSignUp(
|
||||
email,
|
||||
password,
|
||||
options
|
||||
)
|
||||
const { signUpEmailPassword, error, needsEmailVerification, isSuccess } =
|
||||
useSignUpEmailPassword(options)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
useEffect(() => {
|
||||
if (needsVerification) navigate('/sign-up/verification-email-sent')
|
||||
if (needsEmailVerification) navigate('/sign-up/verification-email-sent')
|
||||
else if (isSuccess) navigate('/')
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [needsVerification, isSuccess])
|
||||
}, [needsEmailVerification, isSuccess])
|
||||
|
||||
// * Set error message from the registration hook errors
|
||||
useEffect(() => {
|
||||
@@ -37,7 +36,7 @@ export const EmailPassword: React.FC = () => {
|
||||
}, [email, password])
|
||||
// * Show an error message when passwords are different
|
||||
useEffect(() => {
|
||||
if (password !== confirmPassword) setErrorMessage('Provided passwords must be the same')
|
||||
if (password !== confirmPassword) setErrorMessage('Both passwords must be the same')
|
||||
else setErrorMessage('')
|
||||
}, [password, confirmPassword])
|
||||
return (
|
||||
@@ -89,9 +88,10 @@ export const EmailPassword: React.FC = () => {
|
||||
|
||||
<Button
|
||||
appearance="primary"
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
setErrorMessage('')
|
||||
signUp()
|
||||
const result = await signUpEmailPassword(email, password)
|
||||
console.log(result)
|
||||
}}
|
||||
block
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from 'rsuite'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import React from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { Button } from 'rsuite'
|
||||
|
||||
import { EmailPasswordlessForm } from '../components/email-passwordless-form'
|
||||
export const EmailPasswordless: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Icon } from '@rsuite/icons'
|
||||
import React from 'react'
|
||||
import { FaLock } from 'react-icons/fa'
|
||||
import { Button, Divider, FlexboxGrid, IconButton, Panel } from 'rsuite'
|
||||
import { Link, NavLink, Route, Routes } from 'react-router-dom'
|
||||
import { Button, Divider, FlexboxGrid, IconButton, Panel } from 'rsuite'
|
||||
|
||||
import { Icon } from '@rsuite/icons'
|
||||
|
||||
import { OAuthLinks } from '../components'
|
||||
import { VerificationEmailSent } from '../verification-email-sent'
|
||||
|
||||
import { EmailPassword } from './email-password'
|
||||
import { EmailPasswordless } from './email-passwordless'
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAuthenticated } from '@nhost/react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { useAuthenticated } from '@nhost/react'
|
||||
|
||||
export const VerificationEmailSent: React.FC = () => {
|
||||
const isAuthenticated = useAuthenticated()
|
||||
const navigate = useNavigate()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ services:
|
||||
environment:
|
||||
hasura_graphql_enable_remote_schema_permissions: false
|
||||
auth:
|
||||
version: 0.2.1
|
||||
version: 0.4.2
|
||||
auth:
|
||||
access_control:
|
||||
email:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@nhost-examples/testing-project",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"description": "Wrapper to run the Nhost CLI for development and testing",
|
||||
"scripts": {
|
||||
"start": "nhost -d"
|
||||
},
|
||||
"author": "Pierre-Louis Mercereau",
|
||||
"license": "MIT"
|
||||
}
|
||||
"name": "@nhost-examples/testing-project",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"description": "Wrapper to run the Nhost CLI for development and testing",
|
||||
"scripts": {
|
||||
"start": "nhost -d"
|
||||
},
|
||||
"author": "Pierre-Louis Mercereau",
|
||||
"license": "MIT"
|
||||
}
|
||||
26
package.json
26
package.json
@@ -28,11 +28,12 @@
|
||||
"prettier:fix": "prettier --write .",
|
||||
"lint": "pnpm turbo run lint --stream",
|
||||
"lint:fix": "pnpm turbo run lint:fix --stream",
|
||||
"test": "pnpm turbo run test --scope='@nhost/*' --no-deps --include-dependencies",
|
||||
"prerelease": "pnpm clean && pnpm install && pnpm build",
|
||||
"release": "pnpm run prerelease && changeset publish && git push --follow-tags && git status && pnpm -r publish",
|
||||
"release": "pnpm run prerelease && changeset publish",
|
||||
"snapshot": "pnpm prerelease && changeset version --snapshot preview && pnpm install && changeset publish --tag preview",
|
||||
"test": "pnpm turbo run test --scope='@nhost/*' --no-deps --include-dependencies",
|
||||
"changeset": "changeset",
|
||||
"snapshot": "pnpm prerelease && changeset version --snapshot preview && pnpm install && changeset publish --tag preview"
|
||||
"wait": "wait-on http://localhost:1337/v1/auth/healthz -i 500 -t 120000"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -43,14 +44,14 @@
|
||||
"@babel/eslint-parser": "^7.17.0",
|
||||
"@babel/plugin-syntax-flow": "^7.16.7",
|
||||
"@babel/plugin-transform-react-jsx": "^7.17.3",
|
||||
"@changesets/cli": "^2.21.0",
|
||||
"@faker-js/faker": "^6.0.0-alpha.7",
|
||||
"@changesets/cli": "^2.21.1",
|
||||
"@faker-js/faker": "^6.0.0-beta.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||
"@typescript-eslint/parser": "^5.14.0",
|
||||
"@vitejs/plugin-react": "^1.2.0",
|
||||
"esbuild": "^0.14.23",
|
||||
"esbuild": "^0.14.25",
|
||||
"esbuild-node-externals": "^1.4.1",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
@@ -59,7 +60,7 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react": "^7.29.3",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"husky": "^7.0.4",
|
||||
@@ -68,11 +69,12 @@
|
||||
"prettier": "^2.5.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
"tsconfig-paths-jest": "^0.0.1",
|
||||
"turbo": "^1.1.4",
|
||||
"turbo": "1.1.6",
|
||||
"typescript": "4.5.5",
|
||||
"vite": "^2.8.5",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-dts": "^0.9.9",
|
||||
"vite-tsconfig-paths": "^3.4.1"
|
||||
"vite-tsconfig-paths": "^3.4.1",
|
||||
"wait-on": "^6.0.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"graphql": "15.7.2"
|
||||
|
||||
@@ -1,5 +1,70 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [63d6059]
|
||||
- Updated dependencies [63d6059]
|
||||
- @nhost/core@0.3.10
|
||||
|
||||
## 0.3.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2c97db6]
|
||||
- @nhost/core@0.3.9
|
||||
|
||||
## 0.3.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [058956b]
|
||||
- Updated dependencies [7cf875f]
|
||||
- @nhost/core@0.3.8
|
||||
|
||||
## 0.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [16a6c50]
|
||||
- @nhost/core@0.3.4
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- correct dependencies
|
||||
|
||||
See this related issues:
|
||||
|
||||
- [nhost](https://github.com/nhost/nhost/issues/326)
|
||||
- [pnpm](https://github.com/pnpm/pnpm/issues/4348)
|
||||
|
||||
- Updated dependencies
|
||||
- @nhost/core@0.3.2
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 113beed: fix: Refetched queries and leaking subscriptions [#301](https://github.com/nhost/nhost/issues/301)
|
||||
- Updated dependencies [4420c0e]
|
||||
- @nhost/core@0.3.1
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 744fd69: Unify vanilla, react and next APIs so they can work together
|
||||
React and NextJS libraries now works together with `@nhost/nhost-js`. It also means the Nhost client needs to be initiated before passing it to the React provider.
|
||||
See the [React](https://docs.nhost.io/reference/react#configuration) and [NextJS](https://docs.nhost.io/reference/nextjs/configuration) configuration documentation for additional information.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [744fd69]
|
||||
- @nhost/core@0.3.0
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Nhost
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.9",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -36,26 +36,30 @@
|
||||
"main": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.umd.js",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.umd.js"
|
||||
"require": "./dist/index.cjs.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@apollo/client": "^3.5.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.5.8",
|
||||
"@nhost/client": "workspace:*",
|
||||
"@nhost/core": "workspace:^",
|
||||
"graphql": "16",
|
||||
"subscriptions-transport-ws": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"xstate": "^4.30.5"
|
||||
"@apollo/client": "^3.5.8",
|
||||
"xstate": "^4.30.5",
|
||||
"@nhost/nhost-js": "workspace:^"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
import { setContext } from '@apollo/client/link/context'
|
||||
import { WebSocketLink } from '@apollo/client/link/ws'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { Nhost } from '@nhost/client'
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
export type NhostApolloClientOptions = {
|
||||
nhost?: Nhost
|
||||
nhost?: NhostClient
|
||||
graphqlUrl?: string
|
||||
headers?: any
|
||||
publicRole?: string
|
||||
fetchPolicy?: WatchQueryFetchPolicy
|
||||
@@ -28,6 +29,7 @@ export type NhostApolloClientOptions = {
|
||||
|
||||
export const createApolloClient = ({
|
||||
nhost,
|
||||
graphqlUrl,
|
||||
headers = {},
|
||||
publicRole = 'public',
|
||||
fetchPolicy,
|
||||
@@ -35,11 +37,13 @@ export const createApolloClient = ({
|
||||
connectToDevTools = isBrowser && process.env.NODE_ENV === 'development',
|
||||
onError
|
||||
}: NhostApolloClientOptions) => {
|
||||
if (!nhost?.interpreter) {
|
||||
console.error("Nhost has not be initiated. Apollo client can't be created")
|
||||
let backendUrl = graphqlUrl || nhost?.graphql.getUrl()
|
||||
if (!backendUrl) {
|
||||
console.error("Can't initialize the Apollo Client: no backend Url has been provided")
|
||||
return null
|
||||
}
|
||||
const { interpreter, backendUrl } = nhost
|
||||
const interpreter = nhost?.auth.client.interpreter
|
||||
|
||||
let token: string | null = null
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
@@ -61,7 +65,7 @@ export const createApolloClient = ({
|
||||
return resHeaders
|
||||
}
|
||||
|
||||
const uri = `${backendUrl}/v1/graphql`
|
||||
const uri = backendUrl
|
||||
const wsUri = uri.startsWith('https') ? uri.replace(/^https/, 'wss') : uri.replace(/^http/, 'ws')
|
||||
|
||||
let webSocketClient: SubscriptionClient | null = null
|
||||
@@ -127,26 +131,22 @@ export const createApolloClient = ({
|
||||
|
||||
if (token !== newToken) {
|
||||
token = newToken
|
||||
client.reFetchObservableQueries()
|
||||
|
||||
if (isBrowser && webSocketClient) {
|
||||
if (newToken) {
|
||||
if (webSocketClient.status === 1) {
|
||||
// @ts-expect-error
|
||||
webSocketClient.tryReconnect()
|
||||
}
|
||||
} else {
|
||||
if (webSocketClient.status === 1) {
|
||||
// must close first to avoid race conditions
|
||||
webSocketClient.close()
|
||||
// reconnect
|
||||
// @ts-expect-error
|
||||
webSocketClient.tryReconnect()
|
||||
}
|
||||
if (event.type === 'SIGNOUT') {
|
||||
await client.resetStore().catch((error) => {
|
||||
console.error('Error resetting Apollo client cache')
|
||||
console.error(error)
|
||||
})
|
||||
if (webSocketClient.status === 1) {
|
||||
// must close first to avoid race conditions
|
||||
webSocketClient.close()
|
||||
|
||||
// @ts-expect-error
|
||||
webSocketClient.tryReconnect()
|
||||
}
|
||||
|
||||
if (!newToken && event.type === 'SIGNOUT') {
|
||||
try {
|
||||
await client.resetStore()
|
||||
} catch (error) {
|
||||
console.error('Error resetting Apollo client cache')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# @nhost/client
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0d8afde: Bump xstate version 4.30.5
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 207ae38: Improvements on `autoSignIn`
|
||||
|
||||
Auto login enables authentication from a link sent by email.
|
||||
It parses the url query parameters of the browser and looks for a possible refresh token to consume and authenticate.
|
||||
Although the mechanism existed already, it now broadcasts the refresh token to other tabs in the same browser, so they can also authenticate automatically.
|
||||
|
||||
- 207ae38: Improvements on `autoRefreshToken`
|
||||
|
||||
Auto refresh now uses a client-side timestamp from the instant of its creation to the access token expiration interval. As a result, there is less change of refresh and access token becoming stale or out of sync.
|
||||
|
||||
- 207ae38: Tree-shakable API
|
||||
|
||||
The new `@nhost/client` package is written with tree-shakability in mind. No dead code should be included by a subsequent bundler.
|
||||
|
||||
See [#198](https://github.com/nhost/nhost/issues/198)
|
||||
|
||||
- 207ae38: ## Stable authentication state
|
||||
|
||||
Until now, the Nhost SDK authentication state and its context (access Token, refresh token...) was not held with a reliable system, ending in unconsistencies e.g. [#189](https://github.com/nhost/nhost/issues/189), [#202](https://github.com/nhost/nhost/issues/202), [#186](https://github.com/nhost/nhost/issues/186), [#195](https://github.com/nhost/nhost/issues/195).
|
||||
The `@nhost/client` handles authentication state as a finite state machine with [xstate](https://github.com/statelyai/xstate). Xstate is framework agnostic and the authentication state will be easily plugable in most reactive frameworks such as React, Vue and Svelte.
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Nhost
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,48 +0,0 @@
|
||||
import { BroadcastChannel } from 'broadcast-channel'
|
||||
import { InterpreterFrom } from 'xstate'
|
||||
|
||||
import { createNhostMachine, NhostMachine, NhostMachineOptions } from './machines'
|
||||
import { defaultStorageGetter, defaultStorageSetter } from './storage'
|
||||
|
||||
export type NhostClientOptions = NhostMachineOptions
|
||||
|
||||
export class Nhost {
|
||||
readonly backendUrl: string
|
||||
readonly clientUrl: string
|
||||
readonly machine: NhostMachine
|
||||
interpreter?: InterpreterFrom<NhostMachine>
|
||||
#channel?: BroadcastChannel
|
||||
|
||||
constructor({
|
||||
backendUrl,
|
||||
clientUrl = typeof window !== 'undefined' ? window.location.origin : '',
|
||||
storageGetter = defaultStorageGetter,
|
||||
storageSetter = defaultStorageSetter,
|
||||
autoSignIn = true,
|
||||
autoRefreshToken = true
|
||||
}: NhostClientOptions) {
|
||||
this.backendUrl = backendUrl
|
||||
this.clientUrl = clientUrl
|
||||
|
||||
const machine = createNhostMachine({
|
||||
backendUrl,
|
||||
clientUrl,
|
||||
storageGetter,
|
||||
storageSetter,
|
||||
autoSignIn,
|
||||
autoRefreshToken
|
||||
})
|
||||
|
||||
this.machine = machine
|
||||
|
||||
if (typeof window !== 'undefined' && autoSignIn) {
|
||||
this.#channel = new BroadcastChannel<string>('nhost')
|
||||
this.#channel.addEventListener('message', (token) => {
|
||||
const existingToken = this.interpreter?.state.context.refreshToken
|
||||
if (this.interpreter && token !== existingToken) {
|
||||
this.interpreter.send({ type: 'TRY_TOKEN', token })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Nhost, NhostClientOptions } from './client'
|
||||
import { cookieStorageGetter, cookieStorageSetter } from './storage'
|
||||
const isBrowser = typeof window !== undefined
|
||||
|
||||
export class NhostSSR extends Nhost {
|
||||
constructor({ backendUrl }: NhostClientOptions) {
|
||||
super({
|
||||
backendUrl,
|
||||
autoSignIn: isBrowser,
|
||||
autoRefreshToken: isBrowser,
|
||||
storageGetter: cookieStorageGetter,
|
||||
storageSetter: cookieStorageSetter
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ErrorPayload } from '../errors'
|
||||
import type { NhostSession, PasswordlessOptions, SignUpOptions } from '../types'
|
||||
|
||||
export type NhostEvents =
|
||||
| { type: 'SESSION_UPDATE'; data: { session: NhostSession } }
|
||||
| { type: 'TRY_TOKEN'; token: string }
|
||||
| { type: 'SIGNIN_ANONYMOUS' }
|
||||
| { type: 'SIGNIN_PASSWORD'; email?: string; password?: string }
|
||||
| {
|
||||
type: 'SIGNIN_PASSWORDLESS_EMAIL'
|
||||
email?: string
|
||||
options?: PasswordlessOptions
|
||||
}
|
||||
| { type: 'SIGNUP_EMAIL_PASSWORD'; email?: string; password?: string; options?: SignUpOptions }
|
||||
| { type: 'TOKEN_REFRESH_ERROR'; error: ErrorPayload }
|
||||
| { type: 'SIGNOUT'; all?: boolean }
|
||||
102
packages/core/CHANGELOG.md
Normal file
102
packages/core/CHANGELOG.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# @nhost/core
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 63d6059: Set onTokenChanged before the state interpreter started
|
||||
Fixes [#384](https://github.com/nhost/nhost/issues/384), thanks [@noverby](https://github.com/noverby)
|
||||
- 63d6059: Trigger onTokenChanged when token changes
|
||||
Fixes [#373](https://github.com/nhost/nhost/issues/373), thanks [@yureckey](https://github.com/yureckey)
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2c97db6: Keep authentication status and access token in sync
|
||||
The authentication events where not set correctly, leading the main Nhost client not to update internal states of storage/graphql/functions sub-clients when using non-react clients.
|
||||
The use of private fields (`#`) is also avoided as they conflict with the use of proxies in Vue, leading to errors in the upcoming Vue library.
|
||||
Fixes #373 and #378
|
||||
|
||||
## 0.3.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 058956b: Add missing provider types
|
||||
`strava`, `gitlab`, and `bitbucket` were missing from the list of providers in Typescript and are now available.
|
||||
- 058956b: Add `emailVerified`, `phoneNumber`, `phoneNumberVerified`, and `activeMfaType` to User type
|
||||
|
||||
Some information is missing in the `User` payload (see [this issue](https://github.com/nhost/nhost/issues/306)). The above properties have been added in the Typescript `User` type and are available when using Hasura Auth versions from [this pull request](https://github.com/nhost/hasura-auth/pull/128) (tentative version number: `0.5.1`)
|
||||
|
||||
- 7cf875f: Export error code payloads and type
|
||||
|
||||
## 0.3.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 16a6c50: Correct autoSignIn
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- correct dependencies
|
||||
|
||||
See this related issues:
|
||||
|
||||
- [nhost](https://github.com/nhost/nhost/issues/326)
|
||||
- [pnpm](https://github.com/pnpm/pnpm/issues/4348)
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4420c0e: Check if `window.location` exists
|
||||
|
||||
When using [Expo](https://expo.dev/), `window` can be an object while `window.location` is `undefined`. It lead to [this issue](https://github.com/nhost/nhost/issues/309).
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 744fd69: Unify vanilla, react and next APIs so they can work together
|
||||
React and NextJS libraries now works together with `@nhost/nhost-js`. It also means the Nhost client needs to be initiated before passing it to the React provider.
|
||||
See the [React](https://docs.nhost.io/reference/react#configuration) and [NextJS](https://docs.nhost.io/reference/nextjs/configuration) configuration documentation for additional information.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 744fd69: Rename `@nhost/client` to `@nhost/core`
|
||||
The `@nhost/client` name was somehow misleading, as it was implying it could somehow work as a vanilla client, whereas it only contained the state machine that could be used for vanilla or framework specific libraries e.g. `@nhost/react`.
|
||||
|
||||
It is therefore renamed to `@nhost/core`, and keeps the same versionning and changelog.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0d8afde: Bump xstate version 4.30.5
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 207ae38: Improvements on `autoSignIn`
|
||||
|
||||
Auto login enables authentication from a link sent by email.
|
||||
It parses the url query parameters of the browser and looks for a possible refresh token to consume and authenticate.
|
||||
Although the mechanism existed already, it now broadcasts the refresh token to other tabs in the same browser, so they can also authenticate automatically.
|
||||
|
||||
- 207ae38: Improvements on `autoRefreshToken`
|
||||
|
||||
Auto refresh now uses a client-side timestamp from the instant of its creation to the access token expiration interval. As a result, there is less change of refresh and access token becoming stale or out of sync.
|
||||
|
||||
- 207ae38: Tree-shakable API
|
||||
|
||||
The new `@nhost/client` package is written with tree-shakability in mind. No dead code should be included by a subsequent bundler.
|
||||
|
||||
See [#198](https://github.com/nhost/nhost/issues/198)
|
||||
|
||||
- 207ae38: ## Stable authentication state
|
||||
|
||||
Until now, the Nhost SDK authentication state and its context (access Token, refresh token...) was not held with a reliable system, ending in unconsistencies e.g. [#189](https://github.com/nhost/nhost/issues/189), [#202](https://github.com/nhost/nhost/issues/202), [#186](https://github.com/nhost/nhost/issues/186), [#195](https://github.com/nhost/nhost/issues/195).
|
||||
The `@nhost/client` handles authentication state as a finite state machine with [xstate](https://github.com/statelyai/xstate). Xstate is framework agnostic and the authentication state will be easily plugable in most reactive frameworks such as React, Vue and Svelte.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user