Compare commits

...

12 Commits

Author SHA1 Message Date
github-actions[bot]
58dec6e7b2 chore: update versions (#2853)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@7.1.6

### Patch Changes

-   @nhost/nhost-js@3.1.9

## @nhost/react-apollo@12.0.6

### Patch Changes

-   @nhost/apollo@7.1.6
-   @nhost/react@3.5.6

## @nhost/react-urql@9.0.6

### Patch Changes

-   @nhost/react@3.5.6

## @nhost/hasura-auth-js@2.5.6

### Patch Changes

- 8b12426: fix: correct signout to send accessToken when clearing all
session

## @nhost/nextjs@2.1.20

### Patch Changes

-   @nhost/react@3.5.6

## @nhost/nhost-js@3.1.9

### Patch Changes

-   Updated dependencies [8b12426]
    -   @nhost/hasura-auth-js@2.5.6

## @nhost/react@3.5.6

### Patch Changes

-   @nhost/nhost-js@3.1.9

## @nhost/vue@2.6.6

### Patch Changes

-   @nhost/nhost-js@3.1.9

## @nhost-examples/react-apollo@1.0.0

### Major Changes

-   cffdec5: feat: rewrite example using shadcn ui components

### Patch Changes

-   @nhost/react@3.5.6
-   @nhost/react-apollo@12.0.6

## @nhost/dashboard@1.28.0

### Minor Changes

- 526183a: feat: allow filtering users in "make request as" in graphql
section
-   be3b85b: feat: add conceal errors toggle on auth settings page

### Patch Changes

- 35a2f12: fix: prevent run service details from opening when attempting
to delete
    -   @nhost/react-apollo@12.0.6
    -   @nhost/nextjs@2.1.20

## @nhost/docs@2.17.0

### Minor Changes

- cffdec5: feat: update react quickstart guide to use the nhost react
apollo template
-   4cf6677: feat: update list of postgres extensions

## @nhost-examples/cli@0.3.11

### Patch Changes

-   @nhost/nhost-js@3.1.9

## @nhost-examples/codegen-react-apollo@0.4.11

### Patch Changes

-   @nhost/react@3.5.6
-   @nhost/react-apollo@12.0.6

## @nhost-examples/codegen-react-query@0.4.11

### Patch Changes

-   @nhost/react@3.5.6

## @nhost-examples/codegen-react-urql@0.3.11

### Patch Changes

-   @nhost/react@3.5.6
-   @nhost/react-urql@9.0.6

## @nhost-examples/multi-tenant-one-to-many@2.2.11

### Patch Changes

-   @nhost/nhost-js@3.1.9

## @nhost-examples/nextjs@0.3.11

### Patch Changes

-   @nhost/react@3.5.6
-   @nhost/react-apollo@12.0.6
-   @nhost/nextjs@2.1.20

## @nhost-examples/node-storage@0.2.11

### Patch Changes

-   @nhost/nhost-js@3.1.9

## @nhost-examples/nextjs-server-components@0.4.12

### Patch Changes

-   @nhost/nhost-js@3.1.9

## @nhost-examples/react-gqty@1.2.11

### Patch Changes

-   @nhost/react@3.5.6

## @nhost-examples/react-native@0.0.5

### Patch Changes

-   @nhost/react@3.5.6
-   @nhost/react-apollo@12.0.6

## @nhost-examples/vue-apollo@0.6.11

### Patch Changes

-   @nhost/nhost-js@3.1.9
-   @nhost/apollo@7.1.6
-   @nhost/vue@2.6.6

## @nhost-examples/vue-quickstart@0.2.11

### Patch Changes

-   @nhost/apollo@7.1.6
-   @nhost/vue@2.6.6

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-09 12:19:53 +01:00
David BM
526183ab88 feat (dashboard): allow filtering users in "make request as" feature of graphql section (#2805)
Resolves #2593
2024-09-09 05:55:49 -04:00
Hassan Ben Jobrane
435b65a65a fix(react-apollo): change relyingParty ID to nhost.io (#2861) 2024-09-04 14:56:54 +01:00
Hassan Ben Jobrane
35a2f1203c fix(e2e): fix run service e2e test (#2859)
### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Fixed the button selector in the end-to-end test for deleting a
service, ensuring the correct button is targeted.
- Refactored the `ServicesList` component to standardize icon sizes and
improve the order of class attributes for better readability.
- Added event stop propagation to the delete service action to prevent
unintended event bubbling.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Bug
fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>run.test.ts</strong><dd><code>Fix button selector in
e2e test for service deletion</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/e2e/run/run.test.ts

<li>Updated button selector for deleting a service.<br> <li> Removed
unnecessary click action on 'Close' button.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2859/files#diff-3b81821630a8e66e8f580609a834499bdfec9ac228ff07b99f398ec07c329095">+1/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ServicesList.tsx</strong><dd><code>Refactor
ServicesList component for consistency and
readability</code></dd></summary>
<hr>

dashboard/src/features/services/components/ServicesList/ServicesList.tsx

<li>Standardized icon size and order of class attributes.<br> <li> Added
event stop propagation for delete service action.<br> <li> Improved code
readability and consistency.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2859/files#diff-efb3008c23436b2db5bb94de15e91c78cf76ef6481ecb02eb542cf660ba98653">+12/-9</a>&nbsp;
&nbsp; </td>

</tr>                    
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-09-04 11:39:32 +01:00
David BM
be3b85bbc8 feat (dashboard): add conceal errors toggle on auth settings page (#2858) 2024-09-03 10:07:40 -04:00
Hassan Ben Jobrane
cffdec585c feat: templates: react-apollo (#2834)
### **PR Type**
Enhancement, Tests


___

### **Description**
- Added multiple new UI components including `Layout`, `DropdownMenu`,
`Sheet`, `Form`, `Dialog`, `Table`, `Card`, `Button`, `Alert`,
`OAuthLinks`, and more.
- Integrated Apollo Client for GraphQL queries and mutations in various
components.
- Implemented form handling using `react-hook-form` and validation with
`zod`.
- Refactored `App` component to use a new route structure and removed
Mantine components.
- Updated E2E tests for new UI components and improved test reliability
and readability.
- Removed `pnpm-lock.yaml` file.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>45
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>layout.tsx</strong><dd><code>Implement Layout Component
with Navigation and Sign-Out</code>&nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/routes/app/layout.tsx

<li>Added <code>Layout</code> component with navigation and sign-out
functionality.<br> <li> Integrated <code>Tooltip</code>,
<code>DropdownMenu</code>, and <code>Sheet</code> components for UI
<br>elements.<br> <li> Implemented mobile navigation toggle.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-dfb4bf452a609fab7a9d6f9dc7906365ffaed54e830077310f9d505836da2c75">+228/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>dropdown-menu.tsx</strong><dd><code>Add DropdownMenu
Component with Radix UI Integration</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/ui/dropdown-menu.tsx

<li>Added <code>DropdownMenu</code> component with various
sub-components.<br> <li> Integrated Radix UI primitives for dropdown
functionality.<br> <li> Styled dropdown menu items and content.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-8e4a2a51701c3205e7d5199dc9f25d3fe348169ad72d6b4739687b106e7159f2">+198/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>dropdown-menu.tsx</strong><dd><code>Add DropdownMenu
Component with Radix UI Integration</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/ui/dropdown-menu.tsx

<li>Added <code>DropdownMenu</code> component with various
sub-components.<br> <li> Integrated Radix UI primitives for dropdown
functionality.<br> <li> Styled dropdown menu items and content.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-82ecd25c0e1deeffefeb11e66d3b1d625f5cbdaf64934a325360ef519d46734d">+198/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>security-keys.tsx</strong><dd><code>Add SecurityKeys
Component for Managing Security Keys</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

examples/react-apollo/src/components/profile/security-keys.tsx

<li>Added <code>SecurityKeys</code> component for managing security
keys.<br> <li> Integrated Apollo Client for GraphQL queries and
mutations.<br> <li> Implemented form validation with
<code>react-hook-form</code> and <code>zod</code>.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-20c5d7ececb3f500fc179a36ec957b0744197e88ca47d050e29b401967781be3">+178/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>App.tsx</strong><dd><code>Refactor App Component with
New Route Structure</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/App.tsx

<li>Refactored <code>App</code> component to use new route
structure.<br> <li> Added routes for authentication and application
pages.<br> <li> Removed Mantine components and replaced with custom
layout.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-cce00ef2ed124ca9c4fb6d5a27065cfb227de957db19fee484d79526bd243405">+48/-123</a></td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>layout.tsx</strong><dd><code>Implement Layout Component
with Navigation and Sign-Out</code>&nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/app/layout.tsx

<li>Added <code>Layout</code> component with navigation and sign-out
functionality.<br> <li> Integrated <code>Tooltip</code> and
<code>Sheet</code> components for UI elements.<br> <li> Implemented
mobile navigation toggle.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-c2700d3324f3a2d1b5cb71c45c6599d810d4deb4dedd48a83a1c097ac745546d">+125/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>protected-notes.tsx</strong><dd><code>Add
ProtectedNotes Component for Managing Protected
Notes</code></dd></summary>
<hr>

examples/react-apollo/src/components/routes/app/protected-notes.tsx

<li>Added <code>ProtectedNotes</code> component for managing protected
notes.<br> <li> Integrated Apollo Client for GraphQL queries and
mutations.<br> <li> Implemented permission elevation for secure
actions.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-2ff0d7eccbad3f4bf3769e3b4db6a8c0b2630b5e600a3bae8b565b2ad16df6b3">+196/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sheet.tsx</strong><dd><code>Add Sheet Component with
Radix UI Integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/ui/sheet.tsx

<li>Added <code>Sheet</code> component with various sub-components.<br>
<li> Integrated Radix UI primitives for sheet functionality.<br> <li>
Styled sheet content and overlay.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-ae92d11cc983a782928cdebe08c27a7b0954a9212e8e60ed339df57cbcca3b71">+138/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sheet.tsx</strong><dd><code>Add Sheet Component with
Radix UI Integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/ui/sheet.tsx

<li>Added <code>Sheet</code> component with various sub-components.<br>
<li> Integrated Radix UI primitives for sheet functionality.<br> <li>
Styled sheet content and overlay.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-d8caf3ac16eb4b67f59f860323cc131cd02390141f964f9ca2955987d639e8db">+138/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>form.tsx</strong><dd><code>Add Form Component with
React Hook Form Integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/ui/form.tsx

<li>Added <code>Form</code> component with various sub-components.<br>
<li> Integrated <code>react-hook-form</code> for form handling.<br> <li>
Styled form fields and messages.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-d2f42eb4934b32e1b4572330c17af13138835127adade0989aaa294e78d5b874">+176/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-in-email-password.tsx</strong><dd><code>Add
SignInEmailPassword Component for Email/Password
Sign-In</code></dd></summary>
<hr>


examples/react-apollo/src/components/routes/auth/sign-in/sign-in-email-password.tsx

<li>Added <code>SignInEmailPassword</code> component for email/password
sign-in.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-a919b17649003a1b961e1f84a211f7d3913d7c8a57ea5c2c3aed08b9c900b73f">+135/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>form.tsx</strong><dd><code>Add Form Component with
React Hook Form Integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/ui/form.tsx

<li>Added <code>Form</code> component with various sub-components.<br>
<li> Integrated <code>react-hook-form</code> for form handling.<br> <li>
Styled form fields and messages.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-5367598c9a64936b77080af5c57bd2110e2e835c23b306974b8d84fe8295e7f7">+168/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-up-email-password.tsx</strong><dd><code>Add
SignUpEmailPassword Component for Email/Password
Sign-Up</code></dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/auth/sign-up/sign-up-email-password.tsx

<li>Added <code>SignUpEmailPassword</code> component for email/password
sign-up.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-4507fe991337f37a30f72f9411947046df23427c37b97e8c357c609d98288ff4">+143/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-up-email-password.tsx</strong><dd><code>Add
SignUpEmailPassword Component for Email/Password
Sign-Up</code></dd></summary>
<hr>


examples/react-apollo/src/components/routes/auth/sign-up/sign-up-email-password.tsx

<li>Added <code>SignUpEmailPassword</code> component for email/password
sign-up.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-4192d80c13ac7212e63c5ad0729fb4b509ac4ff8876fb919be24ab93dbfbdfcc">+143/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>dialog.tsx</strong><dd><code>Add Dialog Component with
Radix UI Integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/ui/dialog.tsx

<li>Added <code>Dialog</code> component with various sub-components.<br>
<li> Integrated Radix UI primitives for dialog functionality.<br> <li>
Styled dialog content and overlay.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-233c2734414990fe89dc642d88a85cf8964f3c139c4fdb3b8d1845616118d41a">+120/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>dialog.tsx</strong><dd><code>Add Dialog Component with
Radix UI Integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/ui/dialog.tsx

<li>Added <code>Dialog</code> component with various sub-components.<br>
<li> Integrated Radix UI primitives for dialog functionality.<br> <li>
Styled dialog content and overlay.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-f74c4636490d03c847d91cc3aee450023f28c7ef7a6e3088f900b6498e0bb349">+120/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>todos.tsx</strong><dd><code>Add Todos Component for
Managing To-Do Items</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/routes/app/todos.tsx

<li>Added <code>Todos</code> component for managing to-do items.<br>
<li> Integrated Apollo Client for GraphQL queries and mutations.<br>
<li> Implemented UI for adding and deleting to-dos.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-56a00354330376b2f9bf79a2504fe05450ae972b4206b949f26ac58458c2c6aa">+132/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>upload-multiple-files.tsx</strong><dd><code>Add
UploadMultipleFiles Component for Multiple File
Uploads</code></dd></summary>
<hr>

examples/react-apollo/src/components/storage/upload-multiple-files.tsx

<li>Added <code>UploadMultipleFiles</code> component for uploading
multiple files.<br> <li> Integrated <code>react-dropzone</code> for file
drag-and-drop.<br> <li> Styled file upload progress and status
indicators.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-b8dda6ac0ba5b9703676b38e3ddbbb553c5e1368665c30307ec04e70ef9643ce">+99/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-in-email-password.tsx</strong><dd><code>Add
SignInEmailPassword Component for Email/Password
Sign-In</code></dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/auth/sign-in/sign-in-email-password.tsx

<li>Added <code>SignInEmailPassword</code> component for email/password
sign-in.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-b0c349a0e172774781f32b7b2956feadd832e5c25a314858a6de856bb04dc18b">+105/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-up-security-key.tsx</strong><dd><code>Add
SignUpSecurityKey Component for Security Key Sign-Up</code>&nbsp;
</dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/auth/sign-up/sign-up-security-key.tsx

<li>Added <code>SignUpSecurityKey</code> component for security key
sign-up.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-d7a694447b86326dc05407d24966d54b3c04749a111fc6f317d2feb9939c05f1">+100/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-up-security-key.tsx</strong><dd><code>Add
SignUpSecurityKey Component for Security Key Sign-Up</code>&nbsp;
</dd></summary>
<hr>


examples/react-apollo/src/components/routes/auth/sign-up/sign-up-security-key.tsx

<li>Added <code>SignUpSecurityKey</code> component for security key
sign-up.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-fb97711c3deb9ee7739371997b9f6f5ab4b1ebc926ca37311006c865d9eb97d8">+100/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-in-security-key.tsx</strong><dd><code>Add
SignInSecurityKey Component for Security Key Sign-In</code>&nbsp;
</dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/auth/sign-in/sign-in-security-key.tsx

<li>Added <code>SignInSecurityKey</code> component for security key
sign-in.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-95755a5056e7f57caa2eeaec3801e06382792395f7436c60f3e4d0b7382c2703">+99/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-in-security-key.tsx</strong><dd><code>Add
SignInSecurityKey Component for Security Key Sign-In</code>&nbsp;
</dd></summary>
<hr>


examples/react-apollo/src/components/routes/auth/sign-in/sign-in-security-key.tsx

<li>Added <code>SignInSecurityKey</code> component for security key
sign-in.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-c53d13936f5d9c0cc31811f17c9721f9d7a2795d0cff4a1333fdb147846f5cd8">+99/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-in-magic-link.tsx</strong><dd><code>Add
SignInMagicLink Component for Magic Link Sign-In</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/auth/sign-in/sign-in-magic-link.tsx

<li>Added <code>SignInMagicLink</code> component for magic link
sign-in.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-6ad692dad5c7240a085ab15146cc515308c295c6118562e81b41db89154a7f2a">+98/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-up-magic-link.tsx</strong><dd><code>Add
SignUpMagicLink Component for Magic Link Sign-Up</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/auth/sign-up/sign-up-magic-link.tsx

<li>Added <code>SignUpMagicLink</code> component for magic link
sign-up.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-756d7107028cebec7b2f6da051f9003fa7689f33083e2388af887dc9de5886c9">+98/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-in-magic-link.tsx</strong><dd><code>Add
SignInMagicLink Component for Magic Link Sign-In</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


examples/react-apollo/src/components/routes/auth/sign-in/sign-in-magic-link.tsx

<li>Added <code>SignInMagicLink</code> component for magic link
sign-in.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-05d6dec736df35bf6b430e76320dff84744131f602c2d3752f82df43180f9660">+98/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>sign-up-magic-link.tsx</strong><dd><code>Add
SignUpMagicLink Component for Magic Link Sign-Up</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


examples/react-apollo/src/components/routes/auth/sign-up/sign-up-magic-link.tsx

<li>Added <code>SignUpMagicLink</code> component for magic link
sign-up.<br> <li> Integrated <code>react-hook-form</code> for form
handling.<br> <li> Implemented email verification dialog.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-2d9446d69773256ac86513def9db66e8686f9892d74476f6206e86c39ff78176">+98/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>change-password.tsx</strong><dd><code>Refactor
ChangePassword Component with New UI Components</code>&nbsp;
</dd></summary>
<hr>

examples/react-apollo/src/components/profile/change-password.tsx

<li>Refactored <code>ChangePassword</code> component with new UI
components.<br> <li> Integrated Apollo Client for GraphQL queries.<br>
<li> Implemented form validation with <code>react-hook-form</code>.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-d4ea801f2778896b2e41f1f62e1d335b6b77a490a95adac7786bf896b1fd1bac">+40/-33</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>table.tsx</strong><dd><code>Add Table Component with
Styling and Utility Functions</code>&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/ui/table.tsx

<li>Added <code>Table</code> component with various sub-components.<br>
<li> Styled table header, body, and rows.<br> <li> Integrated utility
functions for class names.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-2c2db0130b7a45429d43be2268e27ec92575a83594079a6faad6bb8e1ad7b505">+117/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>forgot-password.tsx</strong><dd><code>Add
ForgotPassword Component for Password Reset</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/routes/auth/forgot-password.tsx

<li>Added <code>ForgotPassword</code> component for password reset.<br>
<li> Integrated <code>react-hook-form</code> for form handling.<br> <li>
Implemented password reset functionality.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-d8be229cc3e22a295099c952ed7553dcbf150e0f4d1e417c20728aee369fc36a">+94/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>mfa.tsx</strong><dd><code>Add Mfa Component for
Managing Multi-Factor Authentication</code></dd></summary>
<hr>

examples/react-apollo/src/components/profile/mfa.tsx

<li>Added <code>Mfa</code> component for managing multi-factor
authentication.<br> <li> Integrated Apollo Client for GraphQL
queries.<br> <li> Implemented QR code generation and activation.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-a64ad0612d2947e40c2d570e7b949c91ec4737d74bbab2a12e5bfffbeb0ff991">+101/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>storage.tsx</strong><dd><code>Add Storage Component for
Managing File Storage</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/routes/app/storage.tsx

<li>Added <code>Storage</code> component for managing file storage.<br>
<li> Integrated Apollo Client for GraphQL queries.<br> <li> Implemented
file download functionality.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-fa67a80832e2e84d6d0bba38a5f241fd5f1c57511b1f652e9557a5d465efc124">+99/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>change-email.tsx</strong><dd><code>Add ChangeEmail
Component for Changing User Email</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/profile/change-email.tsx

<li>Added <code>ChangeEmail</code> component for changing user
email.<br> <li> Integrated Apollo Client for GraphQL queries.<br> <li>
Implemented form validation with <code>react-hook-form</code>.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-96cca643537cb9da5002543183dd894548fc0d7e34945686da8b966de79ec998">+91/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>forgot-password.tsx</strong><dd><code>Add
ForgotPassword Component for Password Reset</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/routes/auth/forgot-password.tsx

<li>Added <code>ForgotPassword</code> component for password reset.<br>
<li> Integrated <code>react-hook-form</code> for form handling.<br> <li>
Implemented password reset functionality.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-3a657829492f9fb34157a7b9bc72f8e2de051710b9a9fe3d52c1f868ae39ff07">+74/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>upload-single-file.tsx</strong><dd><code>Add
UploadSingleFile Component for Single File Upload</code>&nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/storage/upload-single-file.tsx

<li>Added <code>UploadSingleFile</code> component for uploading a single
file.<br> <li> Integrated <code>react-dropzone</code> for file
drag-and-drop.<br> <li> Styled file upload progress and status
indicators.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-e21ae43a38d184164fc3dd791f396fd3a13f6a2c95fee31b1797252a4f16c56e">+69/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>upload-single-file.tsx</strong><dd><code>Add
UploadSingleFile Component for Single File Upload</code>&nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/storage/upload-single-file.tsx

<li>Added <code>UploadSingleFile</code> component for uploading a single
file.<br> <li> Integrated <code>react-dropzone</code> for file
drag-and-drop.<br> <li> Styled file upload progress and status
indicators.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-cf8adab15beb189ea30207b9c4ebc9ed4ef0fe9901a9cc06b1d02aafba9d7d46">+64/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>card.tsx</strong><dd><code>Add Card Component with
Styling and Utility Functions</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/ui/card.tsx

<li>Added <code>Card</code> component with various sub-components.<br>
<li> Styled card header, content, and footer.<br> <li> Integrated
utility functions for class names.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-6c08563777faac205d377afaf3160c75e80474663f2554e5dc23c691b82f2cfb">+79/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>card.tsx</strong><dd><code>Add Card Component with
Styling and Utility Functions</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

examples/react-apollo/src/components/ui/card.tsx

<li>Added <code>Card</code> component with various sub-components.<br>
<li> Styled card header, content, and footer.<br> <li> Integrated
utility functions for class names.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-caa6940dfb36b33fb7431d74fc74bd86a0ab549ac07ee4f8931767f2e401f22b">+79/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>connect-github.tsx</strong><dd><code>Add ConnectGithub
Component for Connecting GitHub Account</code></dd></summary>
<hr>

examples/react-apollo/src/components/profile/connect-github.tsx

<li>Added <code>ConnectGithub</code> component for connecting GitHub
account.<br> <li> Integrated Apollo Client for GraphQL queries.<br> <li>
Implemented GitHub connection status and link.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-8fb7c2dd3c82dfc83ad53118b884af3fd7563f7d8694da1afbe170b5e523570a">+63/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>oauth-links.tsx</strong><dd><code>Add OAuthLinks
Component for Social Sign-In Options</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/auth/oauth-links.tsx

<li>Added <code>OAuthLinks</code> component for social sign-in
options.<br> <li> Integrated provider links for GitHub, Google, Apple,
and LinkedIn.<br> <li> Styled social sign-in buttons.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-f5999fa99948c7a83619e69ab669da87ca10146ad5742f93112e21b00932bc0e">+58/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>oauth-links.tsx</strong><dd><code>Add OAuthLinks
Component for Social Sign-In Options</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/auth/oauth-links.tsx

<li>Added <code>OAuthLinks</code> component for social sign-in
options.<br> <li> Integrated provider links for GitHub, Google, Apple,
and LinkedIn.<br> <li> Styled social sign-in buttons.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-97b7775bbdf0f75091067d77b6638f6a81a15467e2ab080a769602c7ab345010">+56/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>App.tsx</strong><dd><code>Add App Component with Route
Structure</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

templates/cra-template-nhost-react-apollo-template/template/src/App.tsx

<li>Added <code>App</code> component with route structure.<br> <li>
Integrated authentication and application routes.<br> <li> Implemented
layout and page components.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-4a551eeb9e74ac9467ea078b159befed8370562b592bb09124200bbbdb146c3d">+51/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>button.tsx</strong><dd><code>Add Button Component with
Styling and Utility Functions</code>&nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/ui/button.tsx

<li>Added <code>Button</code> component with various styles.<br> <li>
Integrated utility functions for class names.<br> <li> Styled button
variants and sizes.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-5f3def0840b2a2d0c618123db3b679f5afa7bfe089301e6df15556ea7ea567de">+56/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>button.tsx</strong><dd><code>Add Button Component with
Styling and Utility Functions</code>&nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/src/components/ui/button.tsx

<li>Added <code>Button</code> component with various styles.<br> <li>
Integrated utility functions for class names.<br> <li> Styled button
variants and sizes.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-66624bef5f7762c254328a05baf2e434d8a431d2778ba5d51de1fb8d603090d2">+50/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>alert.tsx</strong><dd><code>Add Alert Component with
Styling and Utility Functions</code>&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


templates/cra-template-nhost-react-apollo-template/template/src/components/ui/alert.tsx

<li>Added <code>Alert</code> component with various styles.<br> <li>
Integrated utility functions for class names.<br> <li> Styled alert
variants and descriptions.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-223df6c627e0f93e279f9570b192ed98ac8c5cbe0e5dd553ce0e5dd6b8cbff8c">+59/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

</table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>6
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>utils.ts</strong><dd><code>Update Utility Functions for
E2E Tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

examples/react-apollo/e2e/utils.ts

<li>Updated utility functions for E2E tests.<br> <li> Modified selectors
and actions for new UI components.<br> <li> Improved test reliability
and readability.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-3bdd9b675af03a22eb7e8077183e8179504a9c3a085980da4938fd0c5e4b8907">+15/-19</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>file-upload.test.ts</strong><dd><code>Update File
Upload E2E Tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/e2e/authenticated/file-upload.test.ts

<li>Updated file upload E2E tests.<br> <li> Modified selectors and
actions for new UI components.<br> <li> Improved test reliability and
readability.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-ad496daecb0035a49fe21b4b54182bcca8188a20475bcc79a21bd433914a9200">+21/-13</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>change-email.test.ts</strong><dd><code>Update Change
Email E2E Tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

examples/react-apollo/e2e/authenticated/change-email.test.ts

<li>Updated change email E2E tests.<br> <li> Modified selectors and
actions for new UI components.<br> <li> Improved test reliability and
readability.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-5bf556a7e19bcc9932603bd52dd41929f1cabd65924ea88ad4123efcd9daad13">+26/-9</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>apollo.test.ts</strong><dd><code>Update Apollo E2E
Tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/e2e/authenticated/apollo.test.ts

<li>Updated Apollo E2E tests.<br> <li> Modified selectors and actions
for new UI components.<br> <li> Improved test reliability and
readability.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-56686466b936643bcf1a1b180a367f11e52a123df8f1298f31a9eb077ddd4ffd">+13/-9</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>change-password.test.ts</strong><dd><code>Update Change
Password E2E Tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

examples/react-apollo/e2e/authenticated/change-password.test.ts

<li>Updated change password E2E tests.<br> <li> Modified selectors and
actions for new UI components.<br> <li> Improved test reliability and
readability.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-ab8eb0581f78f98285a7d016170b4f076ecda83ad84a40a4a167ed1601b38874">+19/-6</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>email-password.test.ts</strong><dd><code>Update
Email/Password Sign-In E2E Tests</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/e2e/sign-in/email-password.test.ts

<li>Updated email/password sign-in E2E tests.<br> <li> Modified
selectors and actions for new UI components.<br> <li> Improved test
reliability and readability.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2834/files#diff-ea8f3b725904df5b4d7881bc3e2d66db6e50b2d78c836c04041b387e84e22262">+4/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></details></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-09-03 09:40:58 +01:00
Hassan Ben Jobrane
8b12426157 fix(hasura-auth-js): update signout to use accessToken when clearing all sessions (#2857)
### **User description**
fixes https://github.com/nhost/nhost/issues/2836


___

### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Enhanced the signout process to utilize the access token when clearing
all sessions.
- Updated the `signingOut` state to manage both access and refresh
tokens.
- Introduced a new function `destroyAccessToken` to clear the access
token.
- Adjusted type definitions to reflect changes in the signout process.
- Added a changeset document to outline the patch changes.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>machine.ts</strong><dd><code>Enhance signout process to
handle access token</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/hasura-auth-js/src/machines/authentication/machine.ts

<li>Updated <code>signingOut</code> state to handle access token.<br>
<li> Modified <code>clearContextExceptTokens</code> to retain access
token.<br> <li> Added <code>destroyAccessToken</code> function to clear
access token.<br> <li> Updated <code>signout</code> function to
optionally use access token.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2857/files#diff-a8fdfee087ad5a72ea0a64667e2a0c7f25baa84eaaf73ebfee3f5a5a1b7584d1">+24/-9</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>machine.typegen.ts</strong><dd><code>Update type
definitions for signout enhancements</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/hasura-auth-js/src/machines/authentication/machine.typegen.ts

<li>Updated type definitions for
<code>clearContextExceptTokens</code>.<br> <li> Added type definitions
for <code>destroyAccessToken</code>.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2857/files#diff-b0050ab06a8f00d3ae5decd65565adb1bdae3b4b6d19d4f67b9013ffb14e18ee">+11/-1</a>&nbsp;
&nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>popular-rabbits-bake.md</strong><dd><code>Add changeset
for signout fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/popular-rabbits-bake.md

- Added changeset for signout fix using access token.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2857/files#diff-cfd522f2d71d4a7da79f890c87603a0d8e85f066701af747b08712f1fd8890cd">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-09-02 14:28:13 +01:00
David Barroso
4cf6677284 feat (docs): update list of postgres extensions (#2852)
### **PR Type**
Documentation, Enhancement, Other


___

### **Description**
- Updated the documentation to include new PostgreSQL extensions:
`hypopg`, `http`, `pg_hashids`, and `pg_squeeze`.
- Provided detailed installation and uninstallation instructions for
each new extension.
- Updated the `timescaledb` extension information and resources.
- Addressed vulnerabilities by updating the `svelte` dependency in the
SvelteKit quickstart example.
- Added `webpack` as a new dependency in the main `package.json`.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>hungry-news-film.md</strong><dd><code>Add changeset
entry for PostgreSQL extensions update</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/hungry-news-film.md

<li>Added changeset entry for updating the list of PostgreSQL
extensions.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2852/files#diff-f070b809ea80a0cd0c3718b71b17f2a46eb6d96fbef75ef5cc19de9d870ccefb">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>extensions.mdx</strong><dd><code>Update and expand
PostgreSQL extensions documentation</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

docs/guides/database/extensions.mdx

<li>Added new PostgreSQL extensions: <code>hypopg</code>,
<code>http</code>, <code>pg_hashids</code>, <code>pg_squeeze</code>.<br>
<li> Updated information for <code>timescaledb</code>.<br> <li> Provided
installation and uninstallation instructions for each <br>extension.<br>
<li> Included resource links for each extension.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2852/files#diff-7a41fa45d84db83a8c01a76ddb42ad614022ad94a4c3a6aa321f5b9a5300da8c">+73/-23</a>&nbsp;
</td>

</tr>                    
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update Svelte dependency
version in package.json</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/quickstarts/sveltekit/package.json

- Updated `svelte` dependency version to `^4.2.19`.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2852/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Add webpack dependency
to package.json</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

package.json

- Added `webpack` dependency with version `^5.94.0`.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2852/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+2/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions

---------

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-09-02 08:37:56 +02:00
github-actions[bot]
fdaaf19057 chore: update versions (#2844)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.27.0

### Minor Changes

-   a7cd02c: fix: resolve rate limit query

## @nhost/docs@2.16.0

### Minor Changes

-   ba55c1b: feat: run: added a guide on using a private registry
-   3d70c63: feat: added rate-limiter guide for auth service

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-27 14:51:52 +01:00
David BM
a7cd02c965 fix (dashboard): resolve rate limit query (#2845)
### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Removed the 'Auth' switch from the `AuthLimitingForm` component to
simplify the settings interface.
- Updated the rate limit query in `useGetRateLimits` hook to resolve by
default, fixing a potential issue.
- Added a changeset to document the fix for the rate limit query.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>AuthLimitingForm.tsx</strong><dd><code>Remove 'Auth'
switch from AuthLimitingForm component</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>


dashboard/src/features/projects/rate-limiting/settings/components/AuthLimitingForm/AuthLimitingForm.tsx

<li>Removed the 'Auth' switch from the settings container.<br> <li>
Simplified the form component by removing unused props.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2845/files#diff-cd300f74c3d921fde5b705b25f63e22a3e66dfb9182ca818102cb1a5f508eb5e">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Bug fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>useGetRateLimits.ts</strong><dd><code>Update rate limit
query to resolve by default</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/projects/rate-limiting/settings/hooks/useGetRateLimits/useGetRateLimits.ts

- Changed the 'resolve' variable to true in the rate limit query.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2845/files#diff-82e380da100404643bd31504d42eb4d27a406dd9e1cccffc17b9dbcb0df5e8fa">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>smooth-bears-confess.md</strong><dd><code>Add changeset
for rate limit query fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/smooth-bears-confess.md

- Added a changeset for the rate limit query fix.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2845/files#diff-7def3298a6278543c9953ddf387b04c552ddefc4cd5ad9217366c59926b3cf63">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions

---------

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-08-27 14:45:08 +01:00
David Barroso
3d70c63d1b feat (docs): added docs about rate-limits (#2812) 2024-08-27 15:17:03 +02:00
David Barroso
ba55c1b779 feat (docs): run: added a guide on using a private registry (#2843) 2024-08-27 12:36:09 +02:00
255 changed files with 69101 additions and 17270 deletions

View File

@@ -1 +1,3 @@
link-workspace-packages = false
link-workspace-packages = false
auto-install-peers = false
resolution-mode=highest

View File

@@ -1,5 +1,24 @@
# @nhost/dashboard
## 1.28.0
### Minor Changes
- 526183a: feat: allow filtering users in "make request as" in graphql section
- be3b85b: feat: add conceal errors toggle on auth settings page
### Patch Changes
- 35a2f12: fix: prevent run service details from opening when attempting to delete
- @nhost/react-apollo@12.0.6
- @nhost/nextjs@2.1.20
## 1.27.0
### Minor Changes
- a7cd02c: fix: resolve rate limit query
## 1.26.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
@@ -12,9 +12,9 @@ test('should be able to ban and unban a user', async ({ page }) => {
await openProject({
page,
projectName: TEST_PROJECT_NAME,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
@@ -22,7 +22,9 @@ test('should be able to ban and unban a user', async ({ page }) => {
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
);
const email = generateTestEmail();
const password = faker.internet.password();

View File

@@ -1,6 +1,6 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
@@ -19,9 +19,9 @@ test.beforeEach(async () => {
await openProject({
page,
projectName: TEST_PROJECT_NAME,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
@@ -29,7 +29,9 @@ test.beforeEach(async () => {
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
);
});
test.afterAll(async () => {

View File

@@ -1,6 +1,6 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
@@ -19,9 +19,9 @@ test.beforeEach(async () => {
await openProject({
page,
projectName: TEST_PROJECT_NAME,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
@@ -29,7 +29,9 @@ test.beforeEach(async () => {
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
);
});
test.afterAll(async () => {

View File

@@ -1,6 +1,6 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
@@ -19,9 +19,9 @@ test.beforeEach(async () => {
await openProject({
page,
projectName: TEST_PROJECT_NAME,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
@@ -29,7 +29,9 @@ test.beforeEach(async () => {
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
);
});
test.afterAll(async () => {

View File

@@ -1,6 +1,6 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { openProject, prepareTable } from '@/e2e/utils';
@@ -20,9 +20,9 @@ test.beforeEach(async () => {
await openProject({
page,
projectName: TEST_PROJECT_NAME,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
@@ -55,7 +55,7 @@ test('should create a simple table', async () => {
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
);
await expect(
@@ -84,7 +84,7 @@ test('should create a table with unique constraints', async () => {
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
);
await expect(
@@ -113,7 +113,7 @@ test('should create a table with nullable columns', async () => {
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
);
await expect(
@@ -146,7 +146,7 @@ test('should create a table with an identity column', async () => {
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
);
await expect(
@@ -174,7 +174,7 @@ test('should create table with foreign key constraint', async () => {
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
);
await page.getByRole('button', { name: /new table/i }).click();
@@ -219,7 +219,7 @@ test('should create table with foreign key constraint', async () => {
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
);
await expect(
@@ -247,7 +247,7 @@ test('should not be able to create a table with a name that already exists', asy
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
);
await page.getByRole('button', { name: /new table/i }).click();

View File

@@ -1,6 +1,6 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { deleteTable, openProject, prepareTable } from '@/e2e/utils';
@@ -20,9 +20,9 @@ test.beforeEach(async () => {
await openProject({
page,
projectName: TEST_PROJECT_NAME,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
@@ -53,7 +53,7 @@ test('should delete a table', async () => {
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
);
await deleteTable({
@@ -63,7 +63,7 @@ test('should delete a table', async () => {
// navigate to next URL
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/**`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/**`,
);
await expect(
@@ -91,7 +91,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
);
await page.getByRole('button', { name: /new table/i }).click();
@@ -138,7 +138,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
await page.getByRole('button', { name: /create/i }).click();
await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
);
await expect(

View File

@@ -61,12 +61,8 @@ test('should create and delete a run service', async () => {
page.getByRole('heading', { name: /confirm resources/i }),
).toBeVisible();
await page.waitForTimeout(1000);
await page.getByRole('button', { name: /confirm/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(
page.getByRole('heading', { name: /service details/i }),
).toBeVisible();
@@ -74,16 +70,14 @@ test('should create and delete a run service', async () => {
await page.getByRole('button', { name: /ok/i }).click();
await expect(page.getByRole('heading', { name: /test/i })).toBeVisible();
await page.getByLabel(/more options/i).click();
await page.getByRole('menuitem', { name: /delete service/i }).click();
await page.getByLabel(/confirm delete project #/i).check();
await page
.getByText(/delete service/i)
.nth(2)
.click();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: /delete service/i }).click();
await expect(
page

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "1.26.0",
"version": "1.28.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -10,10 +10,10 @@ export default defineConfig({
expect: {
timeout: 5000,
},
fullyParallel: true,
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
workers: 1,
reporter: 'html',
globalTeardown: require.resolve('./global-teardown'),
use: {

View File

@@ -0,0 +1,136 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/utils/__generated__/graphql';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const validationSchema = Yup.object({
enabled: Yup.boolean(),
});
export type ToggleConcealErrorsFormValues = Yup.InferType<
typeof validationSchema
>;
export default function ConcealErrorsSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<ToggleConcealErrorsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled: data?.config?.auth?.misc?.concealErrors,
},
});
useEffect(() => {
if (!loading) {
form.reset({
enabled: data?.config?.auth?.misc?.concealErrors,
});
}
}, [loading, data, form]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading conceal error settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState } = form;
const handleToggleConcealErrors = async (
values: ToggleConcealErrorsFormValues,
) => {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
misc: {
concealErrors: values.enabled,
},
},
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating conceal error settings...',
successMessage: 'Conceal error settings updated successfully.',
errorMessage:
'Failed to update conceal error settings. Please try again.',
},
);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleToggleConcealErrors}>
<SettingsContainer
title="Conceal errors"
description="If set, conceals sensitive error messages to prevent leaking information about user accounts."
switchId="enabled"
showSwitch
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="hidden"
/>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1 @@
export { default as ConcealErrorsSettings } from './ConcealErrorsSettings';

View File

@@ -50,6 +50,9 @@ query GetAuthenticationSettings($appId: uuid!) {
default
}
}
misc {
concealErrors
}
version
}
}

View File

@@ -1,11 +1,12 @@
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select';
import { Autocomplete } from '@/components/ui/v2/Autocomplete';
import { DEFAULT_ROLES } from '@/features/graphql/common/utils/constants';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { RemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
import { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
import {
useRemoteAppGetUsersCustomLazyQuery,
type RemoteAppGetUsersCustomQuery,
} from '@/utils/__generated__/graphql';
import { debounce } from '@mui/material/utils';
import { useCallback, useEffect, useMemo, useState } from 'react';
export interface UserSelectProps {
/**
@@ -13,7 +14,7 @@ export interface UserSelectProps {
*/
onUserChange: (userId: string, availableRoles?: string[]) => void;
/**
* Class name to be applied to the `<Select />` element.
* Class name to be applied to the `<Autocomplete />` element.
*/
className?: string;
}
@@ -22,35 +23,87 @@ export default function UserSelect({
onUserChange,
...props
}: UserSelectProps) {
const { currentProject } = useCurrentWorkspaceAndProject();
const [inputValue, setInputValue] = useState('');
const [users, setUsers] = useState([]);
const [active, setActive] = useState(true);
const userApplicationClient = useRemoteApplicationGQLClient();
const { data, loading, error } = useRemoteAppGetUsersCustomQuery({
const [fetchAppUsers, { loading }] = useRemoteAppGetUsersCustomLazyQuery({
client: userApplicationClient,
variables: { where: {}, limit: 250, offset: 0 },
skip: !currentProject,
variables: {
where: {},
limit: 250,
offset: 0,
},
});
if (loading) {
return (
<div className={props.className}>
<ActivityIndicator label="Loading users..." delay={500} />
</div>
);
}
const fetchUsers = useCallback(
async (
request: { input: string },
callback: (results?: RemoteAppGetUsersCustomQuery['users']) => void,
) => {
const ilike = `%${request.input === 'Admin' ? '' : request.input}%`;
const { data } = await fetchAppUsers({
client: userApplicationClient,
variables: {
where: {
displayName: { _ilike: ilike },
},
limit: 250,
offset: 0,
},
});
if (error) {
throw error;
}
callback(data?.users);
},
[fetchAppUsers, userApplicationClient],
);
const fetchOptions = useMemo(() => debounce(fetchUsers, 1000), [fetchUsers]);
useEffect(() => {
fetchOptions({ input: inputValue }, (results) => {
if (active || inputValue === '') {
setUsers(results);
}
});
}, [inputValue, fetchOptions, active]);
const autocompleteOptions = [
{
value: 'admin',
label: 'Admin',
group: 'Admin',
},
...users.map((user) => ({
value: user.id,
label: user.displayName,
group: 'Users',
})),
];
return (
<Select
<Autocomplete
{...props}
id="user-select"
label="Make Request As"
hideEmptyHelperText
defaultValue="admin"
slotProps={{ root: { className: 'truncate' } }}
onChange={(_event, userId) => {
label="Make request as"
options={autocompleteOptions}
defaultValue={{
value: 'admin',
label: 'Admin',
group: 'Admin',
}}
autoComplete
fullWidth
autoSelect
groupBy={(option) => option.group}
autoHighlight
includeInputInList
loading={loading}
onChange={(_event, _value, reason, details) => {
setActive(false);
const userId = details.option.value;
if (typeof userId !== 'string') {
return;
}
@@ -61,22 +114,23 @@ export default function UserSelect({
return;
}
const user: RemoteAppGetUsersCustomQuery['users'][0] = data?.users.find(
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
({ id }) => id === userId,
);
const roles = user?.roles.map(({ role }) => role);
const roles = user?.roles?.map(({ role }) => role);
onUserChange(user.id, roles);
onUserChange(userId, roles ?? DEFAULT_ROLES);
fetchUsers({ input: '' }, (results) => {
if (results) {
setUsers(results);
}
});
}}
>
<Option value="admin">Admin</Option>
{data?.users.map(({ id, displayName, email, phoneNumber }) => (
<Option key={id} value={id}>
{displayName || email || phoneNumber || id}
</Option>
))}
</Select>
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
/>
);
}

View File

@@ -0,0 +1 @@
export { default as useGetAppUsers } from './useGetAppUsers';

View File

@@ -0,0 +1,54 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type {
RemoteAppGetUsersCustomQuery,
RemoteAppGetUsersCustomQueryVariables,
} from '@/utils/__generated__/graphql';
import { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
import type { QueryHookOptions } from '@apollo/client';
export type UseFilesOptions = {
searchString?: string;
limit?: number;
offset?: number;
/**
* Custom options for the query.
*/
options?: QueryHookOptions<
RemoteAppGetUsersCustomQuery,
RemoteAppGetUsersCustomQueryVariables
>;
};
export default function useGetAppUsers({
searchString,
limit = 250,
offset = 0,
options = {},
}: UseFilesOptions) {
const { currentProject } = useCurrentWorkspaceAndProject();
const userApplicationClient = useRemoteApplicationGQLClient();
const { data, error, loading } = useRemoteAppGetUsersCustomQuery({
...options,
client: userApplicationClient,
variables: {
...options.variables,
where: searchString
? {
displayName: { _ilike: `%${searchString}%` },
}
: {},
limit,
offset,
},
skip: !currentProject,
});
const users = data?.users || [];
return {
users,
loading,
error,
};
}

View File

@@ -248,8 +248,6 @@ export default function AuthLimitingForm() {
>
<SettingsContainer
title="Auth"
switchId="enabled"
showSwitch
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,

View File

@@ -13,7 +13,7 @@ export default function useGetRateLimits() {
const { data, loading } = useGetRateLimitConfigQuery({
variables: {
appId: currentProject?.id,
resolve: false,
resolve: true,
},
skip: !currentProject,
...(!isPlatform ? { client: localMimirClient } : {}),

View File

@@ -50,7 +50,7 @@ export default function ServicesList({
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<CubeIcon className="w-5 h-5" />
<Text>Edit {service.config?.name ?? 'unset'}</Text>
</Box>
),
@@ -108,13 +108,13 @@ export default function ServicesList({
onClick={() => viewService(service)}
>
<Box
className="flex w-full flex-row justify-between"
className="flex flex-row justify-between w-full"
sx={{
backgroundColor: 'transparent',
}}
>
<div className="flex flex-1 flex-row items-center space-x-4">
<CubeIcon className="h-5 w-5" />
<div className="flex flex-row items-center flex-1 space-x-4">
<CubeIcon className="w-5 h-5" />
<div className="flex flex-col">
<Text variant="h4" className="font-semibold">
{service.config?.name ?? 'unset'}
@@ -130,7 +130,7 @@ export default function ServicesList({
</div>
</div>
<div className="hidden flex-row items-center space-x-2 md:flex">
<div className="flex-row items-center hidden space-x-2 md:flex">
<Text variant="subtitle1" className="font-mono text-xs">
{service.id ?? service.serviceID}
</Text>
@@ -143,7 +143,7 @@ export default function ServicesList({
}}
aria-label="Service Id"
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</div>
</Box>
@@ -173,17 +173,20 @@ export default function ServicesList({
onClick={() => viewService(service)}
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<UserIcon className="h-4 w-4" />
<UserIcon className="w-4 h-4" />
<Text className="font-medium">View Service</Text>
</Dropdown.Item>
<Divider component="li" />
<Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() => deleteService(service)}
onClick={(e) => {
e.stopPropagation();
deleteService(service);
}}
disabled={!isPlatform}
>
<TrashIcon className="h-4 w-4" />
<TrashIcon className="w-4 h-4" />
<Text className="font-medium" color="error">
Delete Service
</Text>

View File

@@ -6,6 +6,7 @@ import { AllowedRedirectURLsSettings } from '@/features/authentication/settings/
import { AuthServiceVersionSettings } from '@/features/authentication/settings/components/AuthServiceVersionSettings';
import { BlockedEmailSettings } from '@/features/authentication/settings/components/BlockedEmailSettings';
import { ClientURLSettings } from '@/features/authentication/settings/components/ClientURLSettings';
import { ConcealErrorsSettings } from '@/features/authentication/settings/components/ConcealErrorsSettings';
import { DisableNewUsersSettings } from '@/features/authentication/settings/components/DisableNewUsersSettings';
import { GravatarSettings } from '@/features/authentication/settings/components/GravatarSettings';
import { MFASettings } from '@/features/authentication/settings/components/MFASettings';
@@ -43,7 +44,7 @@ export default function SettingsAuthenticationPage() {
return (
<Container
className="grid max-w-5xl grid-flow-row bg-transparent gap-y-6"
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
rootClassName="bg-transparent"
>
<AuthServiceVersionSettings />
@@ -55,6 +56,7 @@ export default function SettingsAuthenticationPage() {
<SessionSettings />
<GravatarSettings />
<DisableNewUsersSettings />
<ConcealErrorsSettings />
</Container>
);
}

View File

@@ -199,6 +199,7 @@ export type ConfigAuth = {
__typename?: 'ConfigAuth';
elevatedPrivileges?: Maybe<ConfigAuthElevatedPrivileges>;
method?: Maybe<ConfigAuthMethod>;
misc?: Maybe<ConfigAuthMisc>;
rateLimit?: Maybe<ConfigAuthRateLimit>;
redirections?: Maybe<ConfigAuthRedirections>;
/** Resources for the service */
@@ -224,6 +225,7 @@ export type ConfigAuthComparisonExp = {
_or?: InputMaybe<Array<ConfigAuthComparisonExp>>;
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesComparisonExp>;
method?: InputMaybe<ConfigAuthMethodComparisonExp>;
misc?: InputMaybe<ConfigAuthMiscComparisonExp>;
rateLimit?: InputMaybe<ConfigAuthRateLimitComparisonExp>;
redirections?: InputMaybe<ConfigAuthRedirectionsComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>;
@@ -257,6 +259,7 @@ export type ConfigAuthElevatedPrivilegesUpdateInput = {
export type ConfigAuthInsertInput = {
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesInsertInput>;
method?: InputMaybe<ConfigAuthMethodInsertInput>;
misc?: InputMaybe<ConfigAuthMiscInsertInput>;
rateLimit?: InputMaybe<ConfigAuthRateLimitInsertInput>;
redirections?: InputMaybe<ConfigAuthRedirectionsInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>;
@@ -687,6 +690,26 @@ export type ConfigAuthMethodWebauthnUpdateInput = {
relyingParty?: InputMaybe<ConfigAuthMethodWebauthnRelyingPartyUpdateInput>;
};
export type ConfigAuthMisc = {
__typename?: 'ConfigAuthMisc';
concealErrors?: Maybe<Scalars['Boolean']>;
};
export type ConfigAuthMiscComparisonExp = {
_and?: InputMaybe<Array<ConfigAuthMiscComparisonExp>>;
_not?: InputMaybe<ConfigAuthMiscComparisonExp>;
_or?: InputMaybe<Array<ConfigAuthMiscComparisonExp>>;
concealErrors?: InputMaybe<ConfigBooleanComparisonExp>;
};
export type ConfigAuthMiscInsertInput = {
concealErrors?: InputMaybe<Scalars['Boolean']>;
};
export type ConfigAuthMiscUpdateInput = {
concealErrors?: InputMaybe<Scalars['Boolean']>;
};
export type ConfigAuthRateLimit = {
__typename?: 'ConfigAuthRateLimit';
bruteForce?: Maybe<ConfigRateLimit>;
@@ -873,6 +896,7 @@ export type ConfigAuthTotpUpdateInput = {
export type ConfigAuthUpdateInput = {
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesUpdateInput>;
method?: InputMaybe<ConfigAuthMethodUpdateInput>;
misc?: InputMaybe<ConfigAuthMiscUpdateInput>;
rateLimit?: InputMaybe<ConfigAuthRateLimitUpdateInput>;
redirections?: InputMaybe<ConfigAuthRedirectionsUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>;
@@ -2198,6 +2222,8 @@ export type ConfigRunServiceConfigWithId = {
export type ConfigRunServiceImage = {
__typename?: 'ConfigRunServiceImage';
image: Scalars['String'];
/** content of "auths", i.e., { "auths": $THIS } */
pullCredentials?: Maybe<Scalars['String']>;
};
export type ConfigRunServiceImageComparisonExp = {
@@ -2205,14 +2231,17 @@ export type ConfigRunServiceImageComparisonExp = {
_not?: InputMaybe<ConfigRunServiceImageComparisonExp>;
_or?: InputMaybe<Array<ConfigRunServiceImageComparisonExp>>;
image?: InputMaybe<ConfigStringComparisonExp>;
pullCredentials?: InputMaybe<ConfigStringComparisonExp>;
};
export type ConfigRunServiceImageInsertInput = {
image: Scalars['String'];
pullCredentials?: InputMaybe<Scalars['String']>;
};
export type ConfigRunServiceImageUpdateInput = {
image?: InputMaybe<Scalars['String']>;
pullCredentials?: InputMaybe<Scalars['String']>;
};
export type ConfigRunServiceNameComparisonExp = {
@@ -22885,7 +22914,7 @@ export type GetAuthenticationSettingsQueryVariables = Exact<{
}>;
export type GetAuthenticationSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', auth?: { __typename: 'ConfigAuth', version?: string | null, id: 'ConfigAuth', redirections?: { __typename?: 'ConfigAuthRedirections', clientUrl?: any | null, allowedUrls?: Array<string> | null } | null, totp?: { __typename?: 'ConfigAuthTotp', enabled?: boolean | null, issuer?: string | null } | null, signUp?: { __typename?: 'ConfigAuthSignUp', enabled?: boolean | null } | null, session?: { __typename?: 'ConfigAuthSession', accessToken?: { __typename?: 'ConfigAuthSessionAccessToken', expiresIn?: any | null } | null, refreshToken?: { __typename?: 'ConfigAuthSessionRefreshToken', expiresIn?: any | null } | null } | null, resources?: { __typename?: 'ConfigResources', networking?: { __typename?: 'ConfigNetworking', ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null } | null } | null, user?: { __typename?: 'ConfigAuthUser', email?: { __typename?: 'ConfigAuthUserEmail', allowed?: Array<any> | null, blocked?: Array<any> | null } | null, emailDomains?: { __typename?: 'ConfigAuthUserEmailDomains', allowed?: Array<string> | null, blocked?: Array<string> | null } | null, gravatar?: { __typename?: 'ConfigAuthUserGravatar', enabled?: boolean | null, default?: string | null, rating?: string | null } | null, locale?: { __typename?: 'ConfigAuthUserLocale', allowed?: Array<any> | null, default?: any | null } | null } | null } | null } | null };
export type GetAuthenticationSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', auth?: { __typename: 'ConfigAuth', version?: string | null, id: 'ConfigAuth', redirections?: { __typename?: 'ConfigAuthRedirections', clientUrl?: any | null, allowedUrls?: Array<string> | null } | null, totp?: { __typename?: 'ConfigAuthTotp', enabled?: boolean | null, issuer?: string | null } | null, signUp?: { __typename?: 'ConfigAuthSignUp', enabled?: boolean | null } | null, session?: { __typename?: 'ConfigAuthSession', accessToken?: { __typename?: 'ConfigAuthSessionAccessToken', expiresIn?: any | null } | null, refreshToken?: { __typename?: 'ConfigAuthSessionRefreshToken', expiresIn?: any | null } | null } | null, resources?: { __typename?: 'ConfigResources', networking?: { __typename?: 'ConfigNetworking', ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null } | null } | null, user?: { __typename?: 'ConfigAuthUser', email?: { __typename?: 'ConfigAuthUserEmail', allowed?: Array<any> | null, blocked?: Array<any> | null } | null, emailDomains?: { __typename?: 'ConfigAuthUserEmailDomains', allowed?: Array<string> | null, blocked?: Array<string> | null } | null, gravatar?: { __typename?: 'ConfigAuthUserGravatar', enabled?: boolean | null, default?: string | null, rating?: string | null } | null, locale?: { __typename?: 'ConfigAuthUserLocale', allowed?: Array<any> | null, default?: any | null } | null } | null, misc?: { __typename?: 'ConfigAuthMisc', concealErrors?: boolean | null } | null } | null } | null };
export type GetPostgresSettingsQueryVariables = Exact<{
appId: Scalars['uuid'];
@@ -24329,6 +24358,9 @@ export const GetAuthenticationSettingsDocument = gql`
default
}
}
misc {
concealErrors
}
version
}
}

View File

@@ -1,5 +1,19 @@
# @nhost/docs
## 2.17.0
### Minor Changes
- cffdec5: feat: update react quickstart guide to use the nhost react apollo template
- 4cf6677: feat: update list of postgres extensions
## 2.16.0
### Minor Changes
- ba55c1b: feat: run: added a guide on using a private registry
- 3d70c63: feat: added rate-limiter guide for auth service
## 2.15.0
### Minor Changes

View File

@@ -4,6 +4,57 @@ description: List of available extensions with Nhost Postgres.
icon: grid
---
## hypopg
HypoPG is a PostgreSQL extension adding support for hypothetical indexes.
An hypothetical -- or virtual -- index is an index that doesn't really exists, and thus doesn't cost CPU, disk or any resource to create. They're useful to know if specific indexes can increase performance for problematic queries, since you can know if PostgreSQL will use these indexes or not without having to spend resources to create them.
### Managing
To install the extension you can create a migration with the following contents:
```sql SQL
SET ROLE postgres;
CREATE EXTENSION hypopg;
```
To uninstall it, you can use the following migration:
```sql SQL
SET ROLE postgres;
DROP EXTENSION hypopg;
```
### Resources
- [GitHub](https://github.com/HypoPG/hypopg)
- [Documentation](https://hypopg.readthedocs.io/)
## http
HTTP client for PostgreSQL, retrieve a web page from inside the database.
### Managing
To install the extension you can create a migration with the following contents:
```sql SQL
SET ROLE postgres;
CREATE EXTENSION http;
```
To uninstall it, you can use the following migration:
```sql SQL
SET ROLE postgres;
DROP EXTENSION http;
```
### Resources
- [GitHub](https://github.com/pramsey/pgsql-http)
## postgis
PostGIS extends the capabilities of the PostgreSQL relational database by adding support storing, indexing and querying geographic data.
@@ -84,11 +135,9 @@ DROP EXTENSION pg_cron;
- [GitHub](https://github.com/citusdata/pg_cron)
## hypopg
## pg_hashids
HypoPG is a PostgreSQL extension adding support for hypothetical indexes.
An hypothetical -- or virtual -- index is an index that doesn't really exists, and thus doesn't cost CPU, disk or any resource to create. They're useful to know if specific indexes can increase performance for problematic queries, since you can know if PostgreSQL will use these indexes or not without having to spend resources to create them.
Hashids is a small open-source library that generates short, unique, non-sequential ids from numbers. It converts numbers like 347 into strings like “yr8”. You can also decode those ids back. This is useful in bundling several parameters into one or simply using them as short UIDs.
### Managing
@@ -96,20 +145,68 @@ To install the extension you can create a migration with the following contents:
```sql SQL
SET ROLE postgres;
CREATE EXTENSION hypopg;
CREATE EXTENSION pg_hashids;
```
To uninstall it, you can use the following migration:
```sql SQL
SET ROLE postgres;
DROP EXTENSION hypopg;
DROP EXTENSION pg_hashids;
```
### Resources
- [GitHub](https://github.com/HypoPG/hypopg)
- [Documentation](https://hypopg.readthedocs.io/)
- [GitHub](https://github.com/iCyberon/pg_hashids)
## pg_squeeze
PostgreSQL extension that removes unused space from a table and optionally sorts tuples according to particular index (as if CLUSTER command was executed concurrently with regular reads / writes). In fact we try to replace pg_repack extension.
### Managing
To install the extension you can create a migration with the following contents:
```sql SQL
SET ROLE postgres;
CREATE EXTENSION pg_squeeze;
```
In addition, you may need to configure the WAL level and replication slots. Check the official documentation for details.
To uninstall it, you can use the following migration:
```sql SQL
SET ROLE postgres;
DROP EXTENSION pg_squeeze;
```
### Resources
- [GitHub](https://github.com/cybertec-postgresql/pg_squeeze)
## pg_stat_statements
The pg_stat_statements module provides a means for tracking planning and execution statistics of all SQL statements executed by a server.
### Managing
To install the extension you can create a migration with the following contents:
```sql SQL
SET ROLE postgres;
CREATE EXTENSION pg_stat_statements;
```
To uninstall it, you can use the following migration:
```sql SQL
SET ROLE postgres;
DROP EXTENSION pg_stat_statements;
```
### Resources
- [Documentation](https://www.postgresql.org/docs/14/pgstatstatements.html)
## timescaledb
@@ -140,50 +237,3 @@ DROP EXTENSION timescaledb;
- [Documentation](https://docs.timescale.com/)
- [Website](https://www.timescale.com/)
## pg_stat_statements
The pg_stat_statements module provides a means for tracking planning and execution statistics of all SQL statements executed by a server.
### Managing
To install the extension you can create a migration with the following contents:
```sql SQL
SET ROLE postgres;
CREATE EXTENSION pg_stat_statements;
```
To uninstall it, you can use the following migration:
```sql SQL
SET ROLE postgres;
DROP EXTENSION pg_stat_statements;
```
### Resources
- [Documentation](https://www.postgresql.org/docs/14/pgstatstatements.html)
## http
HTTP client for PostgreSQL, retrieve a web page from inside the database.
### Managing
To install the extension you can create a migration with the following contents:
```sql SQL
SET ROLE postgres;
CREATE EXTENSION http;
```
To uninstall it, you can use the following migration:
```sql SQL
SET ROLE postgres;
DROP EXTENSION http;
```
### Resources
- [GitHub](https://github.com/pramsey/pgsql-http)

View File

@@ -1,149 +1,301 @@
---
title: Setup Nhost with React
title: Get up and running with Nhost and React
sidebarTitle: React
description: Get up and running with Nhost and React
icon: react
---
<Steps>
<Step title="Create Project">
If you haven't, please create a project through the [Nhost Dashboard](https://app.nhost.io/new).
<Step title="Create Nhost Project">
Create your project through the [Nhost Dashboard](https://app.nhost.io/new).
</Step>
<Step title="Setup Database">
Navigate to the **SQL Editor** of the database and run the following SQL to create a new table `movies` with some great movies.
```sql SQL Editor
CREATE TABLE movies (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director VARCHAR(255),
release_year INTEGER,
genre VARCHAR(100),
rating FLOAT
);
INSERT INTO movies (title, director, release_year, genre, rating) VALUES
('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8),
('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2),
('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8),
('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7);
```
Navigate to the **SQL Editor** of the database and run the following SQL to create a new table `todos`.
<Warning>Make sure the option `Track this` is enabled</Warning>
![SQL Editor](/images/guides/quickstarts/react/sql-editor.png)
</Step>
<Step title="permissions">
Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action.
![Permission Rules](/images/guides/quickstarts/react/permissions.png)
</Step>
<Step title="Setup a React Application">
Create a React application using Vite.
```bash Terminal
npm create vite@latest nhost-react-quickstart -- --template react
```sql SQL Editor
CREATE TABLE todos (
id uuid NOT NULL DEFAULT gen_random_uuid(),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
user_id uuid NOT NULL,
contents text NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE cascade ON DELETE cascade
);
```
<Frame caption="Create Todos Table">
<img src="/images/guides/quickstarts/react-native/create-table-todos.png" />
</Frame>
</Step>
<Step title="Install the Nhost package for React">
Navidate to the React application and install `@nhost/react`.
<Step title="Configure the todos table permissions">
To set permissions for the new `todos` table, select the table, click on the `...` to open the actions dialog,
then click on **Edit Permissions**. Set the following permissions for the `user` role:
1. `Insert`
- Set `Row insert permissions` to `Without any checks`
- Select all columns except `user_id` on `Column insert permissions`
- Add a new `Column preset` and set `Column Name` to `user_id` and `Column Value` to `X-Hasura-User-Id`
- Save
<Frame caption="Insert Permissions">
<img src="/images/guides/quickstarts/react-native/todos-insert-permissions.png" />
</Frame>
2. `Select`
- Set `Row select permissions` to `With custom check` and fill in the following rule:
- Set `Where` to `todos.user_id`
- Set the operator to `_eq`
- Set the value to `X-Hasura-User-Id`
- Select all columns except `user_id` on `Column select permissions`
- Save
<Frame caption="Select Permissions">
<img src="/images/guides/quickstarts/react-native/todos-select-permissions.png" />
</Frame>
3. `Update`
- Set `Row update permissions` to `With custom check` and fill in the following rule:
- Set `Where` to `todos.user_id`
- Set the operator to `_eq`
- Set the value to `X-Hasura-User-Id`
- Select all columns except `user_id` on `Column select permissions`
- Save
<Frame caption="Update permissions">
<img src="/images/guides/quickstarts/react-native/todos-update-permissions.png" />
</Frame>
4. `Delete`
- Set `Row delete permissions` to `With custom check` and fill in the following rule:
- Set `Where` to `todos.user_id`
- Set the operator to `_eq`
- Set the value to `X-Hasura-User-Id`
- Save
<Frame caption="Delete permissions">
<img src="/images/guides/quickstarts/react-native/todos-delete-permissions.png" />
</Frame>
</Step>
<Step title="Configure permissions to enable user file uploads">
To enable file uploads by users, set the permissions as follows:
1. Edit the **files** table permissions
1. Navigate to the files table within the [Database tab](https://app.nhost.io/_/_/database/browser/default/storage/files)
2. Click on the three dots (...) next to the files table
3. Click on **Edit Permissions**
2. Modify the `Insert` permission for the `user` role:
1. Set `Row insert permissions` to `Without any checks`
2. Select all columns on `Column insert permissions`
4. Save
<Frame caption="Insert Permissions">
<img src="/images/guides/quickstarts/react-native/files-insert-permissions.png" />
</Frame>
3. `Select`
- Set `Row select permissions` to `With custom check` and fill in the following rule:
- Set `Where` to `files.uploaded_by_user_id`
- Set the operator to `_eq`
- Set the value to `X-Hasura-User-Id`
- Select all columns on `Column select permissions`
- Save
<Frame caption="Select permissions">
<img src="/images/guides/quickstarts/react-native/files-select-permissions.png" />
</Frame>
</Step>
<Step title="Bootstrap your React app">
Intialize a new React project using the template [`@nhost/react-apollo`](https://www.npmjs.com/package/@nhost/cra-template-react-apollo)
```bash Terminal
cd nhost-react-quickstart && npm install @nhost/react
npx create-react-app myapp --template @nhost/react-apollo
```
</Step>
<Step title="Configure the Nhost client and fetch the list of movies">
<Step title="Connect your React app to the Nhost project">
Copy your project's `<subdomain>` and `<region>` values available on the dashboard overview
Create a new file with the following code to creates the Nhost client.
```js ./src/lib/nhost.js
import { NhostClient } from "@nhost/react";
export const nhost = new NhostClient({
subdomain: "<subdomain>",
region: "<region>",
})
```tsx src/index.tsx
const nhost = new NhostClient({
subdomain: "<subdomain>", // replace the subdomain value e.g. "hjcuuqweqwezolpolrep"
region: "<region>", // replace the region value e.g. "eu-central-1"
});
```
<Note>Replace `<subdomain>` and `<region>` with the subdomain and region for the project</Note>
</Step>
Finally, update `./src/App.jsx` to fetch the list of movies.
<Step title="Create the Todos Page and Add It to the Sidebar Navigation">
<CodeGroup>
```tsx src/components/routes/app/todos.tsx
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { gql, useMutation } from '@apollo/client'
import { useAuthQuery } from '@nhost/react-apollo'
import { Check, Info, Plus, Trash } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
```js src/App.jsx
import { useEffect, useState } from "react";
import { NhostProvider } from "@nhost/react";
import { nhost } from './lib/nhost'
export default function Todos() {
const { data, refetch: refetchTodos } = useAuthQuery<{
todos: Array<{
id: string
contents: string
}>
}>(gql`
query {
todos(order_by: { createdAt: desc }) {
id
contents
}
}
`)
const getMovies = `
query {
movies {
title
genre
rating
const [contents, setContents] = useState('')
const [addTodo] = useMutation<{
insertTodo?: {
id: string
contents: string
}
}>(gql`
mutation ($contents: String!) {
insertTodo(object: { contents: $contents }) {
id
contents
}
}
`)
const [deleteTodo] = useMutation<{
deleteNote?: {
id: string
content: string
}
}>(gql`
mutation deleteTodo($todoId: uuid!) {
deleteTodo(id: $todoId) {
id
contents
}
}
`)
const handleAddTodo = () => {
if (contents) {
addTodo({
variables: { contents },
onCompleted: async () => {
setContents('')
await refetchTodos()
},
onError: (error) => {
toast.error(error.message)
}
})
}
}
const handleDeleteTodo = async (todoId: string) => {
await deleteTodo({
variables: { todoId },
onCompleted: async () => {
await refetchTodos()
},
onError: (error) => {
toast.error(error.message)
}
})
await refetchTodos()
}
return (
<div className="w-full">
<Card className="mb-4">
<CardHeader>
<CardTitle>Todos</CardTitle>
</CardHeader>
</Card>
<Card className="w-full pt-6">
<CardContent className="flex flex-col gap-4">
<div className="flex flex-row gap-4">
<Input
value={contents}
onChange={(e) => setContents(e.target.value)}
onKeyDown={(e) => e.code === 'Enter' && handleAddTodo()}
/>
<Button className="m-0" onClick={handleAddTodo}>
<Plus />
Add
</Button>
</div>
<div>
{data?.todos.length === 0 && (
<Alert className="w-full">
<Info className="w-4 h-4" />
<AlertTitle>Empty</AlertTitle>
<AlertDescription className="mt-2">Start by adding a todo</AlertDescription>
</Alert>
)}
{data?.todos.map((todo) => (
<div
key={todo.id}
className="flex flex-row items-center justify-between w-full p-4 border-b last:pb-0 last:border-b-0"
>
<div className="flex flex-row gap-2">
<Check className="w-5 h-5" />
<span>{todo.contents}</span>
</div>
<Button variant="ghost" onClick={() => handleDeleteTodo(todo.id)}>
<Trash className="w-5 h-5" />
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}
`;
```
function App() {
return (
<NhostProvider nhost={nhost}>
<Home />
</NhostProvider>
);
}
```tsx src/components/routes/app/layout.tsx
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5">
--
<Tooltip>
<TooltipTrigger asChild>
<NavLink
to="/todos"
className="flex items-center justify-center transition-colors rounded-lg h-9 w-9 text-muted-foreground hover:text-foreground md:h-8 md:w-8 aria-[current]:bg-accent aria-[current]:text-accent-foreground"
>
<SquareCheckBig className="w-5 h-5" />
<span className="sr-only">Todos</span>
</NavLink>
</TooltipTrigger>
<TooltipContent side="right">Todos</TooltipContent>
</Tooltip>
--
</nav>
```
</CodeGroup>
function Home() {
const [loading, setLoading] = useState(true);
const [movies, setMovies] = useState([]);
useEffect(() => {
async function fetchMovies() {
setLoading(true);
const { data, error } = await nhost.graphql.request(getMovies);
setMovies(data.movies);
setLoading(false);
}
fetchMovies();
}, []);
return (
<div>
{loading ? (
<p>Loading...</p>
) : (
<table>
<tbody>
{movies.map((movie, index) => (
<tr key={index}>
<td>{movie.title}</td>
<td>{movie.genre}</td>
<td>{movie.rating}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
export default App;
```
</Step>
<Step title="The end">
Run your project with `npm run dev -- --open --port 3000` and enter `http://localhost:3000` in your browser.
<Step title="Run the project">
Run your project with `npm start` and enter `http://localhost:3000` in your browser.
</Step>
</Steps>
```
```

View File

@@ -93,3 +93,56 @@ Wait a few seconds until the project is done updating the new service and visit
![visit url](/images/guides/run/registry_7.png)
## Using your own private registry
If you are publishing your images in your own private registry you can add pull credentials to your Run configuration so the image can be pulled successfully. To do so follow the next steps:
1. Figure out the credentials you need. This might depend on your registry. For instructions on various registries see the next section.
2. The credentials will be similar to:
```json
{
"auths": {
"https://myregistry.com/v1": {
"username": "myuser",
"password": "mypassword"
}
}
}
```
3. Create a secret under Settings -> Secrets with the contents of the auth section. For instance:
![pull secret](/images/guides/run/registry_8.png)
Pay attention that **only** the object inside "auths" is to be added.
4. Configure the `pullCredentials` in your run configuration.
```toml
[image]
image = 'myprivaterepo/myservice:1.0.1'
pullCredentials = '{{ secrets.CONTAINER_REGISTRY_CREDENTIALS }}'
```
Pulling your image should work now.
### Docker Hub Credentials
To create a credential that allows you to pull private images from Docker hub follow the next steps:
1. Login to https://hub.docker.com with a user that can pull the image you want.
2. Head to "Account Settings" -> "Personal access tokens"
3. Create a new token with "Read Only" access permissions
4. Copy the token you got
Your credentials will be:
```json
{
"https://index.docker.io/v1/": {
"username":"<yourusername>",
"password":"<the_token_you_just_got>"
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

View File

@@ -84,7 +84,8 @@
"platform/environment-variables",
"platform/secrets",
"platform/deployments",
"platform/custom-domains"
"platform/custom-domains",
"platform/rate-limits"
]
},
{

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "2.15.0",
"version": "2.17.0",
"private": true,
"scripts": {
"start": "mintlify dev"

View File

@@ -0,0 +1,112 @@
---
title: Rate Limits
sidebarTitle: Rate Limits
description: Protecting your service against abuse
icon: shield
---
Rate limits in an HTTP API are essential for protecting services against abuse and brute force attacks by restricting the number of requests a client can make within a specified time period. By enforcing rate limits, we can mitigate the risk of unauthorized access, denial of service attacks, and excessive consumption of resources.
Limits work by setting a maximum number of requests (burst amount) allowed for a key within a specified time frame (recovery time). For example, with a limit of 30 requests and a recovery time of 5 minutes, a user can make up to 30 requests before hitting the limit. Additionally, the user receives an extra request every 10 seconds (5 * 60 / 30) until reaching the limit.
## GraphQL/Storage/Functions
You can rate-limit the GraphQL, Storage, and Functions services independently of each other. These rate limits are based on the client IP, and requests made to one service do not count toward the rate limits of another service.
### Configuration
<Tabs>
<Tab title="Dashboard">
**Project Dashboard -> Settings -> Rate Limiting**
![Rate limit services](/images/platform/rate-limiting/misc.png)
</Tab>
<Tab title="Config">
```toml
[hasura.rateLimit]
limit = 100
interval = '15m'
[functions.rateLimit]
limit = 100
interval = '15m'
[storage.rateLimit]
limit = 100
interval = '15m'
```
</Tab>
</Tabs>
## Auth
Given that not all endpoints are equally sensitive, Auth supports more complex rate-limiting rules, allowing you to set different configurations depending on the properties of each endpoint.
| Endpoints | Key | Limits | Description | Minimum version |
| ----------------------|-----|--------|-------------|-----------------|
| Any that sends emails<sup>1</sup> | Global | 50 / hour | Not configurable. This limit applies to any project without custom SMTP settings | 0.33.0 |
| Any that sends emails<sup>1</sup> | Client IP | 10 / hour | Configurable. This limit applies to any project with custom SMTP settings and is configurable | 0.33.0 |
| Any that sends SMS<sup>2</sup> | Client IP | 10 / hour | Configurable. | 0.33.0 |
| Any endpoint that an attacker may try to brute-force. This includes sign-in and verify endpoints<sup>3</sup> | Client IP | 10 / 5 minutes | Configurable | 0.33.0 |
| Signup endpoints<sup>4</sup> | Client IP | 10 / 5 minutes | Configurable | 0.33.0 |
| Any | Client IP | 100 / minute | The total sum of requests to any endpoint (including previous ones) can not exceed this limit | 0.33.0 |
<Note>
Limits are grouped within a given category. For instance, with a limit of 10 per hour for the sign-in/verify category, if a user attempts to sign in 10 times and then tries to verify an OTP code, the latter will be rate-limited alongside the sign-in attempts.
</Note>
<sup>1</sup> Paths included:
- `/signin/passwordless/email`
- `/user/email/change`
- `/user/email/send-verification-email`
- `/user/password/reset`
- `/signup/email-password` - If email verification enabled
- `/user/deanonymize` - If email verification enabled
<sup>2</sup> Paths included:
- `/signin/passwordless/sms`
<sup>3</sup> Paths included:
- `/signin/*`
- `*/verify`
- `*/otp`
<sup>4</sup> Paths included:
- `/signup/*`
### Configuration
<Tabs>
<Tab title="Dashboard">
**Project Dashboard -> Settings -> Rate Limiting**
![Rate limit Auth](/images/platform/rate-limiting/auth.png)
</Tab>
<Tab title="Config">
```toml
[auth.rateLimit]
[auth.rateLimit.emails]
limit = 10
interval = '1h'
[auth.rateLimit.sms]
limit = 10
interval = '1h'
[auth.rateLimit.bruteForce]
limit = 10
interval = '5m'
[auth.rateLimit.signups]
limit = 10
interval = '5m'
[auth.rateLimit.global]
limit = 100
interval = '1m'
```
</Tab>
</Tabs>

View File

@@ -1,5 +1,11 @@
# @nhost-examples/cli
## 0.3.11
### Patch Changes
- @nhost/nhost-js@3.1.9
## 0.3.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/cli",
"version": "0.3.10",
"version": "0.3.11",
"main": "src/index.mjs",
"private": true,
"scripts": {

View File

@@ -1,5 +1,12 @@
# @nhost-examples/codegen-react-apollo
## 0.4.11
### Patch Changes
- @nhost/react@3.5.6
- @nhost/react-apollo@12.0.6
## 0.4.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/codegen-react-apollo",
"version": "0.4.10",
"version": "0.4.11",
"private": true,
"scripts": {
"codegen": "graphql-codegen",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/codegen-react-query
## 0.4.11
### Patch Changes
- @nhost/react@3.5.6
## 0.4.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/codegen-react-query",
"version": "0.4.10",
"version": "0.4.11",
"private": true,
"scripts": {
"codegen": "graphql-codegen",

View File

@@ -1,5 +1,12 @@
# @nhost-examples/react-urql
## 0.3.11
### Patch Changes
- @nhost/react@3.5.6
- @nhost/react-urql@9.0.6
## 0.3.10
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/codegen-react-urql",
"private": true,
"version": "0.3.10",
"version": "0.3.11",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -15,7 +15,7 @@
"graphql": "16.8.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"urql": "^3.0.4"
"urql": "^3.0.3"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.2",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/multi-tenant-one-to-many
## 2.2.11
### Patch Changes
- @nhost/nhost-js@3.1.9
## 2.2.10
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/multi-tenant-one-to-many",
"private": true,
"version": "2.2.10",
"version": "2.2.11",
"description": "",
"main": "index.js",
"scripts": {},

View File

@@ -1,5 +1,13 @@
# @nhost-examples/nextjs
## 0.3.11
### Patch Changes
- @nhost/react@3.5.6
- @nhost/react-apollo@12.0.6
- @nhost/nextjs@2.1.20
## 0.3.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/nextjs",
"version": "0.3.10",
"version": "0.3.11",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/node-storage
## 0.2.11
### Patch Changes
- @nhost/nhost-js@3.1.9
## 0.2.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/node-storage",
"version": "0.2.10",
"version": "0.2.11",
"private": true,
"description": "This is an example of how to use the Storage with Node.js",
"main": "src/index.mjs",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/nextjs-server-components
## 0.4.12
### Patch Changes
- @nhost/nhost-js@3.1.9
## 0.4.11
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/nextjs-server-components",
"version": "0.4.11",
"version": "0.4.12",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -1 +1 @@
hoist-pattern=[]
hoist-pattern[]=!@nhost/nhost-js

View File

@@ -14,7 +14,7 @@
},
"devDependencies": {
"@nhost/nhost-js": "^3.1.5",
"@playwright/test": "^1.42.1",
"@playwright/test": "^1.41.0",
"@sveltejs/adapter-auto": "^2.1.1",
"@sveltejs/kit": "^1.30.4",
"@types/js-cookie": "^3.0.6",
@@ -25,7 +25,7 @@
"postcss": "^8.4.38",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^3.59.2",
"svelte": "^4.2.19",
"svelte-check": "^3.6.8",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.3",

View File

@@ -1,32 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.nhost
functions/node_modules
node_modules
dist
dist-ssr
*.local
/test-results/
/playwright-report/
/playwright/.cache/
storageState.json
.secrets
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
test-results
playwright-report

View File

@@ -1,5 +1,16 @@
# @nhost-examples/react-apollo
## 1.0.0
### Major Changes
- cffdec5: feat: rewrite example using shadcn ui components
### Patch Changes
- @nhost/react@3.5.6
- @nhost/react-apollo@12.0.6
## 0.8.11
### Patch Changes

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -15,11 +15,13 @@ test('should add an item to the todo list when authenticated with email and pass
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /apollo/i }).click()
await expect(newPage.getByText(/todo list/i)).toBeVisible()
await newPage.getByRole('link', { name: /todos/i }).click()
await expect(newPage.getByRole('heading', { name: /todos/i })).toBeVisible()
await newPage.getByRole('textbox').fill(sentence)
await newPage.getByRole('button', { name: /add/i }).click()
await expect(newPage.getByRole('listitem').first()).toHaveText(sentence)
await expect(newPage.getByRole('main')).toContainText(sentence)
})
test('should add an item to the todo list when authenticated anonymously', async ({ page }) => {
@@ -28,11 +30,13 @@ test('should add an item to the todo list when authenticated anonymously', async
await page.goto('/')
await signInAnonymously({ page })
await page.getByRole('button', { name: /apollo/i }).click()
await expect(page.getByText(/todo list/i)).toBeVisible()
await page.getByRole('link', { name: /todos/i }).click()
await expect(page.getByRole('heading', { name: /todos/i })).toBeVisible()
await page.getByRole('textbox').fill(sentence)
await page.getByRole('button', { name: /add/i }).click()
await expect(page.getByRole('listitem').first()).toHaveText(sentence)
await expect(page.getByRole('main')).toContainText(sentence)
})
test('should fail when network is not available', async ({ page }) => {
@@ -41,12 +45,12 @@ test('should fail when network is not available', async ({ page }) => {
await page.goto('/')
await signInAnonymously({ page })
await page.getByRole('button', { name: /apollo/i }).click()
await expect(page.getByText(/todo list/i)).toBeVisible()
await page.getByRole('link', { name: /todos/i }).click()
await expect(page.getByRole('heading', { name: /todos/i })).toBeVisible()
await page.route('**', (route) => route.abort('internetdisconnected'))
await page.getByRole('textbox').fill(sentence)
await page.getByRole('button', { name: /add/i }).click()
await expect(page.getByText(/network error/i)).toBeVisible()
await expect(page.getByText(/failed to fetch/i)).toBeVisible()
})

View File

@@ -12,18 +12,28 @@ test('should be able to change email', async ({ page, browser }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /profile/i }).click()
await newPage.getByRole('link', { name: /profile/i }).click()
const newEmail = faker.internet.email()
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
await expect(
newPage.getByText(/please check your inbox and follow the link to confirm the email change/i)
).toBeVisible()
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
await newPage
.locator('div')
.filter({ hasText: /^Change emailChange$/ })
.getByRole('button')
.click()
await newPage.getByRole('button', { name: /sign out/i }).click()
// await expect(
// newPage.getByText(/please check your inbox and follow the link to confirm the email change./i)
// ).toBeVisible()
await expect(newPage.getByRole('status')).toContainText(
'Please check your inbox and follow the link to confirm the email change.'
)
await newPage.getByRole('link', { name: /sign out/i }).click()
const mailhogPage = await browser.newPage()
@@ -35,7 +45,8 @@ test('should be able to change email', async ({ page, browser }) => {
requestType: 'email-confirm-change'
})
await expect(updatedEmailPage.getByText(/profile page/i)).toBeVisible()
// await expect(updatedEmailPage.getByText(/profile page/i)).toBeVisible()
await expect(updatedEmailPage.getByRole('heading', { name: /profile/i })).toBeVisible()
})
test('should not accept an invalid email', async ({ page }) => {
@@ -48,12 +59,18 @@ test('should not accept an invalid email', async ({ page }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /profile/i }).click()
await newPage.getByRole('link', { name: /profile/i }).click()
const newEmail = faker.random.alphaNumeric()
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
await newPage
.locator('div')
.filter({ hasText: /^Change emailChange$/ })
.getByRole('button')
.click()
await expect(newPage.getByText(/email is incorrectly formatted/i)).toBeVisible()
})

View File

@@ -12,15 +12,22 @@ test('should be able to change password', async ({ page }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /profile/i }).click()
await newPage.getByRole('link', { name: /profile/i }).click()
const newPassword = faker.internet.password()
await newPage.getByPlaceholder(/new password/i).fill(newPassword)
await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
await expect(newPage.getByText(/password changed successfully/i)).toBeVisible()
await newPage.getByRole('button', { name: /sign out/i }).click()
// await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
await newPage
.locator('div')
.filter({ hasText: /^Change passwordChange$/ })
.getByRole('button')
.click()
await expect(newPage.getByText(/password changed successfully./i)).toBeVisible()
await newPage.getByRole('link', { name: 'Sign out' }).click()
await signInWithEmailAndPassword({ page: newPage, email, password: newPassword })
await expect(newPage.getByText(/you are authenticated/i)).toBeVisible()
@@ -36,12 +43,18 @@ test('should not accept an invalid email', async ({ page }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /profile/i }).click()
await newPage.getByRole('link', { name: /profile/i }).click()
const newPassword = faker.internet.password(2)
await newPage.getByPlaceholder(/new password/i).fill(newPassword)
await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
// await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
await newPage
.locator('div')
.filter({ hasText: /^Change passwordChange$/ })
.getByRole('button')
.click()
await expect(newPage.getByText(/password is incorrectly formatted/i)).toBeVisible()
})

View File

@@ -12,10 +12,12 @@ test('should upload a single file', async ({ page }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /storage/i }).click()
await newPage.getByRole('link', { name: /storage/i }).click()
await newPage
.getByRole('button', { name: /drag a file here or click to select/i })
.locator('div')
.filter({ hasText: /^Drag a file here or click to select$/ })
.nth(1)
.locator('input[type=file]')
.setInputFiles({
buffer: Buffer.from('file contents', 'utf-8'),
@@ -23,7 +25,7 @@ test('should upload a single file', async ({ page }) => {
mimeType: 'text/plain'
})
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
})
test('should upload two files using the same single file uploader', async ({ page }) => {
@@ -36,10 +38,12 @@ test('should upload two files using the same single file uploader', async ({ pag
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /storage/i }).click()
await newPage.getByRole('link', { name: /storage/i }).click()
await newPage
.getByRole('button', { name: /drag a file here or click to select/i })
.locator('div')
.filter({ hasText: /^Drag a file here or click to select$/ })
.nth(1)
.locator('input[type=file]')
.setInputFiles({
buffer: Buffer.from('file contents 1', 'utf-8'),
@@ -47,10 +51,12 @@ test('should upload two files using the same single file uploader', async ({ pag
mimeType: 'text/plain'
})
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
await newPage
.getByRole('button', { name: /successfully uploaded/i })
.locator('div')
.filter({ hasText: /^Uploaded successfully$/ })
.nth(1)
.locator('input[type=file]')
.setInputFiles({
buffer: Buffer.from('file contents 2', 'utf-8'),
@@ -58,7 +64,7 @@ test('should upload two files using the same single file uploader', async ({ pag
mimeType: 'text/plain'
})
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
})
test('should upload multiple files at once', async ({ page }) => {
@@ -71,10 +77,12 @@ test('should upload multiple files at once', async ({ page }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const newPage = await verifyEmail({ page, email, context: page.context() })
await newPage.getByRole('button', { name: /storage/i }).click()
await newPage.getByRole('link', { name: /storage/i }).click()
await newPage
.getByRole('button', { name: /drag files here or click to select/i })
.locator('div')
.filter({ hasText: /^Drag a file here or click to select$/ })
.nth(3)
.locator('input[type=file]')
.setInputFiles([
{
@@ -89,9 +97,9 @@ test('should upload multiple files at once', async ({ page }) => {
}
])
await expect(newPage.getByRole('row').nth(0)).toHaveText('file1.txt')
await expect(newPage.getByRole('row').nth(1)).toHaveText('file2.txt')
await expect(newPage.getByText('file1.txt')).toBeVisible()
await expect(newPage.getByText('file2.txt')).toBeVisible()
await newPage.getByRole('button', { name: /upload/i }).click()
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
})

View File

@@ -31,11 +31,11 @@ test('should sign in automatically with a refresh token', async ({ page }) => {
await clearStorage({ page: newPage })
await newPage.reload()
await expect(newPage.getByText(/sign in to the application/i)).toBeVisible()
await expect(newPage.getByText(/sign in/i).nth(1)).toBeVisible()
// User should be signed in automatically
await newPage.goto(`${baseURL}/profile#refreshToken=${refreshToken}`)
await expect(newPage.getByText(/profile page/i)).toBeVisible()
await newPage.goto(`${baseURL}/profile?refreshToken=${refreshToken}`)
await expect(newPage.getByRole('heading', { name: 'Profile' })).toBeVisible()
})
test('should fail automatic sign-in when network is not available', async ({ page }) => {
@@ -61,11 +61,11 @@ test('should fail automatic sign-in when network is not available', async ({ pag
await clearStorage({ page: newPage })
await newPage.reload()
await expect(newPage.getByText(/sign in to the application/i)).toBeVisible()
await expect(newPage.getByText(/sign in/i).nth(1)).toBeVisible()
await newPage.route(`${authBackendURL}/**`, (route) => route.abort('internetdisconnected'))
// User should be signed in automatically
await newPage.goto(`${baseURL}/profile#refreshToken=${refreshToken}`)
await newPage.goto(`${baseURL}/profile?refreshToken=${refreshToken}`)
await expect(
newPage.getByText(/could not sign in automatically. retrying to get user information/i)
).toBeVisible()

View File

@@ -26,13 +26,13 @@ test.beforeAll(async ({ browser }) => {
const newPage = await verifyEmail({ page, email, context: page.context() })
await expect(newPage.getByText(/you are authenticated/i)).toBeVisible()
await newPage.getByRole('button', { name: /sign out/i }).click()
await newPage.getByRole('link', { name: /sign out/i }).click()
page = newPage
})
test.afterEach(async () => {
await page.getByRole('button', { name: /sign out/i }).click()
await page.getByRole('link', { name: /sign out/i }).click()
})
test.afterAll(() => {
@@ -53,7 +53,7 @@ test('should activate and sign in with MFA', async () => {
await signInWithEmailAndPassword({ page, email, password })
await page.waitForURL(baseURL)
await page.getByRole('button', { name: /profile/i }).click()
await page.getByRole('link', { name: /profile/i }).click()
await page.getByRole('button', { name: /generate/i }).click()
const image = page.getByAltText(/qrcode/i)
@@ -70,9 +70,8 @@ test('should activate and sign in with MFA', async () => {
await page.getByPlaceholder(/enter activation code/i).fill(code)
await page.getByRole('button', { name: /activate/i }).click()
await expect(page.getByText(/mfa has been activated/i)).toBeVisible()
await page.getByRole('button', { name: /sign out/i }).click()
await page.getByRole('link', { name: /sign out/i }).click()
await page.getByRole('button', { name: /continue with email \+ password/i }).click()
await signInWithEmailAndPassword({ page, email, password })
await expect(page.getByText(/send 2-step verification code/i)).toBeVisible()

View File

@@ -16,7 +16,7 @@ test('should deanonymize with email and password', async ({ page, context }) =>
await page.goto('/')
await signInAnonymously({ page })
await page.getByRole('button', { name: /profile/i }).click()
await page.getByRole('link', { name: /profile/i }).click()
const userData = await getUserData(page)
@@ -24,7 +24,7 @@ test('should deanonymize with email and password', async ({ page, context }) =>
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const authenticatedPage = await verifyEmail({ page, context, email })
await authenticatedPage.getByRole('button', { name: /profile/i }).click()
await authenticatedPage.getByRole('link', { name: /profile/i }).click()
const updatedUserData = await getUserData(authenticatedPage)
expect(updatedUserData.id).toBe(userData.id)
@@ -37,7 +37,7 @@ test('should deanonymize with a magic link', async ({ page, context }) => {
await page.goto('/')
await signInAnonymously({ page })
await page.getByRole('button', { name: /profile/i }).click()
await page.getByRole('link', { name: /profile/i }).click()
const userData = await getUserData(page)
@@ -45,7 +45,7 @@ test('should deanonymize with a magic link', async ({ page, context }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const authenticatedPage = await verifyMagicLink({ page, context, email })
await authenticatedPage.getByRole('button', { name: /profile/i }).click()
await authenticatedPage.getByRole('link', { name: /profile/i }).click()
const updatedUserData = await getUserData(authenticatedPage)
expect(updatedUserData.id).toBe(userData.id)

View File

@@ -16,7 +16,7 @@ test('should sign up with email and password', async ({ page, context }) => {
})
test('should raise an error when trying to sign up with an existing email', async ({ page }) => {
page.goto('/')
await page.goto('/')
const email = faker.internet.email()
const password = faker.internet.password()
@@ -27,6 +27,8 @@ test('should raise an error when trying to sign up with an existing email', asyn
// close modal
await page.getByRole('dialog').getByRole('button').click()
await page.goto('/')
await signUpWithEmailAndPassword({ page, email, password })
await expect(page.getByText(/email already in use/i)).toBeVisible()
})

View File

@@ -11,8 +11,12 @@ test('should sign up with a magic link', async ({ page, context }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
const authenticatedPage = await verifyMagicLink({ page, context, email })
await authenticatedPage.getByRole('button', { name: /home/i }).click()
await expect(authenticatedPage.getByText(/you are authenticated/i)).toBeVisible()
await authenticatedPage.getByRole('link', { name: /home/i }).click()
await expect(
authenticatedPage.getByText(
/You are authenticated. You have now access to the authorised part of the application./i
)
).toBeVisible()
})
test('should fail when network is not available', async ({ page }) => {

View File

@@ -5,10 +5,10 @@ test('should redirect to /sign-in when not authenticated', async ({ page }) => {
await page.goto(`${baseURL}`)
await page.waitForURL(`${baseURL}/sign-in`)
await expect(page.getByText(/sign in to the application/i)).toBeVisible()
await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible()
await page.goto(`${baseURL}/apollo`)
await page.goto(`${baseURL}/todos`)
await page.waitForURL(`${baseURL}/sign-in`)
await expect(page.getByText(/sign in to the application/i)).toBeVisible()
await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible()
})

View File

@@ -13,16 +13,16 @@ test('should reset password', async ({ page, context }) => {
await expect(page.getByText(/verification email sent/i)).toBeVisible()
await page.goto(`${baseURL}/sign-in`)
await page.getByRole('button', { name: /continue with email \+ password/i }).click()
await page.getByRole('button', { name: /forgot password?/i }).click()
await page.getByRole('link', { name: /continue with email \+ password/i }).click()
await page.getByRole('link', { name: /forgot password?/i }).click()
await page.getByPlaceholder('Email Address').type(email)
await page.getByPlaceholder('email').type(email)
await page.getByRole('button', { name: /reset your password/i }).click()
const authenticatedPage = await resetPassword({ page, context, email })
await authenticatedPage.waitForLoadState()
await authenticatedPage.getByRole('button', { name: /profile/i }).click()
await authenticatedPage.getByRole('link', { name: /profile/i }).click()
await expect(authenticatedPage.getByText(/profile page/i)).toBeVisible()
await expect(authenticatedPage.getByRole('heading', { name: 'Profile' })).toBeVisible()
})

View File

@@ -12,8 +12,9 @@ import { baseURL, mailhogURL } from './config'
* @returns The user data.
*/
export async function getUserData(page: Page) {
const textContent = await page.locator('h1:has-text("User information") + div pre').textContent()
const userData = textContent ? JSON.parse(textContent) : {}
const userInformation = await page.locator('pre').nth(0).textContent()
const userData = userInformation ? JSON.parse(userInformation) : {}
return userData as User
}
@@ -34,15 +35,13 @@ export async function signUpWithEmailAndPassword({
email: string
password: string
}) {
await page.getByRole('button', { name: /home/i }).click()
await page.getByRole('link', { name: /sign up/i }).click()
await page.getByRole('button', { name: /continue with email \+ password/i }).click()
await page.getByRole('link', { name: /continue with email \+ password/i }).click()
await page.getByPlaceholder(/first name/i).type(faker.name.firstName())
await page.getByPlaceholder(/last name/i).type(faker.name.lastName())
await page.getByPlaceholder(/email address/i).type(email)
await page.getByPlaceholder(/email/i).type(email)
await page.getByPlaceholder(/^password$/i).type(password)
await page.getByPlaceholder(/confirm password/i).type(password)
await page.getByRole('button', { name: /continue with email \+ password/i }).click()
await page.getByRole('button', { name: /sign up/i }).click()
}
/**
@@ -61,11 +60,10 @@ export async function signInWithEmailAndPassword({
email: string
password: string
}) {
await page.getByRole('button', { name: /home/i }).click()
await page.getByRole('button', { name: /continue with email \+ password/i }).click()
await page.getByPlaceholder(/email address/i).type(email)
await page.getByRole('link', { name: /continue with email \+ password/i }).click()
await page.getByPlaceholder(/email/i).type(email)
await page.getByPlaceholder(/password/i).type(password)
await page.getByRole('button', { name: /sign in/i }).click()
await page.getByRole('button', { name: 'Sign In', exact: true }).click()
}
/**
@@ -74,8 +72,7 @@ export async function signInWithEmailAndPassword({
* @param page - The page to sign in with.
*/
export async function signInAnonymously({ page }: { page: Page }) {
await page.getByRole('button', { name: /home/i }).click()
await page.getByRole('link', { name: /sign in anonymously/i }).click()
await page.getByRole('button', { name: /sign in anonymously/i }).click()
await page.waitForURL(baseURL)
}
@@ -86,11 +83,10 @@ export async function signInAnonymously({ page }: { page: Page }) {
* @param email - The email address to sign up with.
*/
export async function signUpWithEmailPasswordless({ page, email }: { page: Page; email: string }) {
await page.getByRole('button', { name: /home/i }).click()
await page.getByRole('link', { name: /sign up/i }).click()
await page.getByRole('button', { name: /continue with a magic link/i }).click()
await page.getByPlaceholder(/email address/i).fill(email)
await page.getByRole('button', { name: /continue with email/i }).click()
await page.getByRole('link', { name: /continue with a magic link/i }).click()
await page.getByPlaceholder(/email/i).fill(email)
await page.getByRole('button', { name: /sign up/i }).click()
}
/**
@@ -158,7 +154,7 @@ export async function resetPassword({
.click()
const authenticatedPage = await authenticatedPagePromise
await authenticatedPage.getByRole('button', { name: /Verify/i }).click()
await authenticatedPage.getByRole('link', { name: /Verify/i }).click()
await authenticatedPage.waitForLoadState()
return authenticatedPage
}
@@ -199,7 +195,7 @@ export async function verifyEmail({
return verifyEmailPage
}
await verifyEmailPage.getByRole('button', { name: /Verify/i }).click()
await verifyEmailPage.getByRole('link', { name: /verify/i }).click()
await verifyEmailPage.waitForLoadState()
return verifyEmailPage
}

View File

@@ -0,0 +1,26 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config({
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
ignores: ['dist'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
})

View File

@@ -1,20 +0,0 @@
schema:
- https://local.hasura.nhost.run/v1/graphql:
headers:
x-hasura-admin-secret: nhost-admin-secret
x-hasura-role: user
documents: 'src/**/!(*.d).{ts,tsx}'
generates:
./src/generated.ts:
config:
namingConvention:
typeNames: change-case-all#pascalCase
transformUnderscore: true
scalars:
uuid: string
bigint: number
citext: string
timestamptz: string
plugins:
- typescript
- typescript-operations

View File

@@ -1,16 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nhost with React and Apollo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nhost <> React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -55,6 +55,8 @@ insert_permissions:
check:
size:
_lt: 1024000
set:
uploaded_by_user_id: x-hasura-User-Id
columns:
- is_uploaded
- size
@@ -69,6 +71,8 @@ insert_permissions:
- role: user
permission:
check: {}
set:
uploaded_by_user_id: x-hasura-User-Id
columns:
- is_uploaded
- size
@@ -94,21 +98,26 @@ select_permissions:
- updated_at
- id
- uploaded_by_user_id
filter: {}
filter:
uploaded_by_user_id:
_eq: X-Hasura-User-Id
- role: user
permission:
columns:
- is_uploaded
- size
- bucket_id
- created_at
- etag
- id
- is_uploaded
- metadata
- mime_type
- name
- created_at
- size
- updated_at
- id
- uploaded_by_user_id
filter: {}
filter:
uploaded_by_user_id:
_eq: X-Hasura-User-Id
update_permissions:
- role: anonymous
permission:
@@ -123,8 +132,12 @@ update_permissions:
- updated_at
- id
- uploaded_by_user_id
filter: {}
check: {}
filter:
uploaded_by_user_id:
_eq: X-Hasura-User-Id
check:
uploaded_by_user_id:
_eq: X-Hasura-User-Id
- role: user
permission:
columns:
@@ -138,12 +151,18 @@ update_permissions:
- updated_at
- id
- uploaded_by_user_id
filter: {}
filter:
uploaded_by_user_id:
_eq: X-Hasura-User-Id
check: {}
delete_permissions:
- role: anonymous
permission:
filter: {}
filter:
uploaded_by_user_id:
_eq: X-Hasura-User-Id
- role: user
permission:
filter: {}
filter:
uploaded_by_user_id:
_eq: X-Hasura-User-Id

View File

@@ -29,14 +29,14 @@ httpPoolSize = 100
version = 18
[auth]
version = '0.32.0'
version = '0.32.1'
[auth.elevatedPrivileges]
mode = 'required'
[auth.redirections]
clientUrl = 'https://react-apollo.example.nhost.io/'
allowedUrls = ['https://react-apollo.example.nhost.io', 'https://react-apollo.example.nhost.io/profile', 'https://vue-apollo.example.nhost.io', 'https://vue-apollo.example.nhost.io/profile', 'https://*.vercel.app', 'http://localhost:3000', 'http://localhost:3000/profile', 'http://localhost:5173', 'http://localhost:5173/profile']
allowedUrls = ['https://react-apollo.example.nhost.io', 'https://react-apollo.example.nhost.io/profile', 'https://vue-apollo.example.nhost.io', 'https://vue-apollo.example.nhost.io/profile', 'https://*.vercel.app', 'http://localhost:5174', 'http://localhost:5174/profile', 'http://localhost:5174/', 'http://localhost:5174/profile']
[auth.signUp]
enabled = true
@@ -143,6 +143,7 @@ enabled = false
enabled = true
[auth.method.webauthn.relyingParty]
id = 'nhost.io'
name = 'apollo-example'
origins = ['https://react-apollo.example.nhost.io']
@@ -154,12 +155,12 @@ enabled = true
issuer = 'nhost'
[postgres]
version = '14.11-20240515-1'
version = '16.2-20240718-1'
[provider]
[storage]
version = '0.6.0'
version = '0.6.1'
[observability]
[observability.grafana]

View File

@@ -25,11 +25,54 @@
"op": "remove",
"path": "/auth/method/oauth/apple/teamId"
},
{
"value": "localhost",
"op": "replace",
"path": "/auth/method/webauthn/relyingParty/id"
},
{
"value": "http://localhost:3000",
"op": "replace",
"path": "/auth/method/webauthn/relyingParty/origins/0"
},
{
"value": "http://localhost:3000",
"op": "replace",
"path": "/auth/redirections/allowedUrls/0"
},
{
"value": "http://localhost:3000/profile",
"op": "replace",
"path": "/auth/redirections/allowedUrls/1"
},
{
"op": "remove",
"path": "/auth/redirections/allowedUrls/2"
},
{
"op": "remove",
"path": "/auth/redirections/allowedUrls/2"
},
{
"op": "remove",
"path": "/auth/redirections/allowedUrls/2"
},
{
"op": "remove",
"path": "/auth/redirections/allowedUrls/2"
},
{
"op": "remove",
"path": "/auth/redirections/allowedUrls/2"
},
{
"op": "remove",
"path": "/auth/redirections/allowedUrls/2"
},
{
"op": "remove",
"path": "/auth/redirections/allowedUrls/2"
},
{
"value": "http://localhost:3000",
"op": "replace",

View File

@@ -1,71 +1,67 @@
{
"name": "@nhost-examples/react-apollo",
"version": "0.8.11",
"version": "1.0.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.9.9",
"@mantine/core": "^4.2.12",
"@mantine/dropzone": "^4.2.12",
"@mantine/hooks": "^4.2.12",
"@mantine/notifications": "^4.2.12",
"@mantine/prism": "^4.2.12",
"@nhost/react": "workspace:^",
"@nhost/react-apollo": "workspace:^",
"graphql": "16.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-router": "^6.22.3",
"react-router-dom": "^6.22.3",
"tabler-icons-react": "^1.56.0"
},
"type": "module",
"scripts": {
"dev": "vite --host localhost --port 3000",
"generate": "graphql-codegen --config graphql.config.yaml",
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
"e2e": "pnpm e2e:start-backend && pnpm e2e:test",
"e2e:test": "pnpm install-browsers && pnpm playwright test",
"e2e:start-backend": "cp .secrets.example .secrets && nhost up",
"e2e:start-ui": "run-s build preview",
"build": "vite build",
"preview": "vite preview --host localhost --port 3000",
"prettier": "prettier --check .",
"prettier:fix": "prettier --write .",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"verify": "run-p prettier lint",
"verify:fix": "run-p prettier:fix lint:fix"
"e2e:start-backend": "cp .secrets.example .secrets && nhost up"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"dependencies": {
"@apollo/client": "^3.11.4",
"@hookform/resolvers": "^3.9.0",
"@icons-pack/react-simple-icons": "^9.6.0",
"@nhost/react": "workspace:^",
"@nhost/react-apollo": "workspace:^",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"lucide-react": "^0.416.0",
"next-themes": "^0.3.0",
"prism-react-renderer": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-code-block": "^1.0.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.52.2",
"react-router-dom": "^6.22.3",
"sonner": "^1.5.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@graphql-codegen/cli": "^5.0.2",
"@nuintun/qrcode": "^3.4.0",
"@playwright/test": "1.41.0",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.2.73",
"@types/react-dom": "^18.2.23",
"@types/totp-generator": "^0.0.4",
"@vitejs/plugin-react": "^3.1.0",
"@xstate/inspect": "^0.6.5",
"@eslint/js": "^9.8.0",
"@playwright/test": "^1.41.0",
"@types/node": "^22.2.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"dotenv": "^16.4.5",
"eslint": "^9.8.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"jsqr": "^1.4.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.9",
"totp-generator": "^0.0.13",
"typescript": "^4.9.5",
"vite": "^5.2.7",
"ws": "^8.16.0",
"xstate": "^4.38.3"
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.0"
}
}
}

View File

@@ -2,6 +2,10 @@ import { defineConfig, devices } from '@playwright/test'
import dotenv from 'dotenv'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
dotenv.config({ path: path.resolve(__dirname, '.env.test') })
@@ -12,7 +16,7 @@ export default defineConfig({
timeout: 5000
},
webServer: {
command: 'pnpm e2e:start-ui',
command: 'pnpm dev',
port: 3000
},
use: {
@@ -21,7 +25,7 @@ export default defineConfig({
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
workers: 1,
reporter: 'html',
projects: [
{

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,17 +0,0 @@
<svg width="233" height="79" viewBox="0 0 233 79" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1:5)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M124.791 37.57H136.596V28.185H142.841V52.26H136.596V42.875H124.791V52.26H118.766V28.185H124.791V37.57ZM166.661 28.185H157.021C154.351 28.185 152.726 28.69 151.496 29.92C150.271 31.145 149.801 32.7 149.801 35.445V45.01C149.801 47.755 150.271 49.305 151.496 50.535C152.721 51.765 154.346 52.27 157.021 52.27H166.661C169.331 52.27 170.956 51.765 172.186 50.535C173.416 49.31 173.881 47.755 173.881 45.01V35.445C173.881 32.7 173.411 31.15 172.186 29.92C170.956 28.69 169.331 28.185 166.661 28.185ZM167.851 45.15C167.851 46.56 167.381 46.955 165.686 46.955H157.996C156.301 46.955 155.831 46.56 155.831 45.15V35.295C155.831 33.885 156.301 33.49 157.996 33.49H165.686C167.421 33.49 167.851 33.85 167.851 35.295V45.15ZM188.82 37.82H197.445C200.115 37.82 201.775 38.325 202.97 39.555C204.05 40.64 204.665 42.405 204.665 44.32V45.765C204.665 47.68 204.05 49.41 202.97 50.53C201.775 51.76 200.15 52.265 197.445 52.265H180.985V46.96H196.435C198.13 46.96 198.6 46.565 198.6 45.155V44.435C198.6 43.025 198.13 42.63 196.435 42.63H187.81C185.135 42.63 183.475 42.12 182.285 40.895C181.205 39.81 180.59 38.045 180.59 36.13V34.685C180.59 32.77 181.2 31.04 182.285 29.92C183.48 28.69 185.1 28.185 187.81 28.185H203.41V33.49H188.82C187.125 33.49 186.655 33.885 186.655 35.295V36.015C186.655 37.425 187.125 37.82 188.82 37.82ZM208.484 33.49V28.185H233.029V33.49H223.789V52.26H217.759V33.49H208.484Z" fill="#21324B"/>
<path d="M104.34 27.995H94.6995C92.0295 27.995 90.4045 28.5 89.1745 29.73C87.9495 30.955 87.4795 32.51 87.4795 35.25V38.905V40.03V52.255H93.5095V40.035V38.91V35.11C93.5095 33.7 93.9795 33.305 95.6745 33.305H103.365C105.1 33.305 105.53 33.665 105.53 35.11V38.91V40.035V52.26H111.56V40.035V38.91V35.255C111.56 32.51 111.09 30.96 109.865 29.735C108.64 28.5 107.015 27.995 104.34 27.995Z" fill="#0052CD"/>
<g clip-path="url(#clip1_1:5)">
<path d="M67.025 16.93L39.55 1.065C37.085 -0.355 34.025 -0.355 31.555 1.065C29.09 2.49 27.56 5.14 27.56 7.985V10.055L25.77 9.02C23.305 7.6 20.245 7.6 17.775 9.02C15.31 10.445 13.78 13.095 13.78 15.945V18.015L11.99 16.98C9.525 15.56 6.465 15.56 3.995 16.98C1.53 18.405 0 21.055 0 23.905V73.615C0 75.045 0.829995 76.375 2.12 76.995C3.40499 77.62 4.965 77.45 6.085 76.565L19.71 65.82L40.72 77.95C41.3 78.285 41.95 78.45 42.6 78.45C43.25 78.45 43.9 78.28 44.48 77.95C45.64 77.28 46.36 76.035 46.36 74.695V44.78C46.36 39.87 43.72 35.3 39.47 32.845L32.58 28.865V7.99C32.58 6.93 33.15 5.94 34.07 5.41C34.99 4.88 36.13 4.88 37.05 5.41L64.525 21.27C67.23 22.83 68.91 25.745 68.91 28.865V66.115C68.91 67.175 68.34 68.165 67.42 68.695L60.14 72.9V36.82C60.14 31.91 57.5 27.34 53.25 24.885L36.335 15.12V20.905L50.745 29.225C53.45 30.785 55.13 33.695 55.13 36.82V75.065C55.13 76.4 55.85 77.65 57.01 78.32C57.59 78.655 58.24 78.82 58.89 78.82C59.54 78.82 60.19 78.65 60.77 78.32L69.93 73.03C72.395 71.605 73.925 68.955 73.925 66.105V28.855C73.915 23.96 71.275 19.385 67.025 16.93ZM36.955 37.185C39.66 38.745 41.34 41.655 41.34 44.78V72.53L23.94 62.485L29.525 58.085C31.46 56.56 32.57 54.275 32.57 51.81V34.66L36.955 37.185ZM27.56 31.76V51.8C27.56 52.72 27.145 53.575 26.425 54.14L5.01 71.025V23.9C5.01 22.84 5.58 21.85 6.5 21.32C7.42 20.79 8.56 20.79 9.48 21.32L13.78 23.8V59.325L18.79 55.375V15.945C18.79 14.885 19.36 13.895 20.28 13.365C21.2 12.835 22.34 12.835 23.26 13.365L27.56 15.845V25.97L22.55 23.075V28.865L27.56 31.76Z" fill="#0052CD"/>
</g>
</g>
<defs>
<clipPath id="clip0_1:5">
<rect width="233" height="79" fill="white"/>
</clipPath>
<clipPath id="clip1_1:5">
<rect width="73.925" height="78.82" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,33 +0,0 @@
import { Link } from 'react-router-dom'
import { Container, Title } from '@mantine/core'
export const AboutPage: React.FC = () => {
return (
<Container>
<Title>About this example</Title>
<p>This application demonstrates the available features of the Nhost stack.</p>
<div>
Nhost cloud leverages the following services in the backend:
<ul>
<li>Hasura</li>
<li>Hasura Auth</li>
<li>Hasura Storage</li>
<li>Custom functions</li>
</ul>
</div>
<div>
This frontend is built with the following technologies:
<ul>
<li>React</li>
<li>React-router</li>
<li>Mantine</li>
<li>and of course, the Nhost React client</li>
</ul>
</div>
<div>
Now let&apos;s go to the <Link to="/">index page</Link>
</div>
</Container>
)
}

View File

@@ -1,27 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
button {
margin-bottom: 20px;
}
.App > * {
padding-left: 10px;
}
.App-header {
background-color: #282c34;
min-height: 10vh;
display: flex;
font-size: calc(10px + 2vmin);
color: white;
}

View File

@@ -1,132 +1,57 @@
import { Route, Routes } from 'react-router-dom'
import { BrandGithub } from 'tabler-icons-react'
import { AppShell, Button, Group, Header, Image, MantineProvider, Title } from '@mantine/core'
import { NotificationsProvider } from '@mantine/notifications'
import { AboutPage } from './About'
import { ApolloPage } from './apollo'
import { AuthGate, PublicGate } from './components/auth-gates'
import NavBar from './components/NavBar'
import Home from './Home'
import { ProfilePage } from './profile'
import { SignInPage } from './sign-in'
import { SignUpPage } from './sign-up'
import { StoragePage } from './Storage'
import './App.css?inline'
import { NotesPage } from './components/notes'
import VerifyPage from './Verify'
const title = 'Nhost with React and Apollo'
import { AuthGate } from '@/components/auth/auth-gate'
import Home from '@/components/routes/app/home'
import Layout from '@/components/routes/app/layout'
import Profile from '@/components/routes/app/profile'
import ProtectedNotes from '@/components/routes/app/protected-notes'
import Storage from '@/components/routes/app/storage'
import Todos from '@/components/routes/app/todos'
import ForgotPassword from '@/components/routes/auth/forgot-password'
import SignIn from '@/components/routes/auth/sign-in/sign-in'
import SignInEmailPassword from '@/components/routes/auth/sign-in/sign-in-email-password'
import SignInMagicLink from '@/components/routes/auth/sign-in/sign-in-magic-link'
import SignInSecurityKey from '@/components/routes/auth/sign-in/sign-in-security-key'
import SignUp from '@/components/routes/auth/sign-up/sign-up'
import SignUpEmailPassword from '@/components/routes/auth/sign-up/sign-up-email-password'
import SignUpMagicLink from '@/components/routes/auth/sign-up/sign-up-magic-link'
import SignUpSecurityKey from '@/components/routes/auth/sign-up/sign-up-security-key'
import VerifyEmail from './components/routes/auth/verify-email'
function App() {
const colorScheme = 'light'
return (
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
colorScheme
}}
>
<NotificationsProvider>
<AppShell
padding="md"
navbar={<NavBar />}
header={
<Header height={60} p="xs">
<Group position="apart" noWrap>
<Group noWrap>
<Image src="/logo.svg" height={35} fit="contain" width={120} />
<Title order={3} style={{ whiteSpace: 'nowrap' }}>
{title}
</Title>
</Group>
<Button
leftIcon={<BrandGithub />}
variant="outline"
color={colorScheme}
component="a"
href="https://github.com/nhost/nhost/tree/main/examples/react-apollo"
target="_blank"
>
GitHub
</Button>
</Group>
</Header>
}
styles={(theme) => ({
main: {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0]
}
})}
>
<Routes>
<Route
path="/"
element={
<AuthGate>
<Home />
</AuthGate>
}
/>
<Route path="/verify" element={<VerifyPage />} />
<Route path="/about" element={<AboutPage />} />
<Route
path="/sign-in/*"
element={
<PublicGate>
<SignInPage />
</PublicGate>
}
/>
<Route
path="/sign-up/*"
element={
<PublicGate anonymous>
<SignUpPage />
</PublicGate>
}
/>
<Route
path="/profile"
element={
<AuthGate>
<ProfilePage />
</AuthGate>
}
/>
<Routes>
<Route
path="/"
element={
<AuthGate>
<Layout />
</AuthGate>
}
>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/protected-notes" element={<ProtectedNotes />} />
<Route path="/storage" element={<Storage />} />
<Route path="/todos" element={<Todos />} />
</Route>
<Route
path="/secret-notes"
element={
<AuthGate>
<NotesPage />
</AuthGate>
}
/>
<Route path="/sign-in">
<Route path="/sign-in/" element={<SignIn />} />
<Route path="/sign-in/email-password" element={<SignInEmailPassword />} />
<Route path="/sign-in/security-key" element={<SignInSecurityKey />} />
<Route path="/sign-in/magic-link" element={<SignInMagicLink />} />
<Route path="/sign-in/forgot-password" element={<ForgotPassword />} />
</Route>
<Route
path="/storage"
element={
<AuthGate>
<StoragePage />
</AuthGate>
}
/>
<Route
path="/apollo"
element={
<AuthGate>
<ApolloPage />
</AuthGate>
}
/>
</Routes>
</AppShell>
</NotificationsProvider>
</MantineProvider>
<Route path="/sign-up">
<Route path="/sign-up/" element={<SignUp />} />
<Route path="/sign-up/email-password" element={<SignUpEmailPassword />} />
<Route path="/sign-up/security-key" element={<SignUpSecurityKey />} />
<Route path="/sign-up/magic-link" element={<SignUpMagicLink />} />
</Route>
<Route path="/verify" element={<VerifyEmail />} />
</Routes>
)
}

View File

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

View File

@@ -1,222 +0,0 @@
import React from 'react'
import {
FaCheck,
FaCheckCircle,
FaCloudUploadAlt,
FaExclamationTriangle,
FaMinus
} from 'react-icons/fa'
import {
ActionIcon,
Button,
Card,
Center,
Container,
Grid,
Group,
MantineTheme,
Progress,
RingProgress,
SimpleGrid,
Table,
Text,
ThemeIcon,
Title,
useMantineTheme
} from '@mantine/core'
import { Dropzone, DropzoneStatus } from '@mantine/dropzone'
import { FileItemRef, useFileUpload, useFileUploadItem, useMultipleFilesUpload } from '@nhost/react'
function getIconColor(status: DropzoneStatus, theme: MantineTheme) {
return status.accepted
? theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 4 : 6]
: status.rejected
? theme.colors.red[theme.colorScheme === 'dark' ? 4 : 6]
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.colors.gray[7]
}
export const DropzoneChildren: React.FC<
React.PropsWithChildren<{
status: DropzoneStatus
theme: MantineTheme
success: boolean
error: boolean
progress: number
}>
> = ({ status, theme, success, progress, error, children }) => (
<Grid style={{ pointerEvents: 'none' }} align="center">
<Grid.Col span={4}>
{success ? (
<RingProgress
sections={[{ value: 100, color: 'teal' }]}
label={
<Center>
<ThemeIcon color="teal" variant="light" radius="xl" size="xl">
<FaCheckCircle size={22} />
</ThemeIcon>
</Center>
}
/>
) : error ? (
<Center>
<FaExclamationTriangle style={{ color: 'red', maxWidth: '80px' }} size={80} />
</Center>
) : progress ? (
<RingProgress
sections={[{ value: progress, color: 'blue' }]}
label={
<Center>
<Text color="blue" weight={700} size="xl">
{progress}%
</Text>
</Center>
}
/>
) : (
<Center>
<FaCloudUploadAlt
style={{ color: getIconColor(status, theme), maxWidth: '80px' }}
size={80}
/>
</Center>
)}
</Grid.Col>
<Grid.Col span={8}>
<Center>{children}</Center>
</Grid.Col>
</Grid>
)
const ListItem: React.FC<React.PropsWithChildren<{ fileRef: FileItemRef }>> = ({ fileRef }) => {
const { progress, isUploaded, name, isError, destroy } = useFileUploadItem(fileRef)
return (
<tr>
<td>
{name} {isError && <FaExclamationTriangle color="red" />}
</td>
<td>{progress && <Progress value={progress} />}</td>
<td>
<ActionIcon onClick={destroy}>
{isUploaded ? <FaCheck color="teal" title="success" /> : <FaMinus />}
</ActionIcon>
</td>
</tr>
)
}
export const StoragePage: React.FC = () => {
const { upload, progress, isUploaded, isUploading, isError } = useFileUpload()
const {
add,
upload: uploadAll,
progress: progressAll,
isUploaded: uploadedAll,
isUploading: uploadingAll,
isError: isErrorAll,
files,
clear,
cancel
} = useMultipleFilesUpload()
const theme = useMantineTheme()
return (
<Container>
<Title>Storage</Title>
<Card shadow="sm" p="lg" m="sm">
<Title order={2}>Upload a single file</Title>
<Dropzone
onDrop={([file]) => {
console.log('accepted file', file)
upload({ file })
}}
onReject={(additions) => console.log('rejected files', additions)}
multiple={false}
>
{(status) => (
<DropzoneChildren
status={status}
theme={theme}
success={isUploaded}
progress={progress || 0}
error={isError}
>
{isUploaded ? (
<Text size="xl">Successfully uploaded</Text>
) : isUploading ? (
<Text size="xl">Uploading...</Text>
) : isError ? (
<Text size="xl">Error uploading the file</Text>
) : (
<Text size="xl">Drag a file here or click to select</Text>
)}
</DropzoneChildren>
)}
</Dropzone>
</Card>
<Card shadow="sm" p="lg" m="sm">
<Title order={2}>Upload multiple files</Title>
<SimpleGrid cols={1}>
<Dropzone
onDrop={(additions) => {
console.log('accepted files', additions)
add({ files: additions })
}}
onReject={(additions) => console.log('rejected files', additions)}
>
{(status) => (
<DropzoneChildren
status={status}
theme={theme}
success={uploadedAll}
error={isErrorAll}
progress={progressAll || 0}
>
{uploadedAll ? (
<Text size="xl">Successfully uploaded</Text>
) : uploadingAll ? (
<Text size="xl">Uploading...</Text>
) : isErrorAll ? (
<div>Error uploading some files</div>
) : (
<Text size="xl">Drag files here or click to select</Text>
)}
</DropzoneChildren>
)}
</Dropzone>
<Table style={{ width: '100%', maxWidth: '100%' }}>
<colgroup>
<col />
<col width="20%" />
<col />
</colgroup>
<tbody>
{files.map((ref) => (
<ListItem key={ref.id} fileRef={ref} />
))}
</tbody>
</Table>
<Group grow>
<Button
leftIcon={<FaCloudUploadAlt size={14} />}
onClick={() => uploadAll()}
loading={uploadingAll}
>
Upload
</Button>
{uploadingAll ? (
<Button onClick={cancel}>Cancel</Button>
) : (
<Button leftIcon={<FaCloudUploadAlt size={14} />} onClick={() => clear()}>
Clear
</Button>
)}
</Group>
</SimpleGrid>
</Card>
</Container>
)
}

View File

@@ -1,41 +0,0 @@
import { Button, Card, Container, Stack, Text } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useNhostClient } from '@nhost/react'
import { FaEnvelope } from 'react-icons/fa'
import { useSearchParams } from 'react-router-dom'
const VerifyPage: React.FC = () => {
const nhost = useNhostClient()
const [searchParams] = useSearchParams()
const redirectToVerificationLink = () => {
const ticket = searchParams.get('ticket')
const type = searchParams.get('type')
const redirectTo = searchParams.get('redirectTo')
if (ticket && type && redirectTo) {
window.location.href = `${nhost.auth.url}/verify?ticket=${ticket}&type=${type}&redirectTo=${redirectTo}`
} else {
showNotification({
color: 'red',
title: 'Error',
message: 'An error occured while verifying your account'
})
}
}
return (
<Container>
<Card shadow="sm" p="lg" radius="md" withBorder>
<Stack align="center">
<Text>Please verify your account by clicking the link below.</Text>
<Button leftIcon={<FaEnvelope size={14} />} onClick={redirectToVerificationLink}>
Verify
</Button>
</Stack>
</Card>
</Container>
)
}
export default VerifyPage

View File

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

View File

@@ -0,0 +1,5 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M29.0132 7.30478L17.1202 0.459517C16.5932 0.158403 15.9966 0 15.3895 0C14.7824 0 14.1858 0.158403 13.6588 0.459517C13.1342 0.76236 12.6985 1.19746 12.3951 1.7213C12.0918 2.24514 11.9315 2.83935 11.9303 3.44447V4.33793L11.1551 3.89067C10.6281 3.58956 10.0315 3.43115 9.42437 3.43115C8.81725 3.43115 8.22065 3.58956 7.69369 3.89067C7.16838 4.19405 6.73217 4.63001 6.42879 5.15484C6.1254 5.67968 5.96551 6.27494 5.96514 6.88095V7.77335L5.18991 7.32715C4.66308 7.02623 4.06668 6.86793 3.45976 6.86793C2.85284 6.86793 2.25645 7.02623 1.72961 7.32715C1.20473 7.63003 0.768706 8.06526 0.465167 8.58929C0.161628 9.11332 0.00122225 9.70778 0 10.3132L0 31.7563C0.0007938 32.0609 0.0873376 32.3593 0.249755 32.6172C0.412172 32.8751 0.643921 33.0822 0.918553 33.2149C1.19318 33.3476 1.49964 33.4005 1.80294 33.3676C2.10624 33.3347 2.39417 33.2173 2.63388 33.0288L8.53077 28.3922L17.6267 33.6252C17.8748 33.7656 18.1552 33.8394 18.4403 33.8394C18.7255 33.8394 19.0058 33.7656 19.254 33.6252C19.7551 33.3355 20.0676 32.7988 20.0676 32.2206V19.3191C20.0657 18.2752 19.7892 17.2501 19.2657 16.3464C18.7423 15.4428 17.9903 14.6924 17.085 14.1703L14.1024 12.4536V3.44767C14.103 3.22174 14.163 2.99993 14.2765 2.80447C14.3899 2.60902 14.5529 2.44679 14.7489 2.33406C14.945 2.22133 15.1673 2.16206 15.3935 2.1622C15.6197 2.16233 15.8419 2.22187 16.0379 2.33483L27.9308 9.1769C28.5067 9.50917 28.9851 9.9866 29.3182 10.5615C29.6513 11.1363 29.8274 11.7884 29.8289 12.4526V28.5211C29.8289 28.979 29.5815 29.4049 29.1838 29.6339L26.0327 31.4474V15.8837C26.0306 14.84 25.754 13.8151 25.2306 12.9116C24.7072 12.0082 23.9552 11.2579 23.0502 10.7359L15.7286 6.5242V9.0193L21.9657 12.6081C22.5417 12.9401 23.0203 13.4175 23.3534 13.9924C23.6866 14.5673 23.8626 15.2195 23.8638 15.8837V32.3814C23.8638 32.9564 24.1751 33.4963 24.6774 33.786C24.9256 33.9263 25.2059 34 25.491 34C25.7762 34 26.0565 33.9263 26.3046 33.786L30.2704 31.5039C31.3367 30.8894 32 29.7468 32 28.5168V12.4483C31.9952 11.4052 31.717 10.3814 31.193 9.47903C30.669 8.57663 29.9174 7.82701 29.0132 7.30478V7.30478ZM15.9952 16.0413C16.5714 16.3734 17.0501 16.851 17.3832 17.4261C17.7164 18.0012 17.8923 18.6537 17.8933 19.3181V31.2877L10.3628 26.9546L12.7802 25.0569C13.1919 24.7358 13.5247 24.325 13.7531 23.8559C13.9816 23.3867 14.0996 22.8716 14.0982 22.3499V14.953L15.9963 16.0424L15.9952 16.0413ZM11.9292 13.7017V22.3456C11.9292 22.7428 11.749 23.1124 11.4376 23.3552L2.16788 30.6392V10.31C2.16821 10.0841 2.22804 9.86224 2.34138 9.66675C2.45471 9.47125 2.61755 9.30897 2.81356 9.19621C3.00956 9.08345 3.23182 9.02418 3.45802 9.02434C3.68422 9.0245 3.9064 9.0841 4.10224 9.19714L5.96514 10.2674V25.5915L8.13303 23.8876V6.87882C8.13324 6.65277 8.19305 6.43076 8.30644 6.23511C8.41983 6.03947 8.5828 5.87708 8.77897 5.76429C8.97513 5.6515 9.19758 5.59227 9.42393 5.59257C9.65029 5.59287 9.87258 5.65268 10.0684 5.76598L11.9292 6.83516V11.2034L9.7624 9.95536V12.4526L11.9314 13.7017H11.9292Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,26 +0,0 @@
import { Card, Container, Divider, SimpleGrid, Title } from '@mantine/core'
export const AuthLayout: React.FC<{
title?: string
footer?: React.ReactNode
children: React.ReactNode
}> = ({ title, footer, children }) => {
return (
<Container>
<Card shadow="sm" p="lg" m="lg">
{title && <Title p="lg">{title}</Title>}
<SimpleGrid cols={1} spacing={6}>
{children}
</SimpleGrid>
</Card>
{footer && (
<>
<Divider my="sm" />
{footer}
</>
)}
</Container>
)
}
export default AuthLayout

View File

@@ -1,42 +0,0 @@
import { Link } from 'react-router-dom'
import { Button, ButtonProps, SharedButtonProps } from '@mantine/core'
const AuthButton: <C = 'button'>(props: ButtonProps<C>) => React.ReactElement = ({
color,
...rest
}) => (
<Button
role="button"
fullWidth
radius="sm"
styles={(theme) => ({
root: {
backgroundColor: color,
'&:hover': {
backgroundColor: color && theme.fn.darken(color, 0.05)
}
},
leftIcon: {
marginRight: 15
}
})}
{...rest}
/>
)
const AuthLink: React.FC<
SharedButtonProps & {
link: string
}
> = ({ link, ...rest }) => {
const isExternal = link.startsWith('http://') || link.startsWith('https://')
return isExternal ? (
<AuthButton component={'a'} href={link} {...rest} />
) : (
<AuthButton component={Link} to={link} {...rest} />
)
}
export default AuthLink

View File

@@ -1,135 +0,0 @@
import { FaFile, FaHouseUser, FaQuestion, FaSignOutAlt, FaLock } from 'react-icons/fa'
import { SiApollographql } from 'react-icons/si'
import { useLocation, useNavigate } from 'react-router'
import { Link } from 'react-router-dom'
import { showNotification } from '@mantine/notifications'
import {
Button,
Card,
Group,
MantineColor,
Navbar,
Text,
ThemeIcon,
UnstyledButton
} from '@mantine/core'
import { useAuthenticated, useElevateSecurityKeyEmail, useSignOut, useUserData } from '@nhost/react'
interface MenuItemProps {
icon: React.ReactNode
color?: MantineColor
label: string
link?: string
action?: () => void
}
const MenuItem: React.FC<MenuItemProps> = ({ icon, color, label, link, action }) => {
const location = useLocation()
const active = location.pathname === link
const Button = (
<UnstyledButton
onClick={action}
sx={(theme) => ({
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: active
? theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 4 : 7]
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.black,
'&:hover': {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0]
}
})}
>
<Group>
<ThemeIcon color={color} variant="outline">
{icon}
</ThemeIcon>
<Text size="sm">{label}</Text>
</Group>
</UnstyledButton>
)
return link ? <Link to={link}>{Button}</Link> : Button
}
const data: MenuItemProps[] = [
{ icon: <FaHouseUser size={16} />, label: 'Home', link: '/' },
{ icon: <FaHouseUser size={16} />, label: 'Profile', link: '/profile' },
{ icon: <FaLock size={16} />, label: 'Secret Notes', link: '/secret-notes' },
{ icon: <FaFile size={16} />, label: 'Storage', link: '/storage' },
{ icon: <SiApollographql size={16} />, label: 'Apollo', link: '/apollo' },
{ icon: <FaQuestion size={16} />, label: 'About', link: '/about' }
]
export default function NavBar() {
const userData = useUserData()
const navigate = useNavigate()
const { signOut } = useSignOut()
const authenticated = useAuthenticated()
const { elevateEmailSecurityKey, elevated } = useElevateSecurityKeyEmail()
const handleElevate = async () => {
if (!authenticated) {
showNotification({
color: 'red',
title: 'Logged out',
message: 'Please login first'
})
return
}
if (userData?.email) {
const { elevated, isError } = await elevateEmailSecurityKey(userData.email)
if (elevated) {
showNotification({
title: 'Success',
message: 'You now have an elevated permission'
})
}
if (isError) {
showNotification({
color: 'red',
title: 'Failed',
message: 'Could not elevate permission'
})
}
}
}
const links = data.map((link) => <MenuItem {...link} key={link.label} />)
return (
<Navbar width={{ sm: 300, lg: 400, base: 100 }} aria-label="main navigation">
<Navbar.Section grow mt="md">
{links}
{authenticated && (
<MenuItem
icon={<FaSignOutAlt />}
label="Sign Out"
action={async () => {
await signOut()
navigate('/', { replace: true })
}}
/>
)}
</Navbar.Section>
<Card p="lg" m="sm">
<Group position="apart">
<span>Elevated permissions: {String(elevated)}</span>
<Button onClick={handleElevate}>Elevate</Button>
</Group>
</Card>
</Navbar>
)
}

View File

@@ -1,29 +0,0 @@
import { FaApple, FaGithub, FaGoogle, FaLinkedin } from 'react-icons/fa/index.js'
import { useProviderLink } from '@nhost/react'
import AuthLink from './AuthLink'
export default function OauthLinks() {
const { github, google, apple, linkedin } = useProviderLink({
redirectTo: `${window.location.origin}/profile`
})
return (
<>
<AuthLink leftIcon={<FaGithub />} link={github} color="#333">
Continue with GitHub
</AuthLink>
<AuthLink leftIcon={<FaGoogle />} link={google} color="#de5246">
Continue with Google
</AuthLink>
<AuthLink leftIcon={<FaApple />} link={apple} color="#333333">
Sign In With Apple
</AuthLink>
<AuthLink leftIcon={<FaLinkedin />} link={linkedin} color="#0073B1">
Sign In With LinkedIn
</AuthLink>
</>
)
}

View File

@@ -1,72 +0,0 @@
import { FormEvent, useState } from 'react'
import { Button, Modal, SimpleGrid, TextInput } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useSignInEmailPasswordless } from '@nhost/react'
export const SignUpPasswordlessForm: React.FC = () => {
const { signInEmailPasswordless } = useSignInEmailPasswordless({ redirectTo: '/profile' })
const [emailVerificationToggle, setEmailVerificationToggle] = useState(false)
const [emailNeedsVerificationToggle, setEmailNeedsVerificationToggle] = useState(false)
const [email, setEmail] = useState('')
const signInEmail = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const result = await signInEmailPasswordless(email)
if (result.isError) {
showNotification({
color: 'red',
title: 'Error',
message: result.error?.message || null
})
} else {
setEmailVerificationToggle(true)
}
}
return (
<SimpleGrid cols={1} spacing={6}>
<Modal
title="Verification email sent"
centered
opened={emailVerificationToggle}
onClose={() => {
setEmailVerificationToggle(false)
}}
>
A verification email has been sent. Please check your inbox and follow the link to complete
authentication. This page will automatically redirect you to the authenticated home page
once the email has been verified.
</Modal>
<Modal
title="Awaiting email verification"
transition="fade"
centered
transitionDuration={600}
opened={emailNeedsVerificationToggle}
onClose={() => {
setEmailNeedsVerificationToggle(false)
}}
>
You need to verify your email first. Please check your mailbox and follow the confirmation
link to complete the registration.
</Modal>
<form onSubmit={signInEmail}>
<TextInput
type="email"
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoFocus
style={{ marginBottom: '0.5em' }}
/>
<Button fullWidth type="submit">
Send a magic link
</Button>
</form>
</SimpleGrid>
)
}
export default SignUpPasswordlessForm

View File

@@ -1,51 +0,0 @@
import { useState } from 'react'
import { Button, Modal, SimpleGrid, TextInput } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useSignInEmailPasswordless } from '@nhost/react'
export const SignUpPasswordlessForm: React.FC = () => {
const { signInEmailPasswordless } = useSignInEmailPasswordless({ redirectTo: '/profile' })
const [emailVerificationToggle, setEmailVerificationToggle] = useState(false)
const [email, setEmail] = useState('')
const signIn = async () => {
const result = await signInEmailPasswordless(email)
if (result.isError) {
showNotification({
color: 'red',
title: 'Error',
message: result.error?.message || null
})
} else {
setEmailVerificationToggle(true)
}
}
return (
<SimpleGrid cols={1} spacing={6}>
<Modal
title="Verification email sent"
centered
opened={emailVerificationToggle}
onClose={() => {
setEmailVerificationToggle(false)
}}
>
A verification email has been sent. Please check your inbox and follow the link to complete
authentication. This page will automatically redirect you to the authenticated home page
once the email has been verified.
</Modal>
<TextInput
type="email"
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Button fullWidth onClick={signIn}>
Continue with email
</Button>
</SimpleGrid>
)
}
export default SignUpPasswordlessForm

View File

@@ -1,53 +0,0 @@
import { Navigate, useLocation } from 'react-router-dom'
import { useAuthenticationStatus, useUserIsAnonymous } from '@nhost/react'
const LoadingComponent: React.FC<React.PropsWithChildren<{ connectionAttempts: number }>> = ({
connectionAttempts
}) => {
if (connectionAttempts > 0) {
return (
<div>
Could not sign in automatically. Retrying to get user information
{Array(connectionAttempts).join('.')}
</div>
)
}
return <div>Loading...</div>
}
export const AuthGate: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const { isLoading, isAuthenticated, connectionAttempts } = useAuthenticationStatus()
const location = useLocation()
if (isLoading) {
return <LoadingComponent connectionAttempts={connectionAttempts} />
}
if (!isAuthenticated) {
return <Navigate to="/sign-in" state={{ from: location }} replace />
}
return <div>{children}</div>
}
export const PublicGate: React.FC<
React.PropsWithChildren<{
/** Set to `true` if you want this route to be accessible to anonymous users */
anonymous?: boolean
}>
> = ({ anonymous, children }) => {
const { isLoading, isAuthenticated, connectionAttempts } = useAuthenticationStatus()
const isAnonymous = useUserIsAnonymous()
const location = useLocation()
if (isLoading) {
return <LoadingComponent connectionAttempts={connectionAttempts} />
}
if (isAuthenticated && !anonymous && isAnonymous) {
return <Navigate to={'/'} state={{ from: location }} replace />
}
return <div>{children}</div>
}

View File

@@ -0,0 +1,26 @@
import { useAuthenticationStatus } from '@nhost/react'
import { LoaderCircle } from 'lucide-react'
import { FC, PropsWithChildren } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
export const AuthGate: FC<PropsWithChildren<unknown>> = ({ children }) => {
const location = useLocation()
const { isLoading, isAuthenticated } = useAuthenticationStatus()
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center h-screen gap-4">
<LoaderCircle className="w-10 h-10 animate-spin-fast text-slate-500" />
<span className="max-w-md text-center">
Could not sign in automatically. Retrying to get user information
</span>
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/sign-in" state={{ from: location }} replace />
}
return <>{children}</>
}

View File

@@ -0,0 +1,58 @@
import { SiApple, SiGithub, SiGoogle, SiLinkedin } from '@icons-pack/react-simple-icons'
import { Link } from 'react-router-dom'
import { useProviderLink } from '@nhost/react'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
export default function OAuthLinks() {
const { github, apple, google, linkedin } = useProviderLink({
redirectTo: window.location.origin
})
return (
<div className="flex flex-col w-full max-w-md space-y-2">
<Link
to={github}
className={cn(
buttonVariants({ variant: 'link' }),
'bg-[#131111] text-white hover:opacity-90 hover:no-underline'
)}
>
<SiGithub className="w-4 h-4" />
<span className="flex-1 text-center">Continue with Github</span>
</Link>
<Link
to={google}
className={cn(
buttonVariants({ variant: 'link' }),
'bg-[#DE5246] text-white hover:opacity-90 hover:no-underline'
)}
>
<SiGoogle className="w-4 h-4" />
<span className="flex-1 text-center">Continue with Google</span>
</Link>
<Link
to={apple}
className={cn(
buttonVariants({ variant: 'link' }),
'bg-[#131111] text-white hover:opacity-90 hover:no-underline'
)}
>
<SiApple className="w-4 h-4" />
<span className="flex-1 text-center">Continue with Apple</span>
</Link>
<Link
to={linkedin}
className={cn(
buttonVariants({ variant: 'link' }),
'bg-[#0073B1] text-white hover:opacity-90 hover:no-underline'
)}
>
<SiLinkedin className="w-4 h-4" />
<span className="flex-1 text-center">Continue with LinkedIn</span>
</Link>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import { useSignInAnonymous } from '@nhost/react'
import { Link, useNavigate } from 'react-router-dom'
import { cn } from '@/lib/utils'
import { Button, buttonVariants } from '@/components/ui/button'
export default function SignInFooter() {
const navigate = useNavigate()
const { signInAnonymous } = useSignInAnonymous()
const anonymousHandler = async () => {
const { isSuccess } = await signInAnonymous()
if (isSuccess) {
navigate('/')
}
}
return (
<p className="text-sm text-center">
Don&lsquo;t have an account?{' '}
<Link to="/sign-up" className={cn(buttonVariants({ variant: 'link' }), 'm-0, p-0')}>
Sign up
</Link>{' '}
or{' '}
<Button variant="link" className="p-0 m-0" onClick={anonymousHandler}>
sign in anonymously
</Button>
</p>
)
}

View File

@@ -0,0 +1,14 @@
import { Link } from 'react-router-dom'
import { cn } from '../../lib/utils'
import { buttonVariants } from '../ui/button'
export default function SignUpFooter() {
return (
<p className="text-sm text-center">
Already have an account{' '}
<Link to="/sign-in" className={cn(buttonVariants({ variant: 'link' }), 'p-0')}>
Sign in
</Link>
</p>
)
}

View File

@@ -1,220 +0,0 @@
import {
DeleteNoteMutation,
InsertNoteMutation,
NotesListQuery,
SecurityKeysQuery
} from 'src/generated'
import { gql, useMutation } from '@apollo/client'
import {
Button,
Card,
Container,
Grid,
Loader,
TextInput,
Title,
Group,
ActionIcon
} from '@mantine/core'
import { useInputState } from '@mantine/hooks'
import { showNotification } from '@mantine/notifications'
import { useAuthQuery } from '@nhost/react-apollo'
import { useElevateSecurityKeyEmail, useUserData } from '@nhost/react'
import { FaTrash } from 'react-icons/fa'
import { SECURITY_KEYS_LIST } from 'src/utils'
import { useState } from 'react'
const NOTES_LIST = gql`
query notesList {
notes {
id
content
}
}
`
const INSERT_NOTE = gql`
mutation insertNote($content: String!) {
insertNote(object: { content: $content }) {
id
content
}
}
`
const DELETE_NOTE = gql`
mutation deleteNote($noteId: uuid!) {
deleteNote(id: $noteId) {
id
content
}
}
`
export const NotesPage: React.FC = () => {
const userData = useUserData()
const { loading, data } = useAuthQuery<NotesListQuery>(NOTES_LIST, {
pollInterval: 5000,
fetchPolicy: 'cache-and-network'
})
const [content, setContent] = useInputState('')
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const [userHasSecurityKey, setUserHasSecurityKey] = useState(false)
useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, {
variables: { userId: userData?.id },
onCompleted: ({ authUserSecurityKeys }) => {
setUserHasSecurityKey(authUserSecurityKeys?.length > 0)
}
})
const [addNoteMutation] = useMutation<InsertNoteMutation>(INSERT_NOTE)
const [deleteNoteMutation] = useMutation<DeleteNoteMutation>(DELETE_NOTE)
const checkElevatedPermission = async () => {
if (!elevated && userHasSecurityKey) {
const { elevated } = await elevateEmailSecurityKey(userData?.email as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
}
}
const add = async () => {
if (!content) return
try {
await checkElevatedPermission()
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
return
}
addNoteMutation({
variables: { content },
onCompleted: () => setContent(''),
onError: (error) => {
showNotification({
color: 'red',
title: error.networkError ? 'Network error' : 'Error',
message: error.message
})
},
update: (cache, { data }) => {
cache.modify({
fields: {
notes(existingNotes = []) {
const newNoteRef = cache.writeFragment({
data: data?.insertNote,
fragment: gql`
fragment NewNote on notes {
id
content
}
`
})
return [...existingNotes, newNoteRef]
}
}
})
}
})
}
const deleteNote = async (noteId: string) => {
if (!noteId) return
try {
await checkElevatedPermission()
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
return
}
deleteNoteMutation({
variables: { noteId },
onCompleted: () => setContent(''),
onError: (error) => {
showNotification({
color: 'red',
title: error.networkError ? 'Network error' : 'Error',
message: error.message
})
},
update: (cache, { data }) => {
const deletedNoteId = data?.deleteNote?.id
if (deletedNoteId) {
cache.modify({
fields: {
notes(existingNotes = [], { readField }) {
// @ts-ignore
return existingNotes.filter((noteRef) => noteId !== readField('id', noteRef))
}
}
})
}
}
})
}
return (
<Container>
{loading && <Loader />}
<Card shadow="sm" p="lg" m="sm">
<Title>Secret Notes</Title>
<Grid>
<Grid.Col span={10}>
<TextInput
value={content}
onChange={setContent}
autoFocus
onKeyDown={(e) => e.code === 'Enter' && add()}
/>
</Grid.Col>
<Grid.Col span={2}>
<Button
fullWidth
onClick={(e: React.MouseEvent) => {
e.preventDefault()
add()
}}
>
Add
</Button>
</Grid.Col>
</Grid>
<ul style={{ paddingLeft: 12 }}>
{data?.notes.map((note) => (
<li key={note.id}>
<Group position="apart">
<span>{note.content}</span>
<ActionIcon
size={21}
onClick={(event) => {
event.preventDefault()
deleteNote(note.id)
}}
>
<FaTrash />
</ActionIcon>
</Group>
</li>
))}
</ul>
</Card>
</Container>
)
}

View File

@@ -0,0 +1,91 @@
import { useState } from 'react'
import { gql } from '@apollo/client'
import { useChangeEmail, useElevateSecurityKeyEmail, useUserEmail, useUserId } from '@nhost/react'
import { useAuthQuery } from '@nhost/react-apollo'
import { toast } from 'sonner'
import { Button } from '../ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
import { Input } from '../ui/input'
export const ChangeEmail: React.FC = () => {
const userId = useUserId()
const email = useUserEmail()
const [newEmail, setNewEmail] = useState(email || '')
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const [userHasSecurityKey, setUserHasSecurityKey] = useState(false)
useAuthQuery<{
authUserSecurityKeys: {
id: string
nickname?: string
}[]
}>(
gql`
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}
`,
{
variables: { userId },
onCompleted: ({ authUserSecurityKeys }) => {
setUserHasSecurityKey(authUserSecurityKeys?.length > 0)
}
}
)
const { changeEmail } = useChangeEmail({
redirectTo: '/profile'
})
const change = async () => {
if (newEmail && email === newEmail) {
toast.error('You need to set a different email as the current one')
return
}
if (!elevated && userHasSecurityKey) {
try {
const { elevated } = await elevateEmailSecurityKey(email as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
} catch {
toast.error('Could not elevate permissions')
return
}
}
const result = await changeEmail(newEmail)
if (result.needsEmailVerification) {
toast.info(`Please check your inbox and follow the link to confirm the email change.`)
}
if (result.error) {
toast.error(result.error.message)
}
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between p-6">
<CardTitle>Change email</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-row gap-2">
<Input
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder="New email"
/>
<Button onClick={change}>Change</Button>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,18 +1,18 @@
import { useEffect, useState } from 'react'
import { Button, Card, Grid, PasswordInput, Title } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { gql } from '@apollo/client'
import {
useChangePassword,
useElevateSecurityKeyEmail,
useUserEmail,
useUserId
} from '@nhost/react'
import { SecurityKeysQuery } from 'src/generated'
import { SECURITY_KEYS_LIST } from 'src/utils'
import { useAuthQuery } from '@nhost/react-apollo'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
export const ChangePassword: React.FC = () => {
export default function ChangePassword() {
const userEmail = useUserEmail()
const userId = useUserId()
const [password, setPassword] = useState('')
@@ -20,7 +20,22 @@ export const ChangePassword: React.FC = () => {
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const [userHasSecurityKey, setUserHasSecurityKey] = useState(false)
const { data } = useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, { variables: { userId } })
const { data } = useAuthQuery<{
authUserSecurityKeys: {
id: string
nickname?: string
}[]
}>(
gql`
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}
`,
{ variables: { userId } }
)
useEffect(() => {
const authUserSecurityKeys = data?.authUserSecurityKeys
@@ -38,47 +53,39 @@ export const ChangePassword: React.FC = () => {
if (!elevated) {
throw new Error('Permissions were not elevated')
}
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
} catch {
toast.error('Could not elevate permissions')
return
}
}
const result = await changePassword(password)
if (result.isSuccess) {
showNotification({
message: `Password changed successfully.`
})
toast.success(`Password changed successfully.`)
}
if (result.error) {
showNotification({
color: 'red',
title: 'Error',
message: result.error.message
})
toast.error(result.error.message)
}
}
return (
<Card shadow="sm" p="lg" m="sm">
<Title>Change password</Title>
<Grid>
<Grid.Col>
<PasswordInput
<Card>
<CardHeader className="flex flex-row items-center justify-between p-6">
<CardTitle>Change password</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-row gap-2">
<Input
value={password}
type="password"
onChange={(e) => setPassword(e.target.value)}
placeholder="New password"
/>
</Grid.Col>
<Grid.Col>
<Button onClick={change} fullWidth>
Change
</Button>
</Grid.Col>
</Grid>
<Button onClick={change}>Change</Button>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,63 @@
import { buttonVariants } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { cn } from '@/lib/utils'
import { gql } from '@apollo/client'
import { SiGithub } from '@icons-pack/react-simple-icons'
import { useProviderLink } from '@nhost/react'
import { useAuthQuery } from '@nhost/react-apollo'
import { LoaderCircle } from 'lucide-react'
import { Link } from 'react-router-dom'
export default function ConnectGithub() {
const { github } = useProviderLink({
connect: true,
redirectTo: `${window.location.origin}/profile`
})
const { data, loading } = useAuthQuery<{
authUserProviders: {
id: string
providerId: string
}[]
}>(gql`
query getAuthUserProviders {
authUserProviders {
id
providerId
}
}
`)
const isGithubConnected = data?.authUserProviders?.some((item) => item.providerId === 'github')
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between p-6">
<CardTitle>Connect with Github</CardTitle>
</CardHeader>
<CardContent>
{!loading && isGithubConnected && (
<div className="flex flex-row items-center gap-2 w-fit">
<SiGithub className="w-4 h-4" />
<span className="flex-1 text-center">Github connected</span>
</div>
)}
{!loading && !isGithubConnected && (
<Link
to={github}
className={cn(
buttonVariants({ variant: 'link' }),
'bg-[#131111] text-white hover:opacity-90 hover:no-underline gap-2'
)}
>
<SiGithub className="w-4 h-4" />
<span className="flex-1 text-center">Continue with Github</span>
</Link>
)}
{loading && <LoaderCircle className="w-5 h-5 animate-spin-fast" />}
</CardContent>
</Card>
)
}

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