Compare commits

..

9 Commits

Author SHA1 Message Date
github-actions[bot]
5fed49e05b chore: update versions (#3370)
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/hasura-storage-js@2.8.0

### Minor Changes

-   aee9a80: chore: update typescript version to the latest

## @nhost/nhost-js@3.3.0

### Minor Changes

-   aee9a80: chore: update typescript version to the latest

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/hasura-storage-js@2.8.0

## @nhost/apollo@9.0.0

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

## @nhost/react-apollo@18.0.1

### Patch Changes

-   @nhost/apollo@9.0.0
-   @nhost/react@3.11.1

## @nhost/react-urql@15.0.1

### Patch Changes

-   @nhost/react@3.11.1

## @nhost/nextjs@2.2.9

### Patch Changes

-   @nhost/react@3.11.1

## @nhost/react@3.11.1

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

## @nhost/vue@2.9.7

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

## @nhost/dashboard@2.33.0

### Minor Changes

-   aee9a80: chore: update typescript version to the latest
-   5ef3f76: chore (dashboard): Use the new SDK in the Dashboard

### Patch Changes

- 9ed8ce8: fix (dashboard): Request new Mfa ticket after an invalid totp
when signing in
- fd3b5c7: fix (dashboard): Limit new project's name to a maximum of 32
charachters in E2E tests

## @nhost/docs@2.33.0

### Minor Changes

-   4ca9641: feat: added cloud development documentation

## @nhost-examples/cli@0.3.23

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

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

### Patch Changes

-   @nhost/react@3.11.1
-   @nhost/react-apollo@18.0.1

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

### Patch Changes

-   @nhost/react@3.11.1

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

### Patch Changes

-   @nhost/react@3.11.1
-   @nhost/react-urql@15.0.1

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

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

## @nhost-examples/nextjs@0.4.9

### Patch Changes

-   @nhost/react@3.11.1
-   @nhost/react-apollo@18.0.1
-   @nhost/nextjs@2.2.9

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

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

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

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

## @nhost-examples/sveltekit@0.8.2

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0

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

### Patch Changes

-   @nhost/react@3.11.1
-   @nhost/react-apollo@18.0.1

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

### Patch Changes

-   @nhost/react@3.11.1

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

### Patch Changes

-   @nhost/react@3.11.1
-   @nhost/react-apollo@18.0.1

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

### Patch Changes

-   Updated dependencies [aee9a80]
    -   @nhost/nhost-js@3.3.0
    -   @nhost/apollo@9.0.0
    -   @nhost/vue@2.9.7

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

### Patch Changes

-   @nhost/apollo@9.0.0
-   @nhost/vue@2.9.7

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-01 13:00:12 +02:00
robertkasza
aee9a80ac8 chore: update typescript to latest version (#3376)
### **PR Type**
Enhancement, Other


___

### **Description**
- Update TypeScript to version 5.8.3

- Upgrade react-merge-refs to version 3.0.2

- Refactor import syntax for mergeRefs

- Update tsconfig settings for modern JavaScript


___



### **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>11
files</summary><table>
<tr>
<td><strong>ControlledAutocomplete.tsx</strong><dd><code>Update import
syntax for mergeRefs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-e1bbef866e556facc768a4239b443e193f460321689e368fcaae31c1a7c90478">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ControlledCheckbox.tsx</strong><dd><code>Update import and
type reference for mergeRefs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-6f7d29735900a65f96663da4719bf151c5c98224c0a4e39d84b9e5e1db6b8c42">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ControlledSelect.tsx</strong><dd><code>Update import and
type reference for mergeRefs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-4dda68ad8ea568203b515c9cb81eabd05109f55b10e96712924744aba8c22468">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ControlledSwitch.tsx</strong><dd><code>Update import syntax
for mergeRefs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-173b17a003d4d330b2a7157258d0d34cbbbac6ae10840245294f22a8e6ef89ed">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>Checkbox.tsx</strong><dd><code>Update ForwardedRef type for
Checkbox component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-16e6ada0fb8f9c0ecaa00fcfc61c166d0c4c051efe5345d58ee5e4ab618ca0c5">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>Input.tsx</strong><dd><code>Update import syntax for
mergeRefs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-16643cae2ae39d51faa2ed4d7e045f4ec2af31ec5f91768cff742e0364400cb1">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>Radio.tsx</strong><dd><code>Update ForwardedRef type for
Radio component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-cab2e6a6d80963fb89d93b52ac228ab8f2e2d9d60d3d2567547da562e922ac63">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>LogsServiceFilter.tsx</strong><dd><code>Update ForwardedRef
type for LogsServiceFilter</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-a590a7298a9f040df9f26c4eb37d10fc36f47c32996f71aec47796f08c44e892">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ReferencedSchemaSelect.tsx</strong><dd><code>Update
ForwardedRef type for ReferencedSchemaSelect</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-450038e1607012a27e047ef0fa3e7c3ba58a4be7750940aa21c7166713b15c76">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DataGrid.tsx</strong><dd><code>Update import syntax for
mergeRefs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-3bc6476aed14d8e4f26134fa452d22c41b6d3ecb0989871a8a99230a82496474">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DataGridBooleanCell.tsx</strong><dd><code>Update
useDataGridCell generic type</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-b700eacab9c73b147e248ce58d47a208c1e499124a20444efd73db7ecb68505f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>getAllocatedResources.test.ts</strong><dd><code>Remove
replicas property from test cases</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-c7db4fcf0770eb6e4eba76db64b1c705404538a3c204ea212028cad591c6d85a">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Configuration
changes</strong></td><td><details><summary>4 files</summary><table>
<tr>
<td><strong>tsconfig.base.json</strong><dd><code>Update TypeScript
compiler options for ES2022</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-1542b47836923af953e9f14d4db6843171edb79232ba834276394b4ed3035c63">+4/-15</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>tsconfig.json</strong><dd><code>Update TypeScript target and
adjust paths</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-9c3967d850eef2a3a17b5169df09de68ecb0f24ec46f0a9dd1b3ca7c6da7a384">+9/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>tsconfig.json</strong><dd><code>Add typeRoots to compiler
options</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; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-aa2c7a9ede2c1897e087efac48742cf228b8af42e73dec31ec6403461b98c63a">+6/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>tsconfig.json</strong><dd><code>Add typeRoots to compiler
options</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; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-6b59df76b8f353267e50f13e5fdd23ff7490a417bdc9b7e9f4e94aaafa448dcd">+7/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Dependencies</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>package.json</strong><dd><code>Upgrade react-merge-refs to
version 3.0.2</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-2d8d55c799cd71f1b35e831f075f8178ed1734c4820a2ad548b4dd24d6938d7c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Upgrade TypeScript to version
5.8.3</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3376/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

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

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-07-01 11:07:17 +02:00
robertkasza
5ef3f76ea0 chore (dashboard): use new sdk on the dashboard (#3374)
### **PR Type**
Enhancement, Bug fix


___

### **Description**
- Migrate from @nhost/nextjs to @nhost/nhost-js-beta

- Update authentication flow and components

- Refactor Apollo client and GraphQL operations

- Implement new session management and storage


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Error
handling</strong></td><td><details><summary>1 files</summary><table>
<tr>
<td><strong>MfaOtpForm.tsx</strong><dd><code>Refactor MFA OTP form error
handling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-88ee3610a0658d5eead85db025a5e91e74a4d2f2a836adf7eb44ff80888a613b">+11/-9</a>&nbsp;
&nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>66
files</summary><table>
<tr>
<td><strong>AccountMenu.tsx</strong><dd><code>Update account menu to use
new SDK hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-57e70c7e6a22a5ab5271b2f36a54eabf544d9f62cd18dae83e2e89b125e77e0c">+10/-10</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>AuthenticatedLayout.tsx</strong><dd><code>Refactor
authenticated layout with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-2d69ccffd267658f76d77a864cdece93fc222e08f6025955795fc6f4697f60e7">+15/-13</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>MobileNav.tsx</strong><dd><code>Update mobile navigation to
use new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-88408885daaec8805bd085b53462c9f2d95db32f7e523912837a8167211b4fb2">+14/-8</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>UnauthenticatedLayout.tsx</strong><dd><code>Refactor
unauthenticated layout with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-54ee5ad9c01e99ffb05218020a6b97d091cd97cc53ad27e950480a3e675f2220">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ErrorToast.tsx</strong><dd><code>Update error toast to use
new SDK hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-189ba99303a20e964b5e3f3d6f1cf95c6376780a59604d1dee98aa84d9a2a9dc">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DisableMfaButton.tsx</strong><dd><code>Refactor MFA disable
button with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-8548174093cd8b3bcd754b630c0bcc946e7cdc80176f8e0f0540fd60c9e47486">+17/-18</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>MfaQRCodeAndTOTPSecret.tsx</strong><dd><code>Update MFA
enable process with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-91870bba41e6e807ced8d185a9d61282540ca82741a938b12f20c4f452bdabf8">+30/-29</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>useMfaEnabled.ts</strong><dd><code>Refactor MFA enabled hook
with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-31d2af339a8dd32beff8cce79962fa0dd23b6c89687b21aa75663ebeccb0b154">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>CreatePATForm.tsx</strong><dd><code>Update PAT creation form
with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-75aecb06ebb3e3cd0de6bf253af6966e245e46e9b739314d49073ba2c80a3a90">+6/-6</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DeleteAccount.tsx</strong><dd><code>Refactor account
deletion with new SDK hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3d84927ffa4b91d986ff6c6f601b3476503220e1c1d8cde25ebf72c8d0ed6b9e">+4/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DisplayNameSetting.tsx</strong><dd><code>Update display name
setting with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-a1daec18d5c3196aee5b2c5303db5654724f8d37cfa427594951a4d02fbe32db">+4/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>EmailSetting.tsx</strong><dd><code>Refactor email setting
with new SDK hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-98bdf4ebec67ab2b4cd475c9df16a39a66505da961a8448eb5e41a33544dcb38">+4/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useOnChangePasswordHandler.ts</strong><dd><code>Update
password change handler with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-be0c02de792e4ba1b71258eb3992bbc531bc37658cbad0e01e2db4419a9285f1">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useOnAddNewSecurityKeyHandler.ts</strong><dd><code>Refactor
security key addition with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3514a6d1514269a83f37fc25e9cb24add9d5d74f9cf3341293c0e0f2a4c2e286">+10/-2</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useRemoveSecurityKey.ts</strong><dd><code>Update security
key removal with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-5683e00a14f39018d8fe58a3116c2a8ea6d2f2a83abb2177bbf0ee8ddf0f97b5">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>SocialProvidersSettings.tsx</strong><dd><code>Refactor
social providers settings with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-82d7c0c9eb3a23586998b6eadff9e56b123b14d03179212ca82439d3bdcd6e96">+14/-5</a>&nbsp;
&nbsp; </td>

</tr>

<tr>

<td><strong>useActionWithElevatedPermissions.ts</strong><dd><code>Update
elevated permissions hook with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3390879c4a0cd70aa1db6672a1607c5c2444c0bf653b711d73eda8ee466aa61a">+10/-16</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>useElevatedPermissions.ts</strong><dd><code>Refactor
elevated permissions hook with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-c1e4f573300c771149cc2e59918c9acf2ae5f8a6680800a899707c70800ba144">+11/-10</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>useGetSecurityKeys.ts</strong><dd><code>Update security keys
fetching with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-1f9fed870cab61f15e304342e4913edab0f5537eeb6230070de4b4f7173fa138">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useGithubAuthentication.ts</strong><dd><code>Refactor GitHub
authentication with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-fad4875e0f07391dadcfc7e2dd481cafd5172dbb740c47e56fa75beb271618e1">+16/-8</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>SignInWithSecurityKey.tsx</strong><dd><code>Update security
key sign-in with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-dc892fd3d9fd3cc7efca35c813cea43c63aa691b1d55d376ac69a2d75065bde9">+9/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>VerifyEmailDialog.tsx</strong><dd><code>Refactor email
verification dialog for security key</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-24699641924d25d9b2d72b7df5da44d837e1c1e5c77b9f4b00f7c07d12c72c42">+4/-10</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useSignInWithSecurityKey.ts</strong><dd><code>Update
security key sign-in hook with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-14ce9eae9e1ec03512bdd55198fbce47a81ce8ce769d002164926d2cc76e91aa">+34/-21</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>MfaSignInOtpForm.tsx</strong><dd><code>Refactor MFA OTP form
for sign-in</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; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-91eba232beb0543b1e972ed9a21a0be797ed94b720487834bb3316a5dbd732f5">+1/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>SignInWithEmailAndPassword.tsx</strong><dd><code>Update
email/password sign-in with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-a2b70644663baf4f6f2cdffd846d4d743a5ca1f2a64c4b278b6f04c6c5c92161">+34/-12</a>&nbsp;
</td>

</tr>

<tr>

<td><strong>SignInWithEmailAndPasswordForm.tsx</strong><dd><code>Refactor
email/password sign-in form with loading state</code>&nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-a07fd6bd20c97d0c9c875e690cd3a80068fc58f74d3579feb210e189d32f5031">+5/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>

<td><strong>useOnSignInWithEmailAndPasswordHandler.ts</strong><dd><code>Update
email/password sign-in handler with new SDK</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-1a253bfc02c3267ab1c6b58c07aa06142b7e711d613b672c8420ff2861b12d27">+35/-37</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>useRequestNewMfaTicket.ts</strong><dd><code>Refactor MFA
ticket request with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-64a3a91cf75c8faf5bf6a9fdd23978659d68888744a92f82602b1a2f7290c1f6">+12/-11</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>useOnSignUpWithPasswordHandler.ts</strong><dd><code>Update
email/password sign-up handler with new SDK</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-d1a80c5b1076129735ffff9ea879ca8c8fe88e548d06e98a1fb6bfd7147dae01">+14/-19</a>&nbsp;
</td>

</tr>

<tr>

<td><strong>useSignupWithSecurityKeyHandler.ts</strong><dd><code>Refactor
security key sign-up with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-cef4f710ea89c67e27e9fe77db2d2ebc6d774657e0671b21b7353f3e927126bd">+24/-17</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>CreateOrgFormDialog.tsx</strong><dd><code>Update
organization creation dialog with new SDK</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-9a1ed9e851328393b81356d80ade3509016aa55c254ed1f4deb692b0bd96f02e">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>TransferProjectForm.tsx</strong><dd><code>Refactor project
transfer form with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3324c79d8b4d48777467132ba0f13a95d4b0f1a9fbb4df9fd7f67735ac40cbbd">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>Soc2Download.tsx</strong><dd><code>Update SOC2 report
download with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3768eb3fc718d4780028c34b5c76388e8d93cbbac94868f82c1a262fb9cc1100">+12/-19</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>AnnouncementsTray.tsx</strong><dd><code>Refactor
announcements tray with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-88fdcce3e90fa9e4d172858ae702855f86e6ece724ba443d8a6ed918999a1630">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>NotificationsTray.tsx</strong><dd><code>Update notifications
tray with new SDK hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-8b559ee1d3176203e8a4e1588924d57944d09d792117ed578b27cd5401ee5d4f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>OrgMember.tsx</strong><dd><code>Refactor organization member
component with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-a50b1baab968a7d3bd1459ba01107a13bd25e5077b6ad49a0d7e9dd88992276a">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useFinishOrgCreation.ts</strong><dd><code>Update
organization creation finish hook with new SDK</code>&nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3b8bf7608ab36d8ab0df895e400f0d2d9e29fad2055b40b33d8d9912a27c99c3">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useIsOrgAdmin.ts</strong><dd><code>Refactor organization
admin check with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-216a850dad38e829d0a8892c34d87426cd68f10c92f4c647673667dbbd11464d">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>MessageBox.tsx</strong><dd><code>Update dev assistant
message box with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-d4dd97b5a55f8246836226333d35a1c18c2907e47bdd2654707ed43ac54f2fb8">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ApplicationInfo.tsx</strong><dd><code>Refactor application
info component with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-7372ad22d70c3c354d8e0dd442eb7e49f70f65a386b934b6eee7f8c4b89c3a3f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ApplicationPausedBanner.tsx</strong><dd><code>Update paused
application banner with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-fa94285530f7118d9f27a2d9088f5cf6ba71879d14957d91eb01dba16b6b6f1c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>RemoveApplicationModal.tsx</strong><dd><code>Refactor
application removal modal with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-e454a42c12dcbfcfaa463ab3421037408634e3a539f460525c79d68adfc118ab">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useAppPausedReason.ts</strong><dd><code>Update app paused
reason hook with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-51d220574b3da84f08d2cb134682172ed11b908c4d855ccc8d9de30805921a00">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useCheckProvisioning.ts</strong><dd><code>Refactor
provisioning check hook with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-e1758bb8d3381f814d6619dc33eee8b36e39d2fcb6486d5c8cc3c46bbe62c555">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useIsCurrentUserOwner.ts</strong><dd><code>Update current
user ownership check with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3941cc4f23c66f12e94850e88e05ca142a627ab2d9ec797ff757dab679c58c0f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useProjectRedirectWhenReady.ts</strong><dd><code>Refactor
project redirect hook with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-a234bc908266de3091b23b5134a01fd769f96759eb52aa108d2ad4b796b0303f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ResetDatabasePasswordSettings.tsx</strong><dd><code>Update
database password reset with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-46fc60a26a2de3efb98e9778b1c6e82d62823ae5c7534037eb120728cba26288">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DeploymentListItem.tsx</strong><dd><code>Refactor deployment
list item with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-2a548c457ff2ab8fc1bee326a6a3b5eae9d0d6eb18f5ae95bbdb437c3f6b0a73">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>

<td><strong>SystemEnvironmentVariableSettings.tsx</strong><dd><code>Update
system environment variables with new SDK</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-b952daa2a34e49a14c5a471477fa2d50583091e420d88a3b941503b092d18e5c">+7/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>BaseDirectorySettings.tsx</strong><dd><code>Refactor base
directory settings with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-50bcccdf949a19ce69fa86acdd63b5291fa2beaba07191a62c87d40ea5b94e88">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DeploymentBranchSettings.tsx</strong><dd><code>Update
deployment branch settings with new SDK</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-d8fc80cc734f593c686f873536856bf9103efb1115ca865709bbeb7bd940895e">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useAppClient.ts</strong><dd><code>Refactor app client hook
with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-0aa83222c0e0eac6f0058070de2b199e5e78514cbba405eb98d3693329a93e65">+13/-6</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useCurrentOrg.tsx</strong><dd><code>Update current
organization hook with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-97e1dbde4beece374834d5e81dd56fddeb5f1756a3358f6afecf88df93f6b0b0">+60/-3</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useOrgs.ts</strong><dd><code>Refactor organizations hook
with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-995629b13bac07ec5c0e79caa8f5f8df19fc842d1b8cfe8fd2b1fcd9448868c4">+5/-6</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useProject.ts</strong><dd><code>Update project hook with new
SDK</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></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-ef96f340af7a87a1fc60c42d8f4de846a2a54fde830a9461c64cfbc99dc11128">+8/-10</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useProjectLogs.ts</strong><dd><code>Update project logs hook
with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-10efc67700b3f024dd03442eacd339802e951696d04caa76bd5a864bd5c7c83f">+2/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useProjectWithState.ts</strong><dd><code>Refactor project
with state hook using new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-4fa0e580d9f12e35ff5d2751597bf443bd055cd1c854cf6b356110724d424188">+7/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DataGridPreviewCell.tsx</strong><dd><code>Update data grid
preview cell with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-d7bffe5896d2c9bac505fa9675790c59549d4fb35a2ad0cce903cc0aa31a8321">+7/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>FilesDataGrid.tsx</strong><dd><code>Refactor files data grid
with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-18c8df727e1a4fc6a94d03bd4a3a7a8cb3ad44d754803c4c7988c1c00a4b7caf">+20/-15</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>FilesDataGridControls.tsx</strong><dd><code>Update files
data grid controls with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-b85b40168e9c149331a68cb1a0cbec570c75233fa34385945e094b8f4c032974">+15/-30</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>useAccessToken.ts</strong><dd><code>Add new hook for
accessing token with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-1e7322930841a7ee092650eedafab9b83a8eb2d376aa299f3dfd790304a7ad21">+9/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useDecodedAccessToken.ts</strong><dd><code>Add new hook for
decoding access token</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-ff3f112dfd0dae55404d17d972f5309d9a8cf0859222061e1c6f10e52c442390">+28/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useElevateEmail.ts</strong><dd><code>Add new hook for email
elevation with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-b38d4c8f500fe9f40d4649e78907fc2f8691bd950b377e85be9142226b2b3460">+26/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useHasuraClaims.ts</strong><dd><code>Add new hook for Hasura
claims with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-f5352373468a509527f74db8d16a632905284009ed386ea50cd9fb7f42817431">+11/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useUserData.ts</strong><dd><code>Add new hook for user data
with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-3e3f3684aba10abe1d06d0be625a8077efe2e7d6a17b79d5ecddd43cfc190224">+12/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>index.ts</strong><dd><code>Remove old remote application
GraphQL client hook</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-d2d725306920bf5413fc010843e4ca13570b225febb200330e8c6902ae0b085c">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>useProjectLogs.test.ts</strong><dd><code>Refactor project
logs test with new SDK</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3374/files#diff-13d900aa08d06962a09628136b893801ad62a96c3ff89d380c5c4b7ae92d891e">+19/-31</a>&nbsp;
</td>

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

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-06-30 14:31:29 +02:00
David Barroso
4ca9641304 feat (docs): added cloud development documentation (#3377) 2025-06-25 23:24:10 +02:00
robertkasza
fd3b5c77e4 fix (dashboard): Limit new project name to a max of 32 chars (#3371)
### **PR Type**
Bug fix, Tests


___

### **Description**
- Limit new project name to 32 characters in E2E tests

- Update project creation test in upgrade-project.test.ts

- Add changeset for @nhost/dashboard patch


___



### **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>upgrade-project.test.ts</strong><dd><code>Limit project
name length in E2E test</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/e2e/upgrade-project/upgrade-project.test.ts

<li>Modify project name generation to limit to 32 characters<br> <li>
Use <code>slice(0, 32)</code> on faker-generated project name


</details>


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

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>weak-bottles-stare.md</strong><dd><code>Add changeset
for project name length fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/weak-bottles-stare.md

<li>Add new changeset file for @nhost/dashboard patch<br> <li> Document
fix for limiting project name to 32 characters


</details>


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

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

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-06-18 15:50:56 +02:00
robertkasza
9ed8ce8a5e fix (dashboard): request new mfa ticket after error when signing in (#3369)
### **PR Type**
Bug fix


___

### **Description**
- Request new MFA ticket after invalid TOTP

- Improve error handling in MFA flow

- Add comprehensive tests for MfaOtpForm

- Refactor SignInWithEmailAndPassword component


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>MfaOtpForm.test.tsx</strong><dd><code>Add comprehensive
tests for MfaOtpForm component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-0a0a9d4aa607a60fb4f38712686101d583426536ff6c177ea625cf8ce1946971">+519/-0</a>&nbsp;
</td>

</tr>

</table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>5
files</summary><table>
<tr>
<td><strong>MfaOtpForm.tsx</strong><dd><code>Implement MFA ticket
renewal and improve error handling</code>&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-88ee3610a0658d5eead85db025a5e91e74a4d2f2a836adf7eb44ff80888a613b">+39/-12</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>MfaSignInOtpForm.tsx</strong><dd><code>Add
requestNewMfaTicket prop to MfaSignInOtpForm</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-91eba232beb0543b1e972ed9a21a0be797ed94b720487834bb3316a5dbd732f5">+7/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>SignInWithEmailAndPassword.tsx</strong><dd><code>Implement
requestNewMfaTicket function and pass to
MfaSignInOtpForm</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-a2b70644663baf4f6f2cdffd846d4d743a5ca1f2a64c4b278b6f04c6c5c92161">+20/-4</a>&nbsp;
&nbsp; </td>

</tr>

<tr>

<td><strong>useOnSignInWithEmailAndPasswordHandler.ts</strong><dd><code>Add
emailPasswordRef to store credentials for MFA renewal</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-1a253bfc02c3267ab1c6b58c07aa06142b7e711d613b672c8420ff2861b12d27">+19/-2</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useRequestNewMfaTicket.ts</strong><dd><code>Create new hook
for requesting new MFA ticket</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-64a3a91cf75c8faf5bf6a9fdd23978659d68888744a92f82602b1a2f7290c1f6">+28/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Bug
fix</strong></td><td><details><summary>1 files</summary><table>
<tr>
<td><strong>email.tsx</strong><dd><code>Fix page title for sign-in
page</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; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-b5d7db4460066bc114cb766771612d6f908bd6e440f40de98e4ac311a26b50cd">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>lovely-days-whisper.md</strong><dd><code>Add changeset for
MFA ticket renewal fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3369/files#diff-1d7e7e258210abb910bb9c392731a3195ffca03024082c4b357e61c475dd3e3f">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

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

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-06-18 14:02:18 +02:00
Ivan Kuznetsov
e7762cb2b5 fix (examples/nextjs-server-components): added missing await (#3364) 2025-06-17 11:53:38 +02:00
gssakash-nhost
e353d99de8 feat (examples/react-apollo): add more social providers (#3339)
### **User description**
___

### **PR Type**
Enhancement


___

### **Description**
- **Added OAuth Integrations**  
Enabled OAuth for Spotify, Twitch, GitLab, Bitbucket, WorkOS, Discord,
AzureAD, Facebook, Strava, Windows Live, and Twitter.
- **Enhanced UI**  
  Added login buttons for each provider.
- **Updated Configuration**  
Configured OAuth settings in `nhost.toml` and updated the authentication
version as necessary.
- **Included Example Credentials**  
  Added client credentials to `.secrets.example` for easy setup.

___



### **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>oauth-links.tsx</strong><dd><code>Add Spotify OAuth
button to login options</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

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

<li>Imported Spotify icon from react-simple-icons<br> <li> Added Spotify
to useProviderLink hook<br> <li> Implemented Spotify login button with
styling


</details>


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

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>nhost.toml</strong><dd><code>Enable Spotify OAuth in
e2e test project</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/e2e/e2e-tests-project/nhost/nhost.toml

- Enabled Spotify OAuth method


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>.secrets.example</strong><dd><code>Add Spotify
credentials to secrets example</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

examples/react-apollo/.secrets.example

- Added placeholders for Spotify client ID and secret


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>nhost.toml</strong><dd><code>Configure Spotify OAuth in
example project</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/nhost/nhost.toml

<li>Enabled Spotify OAuth method<br> <li> Added configuration for
Spotify client ID and secret


</details>


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

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

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-06-13 11:06:43 +02:00
David Barroso
c4d289a4d5 chore (docs): added limits section (#3367)
### **PR Type**
Documentation, Enhancement


___

### **Description**
- Added new 'Limits' section to Functions overview

- Detailed function execution timeout limits by project tier

- Included custom timeout option for Enterprise tier


___



### **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>overview.mdx</strong><dd><code>Add Function Execution
Timeout Limits Section</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

docs/products/functions/overview.mdx

<li>Added new 'Limits' section after deployment information<br> <li>
Listed function execution timeout limits for different project tiers<br>
<li> Included Starter, Pro, Teams, and Enterprise tier limits<br> <li>
Mentioned custom timeout values for Enterprise tier


</details>


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

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

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-06-12 14:24:32 +02:00
198 changed files with 3246 additions and 1148 deletions

View File

@@ -11,20 +11,9 @@
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"moduleResolution": "node",
"target": "ES6",
"target": "ESNext",
"module": "CommonJS",
"lib": [
"es5",
"dom",
"es2015.promise",
"es2015.symbol",
"es2015.iterable",
"es2015.collection",
"es2015.symbol.wellknown",
"es2015.core",
"es2017.object",
"es2017.string"
],
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"resolveJsonModule": true,
"esModuleInterop": true,
"sourceMap": true,
@@ -79,4 +68,4 @@
"**/*/__tests__",
"**/*/__mocks__"
]
}
}

View File

@@ -1,8 +1,9 @@
import { NhostProvider } from '@/providers/nhost';
import '@fontsource/inter';
import '@fontsource/inter/500.css';
import '@fontsource/inter/700.css';
import { CssBaseline, ThemeProvider } from '@mui/material';
import { NhostClient, NhostProvider } from '@nhost/nextjs';
import { createClient } from '@nhost/nhost-js-beta';
import { NhostApolloProvider } from '@nhost/react-apollo';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Buffer } from 'buffer';
@@ -58,7 +59,9 @@ export const decorators = [
</NhostApolloProvider>
),
(Story) => (
<NhostProvider nhost={new NhostClient({ subdomain: 'local' })}>
<NhostProvider
nhost={createClient({ subdomain: 'local', region: 'local' })}
>
<Story />
</NhostProvider>
),

View File

@@ -1,5 +1,17 @@
# @nhost/dashboard
## 2.33.0
### Minor Changes
- aee9a80: chore: update typescript version to the latest
- 5ef3f76: chore (dashboard): Use the new SDK in the Dashboard
### Patch Changes
- 9ed8ce8: fix (dashboard): Request new Mfa ticket after an invalid totp when signing in
- fd3b5c7: fix (dashboard): Limit new project's name to a maximum of 32 charachters in E2E tests
## 2.32.0
### Minor Changes

View File

@@ -14,6 +14,7 @@ export const test = base.extend<{ authenticatedNhostPage: Page }>({
);
await use(page);
// update the context to get the new refresh token
await page.waitForLoadState('networkidle');
await page.context().storageState({ path: AUTH_CONTEXT });
await page.close();
},

View File

@@ -25,7 +25,7 @@ test.beforeAll(async ({ browser }) => {
test('should create a new project', async () => {
await gotoUrl(page, `/orgs/${getFreeUserStarterOrgSlug()}/projects/new`);
const projectName = faker.lorem.words(3);
const projectName = faker.lorem.words(3).slice(0, 32);
await page.getByLabel('Project Name').fill(projectName);
await page.getByText('Create Project').click();
@@ -34,7 +34,7 @@ test('should create a new project', async () => {
expect(page.getByText('Internal info')).toBeVisible();
await page.waitForSelector('button:has-text("Upgrade project")', {
timeout: 120000,
timeout: 180000,
});
const newProjectSlug = getProjectSlugFromUrl(page.url());

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "2.32.0",
"version": "2.33.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -45,8 +45,7 @@
"@mui/material": "^5.15.14",
"@mui/system": "^5.15.14",
"@mui/x-date-pickers": "^5.0.20",
"@nhost/nextjs": "workspace:*",
"@nhost/react-apollo": "workspace:*",
"@nhost/nhost-js-beta": "npm:@nhost/nhost-js@5.0.0-beta.7",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.2",
@@ -63,6 +62,7 @@
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.2",
"@segment/analytics-next": "^1.77.0",
"@simplewebauthn/browser": "^9.0.1",
"@stripe/react-stripe-js": "^2.6.2",
"@stripe/stripe-js": "^1.54.2",
"@tailwindcss/forms": "^0.5.7",
@@ -88,6 +88,7 @@
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.16.0",
"just-kebab-case": "^4.2.0",
"jwt-decode": "^4.0.0",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.416.0",
"next": "^14.2.26",
@@ -108,7 +109,7 @@
"react-is": "18.2.0",
"react-loading-skeleton": "^2.2.0",
"react-markdown": "^9.0.1",
"react-merge-refs": "^1.1.0",
"react-merge-refs": "^3.0.2",
"react-resizable-layout": "^0.7.2",
"react-table": "^7.8.0",
"recoil": "^0.7.7",

View File

@@ -0,0 +1,518 @@
import { render, screen, TestUserEvent, waitFor } from '@/tests/testUtils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import MfaOtpForm from './MfaOtpForm';
const mocks = vi.hoisted(() => ({
toastError: vi.fn(),
}));
// Mock react-hot-toast
vi.mock('react-hot-toast', async () => {
const actualToast = await vi.importActual<any>('react-hot-toast');
return {
...actualToast,
default: {
...actualToast.default,
error: mocks.toastError,
},
};
});
// Mock the toast style props utility
vi.mock('@/utils/constants/settings', () => ({
getToastStyleProps: vi.fn(() => ({})),
}));
describe('MfaOtpForm', () => {
const mockSendMfaOtp = vi.fn();
const mockRequestNewMfaTicket = vi.fn();
const user = new TestUserEvent();
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllTimers();
});
const defaultProps = {
sendMfaOtp: mockSendMfaOtp,
loading: false,
requestNewMfaTicket: mockRequestNewMfaTicket,
} as any;
describe('Rendering and Initial State', () => {
it('renders with correct initial state', () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
expect(input).toBeInTheDocument();
expect(input).toHaveValue('');
expect(button).toBeInTheDocument();
expect(button).toBeDisabled();
});
it('focuses input on mount', () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
expect(input).toHaveFocus();
});
});
describe('Input Validation and Formatting', () => {
it('only accepts numeric characters', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, 'abc123def456');
expect(input).toHaveValue('123456');
});
it('filters out non-numeric characters in real time', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, '1a2b3c');
expect(input).toHaveValue('123');
});
it('button is disabled when input has fewer than 6 digits', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '12345');
expect(button).toBeDisabled();
});
it('button is enabled when input has exactly 6 digits', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
expect(button).toBeEnabled();
});
it('button is disabled when input has more than 6 digits', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '6123457');
expect(button).toBeDisabled();
});
});
describe('Loading States', () => {
it('disables input and button when loading prop is true', () => {
render(<MfaOtpForm {...defaultProps} loading />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button');
expect(input).toBeDisabled();
expect(button).toBeDisabled();
expect(
screen.getByRole('button', { name: 'Verifying...' }),
).toBeInTheDocument();
});
it('input and button are disabled during submission', async () => {
// Mock sendMfaOtp to return a promise that we can control
const promise = new Promise(() => {}); // Never resolves
mockSendMfaOtp.mockReturnValue(promise);
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
expect(input).toBeDisabled();
expect(button).toBeDisabled();
});
});
describe('Form Submission', () => {
it('triggers sendMfaOtp with correct code on button click', async () => {
mockSendMfaOtp.mockResolvedValue({ success: true });
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
expect(mockSendMfaOtp).toHaveBeenCalledWith('123456');
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
});
it('does not submit when input has fewer than 6 digits', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '12345');
await user.click(button);
expect(mockSendMfaOtp).not.toHaveBeenCalled();
});
it('does not submit multiple times when already submitting', async () => {
let resolvePromise: (value: any) => void;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
mockSendMfaOtp.mockReturnValue(promise);
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
await user.click(button); // Second click should be ignored
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
// Resolve the promise to clean up
resolvePromise!({ success: true });
await waitFor(async () => {
await promise;
});
});
it('manages submission state properly', async () => {
let resolvePromise: (value: any) => void;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
mockSendMfaOtp.mockReturnValue(promise);
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
// During submission
expect(button).toBeDisabled();
expect(input).toBeDisabled();
// Resolve the promise
resolvePromise!({ success: true });
await waitFor(async () => {
await promise;
});
// After submission
await waitFor(() => {
expect(button).not.toBeDisabled();
expect(input).not.toBeDisabled();
});
});
});
describe('Error Handling', () => {
it('displays error toast when sendMfaOtp returns an error', async () => {
const errorMessage = 'Invalid TOTP code';
mockSendMfaOtp.mockRejectedValueOnce({ message: errorMessage });
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
await waitFor(() => {
expect(mocks.toastError).toHaveBeenCalledWith(errorMessage, {});
});
});
it('shows generic error message when no specific error message is provided', async () => {
mockSendMfaOtp.mockRejectedValueOnce({});
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
await waitFor(() => {
expect(mocks.toastError).toHaveBeenCalledWith(
'An error occurred. Please try again.',
{},
);
});
});
it('handles undefined error gracefully', async () => {
mockSendMfaOtp.mockResolvedValue({
error: undefined,
});
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
// Should not throw an error
await waitFor(() => {
expect(mockSendMfaOtp).toHaveBeenCalled();
});
});
});
describe('MFA Ticket Renewal', () => {
it('calls requestNewMfaTicket when ticket is invalid', async () => {
// First call - set ticket as invalid
mockSendMfaOtp.mockRejectedValueOnce({ message: 'Invalid ticket' });
// Second call - should work
mockSendMfaOtp.mockResolvedValueOnce({ success: true });
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
// First submission - creates error and marks ticket invalid
await user.type(input, '123456');
await user.click(button);
await waitFor(() => {
expect(mocks.toastError).toHaveBeenCalled();
});
// Clear input and try again
await user.clear(input);
await user.type(input, '654321');
await user.click(button);
await waitFor(() => {
expect(mockRequestNewMfaTicket).toHaveBeenCalled();
});
});
it('does not call requestNewMfaTicket on first submission', async () => {
mockSendMfaOtp.mockResolvedValue({ success: true });
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
expect(mockRequestNewMfaTicket).not.toHaveBeenCalled();
});
it('works correctly when requestNewMfaTicket is not provided', async () => {
const propsWithoutTicketRenewal = {
sendMfaOtp: mockSendMfaOtp,
loading: false,
} as any;
mockSendMfaOtp.mockRejectedValueOnce({ message: 'Some error' });
render(<MfaOtpForm {...propsWithoutTicketRenewal} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
// Should not throw an error even without requestNewMfaTicket
await waitFor(() => {
expect(mocks.toastError).toHaveBeenCalled();
});
});
});
describe('User Interactions', () => {
it('updates input value correctly when typing', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, '123');
expect(input).toHaveValue('123');
await user.type(input, '456');
expect(input).toHaveValue('123456');
});
it('can clear and retype input value', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, '123456');
expect(input).toHaveValue('123456');
await user.clear(input);
expect(input).toHaveValue('');
await user.type(input, '654321');
expect(input).toHaveValue('654321');
});
it('button triggers submission with valid code', async () => {
mockSendMfaOtp.mockResolvedValue({ success: true });
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
expect(mockSendMfaOtp).toHaveBeenCalledWith('123456');
});
it('submits form when pressing Enter key with valid code', async () => {
mockSendMfaOtp.mockResolvedValue({ success: true });
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, '123456');
await user.type(input, '{Enter}');
expect(mockSendMfaOtp).toHaveBeenCalledWith('123456');
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
});
it('does not submit when pressing Enter with invalid code length', async () => {
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, '12345');
await user.type(input, '{Enter}');
expect(mockSendMfaOtp).not.toHaveBeenCalled();
});
it('does not submit when pressing Enter while loading', async () => {
render(<MfaOtpForm {...defaultProps} loading />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, '123456');
await user.type(input, '{Enter}');
expect(mockSendMfaOtp).not.toHaveBeenCalled();
});
it('does not submit multiple times when pressing Enter while submitting', async () => {
let resolvePromise: (value: any) => void;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
mockSendMfaOtp.mockReturnValue(promise);
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
await user.type(input, '123456');
await user.type(input, '{Enter}');
await user.type(input, '{Enter}'); // Second Enter should be ignored
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
// Clean up
resolvePromise!({ success: true });
await waitFor(async () => {
await promise;
});
});
});
describe('Edge Cases', () => {
it('handles null error message gracefully', async () => {
mockSendMfaOtp.mockRejectedValueOnce({ message: null });
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
await user.click(button);
await waitFor(() => {
expect(mocks.toastError).toHaveBeenCalledWith(
'An error occurred. Please try again.',
{},
);
});
});
it('prevents multiple rapid submissions', async () => {
let resolvePromise: (value: any) => void;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
mockSendMfaOtp.mockReturnValue(promise);
render(<MfaOtpForm {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter TOTP');
const button = screen.getByRole('button', { name: 'Verify' });
await user.type(input, '123456');
// Rapid clicks
await user.click(button);
await user.click(button);
await user.click(button);
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
// Clean up
resolvePromise!({ success: true });
await waitFor(async () => {
await promise;
});
});
it('handles empty input correctly', async () => {
render(<MfaOtpForm {...defaultProps} />);
const button = screen.getByRole('button', { name: 'Verify' });
await user.click(button);
expect(mockSendMfaOtp).not.toHaveBeenCalled();
expect(button).toBeDisabled();
});
});
});

View File

@@ -1,16 +1,26 @@
import { Button } from '@/components/ui/v3/button';
import { Input } from '@/components/ui/v3/input';
import { type ChangeEvent, useEffect, useRef, useState } from 'react';
import { getToastStyleProps } from '@/utils/constants/settings';
import {
useEffect,
useRef,
useState,
type ChangeEvent,
type KeyboardEvent,
} from 'react';
import toast from 'react-hot-toast';
interface Props {
sendMfaOtp: (code: string) => Promise<any>;
loading: boolean;
requestNewMfaTicket?: () => Promise<void>;
}
function MfaOtpForm({ sendMfaOtp, loading }: Props) {
function MfaOtpForm({ sendMfaOtp, loading, requestNewMfaTicket }: Props) {
const [otpValue, setOtpValue] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const isMfaTicketInvalid = useRef(false);
useEffect(() => {
if (inputRef.current) {
@@ -18,24 +28,39 @@ function MfaOtpForm({ sendMfaOtp, loading }: Props) {
}
}, []);
async function sendMfa(code: string) {
if (code.length === 6 && !isSubmitting) {
setIsSubmitting(true);
const result = await sendMfaOtp(code);
if (!result) {
async function submitTOTP() {
if (otpValue.length === 6 && !isSubmitting) {
try {
setIsSubmitting(true);
if (requestNewMfaTicket && isMfaTicketInvalid.current) {
await requestNewMfaTicket();
}
await sendMfaOtp(otpValue);
} catch (error) {
isMfaTicketInvalid.current = true;
toast.error(
error?.message || 'An error occurred. Please try again.',
getToastStyleProps(),
);
setTimeout(() => {
inputRef.current?.focus();
}, 10);
} finally {
setIsSubmitting(false);
}
}
setIsSubmitting(false);
}
async function handleChange(event: ChangeEvent<HTMLInputElement>) {
const code = event.target.value.replace(/[^0-9]/g, '').slice(0, 6);
const code = event.target.value.replace(/[^0-9]/g, '');
setOtpValue(code);
}
sendMfa(code);
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
submitTOTP();
}
}
const isInputDisabled = loading || isSubmitting;
@@ -50,8 +75,9 @@ function MfaOtpForm({ sendMfaOtp, loading }: Props) {
className="!bg-transparent"
disabled={isInputDisabled}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<Button disabled={isButtonDisabled}>
<Button disabled={isButtonDisabled} onClick={submitTOTP}>
{loading ? 'Verifying...' : 'Verify'}
</Button>
</div>

View File

@@ -8,7 +8,7 @@ import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
import type { FieldValues, UseControllerProps } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import mergeRefs from 'react-merge-refs';
import { mergeRefs } from 'react-merge-refs';
export interface ControlledAutocompleteProps<
TOption extends AutocompleteOption = AutocompleteOption,

View File

@@ -5,7 +5,7 @@ import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
import type { FieldValues, UseControllerProps } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import mergeRefs from 'react-merge-refs';
import { mergeRefs } from 'react-merge-refs';
export interface ControlledCheckboxProps<TFieldValues extends FieldValues = any>
extends CheckboxProps {
@@ -38,7 +38,7 @@ function ControlledCheckbox(
uncheckWhenDisabled,
...props
}: ControlledCheckboxProps,
ref: ForwardedRef<HTMLInputElement>,
ref: ForwardedRef<HTMLButtonElement>,
) {
const { setValue } = useFormContext();
const { field } = useController({

View File

@@ -4,7 +4,7 @@ import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
import type { FieldValues, UseControllerProps } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import mergeRefs from 'react-merge-refs';
import { mergeRefs } from 'react-merge-refs';
export interface ControlledSelectProps<TFieldValues extends FieldValues = any>
extends SelectProps<TFieldValues> {
@@ -24,7 +24,7 @@ export interface ControlledSelectProps<TFieldValues extends FieldValues = any>
function ControlledSelect(
{ controllerProps, name, control, ...props }: ControlledSelectProps,
ref: ForwardedRef<HTMLInputElement>,
ref: ForwardedRef<HTMLButtonElement>,
) {
const { setValue } = useFormContext();
const { field } = useController({

View File

@@ -2,13 +2,13 @@ import type { SwitchProps } from '@/components/ui/v2/Switch';
import { Switch } from '@/components/ui/v2/Switch';
import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type {
ControllerProps,
FieldValues,
UseControllerProps,
} from 'react-hook-form/dist/types';
import mergeRefs from 'react-merge-refs';
} from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import { mergeRefs } from 'react-merge-refs';
export interface ControlledSwitchProps<TFieldValues extends FieldValues = any>
extends SwitchProps {

View File

@@ -6,19 +6,24 @@ import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { Dropdown, useDropdown } from '@/components/ui/v2/Dropdown';
import { Text } from '@/components/ui/v2/Text';
import { useUserData } from '@/hooks/useUserData';
import { useAuth } from '@/providers/Auth';
import { useApolloClient } from '@apollo/client';
import { useSignOut, useUserData } from '@nhost/nextjs';
import getConfig from 'next/config';
import { useRouter } from 'next/router';
function AccountMenuContent() {
const user = useUserData();
const { signOut } = useSignOut();
const router = useRouter();
const { signout } = useAuth();
const apolloClient = useApolloClient();
const { handleClose } = useDropdown();
const { publicRuntimeConfig } = getConfig();
async function handleSignOut() {
handleClose();
await apolloClient.clearStore();
await signout();
}
return (
<Box className="grid grid-flow-row">
<Box className="grid grid-flow-col items-center justify-start gap-3 p-4">
@@ -70,12 +75,7 @@ function AccountMenuContent() {
color="error"
variant="borderless"
className="w-full justify-start"
onClick={async () => {
handleClose();
await apolloClient.clearStore();
await signOut();
await router.push('/signin');
}}
onClick={handleSignOut}
>
Sign out
</Button>

View File

@@ -2,22 +2,23 @@ import type { BaseLayoutProps } from '@/components/layout/BaseLayout';
import { BaseLayout } from '@/components/layout/BaseLayout';
import { Container } from '@/components/layout/Container';
import { Header } from '@/components/layout/Header';
import { MainNav } from '@/components/layout/MainNav';
import { useTreeNavState } from '@/components/layout/MainNav/TreeNavStateContext';
import { HighlightedText } from '@/components/presentational/HighlightedText';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useAuthenticationStatus } from '@nhost/nextjs';
import Analytics from '@/components/analytics/analytics';
import { useMediaQuery } from '@/components/common/useMediaQuery';
import { MainNav } from '@/components/layout/MainNav';
import PinnedMainNav from '@/components/layout/MainNav/PinnedMainNav';
import { useTreeNavState } from '@/components/layout/MainNav/TreeNavStateContext';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { OrgStatus } from '@/features/orgs/components/OrgStatus';
import { useIsHealthy } from '@/features/orgs/projects/common/hooks/useIsHealthy';
import { useNotFoundRedirect } from '@/features/orgs/projects/common/hooks/useNotFoundRedirect';
import { cn } from '@/lib/utils';
import { useAuth } from '@/providers/Auth';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -32,7 +33,7 @@ export default function AuthenticatedLayout({
const isPlatform = useIsPlatform();
const isMdOrLarger = useMediaQuery('md');
const { isAuthenticated, isLoading } = useAuthenticationStatus();
const { isAuthenticated, isLoading } = useAuth();
const isHealthy = useIsHealthy();
const [mainNavContainer, setMainNavContainer] = useState(null);
const { mainNavPinned } = useTreeNavState();
@@ -43,7 +44,6 @@ export default function AuthenticatedLayout({
if (!isPlatform || isLoading || isAuthenticated) {
return;
}
router.push('/signin');
}, [isLoading, isAuthenticated, router, isPlatform]);
@@ -65,11 +65,15 @@ export default function AuthenticatedLayout({
if (isPlatform && isLoading) {
return (
<BaseLayout className="h-full" {...props}>
<Header className="flex max-h-[59px] flex-auto" />
<Header className="flex max-h-[59px] flex-auto py-1" />
</BaseLayout>
);
}
// if (isPlatform && !isLoading && !isAuthenticated) {
// return null;
// }
if (!isPlatform && !isHealthy) {
return (
<BaseLayout className="h-full" {...props}>
@@ -142,6 +146,7 @@ export default function AuthenticatedLayout({
>
<div className="flex h-full w-full flex-col overflow-auto">
<OrgStatus />
<Analytics />
{children}
</div>
</RetryableErrorBoundary>

View File

@@ -10,8 +10,8 @@ import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useAuth } from '@/providers/Auth';
import { useApolloClient } from '@apollo/client';
import { useSignOut } from '@nhost/nextjs';
import getConfig from 'next/config';
import { useRouter } from 'next/router';
import { useState } from 'react';
@@ -22,11 +22,18 @@ export interface MobileNavProps extends ButtonProps {}
export default function MobileNav({ className, ...props }: MobileNavProps) {
const isPlatform = useIsPlatform();
const [menuOpen, setMenuOpen] = useState(false);
const { signOut } = useSignOut();
const { signout } = useAuth();
const apolloClient = useApolloClient();
const router = useRouter();
const { publicRuntimeConfig } = getConfig();
async function handleSignOut() {
setMenuOpen(false);
await apolloClient.clearStore();
await signout();
await router.push('/signin');
}
return (
<>
<Button
@@ -120,12 +127,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
variant="borderless"
sx={{ color: 'error.main' }}
className="justify-start border-none px-2 py-2.5 text-[16px]"
onClick={async () => {
setMenuOpen(false);
await apolloClient.clearStore();
await signOut();
await router.push('/signin');
}}
onClick={handleSignOut}
>
Sign Out
</ListItem.Button>

View File

@@ -6,8 +6,8 @@ import { RetryableErrorBoundary } from '@/components/presentational/RetryableErr
import { Box } from '@/components/ui/v2/Box';
import { ThemeProvider } from '@/components/ui/v2/ThemeProvider';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useAuth } from '@/providers/Auth';
import GlobalStyles from '@mui/material/GlobalStyles';
import { useAuthenticationStatus } from '@nhost/nextjs';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
@@ -20,7 +20,7 @@ export default function UnauthenticatedLayout({
}: UnauthenticatedLayoutProps) {
const router = useRouter();
const isPlatform = useIsPlatform();
const { isAuthenticated, isLoading } = useAuthenticationStatus();
const { isAuthenticated, isLoading } = useAuth();
const isOnResetPassword = router.route === '/password/reset';
useEffect(() => {

View File

@@ -92,7 +92,7 @@ function Checkbox(
'aria-label': ariaLabel,
...props
}: CheckboxProps,
ref: ForwardedRef<HTMLInputElement>,
ref: ForwardedRef<HTMLButtonElement>,
) {
if (!label) {
return (

View File

@@ -3,10 +3,10 @@ import { ChevronUpIcon } from '@/components/ui/v2/icons/ChevronUpIcon';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useUserData } from '@/hooks/useUserData';
import { getToastBackgroundColor } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import type { ApolloError } from '@apollo/client';
import { useUserData } from '@nhost/nextjs';
import { AnimatePresence, motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { useState } from 'react';

View File

@@ -5,7 +5,7 @@ import type { InputBaseProps as MaterialInputBaseProps } from '@mui/material/Inp
import MaterialInputBase, { inputBaseClasses } from '@mui/material/InputBase';
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
import { forwardRef } from 'react';
import mergeRefs from 'react-merge-refs';
import { mergeRefs } from 'react-merge-refs';
export interface InputProps
extends Omit<MaterialInputBaseProps, 'componentsProps' | 'slotProps'>,

View File

@@ -57,7 +57,7 @@ const StyledRadio = styled(MaterialRadio)(({ theme }) => ({
function Radio(
{ label, value, slotProps, ...props }: RadioProps,
ref: ForwardedRef<HTMLInputElement>,
ref: ForwardedRef<HTMLButtonElement>,
) {
return (
<StyledFormControlLabel

View File

@@ -9,37 +9,36 @@ import {
DialogTrigger,
} from '@/components/ui/v3/dialog';
import useMfaEnabled from '@/features/account/settings/components/AccountMfaSettings/hooks/useMfaEnabled';
import { useNhostClient } from '@/providers/nhost';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useConfigMfa } from '@nhost/nextjs';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
const defaultErrorMessage =
'An error occurred while trying to enable multi-factor authentication. Please try again.';
function DisableMfaButton() {
const { disableMfa, isDisabling } = useConfigMfa();
const nhost = useNhostClient();
const [open, setOpen] = useState(false);
const [isDisabling, setIsDisabling] = useState(false);
const { loading, refetch } = useMfaEnabled();
const buttonDisabled = loading || isDisabling;
async function onSendMfaOtp(code: string) {
const result = await disableMfa(code);
if (result.error) {
toast.error(
result.error.message || defaultErrorMessage,
try {
setIsDisabling(true);
await nhost.auth.verifyChangeUserMfa({
code,
activeMfaType: '',
});
toast.success(
'Multi-factor authentication has been disabled.',
getToastStyleProps(),
);
return false;
}
toast.success(
'Multi-factor authentication has been disabled.',
getToastStyleProps(),
);
await refetch();
setOpen(false);
await refetch();
setOpen(false);
return true;
return true;
} finally {
setIsDisabling(false);
}
}
return (

View File

@@ -1,51 +1,52 @@
/* eslint-disable @next/next/no-img-element */
import { MfaOtpForm } from '@/components/common/MfaOtpForm';
import { Spinner } from '@/components/ui/v3/spinner';
import { useNhostClient } from '@/providers/nhost';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useConfigMfa } from '@nhost/nextjs';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
import CopyMfaTOTPSecret from './CopyMfaTOTPSecret';
const defaultErrorMessage =
'An error occurred while trying to enable multi-factor authentication. Please try again.';
interface Props {
onSuccess: () => void;
}
function MfaQRCodeAndTOTPSecret({ onSuccess }: Props) {
const {
generateQrCode,
qrCodeDataUrl,
isGenerated,
isGenerating,
activateMfa,
isActivating,
totpSecret,
} = useConfigMfa();
const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string | undefined>();
const [totpSecret, setTotpSecret] = useState<string | undefined>();
const [isGenerating, setIsGenerating] = useState(false);
const [isActivating, setIsActivating] = useState(false);
const nhost = useNhostClient();
async function onSendMfaOtp(code: string) {
const result = await activateMfa(code);
if (result.error) {
toast.error(
result.error.message || defaultErrorMessage,
try {
setIsActivating(true);
await nhost.auth.verifyChangeUserMfa({
code,
activeMfaType: 'totp',
});
toast.success(
'Multi-factor authentication has been enabled.',
getToastStyleProps(),
);
return false;
onSuccess();
return true;
} finally {
setIsActivating(false);
}
toast.success(
'Multi-factor authentication has been enabled.',
getToastStyleProps(),
);
onSuccess();
return true;
}
useEffect(() => {
async function generate() {
const result = await generateQrCode();
if (result.error) {
toast.error(result.error.message, getToastStyleProps());
try {
setIsGenerating(true);
const response = await nhost.auth.changeUserMfa();
setQrCodeDataUrl(response.body.imageUrl);
setTotpSecret(response.body.totpSecret);
} catch (error) {
toast.error(error?.message, getToastStyleProps());
} finally {
setIsGenerating(false);
}
}
generate();
@@ -55,7 +56,7 @@ function MfaQRCodeAndTOTPSecret({ onSuccess }: Props) {
return (
<div className="flex w-full flex-col items-center justify-center gap-4">
{isGenerating && <Spinner />}
{isGenerated && qrCodeDataUrl && (
{qrCodeDataUrl && (
<>
<div className="flex flex-col justify-center gap-4">
<p className="text-base">

View File

@@ -1,11 +1,11 @@
import { useUserData } from '@/hooks/useUserData';
import { isNotEmptyValue } from '@/lib/utils';
import { useGetActiveMfaTypeQuery } from '@/utils/__generated__/graphql';
import { useUserId } from '@nhost/nextjs';
function useMfaEnabled() {
const userId = useUserId();
const userData = useUserData();
const { data, loading, refetch } = useGetActiveMfaTypeQuery({
variables: { id: userId },
variables: { id: userData?.id },
fetchPolicy: 'cache-first',
});

View File

@@ -11,13 +11,13 @@ import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text';
import useActionWithElevatedPermissions from '@/features/account/settings/hooks/useActionWithElevatedPermissions';
import { useNhostClient } from '@/providers/nhost';
import type { DialogFormProps } from '@/types/common';
import { GetPersonalAccessTokensDocument } from '@/utils/__generated__/graphql';
import { copy } from '@/utils/copy';
import { getDateComponents } from '@/utils/getDateComponents';
import { useApolloClient } from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup';
import { useNhostClient } from '@nhost/nextjs';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -85,15 +85,15 @@ export default function CreatePATForm({
const createPAT = useActionWithElevatedPermissions({
actionFn: async (
expiresAt: Date,
expiresAt: string,
metadata?: Record<string, string | number>,
) => {
const result = await nhostClient.auth.createPAT(expiresAt, metadata);
const result = await nhostClient.auth.createPAT({ expiresAt, metadata });
return result;
},
successMessage: 'The personal access token has been created successfully.',
onSuccess: ({ data }) => {
setPersonalAccessToken(data?.personalAccessToken);
onSuccess: ({ body }) => {
setPersonalAccessToken(body.personalAccessToken);
apolloClient.refetchQueries({
include: [GetPersonalAccessTokensDocument],
});
@@ -111,7 +111,8 @@ export default function CreatePATForm({
}, [isDirty, location, onDirtyStateChange]);
async function handleSubmit(formValues: CreatePATFormValues) {
await createPAT(new Date(formValues.expiresAt), {
const expiresAt = new Date(formValues.expiresAt).toISOString();
await createPAT(expiresAt, {
name: formValues.name,
application: 'dashboard',
userAgent: window.navigator.userAgent,

View File

@@ -5,9 +5,9 @@ import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@/hooks/useUserData';
import { useAuth } from '@/providers/Auth';
import { useDeleteUserAccountMutation } from '@/utils/__generated__/graphql';
import { useSignOut, useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
@@ -82,14 +82,12 @@ function ConfirmDeleteAccountModal({
}
export default function DeleteAccount() {
const router = useRouter();
const { signOut } = useSignOut();
const { signout } = useAuth();
const { openDialog, closeDialog } = useDialog();
const onDelete = async () => {
await signOut();
await router.push('/signin');
await signout();
};
const confirmDeleteAccount = async () => {

View File

@@ -2,9 +2,9 @@ import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@/hooks/useUserData';
import { useUpdateUserDisplayNameMutation } from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -19,7 +19,9 @@ export type DisplayNameSettingFormValues = Yup.InferType<
>;
export default function DisplayNameSetting() {
const { id: userID, displayName } = useUserData() || {};
const user = useUserData();
const { id: userID, displayName } = user || {};
const [updateUserDisplayName] = useUpdateUserDisplayNameMutation();

View File

@@ -2,8 +2,9 @@ import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import useActionWithElevatedPermissions from '@/features/account/settings/hooks/useActionWithElevatedPermissions';
import { useUserData } from '@/hooks/useUserData';
import { useNhostClient } from '@/providers/nhost';
import { yupResolver } from '@hookform/resolvers/yup';
import { useNhostClient, useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -15,11 +16,11 @@ export type EmailSettingFormValues = Yup.InferType<typeof validationSchema>;
export default function EmailSetting() {
const nhost = useNhostClient();
const { email } = useUserData() || {};
const user = useUserData();
const form = useForm<EmailSettingFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { email },
defaultValues: { email: user?.email },
resolver: yupResolver(validationSchema),
});
@@ -28,7 +29,7 @@ export default function EmailSetting() {
const changeEmail = useActionWithElevatedPermissions({
actionFn: async (newEmail: string) => {
const result = await nhost.auth.changeEmail({
const result = await nhost.auth.changeUserEmail({
newEmail,
options: {
redirectTo: `${window.location.origin}/account`,

View File

@@ -1,5 +1,5 @@
import useActionWithElevatedPermissions from '@/features/account/settings/hooks/useActionWithElevatedPermissions';
import { useChangePassword } from '@nhost/nextjs';
import { useNhostClient } from '@/providers/nhost';
import { type ChangePasswordFormValues } from './useChangePasswordForm';
interface Props {
@@ -7,10 +7,10 @@ interface Props {
}
function useOnChangePasswordHandler({ onSuccess }: Props) {
const { changePassword: actionFn } = useChangePassword();
const nhost = useNhostClient();
const changePassword = useActionWithElevatedPermissions({
actionFn,
actionFn: nhost.auth.changeUserPassword,
onSuccess,
successMessage: 'The password has been changed successfully.',
});
@@ -18,7 +18,7 @@ function useOnChangePasswordHandler({ onSuccess }: Props) {
async function onSubmit(values: ChangePasswordFormValues) {
const { newPassword } = values;
await changePassword(newPassword);
await changePassword({ newPassword });
}
return onSubmit;

View File

@@ -1,6 +1,7 @@
import useActionWithElevatedPermissions from '@/features/account/settings/hooks/useActionWithElevatedPermissions';
import useGetSecurityKeys from '@/features/account/settings/hooks/useGetSecurityKeys';
import { useAddSecurityKey } from '@nhost/nextjs';
import { useNhostClient } from '@/providers/nhost';
import { startRegistration } from '@simplewebauthn/browser';
import { type NewSecurityKeyFormValues } from './useNewSecurityKeyForm';
interface Props {
@@ -9,7 +10,14 @@ interface Props {
function useOnAddNewSecurityKeyHandler({ onSuccess }: Props) {
const { refetch } = useGetSecurityKeys();
const { add: actionFn } = useAddSecurityKey();
const nhost = useNhostClient();
async function actionFn(nickname: string) {
const webAuthnOptions = await nhost.auth.addSecurityKey();
const credential = await startRegistration(webAuthnOptions.body);
await nhost.auth.verifyAddSecurityKey({ credential, nickname });
}
const addSecurityKey = useActionWithElevatedPermissions({
actionFn,
onSuccess: async () => {

View File

@@ -4,7 +4,7 @@ import { useRemoveSecurityKeyMutation } from '@/utils/__generated__/graphql';
function useRemoveSecurityKey() {
const [removeSecurityKeyMutation] = useRemoveSecurityKeyMutation();
const { elevatePermissions } = useElevatedPermissions();
const elevatePermissions = useElevatedPermissions();
const { refetch: refetchSecurityKeys } = useGetSecurityKeys();
async function removeSecurityKey(id: string) {

View File

@@ -4,20 +4,29 @@ import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
import { Text } from '@/components/ui/v2/Text';
import { useAccessToken } from '@/hooks/useAccessToken';
import { useNhostClient } from '@/providers/nhost';
import { useGetAuthUserProvidersQuery } from '@/utils/__generated__/graphql';
import { useProviderLink } from '@nhost/nextjs';
import NavLink from 'next/link';
import { useMemo } from 'react';
export default function SocialProvidersSettings() {
const nhost = useNhostClient();
const token = useAccessToken();
const { data, loading, error } = useGetAuthUserProvidersQuery();
const isGithubConnected = data?.authUserProviders?.some(
(item) => item.providerId === 'github',
);
const { github } = useProviderLink({
connect: true,
redirectTo: `${window.location.origin}/account`,
});
const github = useMemo(
() =>
nhost.auth.signInProviderURL('github', {
connect: token,
redirectTo: `${window.location.origin}/account`,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[token],
);
if (!data && loading) {
return (

View File

@@ -1,14 +1,9 @@
import useElevatedPermissions from '@/features/account/settings/hooks/useElevatedPermissions';
import useGetSecurityKeys from '@/features/account/settings/hooks/useGetSecurityKeys';
import type { AuthErrorPayload } from '@nhost/nextjs';
import { toast } from 'react-hot-toast';
type Action = (...args: any[]) => Promise<ActionResult>;
type Action = (...args: any[]) => Promise<any>;
type ActionResult = {
isError?: boolean;
error: AuthErrorPayload;
};
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
interface Props<Fn extends Action> {
@@ -24,11 +19,11 @@ function useActionWithElevatedPermissions<F extends Action>({
onError,
successMessage,
}: Props<F>) {
const { elevated, elevatePermissions } = useElevatedPermissions();
const elevatePermissions = useElevatedPermissions();
const { data } = useGetSecurityKeys();
async function requestPermissions() {
if (elevated || data?.authUserSecurityKeys.length === 0) {
if (data?.authUserSecurityKeys.length === 0) {
return true;
}
const isPermissionsElevated = await elevatePermissions();
@@ -38,19 +33,17 @@ function useActionWithElevatedPermissions<F extends Action>({
async function actionWithElevatedPermissions(...args: Parameters<F>) {
let isSuccess = false;
const permissionGranted = await requestPermissions();
if (!permissionGranted) {
return isSuccess;
}
const response = await actionFn(...args);
if (response.error) {
toast.error(response.error?.message || 'Something went wrong.');
onError?.();
} else {
toast.success(successMessage || 'Success');
try {
const response = await actionFn(...args);
toast.success(successMessage || 'Success.');
onSuccess?.(response as UnwrapPromise<ReturnType<F>>);
isSuccess = true;
} catch (error) {
toast.error(error?.message || 'Something went wrong.');
onError?.();
}
return isSuccess;

View File

@@ -1,23 +1,24 @@
import { useElevateEmail } from '@/hooks/useElevateEmail';
import { useHasuraClaims } from '@/hooks/useHasuraClaims';
import { useUserData } from '@/hooks/useUserData';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useElevateSecurityKeyEmail, useUserData } from '@nhost/nextjs';
import { toast } from 'react-hot-toast';
function useElevatedPermissions() {
const user = useUserData();
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail();
const elevateEmail = useElevateEmail();
const claims = useHasuraClaims();
async function elevatePermissions(shouldThrowError = false) {
const elevated = user
? claims?.['x-hasura-auth-elevated'] === user?.id
: false;
if (elevated) {
return true;
}
try {
const response = await elevateEmailSecurityKey(user.email);
if (response.isError) {
const errorMessage =
response.error?.message || 'Permissions were not elevated';
throw new Error(errorMessage);
}
await elevateEmail();
return true;
} catch (e) {
if (shouldThrowError) {
@@ -30,7 +31,7 @@ function useElevatedPermissions() {
}
}
return { elevated, elevatePermissions };
return elevatePermissions;
}
export default useElevatedPermissions;

View File

@@ -1,11 +1,11 @@
import { useUserData } from '@/hooks/useUserData';
import { useSecurityKeysQuery } from '@/utils/__generated__/graphql';
import { useUserId } from '@nhost/nextjs';
function useGetSecurityKeys() {
const currentUserId = useUserId();
const user = useUserData();
const query = useSecurityKeysQuery({
variables: {
userId: currentUserId,
userId: user?.id,
},
});

View File

@@ -2,6 +2,7 @@ import { getAnonId } from '@/lib/segment';
import { isNotEmptyValue } from '@/lib/utils';
import { getToastStyleProps } from '@/utils/constants/settings';
import { nhost } from '@/utils/nhost';
import type { SignInProviderParams } from '@nhost/nhost-js-beta/auth';
import { useMutation } from '@tanstack/react-query';
import { toast } from 'react-hot-toast';
@@ -18,14 +19,21 @@ function useGithubAuthentication({
}: UseGithubAuthenticationHookProps) {
const githubAuthenticationMutation = useMutation(
async () => {
const options = {
...(isNotEmptyValue(redirectTo) && { redirectTo }),
...(withAnonId && { metadata: { anonId: await getAnonId() } }),
};
return nhost.auth.signIn({
provider: 'github',
...(isNotEmptyValue(options) && { options }),
});
let options: SignInProviderParams | undefined;
if (isNotEmptyValue(redirectTo)) {
options = {
redirectTo,
};
}
if (withAnonId) {
options = {
metadata: { anonId: await getAnonId() },
...options,
};
}
const redirectURl = nhost.auth.signInProviderURL('github', options);
window.location.href = redirectURl;
},
{
onError: () => {

View File

@@ -1,14 +1,20 @@
import { Button } from '@/components/ui/v3/button';
import { useSignInWithSecurityKey } from '@/features/auth/SignIn/SecurityKey/hooks/useSignInWithSecurityKey';
import { Fingerprint } from 'lucide-react';
import { useState } from 'react';
import { VerifyEmailDialog } from './VerifyEmailDialog';
function SignInWithSecurityKey() {
const { disabled, signInWithSecurityKey, needsEmailVerification } =
useSignInWithSecurityKey();
const [open, setOpen] = useState(false);
function onNeedsEmailVerification() {
setOpen(true);
}
const { disabled, signInWithSecurityKey } = useSignInWithSecurityKey({
onNeedsEmailVerification,
});
return (
<>
<VerifyEmailDialog needsEmailVerification={needsEmailVerification} />
<VerifyEmailDialog open={open} setOpen={setOpen} />
<Button
variant="ghost"
className="gap-2 !bg-white text-sm+ !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"

View File

@@ -5,22 +5,16 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/v3/dialog';
import { useEffect, useState } from 'react';
interface Props {
needsEmailVerification: boolean;
open: boolean;
setOpen: (openState: boolean) => void;
}
export function VerifyEmailDialog({ needsEmailVerification }: Props) {
const [open, setOpen] = useState(needsEmailVerification);
useEffect(() => {
setOpen(needsEmailVerification);
}, [needsEmailVerification, open]);
export function VerifyEmailDialog({ open, setOpen }: Props) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className="text-foreground sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Email verification required</DialogTitle>
<DialogDescription>

View File

@@ -1,34 +1,47 @@
import { isNotEmptyValue } from '@/lib/utils';
import { useNhostClient } from '@/providers/nhost';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useSignInSecurityKey } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { startAuthentication } from '@simplewebauthn/browser';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
function useSignInWithSecurityKey() {
const { signInSecurityKey } = useSignInSecurityKey();
const [needsEmailVerification, setNeedsEmailVerification] = useState(false);
interface Props {
onNeedsEmailVerification: () => void;
}
function useSignInWithSecurityKey({ onNeedsEmailVerification }: Props) {
const nhost = useNhostClient();
const [disabled, setDisabled] = useState(false);
const { replace } = useRouter();
async function signInWithSecurityKey() {
setDisabled(true);
const {
isError,
isSuccess,
needsEmailVerification: _needsEmailVerification,
error,
} = await signInSecurityKey();
if (isError) {
toast.error(error?.message, getToastStyleProps());
} else if (_needsEmailVerification) {
setNeedsEmailVerification(true);
} else if (isSuccess) {
replace('/');
try {
setDisabled(true);
const signInWebauthnResponse = await nhost.auth.signInWebauthn();
const { body: options } = signInWebauthnResponse;
const credential = await startAuthentication(options);
await nhost.auth.verifySignInWebauthn({
credential,
});
} catch (error) {
let errorMessage =
error?.message ||
'An error occurred while signing in. Please try again.';
if (isNotEmptyValue(error?.body)) {
const errorCode = error.body.error;
if (errorCode === 'unverified-user') {
onNeedsEmailVerification();
return;
}
errorMessage = error.body.message;
}
toast.error(errorMessage, getToastStyleProps());
} finally {
setDisabled(false);
}
setDisabled(false);
}
return { disabled, signInWithSecurityKey, needsEmailVerification };
return { disabled, signInWithSecurityKey };
}
export default useSignInWithSecurityKey;

View File

@@ -1,20 +1,24 @@
import { MfaOtpForm } from '@/components/common/MfaOtpForm';
import type { SendMfaOtpHandler } from '@nhost/nextjs';
import { Smartphone } from 'lucide-react';
interface Props {
sendMfaOtp: SendMfaOtpHandler;
sendMfaOtp: (code: string) => Promise<any>;
loading: boolean;
requestNewMfaTicket: () => Promise<void>;
}
function MfaSignInOtpForm({ sendMfaOtp, loading }: Props) {
function MfaSignInOtpForm({ sendMfaOtp, loading, requestNewMfaTicket }: Props) {
return (
<div className="ws-full relative grid grid-flow-row gap-4 bg-transparent">
<div className="flex w-full flex-col items-center justify-center gap-3">
<Smartphone size={32} />
<h2 className="text-[1.25rem]">Authentication Code</h2>
</div>
<MfaOtpForm loading={loading} sendMfaOtp={sendMfaOtp} />
<MfaOtpForm
loading={loading}
sendMfaOtp={sendMfaOtp}
requestNewMfaTicket={requestNewMfaTicket}
/>
<p className="text-center">
Open your authenticator app or browser extension to view your
authentication code.

View File

@@ -1,15 +1,53 @@
import useOnSignUpWithPasswordHandler from '@/features/auth/SignIn/SignInWithEmailAndPassword/hooks/useOnSignInWithEmailAndPasswordHandler';
import useOnSignInWithEmailAndPasswordHandler from '@/features/auth/SignIn/SignInWithEmailAndPassword/hooks/useOnSignInWithEmailAndPasswordHandler';
import useRequestNewMfaTicket from '@/features/auth/SignIn/SignInWithEmailAndPassword/hooks/useRequestNewMfaTicket';
import { useNhostClient } from '@/providers/nhost';
import { useRef, useState } from 'react';
import MfaSignInOtpForm from './MfaSignInOtpForm';
import SignInWithEmailAndPasswordForm from './SignInWithEmailAndPasswordForm';
function SignInWithEmailAndPassword() {
const { onSignIWithEmailAndPassword, sendMfaOtp, isLoading, needsMfaOtp } =
useOnSignUpWithPasswordHandler();
const [needsMfaOtp, setNeedsMfaOtp] = useState(false);
const mfaTicket = useRef<string | undefined>();
const [isMfaLoading, setIsMfaLoading] = useState(false);
const nhost = useNhostClient();
function onNeedsMfa(ticket: string) {
mfaTicket.current = ticket;
setNeedsMfaOtp(true);
}
const { onSignInWithEmailAndPassword, isLoading, emailAndPasswordRef } =
useOnSignInWithEmailAndPasswordHandler({ onNeedsMfa });
const requestNewMfaTicketFn = useRequestNewMfaTicket();
async function requestNewMfaTicket() {
const { email, password } = emailAndPasswordRef.current;
mfaTicket.current = await requestNewMfaTicketFn(email, password);
}
async function onHandleSendMfaOtp(otp: string) {
try {
setIsMfaLoading(true);
await nhost.auth.verifySignInMfaTotp({
ticket: mfaTicket.current,
otp,
});
} finally {
setIsMfaLoading(false);
}
}
return needsMfaOtp ? (
<MfaSignInOtpForm sendMfaOtp={sendMfaOtp} loading={isLoading} />
<MfaSignInOtpForm
sendMfaOtp={onHandleSendMfaOtp}
loading={isMfaLoading}
requestNewMfaTicket={requestNewMfaTicket}
/>
) : (
<SignInWithEmailAndPasswordForm onSubmit={onSignIWithEmailAndPassword} />
<SignInWithEmailAndPasswordForm
onSubmit={onSignInWithEmailAndPassword}
isLoading={isLoading}
/>
);
}

View File

@@ -1,5 +1,5 @@
import { FormInput } from '@/components/form/FormInput';
import { Button } from '@/components/ui/v3/button';
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import { Form } from '@/components/ui/v3/form';
import useSignInWithEmailAndPasswordForm, {
type SignInWithEmailAndPasswordFormValues,
@@ -8,9 +8,10 @@ import NextLink from 'next/link';
interface Props {
onSubmit: (values: SignInWithEmailAndPasswordFormValues) => void;
isLoading: boolean;
}
function SignInWithEmailAndPassword({ onSubmit }: Props) {
function SignInWithEmailAndPassword({ onSubmit, isLoading }: Props) {
const form = useSignInWithEmailAndPasswordForm();
return (
<Form {...form}>
@@ -40,6 +41,8 @@ function SignInWithEmailAndPassword({ onSubmit }: Props) {
type="submit"
variant="outline"
className="w-full !bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
disabled={isLoading}
loading={isLoading}
>
Sign In
</Button>

View File

@@ -1,50 +1,65 @@
import { isNotEmptyValue } from '@/lib/utils';
import { useNhostClient } from '@/providers/nhost';
import { getToastStyleProps } from '@/utils/constants/settings';
import {
useSendVerificationEmail,
useSignInEmailPassword,
} from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { useRef, useState } from 'react';
import toast from 'react-hot-toast';
import type { SignInWithEmailAndPasswordFormValues } from './useSignInWithEmailAndPasswordForm';
function useOnSignInWithEmailAndPasswordHandler() {
interface Props {
onNeedsMfa: (mfaTicket: string) => void;
}
type EmailAndPasswordRef = {
email: string;
password: string;
} | null;
function useOnSignInWithEmailAndPasswordHandler({ onNeedsMfa }: Props) {
const [isLoading, setIsloading] = useState(false);
const nhost = useNhostClient();
const router = useRouter();
const { signInEmailPassword, needsMfaOtp, sendMfaOtp, isLoading } =
useSignInEmailPassword();
const emailAndPasswordRef = useRef<EmailAndPasswordRef>();
const { sendEmail } = useSendVerificationEmail();
async function onSignIWithEmailAndPassword({
async function onSignInWithEmailAndPassword({
email,
password,
}: SignInWithEmailAndPasswordFormValues) {
try {
const { needsEmailVerification, error } = await signInEmailPassword(
setIsloading(true);
const response = await nhost.auth.signInEmailPassword({
email,
password,
);
if (error) {
toast.error(
error?.message ||
'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
return;
}
});
if (needsEmailVerification) {
await sendEmail(email);
router.push(`/email/verify?email=${encodeURIComponent(email)}`);
emailAndPasswordRef.current = {
email,
password,
};
if (response.body.mfa) {
onNeedsMfa(response.body.mfa.ticket);
}
} catch {
toast.error(
'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
} catch (error) {
let errorMessage =
error?.message ||
'An error occurred while signing in. Please try again.';
if (isNotEmptyValue(error?.body)) {
const errorCode = error.body.error;
if (errorCode === 'unverified-user') {
await nhost.auth.sendVerificationEmail({ email });
router.push(`/email/verify?email=${encodeURIComponent(email)}`);
return;
}
errorMessage = error.body.message;
}
toast.error(errorMessage, getToastStyleProps());
} finally {
setIsloading(false);
}
}
return { onSignIWithEmailAndPassword, needsMfaOtp, sendMfaOtp, isLoading };
return { onSignInWithEmailAndPassword, isLoading, emailAndPasswordRef };
}
export default useOnSignInWithEmailAndPasswordHandler;

View File

@@ -0,0 +1,29 @@
import { useNhostClient } from '@/providers/nhost';
import { getToastStyleProps } from '@/utils/constants/settings';
import toast from 'react-hot-toast';
function useRequestNewMfaTicket() {
const nhost = useNhostClient();
async function requestNewMfaTicket(email: string, password: string) {
let mfaTicket: string;
try {
const response = await nhost.auth.signInEmailPassword({
email,
password,
});
mfaTicket = response.body?.mfa.ticket;
} catch (error) {
toast.error(
error?.message ||
'An error occurred while verifying TOTP. Please try again.',
getToastStyleProps(),
);
}
return mfaTicket;
}
return requestNewMfaTicket;
}
export default useRequestNewMfaTicket;

View File

@@ -1,12 +1,13 @@
import { getAnonId } from '@/lib/segment';
import { isEmptyValue } from '@/lib/utils';
import { useNhostClient } from '@/providers/nhost';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useSignUpEmailPassword } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import toast from 'react-hot-toast';
import type { SignUpWithEmailAndPasswordFormValues } from './useSignUpWithEmailAndPasswordForm';
function useOnSignUpWithPasswordHandler() {
const { signUpEmailPassword } = useSignUpEmailPassword();
const nhost = useNhostClient();
const router = useRouter();
async function onSignUpWithPassword({
@@ -16,12 +17,14 @@ function useOnSignUpWithPasswordHandler() {
turnstileToken,
}: SignUpWithEmailAndPasswordFormValues) {
try {
const { needsEmailVerification, error } = await signUpEmailPassword(
email,
password,
const response = await nhost.auth.signUpEmailPassword(
{
displayName,
metadata: { anonId: await getAnonId() },
email,
password,
options: {
displayName,
metadata: { anonId: await getAnonId() },
},
},
{
headers: {
@@ -30,21 +33,13 @@ function useOnSignUpWithPasswordHandler() {
},
);
if (error) {
toast.error(
error.message ||
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
return;
}
if (needsEmailVerification) {
if (response.status === 200 && isEmptyValue(response.body)) {
router.push(`/email/verify?email=${encodeURIComponent(email)}`);
}
} catch {
} catch (error) {
toast.error(
'An error occurred while signing up. Please try again.',
error.message ||
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}

View File

@@ -1,25 +1,30 @@
import { getAnonId } from '@/lib/segment';
import { isEmptyValue } from '@/lib/utils';
import { useNhostClient } from '@/providers/nhost';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useSignUpEmailSecurityKeyEmail } from '@nhost/nextjs';
import { startRegistration } from '@simplewebauthn/browser';
import { useRouter } from 'next/router';
import toast from 'react-hot-toast';
import type { SignUpWithSecurityKeyFormValues } from './useSignupWithSecurityKeyForm';
function useOnSignUpWithSecurityKeyHandler() {
const { signUpEmailSecurityKey } = useSignUpEmailSecurityKeyEmail();
const router = useRouter();
const nhost = useNhostClient();
async function onSignUpWithSecurityKey({
email,
displayName,
turnstileToken,
}: SignUpWithSecurityKeyFormValues) {
const metadata = { anonId: await getAnonId() };
try {
const { needsEmailVerification, error } = await signUpEmailSecurityKey(
email,
const webAuthnResponse = await nhost.auth.signUpWebauthn(
{
displayName,
metadata: { anonId: await getAnonId() },
email,
options: {
displayName,
metadata,
},
},
{
headers: {
@@ -27,22 +32,24 @@ function useOnSignUpWithSecurityKeyHandler() {
},
},
);
const { body: webAuthnOptions } = webAuthnResponse;
if (error) {
toast.error(
error.message ||
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
return;
}
const credential = await startRegistration(webAuthnOptions);
if (needsEmailVerification) {
const verifyResponse = await nhost.auth.verifySignUpWebauthn({
credential,
options: {
displayName,
metadata,
},
});
if (verifyResponse.status === 200 && isEmptyValue(verifyResponse.body)) {
router.push(`/email/verify?email=${encodeURIComponent(email)}`);
}
} catch {
} catch (error) {
toast.error(
'An error occurred while signing up. Please try again.',
error.message ||
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}

View File

@@ -27,6 +27,7 @@ import { StripeEmbeddedForm } from '@/features/orgs/components/StripeEmbeddedFor
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { planDescriptions } from '@/features/orgs/projects/common/utils/planDescriptions';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@/hooks/useUserData';
import { cn } from '@/lib/utils';
import {
useCreateOrganizationRequestMutation,
@@ -34,7 +35,6 @@ import {
type PrefetchNewAppPlansFragment,
} from '@/utils/__generated__/graphql';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUserData } from '@nhost/nextjs';
import { DialogDescription } from '@radix-ui/react-dialog';
import { Plus } from 'lucide-react';
import { useState } from 'react';
@@ -197,7 +197,7 @@ export default function CreateOrgDialog({
const isPlatform = useIsPlatform();
const [open, setOpen] = useState(false);
const { data, loading, error } = usePrefetchNewAppQuery({
skip: !user,
skip: !user || !isPlatform,
});
const [createOrganizationRequest] = useCreateOrganizationRequestMutation();
const [stripeClientSecret, setStripeClientSecret] = useState('');

View File

@@ -18,13 +18,13 @@ import {
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@/hooks/useUserData';
import { cn, isNotEmptyValue } from '@/lib/utils';
import {
Organization_Members_Role_Enum,
useBillingTransferAppMutation,
} from '@/utils/__generated__/graphql';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUserId } from '@nhost/nextjs';
import { Plus } from 'lucide-react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
@@ -53,7 +53,7 @@ function TransferProjectForm({
const { push } = useRouter();
const { orgs, currentOrg } = useOrgs();
const { project } = useProject();
const currentUserId = useUserId();
const user = useUserData();
const [transferProject] = useBillingTransferAppMutation();
const form = useForm<z.infer<typeof transferProjectFormSchema>>({
@@ -133,7 +133,7 @@ function TransferProjectForm({
disabled={
org.plan.isFree || // disable the personal org
org.id === currentOrg.id || // disable the current org as it can't be a destination org
!isUserAdminOfOrg(org, currentUserId) // disable orgs that the current user is not admin of
!isUserAdminOfOrg(org, user?.id) // disable orgs that the current user is not admin of
}
>
{org.name}

View File

@@ -1,13 +1,14 @@
import { Button } from '@/components/ui/v3/button';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useNhostClient } from '@/providers/nhost';
import { Organization_Status_Enum } from '@/utils/__generated__/graphql';
import nhost from '@/utils/nhost/nhost';
import { Download } from 'lucide-react';
import { useState } from 'react';
export default function Soc2Download() {
const { org } = useCurrentOrg();
const nhost = useNhostClient();
const [downloading, setDownloading] = useState(false);
const showSoc2Download =
@@ -28,26 +29,18 @@ export default function Soc2Download() {
if (!fileId) {
throw new Error('SOC2 report file ID not configured');
}
try {
const response = await nhost.storage.getFile(fileId);
const url = URL.createObjectURL(response.body);
const link = document.createElement('a');
link.href = url;
link.download = 'Nhost-SOC2-Report.pdf';
link.click();
const { file, error } = await nhost.storage.download({
fileId,
});
if (error) {
URL.revokeObjectURL(url);
} catch (error) {
throw new Error(error.message || 'Failed to download SOC2 report');
}
if (!file) {
throw new Error('No file data available');
}
const url = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = url;
link.download = 'Nhost-SOC2-Report.pdf';
link.click();
URL.revokeObjectURL(url);
},
{
loadingMessage: 'Downloading SOC2 report...',

View File

@@ -14,18 +14,18 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/v3/sheet';
import { useAuth } from '@/providers/Auth';
import {
useDeleteAnnouncementReadMutation,
useGetAnnouncementsQuery,
useInsertAnnouncementReadMutation,
} from '@/utils/__generated__/graphql';
import { useAuthenticationStatus } from '@nhost/nextjs';
import { formatDistance } from 'date-fns';
import { EllipsisVertical, Megaphone } from 'lucide-react';
import Link from 'next/link';
export default function AnnouncementsTray() {
const { isAuthenticated } = useAuthenticationStatus();
const { isAuthenticated } = useAuth();
const {
data,

View File

@@ -16,8 +16,10 @@ import {
SheetTrigger,
} from '@/components/ui/v3/sheet';
import { StripeEmbeddedForm } from '@/features/orgs/components/StripeEmbeddedForm';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@/hooks/useUserData';
import { isEmptyValue } from '@/lib/utils';
import {
CheckoutStatus,
@@ -29,7 +31,6 @@ import {
type OrganizationMemberInvitesQuery,
type PostOrganizationRequestResponse,
} from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
import { formatDistance } from 'date-fns';
import { Bell } from 'lucide-react';
import { useRouter } from 'next/router';
@@ -43,6 +44,7 @@ export default function NotificationsTray() {
const { session_id } = query;
const { refetch: refetchOrgs } = useOrgs();
const [open, setOpen] = useState(false);
const isPlatForm = useIsPlatform();
const [stripeFormDialogOpen, setStripeFormDialogOpen] = useState(false);
@@ -61,21 +63,21 @@ export default function NotificationsTray() {
const [postOrganizationRequest] = usePostOrganizationRequestMutation();
useEffect(() => {
if (userData) {
if (userData?.id) {
getInvites({
variables: {
userId: userData.id,
userId: userData?.id,
},
});
}
}, [asPath, userData, getInvites]);
}, [asPath, userData?.id, getInvites]);
useEffect(() => {
const checkForPendingOrgRequests = async () => {
const { data: { organizationNewRequests = [] } = {} } =
await getOrganizationNewRequests({
variables: {
userID: userData.id,
userID: userData?.id,
},
});
if (organizationNewRequests.length > 0) {
@@ -111,6 +113,7 @@ export default function NotificationsTray() {
};
if (
isPlatForm &&
userData &&
!['/', '/orgs/verify'].includes(route) &&
isRouterReady &&
@@ -125,6 +128,7 @@ export default function NotificationsTray() {
getOrganizationNewRequests,
postOrganizationRequest,
session_id,
isPlatForm,
]);
const [acceptInvite] = useOrganizationMemberInviteAcceptMutation();

View File

@@ -48,6 +48,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/v3/select';
import { useUserData } from '@/hooks/useUserData';
import {
Organization_Members_Role_Enum,
useDeleteOrganizationMemberMutation,
@@ -55,7 +56,6 @@ import {
type GetOrganizationQuery,
} from '@/utils/__generated__/graphql';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUserData } from '@nhost/nextjs';
import { Ellipsis } from 'lucide-react';
import { useRouter } from 'next/router';
import { useState } from 'react';
@@ -76,7 +76,7 @@ const updateMemberRoleFormSchema = z.object({
export default function OrgMember({ member, isAdmin }: OrgMemberProps) {
const { maintenanceActive } = useUI();
const { id } = useUserData();
const user = useUserData();
const { push } = useRouter();
const { refetch: refetchOrgs } = useOrgs();
const { org: { plan: { isFree } = {} } = {}, refetch: refetchCurrentOrg } =
@@ -87,7 +87,7 @@ export default function OrgMember({ member, isAdmin }: OrgMemberProps) {
const [updateMemberRoleDialogOpen, setUpdateMemberRoleDialogOpen] =
useState(false);
const isSelf = id === member.user.id;
const isSelf = user?.id === member.user.id;
const [deleteMember] = useDeleteOrganizationMemberMutation({
variables: {
@@ -98,7 +98,7 @@ export default function OrgMember({ member, isAdmin }: OrgMemberProps) {
const handleRemoveMemberFromOrg = async () => {
await execPromiseWithErrorToast(
async () => {
const isRemovingSelf = id === member.user.id;
const isRemovingSelf = user?.id === member.user.id;
await deleteMember();
// TODO see if it makes sense to unify both of these
await refetchCurrentOrg();
@@ -191,7 +191,7 @@ export default function OrgMember({ member, isAdmin }: OrgMemberProps) {
<DropdownMenu open={dropDownOpen} onOpenChange={setDropDownOpen}>
<DropdownMenuTrigger
disabled={
(!isAdmin && id !== member.user.id) ||
(!isAdmin && user?.id !== member.user.id) ||
isFree ||
maintenanceActive
}

View File

@@ -50,7 +50,7 @@ function ProjectCard({ project }: { project: Project }) {
}
export default function ProjectsGrid() {
const { org } = useCurrentOrg();
const { org, loading: orgLoading } = useCurrentOrg();
const { data, loading, error } = useGetProjectsQuery({
variables: {
@@ -76,7 +76,7 @@ export default function ProjectsGrid() {
throw error;
}
if (loading) {
if (loading || orgLoading) {
return <LoadingScreen />;
}

View File

@@ -1,10 +1,10 @@
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useAuth } from '@/providers/Auth';
import {
CheckoutStatus,
type PostOrganizationRequestMutation,
usePostOrganizationRequestMutation,
} from '@/utils/__generated__/graphql';
import { useAuthenticationStatus } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -24,7 +24,7 @@ function useFinishOrgCreation({
const router = useRouter();
const { session_id } = router.query;
const { isAuthenticated } = useAuthenticationStatus();
const { isAuthenticated } = useAuth();
const [loading, setLoading] = useState(false);
const [postOrganizationRequest] = usePostOrganizationRequestMutation();
const [status, setPostOrganizationRequestStatus] =

View File

@@ -1,6 +1,6 @@
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useUserData } from '@/hooks/useUserData';
import { Organization_Members_Role_Enum } from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
export default function useIsOrgAdmin() {
const { org: { members = [] } = {} } = useCurrentOrg();

View File

@@ -5,9 +5,9 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
import { Text } from '@/components/ui/v2/Text';
import { type Message } from '@/features/orgs/projects/ai/DevAssistant';
import { useUserData } from '@/hooks/useUserData';
import { copy } from '@/utils/copy';
import { useTheme } from '@mui/material';
import { useUserData } from '@nhost/nextjs';
import { onlyText } from 'react-children-utilities';
import Markdown, { type ExtraProps } from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';

View File

@@ -3,7 +3,6 @@ import {
mockApplication,
mockMatchMediaValue,
} from '@/tests/mocks';
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import {
mockPointerEvent,
render,
@@ -41,7 +40,7 @@ Object.defineProperty(window, 'matchMedia', {
value: vi.fn().mockImplementation(mockMatchMediaValue),
});
const server = setupServer(tokenQuery);
const server = setupServer();
const mocks = vi.hoisted(() => ({
useGetPiTrBaseBackupsLazyQuery: vi.fn(),
@@ -85,6 +84,10 @@ describe('ImportBackupContent', () => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
vi.restoreAllMocks();
@@ -97,7 +100,7 @@ describe('ImportBackupContent', () => {
render(<TestComponent />);
expect(
await screen.getByText(
screen.getByText(
`${mockApplication.name} (${mockApplication.region.name})`,
),
).toBeInTheDocument();
@@ -118,7 +121,7 @@ describe('ImportBackupContent', () => {
).not.toBeInTheDocument();
});
test('that warning is displayed if there are no other projects in the same organization', async () => {
test('that warning is displayed if there is no other project in the same organization', async () => {
server.use(getOrganization);
server.use(getEmptyProjectsQuery);
@@ -143,7 +146,7 @@ describe('ImportBackupContent', () => {
render(<TestComponent />);
expect(
await screen.getByText(
screen.getByText(
`${mockApplication.name} (${mockApplication.region.name})`,
),
).toBeInTheDocument();
@@ -161,21 +164,21 @@ describe('ImportBackupContent', () => {
});
expect(
await screen.getByText('Import backup from pitr14 (us-east-1)'),
screen.getByText('Import backup from pitr14 (us-east-1)'),
).toBeInTheDocument();
const startImportButton = await screen.getByRole('button', {
const startImportButton = screen.getByRole('button', {
name: 'Start import',
});
await user.click(startImportButton);
await waitFor(async () =>
expect(
await screen.getByRole('button', { name: 'Import backup' }),
screen.getByRole('button', { name: 'Import backup' }),
).toBeInTheDocument(),
);
const dateTimePickerButton = await screen.getByRole('button', {
const dateTimePickerButton = screen.getByRole('button', {
name: /UTC/i,
});
@@ -183,27 +186,27 @@ describe('ImportBackupContent', () => {
await waitFor(async () =>
expect(
await screen.getByRole('button', { name: 'Select' }),
screen.getByRole('button', { name: 'Select' }),
).toBeInTheDocument(),
);
await user.click(await screen.getByText('13'));
await user.click(screen.getByText('13'));
const hoursInput = await screen.getByLabelText('Hours');
const hoursInput = screen.getByLabelText('Hours');
await waitFor(async () => {
await user.type(hoursInput, '18');
});
const updatedDateTimeButton = await screen.getByRole('button', {
const updatedDateTimeButton = screen.getByRole('button', {
name: /UTC/i,
});
expect(updatedDateTimeButton).toHaveTextContent(
'13 Mar 2025, 18:00:05 (UTC+02:00)',
);
await user.click(await screen.getByRole('button', { name: 'Select' }));
await user.click(screen.getByRole('button', { name: 'Select' }));
await waitFor(async () =>
expect(
await screen.queryByRole('button', { name: 'Select' }),
screen.queryByRole('button', { name: 'Select' }),
).not.toBeInTheDocument(),
);
@@ -214,22 +217,20 @@ describe('ImportBackupContent', () => {
// check checkboxes
await user.click(
await screen.getByLabelText(/I understand that restoring this backup/),
screen.getByLabelText(/I understand that restoring this backup/),
);
await user.click(
await screen.getByLabelText(/I understand this cannot be undone/),
screen.getByLabelText(/I understand this cannot be undone/),
);
await waitFor(async () =>
expect(
await screen.getByRole('button', { name: 'Import backup' }),
screen.getByRole('button', { name: 'Import backup' }),
).not.toBeDisabled(),
);
await user.click(
await screen.getByRole('button', { name: 'Import backup' }),
);
await user.click(screen.getByRole('button', { name: 'Import backup' }));
expect(mocks.restoreApplicationDatabase.mock.calls[0][0].fromAppId).toBe(
'pitr14-id',

View File

@@ -9,9 +9,9 @@ import {
GetOrganizationsDocument,
useBillingDeleteAppMutation,
} from '@/generated/graphql';
import { useUserData } from '@/hooks/useUserData';
import { copy } from '@/utils/copy';
import { getApplicationStatusString } from '@/utils/helpers';
import { useUserData } from '@nhost/nextjs';
import { formatDistance } from 'date-fns';
import { useRouter } from 'next/router';
@@ -23,7 +23,7 @@ export default function ApplicationInfo() {
const [deleteApplication] = useBillingDeleteAppMutation({
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{ query: GetOrganizationsDocument, variables: { userId: userData?.id } },
],
});

View File

@@ -6,13 +6,13 @@ import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@/hooks/useUserData';
import { cn } from '@/lib/utils';
import { ApplicationStatus } from '@/types/application';
import {
GetOrganizationsDocument,
useUnpauseApplicationMutation,
} from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
import Image from 'next/image';
import { useCallback } from 'react';
@@ -39,7 +39,7 @@ export default function ApplicationPausedBanner({
refetchQueries: [
{
query: GetOrganizationsDocument,
variables: { userId: userData.id },
variables: { userId: userData?.id },
},
],
});

View File

@@ -15,7 +15,7 @@ type LogsServiceFilterProps = UseFormRegisterReturn<
service?: AvailableLogsService;
}
>;
const LogsServiceFilter = forwardRef<HTMLInputElement, LogsServiceFilterProps>(
const LogsServiceFilter = forwardRef<HTMLButtonElement, LogsServiceFilterProps>(
(props, ref) => {
const { project } = useProject();
const { data } = useGetServiceLabelValuesQuery({

View File

@@ -5,6 +5,7 @@ import { Divider } from '@/components/ui/v2/Divider';
import { Text } from '@/components/ui/v2/Text';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useUserData } from '@/hooks/useUserData';
import { isEmptyValue } from '@/lib/utils';
import {
GetOrganizationsDocument,
@@ -12,7 +13,6 @@ import {
} from '@/utils/__generated__/graphql';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import { useUserData } from '@nhost/nextjs';
import router from 'next/router';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
@@ -53,7 +53,7 @@ export default function RemoveApplicationModal({
const [loadingRemove, setLoadingRemove] = useState(false);
const [deleteApplication] = useBillingDeleteAppMutation({
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{ query: GetOrganizationsDocument, variables: { userId: userData?.id } },
],
});

View File

@@ -1,10 +1,10 @@
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
import { useUserData } from '@/hooks/useUserData';
import {
useGetFreeAndActiveProjectsQuery,
useGetProjectIsLockedQuery,
} from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
/**
* This hook returns the reason why the application is paused.

View File

@@ -4,9 +4,9 @@ import {
useGetApplicationStateQuery,
useGetOrganizationsLazyQuery,
} from '@/generated/graphql';
import { useUserData } from '@/hooks/useUserData';
import { ApplicationStatus } from '@/types/application';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { useUserData } from '@nhost/nextjs';
import { useCallback, useEffect, useState } from 'react';
type ApplicationStateMetadata = {
@@ -35,7 +35,7 @@ export default function useCheckProvisioning() {
});
async function updateOwnCache() {
await getOrgs({ variables: { userId: userData.id } });
await getOrgs({ variables: { userId: userData?.id } });
}
const memoizedUpdateCache = useCallback(updateOwnCache, [

View File

@@ -5,7 +5,8 @@ export default function useHostName() {
useEffect(() => {
const { port, hostname, protocol } = window.location;
setHostName(`${protocol}//${hostname}:${port}`);
const portSuffix = port ? `:${port}` : '';
setHostName(`${protocol}//${hostname}${portSuffix}`);
}, []);
return hostName;

View File

@@ -1,7 +1,7 @@
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useUserData } from '@/hooks/useUserData';
import { Organization_Members_Role_Enum } from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
/**
* Returns true if the current user is the owner of the current organization.

View File

@@ -1,6 +1,7 @@
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useAuth } from '@/providers/Auth';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
@@ -11,6 +12,7 @@ import { useEffect } from 'react';
*/
export default function useNotFoundRedirect() {
const router = useRouter();
const { isAuthenticated, isLoading } = useAuth();
const {
query: {
orgSlug: urlOrgSlug,
@@ -30,6 +32,8 @@ export default function useNotFoundRedirect() {
useEffect(() => {
if (
!isAuthenticated ||
isLoading ||
// If we're updating, we don't want to redirect to 404
updating ||
// If the router is not ready, we don't want to redirect to 404
@@ -69,5 +73,7 @@ export default function useNotFoundRedirect() {
projectSubdomain,
urlOrgSlug,
isPlatform,
isAuthenticated,
isLoading,
]);
}

View File

@@ -1,4 +1,5 @@
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useUserData } from '@/hooks/useUserData';
import { ApplicationStatus } from '@/types/application';
import type {
GetApplicationStateQuery,
@@ -9,7 +10,6 @@ import {
useGetOrganizationsLazyQuery,
} from '@/utils/__generated__/graphql';
import type { QueryHookOptions } from '@apollo/client';
import { useUserData } from '@nhost/nextjs';
import { useEffect } from 'react';
export interface UseProjectRedirectWhenReadyOptions
@@ -37,7 +37,7 @@ export default function useProjectRedirectWhenReady(
useEffect(() => {
async function updateOwnCache() {
await getOrgs({ variables: { userId: userData.id } });
await getOrgs({ variables: { userId: userData?.id } });
}
if (!data) {
return;

View File

@@ -17,7 +17,7 @@ export interface ReferencedSchemaSelectProps
function ReferencedSchemaSelect(
{ options, ...props }: ReferencedSchemaSelectProps,
ref: ForwardedRef<HTMLInputElement>,
ref: ForwardedRef<HTMLButtonElement>,
) {
const { setValue } = useFormContext<BaseForeignKeyFormValues>();
const { errors } = useFormState({ name: 'referencedSchema' });

View File

@@ -61,6 +61,19 @@ vi.mock('@/utils/__generated__/graphql', async () => {
};
});
vi.mock(
'@/features/orgs/components/common/TransferOrUpgradeProjectDialog',
async () => {
const actual = await vi.importActual<any>(
'@/features/orgs/components/common/TransferOrUpgradeProjectDialog',
);
return {
...actual,
TransferOrUpgradeProjectDialog: () => null,
};
},
);
afterEach(() => {
mocks.useCurrentOrg.mockRestore();
mocks.updateConfigMock.mockRestore();
@@ -83,7 +96,7 @@ test('If the org is free the switch should not be available and the save button
expect(saveButton).toBeDisabled();
expect(await screen.queryByRole('checkbox')).not.toBeInTheDocument();
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
test('the Save button is disabled until the switch in the header is not touched', async () => {

View File

@@ -14,12 +14,12 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useResetDatabasePasswordMutation } from '@/generated/graphql';
import { useLeaveConfirm } from '@/hooks/useLeaveConfirm';
import { useUserData } from '@/hooks/useUserData';
import { copy } from '@/utils/copy';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import { yupResolver } from '@hookform/resolvers/yup';
import { alpha } from '@mui/system';
import { useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
@@ -85,7 +85,7 @@ export default function ResetDatabasePasswordSettings() {
`An error occured while trying to update the database password for ${project.name}`,
);
await discordAnnounce(
`An error occurred while trying to update the database password: ${project.name} (${user.email}): ${e.message}`,
`An error occurred while trying to update the database password: ${project.name} (${user?.email}): ${e.message}`,
);
}
}

View File

@@ -11,8 +11,6 @@ interface Props {
description: string;
}
// P
function UpgradeNotification({ description }: Props) {
const [transferProjectDialogOpen, setTransferProjectDialogOpen] =
useState(false);

View File

@@ -12,12 +12,12 @@ import { DeploymentDurationLabel } from '@/features/orgs/projects/deployments/co
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@/hooks/useUserData';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import {
GetOrganizationsDocument,
useInsertDeploymentMutation,
} from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import type { MouseEvent } from 'react';
import { twMerge } from 'tailwind-merge';
@@ -60,7 +60,7 @@ export default function DeploymentListItem({
const [insertDeployment, { loading }] = useInsertDeploymentMutation({
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{ query: GetOrganizationsDocument, variables: { userId: userData?.id } },
],
});
const { commitMessage } = deployment;

View File

@@ -98,10 +98,13 @@ export default function SystemEnvironmentVariableSettings() {
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
),
},
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.httpUrl },
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
{ key: 'NHOST_FUNCTIONS_URL', value: appClient.functions.url },
{ key: 'NHOST_AUTH_URL', value: appClient.auth.baseURL },
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.baseURL },
{
key: 'NHOST_FUNCTIONS_URL',
value: appClient.functions.baseURL,
},
];
return (

View File

@@ -13,7 +13,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@nhost/nextjs';
import { useUserData } from '@/hooks/useUserData';
export interface BaseDirectoryFormValues {
/**
@@ -52,7 +52,10 @@ export default function BaseDirectorySettings() {
},
},
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{
query: GetOrganizationsDocument,
variables: { userId: userData?.id },
},
],
});

View File

@@ -12,7 +12,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useUserData } from '@nhost/nextjs';
import { useUserData } from '@/hooks/useUserData';
export interface DeploymentBranchFormValues {
/**
@@ -53,7 +53,10 @@ export default function DeploymentBranchSettings() {
},
},
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{
query: GetOrganizationsDocument,
variables: { userId: userData?.id },
},
],
});

View File

@@ -7,12 +7,17 @@ import {
getGraphqlServiceUrl,
getStorageServiceUrl,
} from '@/utils/env';
import type { NhostNextClientConstructorParams } from '@nhost/nextjs';
import { NhostClient } from '@nhost/nextjs';
import { DummySessionStorage } from '@/utils/nhost';
import {
createClient,
type NhostClient,
type NhostClientOptions,
} from '@nhost/nhost-js-beta';
export type UseAppClientOptions = NhostNextClientConstructorParams;
export type UseAppClientOptions = NhostClientOptions;
export type UseAppClientReturn = NhostClient;
const storage = new DummySessionStorage();
/**
* This hook returns an application specific Nhost client instance that can be
* used to interact with the client's backend.
@@ -27,7 +32,7 @@ export default function useAppClient(
const { project } = useProject();
if (!isPlatform) {
return new NhostClient({
return createClient({
authUrl: getAuthServiceUrl(),
graphqlUrl: getGraphqlServiceUrl(),
storageUrl: getStorageServiceUrl(),
@@ -37,8 +42,9 @@ export default function useAppClient(
}
if (process.env.NEXT_PUBLIC_ENV === 'dev' || !project) {
return new NhostClient({
return createClient({
subdomain: 'local',
region: 'local',
...options,
});
}
@@ -64,11 +70,12 @@ export default function useAppClient(
'functions',
);
return new NhostClient({
return createClient({
authUrl,
graphqlUrl,
storageUrl,
functionsUrl,
storage,
...options,
});
}

View File

@@ -1,10 +1,10 @@
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useAuth } from '@/providers/Auth';
import {
useGetOrganizationQuery,
type Exact,
type GetOrganizationQuery,
} from '@/utils/__generated__/graphql';
import { useAuthenticationStatus } from '@nhost/nextjs';
import { useRouter } from 'next/router';
export type Org = GetOrganizationQuery['organizations'][0];
@@ -24,8 +24,7 @@ export interface UseCurrenOrgReturnType {
export default function useCurrentOrg(): UseCurrenOrgReturnType {
const isPlatform = useIsPlatform();
const { isAuthenticated, isLoading: isAuthLoading } =
useAuthenticationStatus();
const { isAuthenticated, isLoading: isAuthLoading } = useAuth();
const {
query: { orgSlug },
@@ -41,12 +40,7 @@ export default function useCurrentOrg(): UseCurrenOrgReturnType {
isAuthenticated &&
!isAuthLoading;
const {
data: { organizations: [org] } = { organizations: [] },
loading,
error,
refetch,
} = useGetOrganizationQuery({
const { data, loading, error, refetch } = useGetOrganizationQuery({
fetchPolicy: 'cache-and-network',
skip: !shouldFetchOrg,
variables: {
@@ -54,6 +48,9 @@ export default function useCurrentOrg(): UseCurrenOrgReturnType {
},
});
const {
organizations: [org],
} = data || { organizations: [] };
return {
org,
loading: org ? false : loading || isAuthLoading,

View File

@@ -1,11 +1,12 @@
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { localOrganization } from '@/features/orgs/utils/local-dashboard';
import { useAuth } from '@/providers/Auth';
import {
useGetOrganizationsQuery,
type Exact,
type GetOrganizationsQuery,
} from '@/utils/__generated__/graphql';
import { useAuthenticationStatus, useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router';
export type Org = GetOrganizationsQuery['organizations'][0];
@@ -27,15 +28,13 @@ export interface UseOrgsReturnType {
export default function useOrgs(): UseOrgsReturnType {
const router = useRouter();
const isPlatform = useIsPlatform();
const userData = useUserData();
const { isAuthenticated, isLoading } = useAuthenticationStatus();
const { isAuthenticated, isLoading, user } = useAuth();
const shouldFetchOrg =
isPlatform && isAuthenticated && !isLoading && userData;
const shouldFetchOrg = isPlatform && isAuthenticated && !isLoading && user;
const { data, loading, error, refetch } = useGetOrganizationsQuery({
variables: {
userId: userData?.id,
userId: user?.id,
},
fetchPolicy: 'cache-and-network',
skip: !shouldFetchOrg,

View File

@@ -1,11 +1,12 @@
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { localApplication } from '@/features/orgs/utils/local-dashboard';
import { useAuth } from '@/providers/Auth';
import { useNhostClient } from '@/providers/nhost';
import {
GetProjectDocument,
type GetProjectQuery,
type ProjectFragment,
} from '@/utils/__generated__/graphql';
import { useAuthenticationStatus, useNhostClient } from '@nhost/nextjs';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
@@ -24,10 +25,9 @@ export default function useProject(): UseProjectReturnType {
query: { appSubdomain },
isReady: isRouterReady,
} = useRouter();
const client = useNhostClient();
const nhost = useNhostClient();
const isPlatform = useIsPlatform();
const { isAuthenticated, isLoading: isAuthLoading } =
useAuthenticationStatus();
const { isAuthenticated, isLoading: isAuthLoading } = useAuth();
const shouldFetchProject = useMemo(
() =>
@@ -42,12 +42,10 @@ export default function useProject(): UseProjectReturnType {
const { data, isLoading, refetch, error } = useQuery(
['project', appSubdomain as string],
async () => {
const response = await client.graphql.request<{
const response = await nhost.graphql.post<{
apps: ProjectFragment[];
}>(GetProjectDocument, {
subdomain: (appSubdomain as string) || '',
});
return response;
}>(GetProjectDocument, { subdomain: (appSubdomain as string) || '' });
return response.body;
},
{
enabled: shouldFetchProject,

View File

@@ -1,6 +1,5 @@
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { AvailableLogsService } from '@/features/orgs/projects/logs/utils/constants/services';
import { useRemoteApplicationGQLClientWithSubscriptions } from '@/hooks/useRemoteApplicationGQLClientWithSubscriptions';
import { mockApplication as mockProject } from '@/tests/mocks';
import { renderHook } from '@/tests/testUtils';
import { useGetProjectLogsQuery } from '@/utils/__generated__/graphql';
@@ -10,27 +9,8 @@ import useProjectLogs, { type UseProjectLogsProps } from './useProjectLogs';
// Mock the dependencies
vi.mock('@/features/orgs/projects/hooks/useProject');
vi.mock('@/hooks/useRemoteApplicationGQLClientWithSubscriptions');
vi.mock('@/utils/__generated__/graphql', async () => {
const actual = await vi.importActual<any>('@/utils/__generated__/graphql');
return {
...actual,
useGetProjectLogsQuery: vi.fn(),
GetLogsSubscriptionDocument: 'GetLogsSubscriptionDocument',
};
});
const mockUseProject = vi.mocked(useProject);
const mockUseRemoteApplicationGQLClientWithSubscriptions = vi.mocked(
useRemoteApplicationGQLClientWithSubscriptions,
);
const mockUseGetProjectLogsQuery = vi.mocked(useGetProjectLogsQuery);
type ProjectLogsReturnType = ReturnType<typeof useGetProjectLogsQuery>;
type SubscribeToMore = ProjectLogsReturnType['subscribeToMore'];
describe('useProjectLogs - Subscription Creation & Cleanup', () => {
const createMockApolloClient = () => ({
vi.mock('@/utils/splitGraphqlClient', () => ({
splitGraphqlClient: {
query: vi.fn(),
mutate: vi.fn(),
watchQuery: vi.fn(),
@@ -64,10 +44,25 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
localState: {},
queryManager: {} as any,
typeDefs: undefined,
});
},
}));
let mockClient: ReturnType<typeof createMockApolloClient>;
vi.mock('@/utils/__generated__/graphql', async () => {
const actual = await vi.importActual<any>('@/utils/__generated__/graphql');
return {
...actual,
useGetProjectLogsQuery: vi.fn(),
GetLogsSubscriptionDocument: 'GetLogsSubscriptionDocument',
};
});
const mockUseProject = vi.mocked(useProject);
const mockUseGetProjectLogsQuery = vi.mocked(useGetProjectLogsQuery);
type ProjectLogsReturnType = ReturnType<typeof useGetProjectLogsQuery>;
type SubscribeToMore = ProjectLogsReturnType['subscribeToMore'];
describe('useProjectLogs - Subscription Creation & Cleanup', () => {
const mockSubscribeToMore = vi.fn();
const mockUnsubscribe = vi.fn();
@@ -89,13 +84,6 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
refetch: vi.fn(),
});
mockClient = createMockApolloClient();
// Mock the GraphQL client
mockUseRemoteApplicationGQLClientWithSubscriptions.mockReturnValue(
mockClient as any,
);
// Mock subscribeToMore to return an unsubscribe function
mockSubscribeToMore.mockReturnValue(mockUnsubscribe);

View File

@@ -1,12 +1,12 @@
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { AvailableLogsService } from '@/features/orgs/projects/logs/utils/constants/services';
import { useRemoteApplicationGQLClientWithSubscriptions } from '@/hooks/useRemoteApplicationGQLClientWithSubscriptions';
import { isNotEmptyValue } from '@/lib/utils';
import {
type GetProjectLogsQuery,
GetLogsSubscriptionDocument,
useGetProjectLogsQuery,
} from '@/utils/__generated__/graphql';
import { splitGraphqlClient } from '@/utils/splitGraphqlClient';
import { useCallback, useEffect, useRef } from 'react';
export interface UseProjectLogsProps {
@@ -50,7 +50,6 @@ export function updateQuery(
function useProjectLogs(props: UseProjectLogsProps) {
const { project, loading: loadingProject } = useProject();
// create a client that sends http requests to Hasura but websocket requests to Bragi
const clientWithSplit = useRemoteApplicationGQLClientWithSubscriptions();
const subscriptionReturn = useRef(null);
const {
@@ -59,7 +58,7 @@ function useProjectLogs(props: UseProjectLogsProps) {
...result
} = useGetProjectLogsQuery({
variables: { appID: project?.id, ...props },
client: clientWithSplit,
client: splitGraphqlClient,
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true,
skip: !project,

View File

@@ -1,11 +1,12 @@
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { localApplication } from '@/features/orgs/utils/local-dashboard';
import { useAuth } from '@/providers/Auth';
import { useNhostClient } from '@/providers/nhost';
import {
GetProjectStateDocument,
type GetProjectQuery,
type ProjectFragment,
} from '@/utils/__generated__/graphql';
import { useAuthenticationStatus, useNhostClient } from '@nhost/nextjs';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
@@ -24,10 +25,9 @@ export default function useProjectWithState(): UseProjectWithStateReturnType {
query: { appSubdomain },
isReady: isRouterReady,
} = useRouter();
const client = useNhostClient();
const nhost = useNhostClient();
const isPlatform = useIsPlatform();
const { isAuthenticated, isLoading: isAuthLoading } =
useAuthenticationStatus();
const { isAuthenticated, isLoading: isAuthLoading } = useAuth();
const shouldFetchProject = useMemo(
() =>
@@ -42,12 +42,12 @@ export default function useProjectWithState(): UseProjectWithStateReturnType {
const { data, isLoading, refetch, error } = useQuery(
['projectWithState', appSubdomain as string],
async () => {
const response = await client.graphql.request<{
const response = await nhost.graphql.post<{
apps: ProjectFragment[];
}>(GetProjectStateDocument, {
subdomain: (appSubdomain as string) || '',
});
return response;
return response?.body.data;
},
{
enabled: shouldFetchProject,
@@ -61,7 +61,7 @@ export default function useProjectWithState(): UseProjectWithStateReturnType {
if (isPlatform) {
return {
project: data?.data?.apps?.[0] || null,
project: data?.apps?.[0] || null,
loading: isLoading && shouldFetchProject,
error: Array.isArray(error || {}) ? error[0] : error,
refetch,

View File

@@ -67,11 +67,12 @@ test('should render an empty state when GitHub is not connected', async () => {
'https://local.graphql.local.nhost.run/v1',
async (req, res, ctx) => {
const { operationName } = await req.json();
if (operationName === 'getProject') {
return res(
ctx.json({
apps: [{ ...mockApplication, githubRepository: null }],
data: {
apps: [{ ...mockApplication, githubRepository: null }],
},
}),
);
}
@@ -79,7 +80,9 @@ test('should render an empty state when GitHub is not connected', async () => {
if (operationName === 'getOrganization') {
return res(
ctx.json({
organizations: [{ ...mockOrganization }],
data: {
organizations: [{ ...mockOrganization }],
},
}),
);
}
@@ -102,7 +105,6 @@ test('should render an empty state when GitHub is not connected', async () => {
await screen.findByRole('button', { name: /connect to github/i }),
).toBeInTheDocument();
});
test('should render an empty state when GitHub is connected, but there are no deployments', async () => {
server.use(
rest.post(
@@ -171,7 +173,6 @@ test('should render a list of deployments', async () => {
}),
);
}
if (operationName === 'getOrganization') {
return res(
ctx.json({
@@ -260,7 +261,6 @@ test('should disable redeployments if a deployment is already in progress', asyn
}),
);
}
if (operationName === 'getOrganization') {
return res(
ctx.json({

View File

@@ -8,7 +8,6 @@ test('should return the total number of allocated resources', () => {
totalAvailableVCPU: 1,
totalAvailableMemory: 2,
database: {
replicas: 1,
vcpu: 0,
memory: 0.5,
},
@@ -36,7 +35,6 @@ test('should return the total number of allocated resources', () => {
totalAvailableVCPU: 1,
totalAvailableMemory: 2,
database: {
replicas: 1,
vcpu: 0.25,
memory: 0,
},

View File

@@ -10,7 +10,7 @@ import type { DataGridHeaderProps } from '@/features/orgs/projects/storage/dataG
import { DataGridHeader } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
import type { ForwardedRef } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import mergeRefs from 'react-merge-refs';
import { mergeRefs } from 'react-merge-refs';
import type { Column, Row, SortingRule, TableOptions } from 'react-table';
import { twMerge } from 'tailwind-merge';
import useDataGrid from './useDataGrid';

View File

@@ -24,7 +24,7 @@ export default function DataGridBooleanCell<TData extends object>({
editCell,
cancelEditCell,
isSelected,
} = useDataGridCell<HTMLInputElement>();
} = useDataGridCell<HTMLButtonElement>();
async function handleMenuClick(
event: MouseEvent<HTMLLIElement> | ReactKeyboardEvent<HTMLLIElement>,

View File

@@ -10,6 +10,7 @@ import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { usePreviewToggle } from '@/features/orgs/projects/storage/dataGrid/hooks/usePreviewToggle';
import { getHasuraAdminSecret } from '@/utils/env';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import { useEffect, useReducer, useState } from 'react';
@@ -50,7 +51,7 @@ function useBlob({
const { previewEnabled } = usePreviewToggle();
// This side-effect fetches the blob of the file from the server and sets the
// relevant `objectUrl` state. Abort controller is reponsible for cancelling
// relevant `objectUrl` state. Abort controller is responsible for cancelling
// the fetch if the component is unmounted.
useEffect(() => {
if (!previewEnabled) {
@@ -172,7 +173,6 @@ export default function DataGridPreviewCell<TData extends object>({
value: { fetchBlob, id, mimeType, alt, blob },
fallbackPreview = null,
}: DataGridPreviewCellProps<TData>) {
const { project } = useProject();
const appClient = useAppClient();
const { objectUrl, loading, error } = useBlob({
fetchBlob,
@@ -181,6 +181,7 @@ export default function DataGridPreviewCell<TData extends object>({
});
const [showModal, setShowModal] = useState(false);
const { previewEnabled } = usePreviewToggle();
const { project } = useProject();
const [
{ loading: previewLoading, error: previewError, data: previewUrl },
@@ -215,9 +216,14 @@ export default function DataGridPreviewCell<TData extends object>({
dispatch({ type: 'PREVIEW_LOADING' });
}
const { presignedUrl } = await appClient.storage
.setAdminSecret(project?.config?.hasura.adminSecret)
.getPresignedUrl({ fileId: id });
const { body: presignedUrl } = await appClient.storage.getPresignedURL(id, {
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: project?.config?.hasura.adminSecret,
},
});
if (!presignedUrl) {
dispatch({

View File

@@ -258,17 +258,26 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
}, 250);
try {
const { fileMetadata, error: fileError } = await appClient.storage
.setAdminSecret(
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: project?.config?.hasura.adminSecret,
)
.upload({
file,
name: encodeURIComponent(file.name),
bucketId: defaultBucket.id,
});
const uploadResponse = await appClient.storage.uploadFiles(
{
'bucket-id': defaultBucket.id,
'file[]': [file],
'metadata[]': [
{
name: encodeURIComponent(file.name),
},
],
},
{
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: project?.config?.hasura.adminSecret,
},
},
);
const fileMetadata = uploadResponse.body.processedFiles[0];
uploaded = true;
@@ -276,10 +285,6 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
toast.remove(toastId);
}
if (fileError) {
throw new Error(fileError.message);
}
if (!fileMetadata) {
throw new Error('File metadata is missing.');
}

View File

@@ -70,39 +70,24 @@ export default function FilesDataGridControls({
setDeleteLoading(true);
try {
const storageWithAdminSecret = appClient.storage.setAdminSecret(
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: project.config?.hasura.adminSecret,
);
// note: this is not an optimal solution, but we don't have a better way
// to batch remove files for now
const response = await Promise.allSettled(
await Promise.allSettled(
selectedFiles.map((file) =>
storageWithAdminSecret.delete({ fileId: file.original.id }),
appClient.storage.deleteFile(file.original.id, {
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: project.config?.hasura.adminSecret,
},
}),
),
);
const failedFiles = response.filter(
(content) =>
content.status === 'rejected' || Boolean(content.value.error),
triggerToast(
selectedFiles.length === 1
? `The file was successfully deleted.`
: `${selectedFiles.length} files were successfully deleted.`,
);
if (failedFiles.length > 0) {
triggerToast(
`Failed to delete ${failedFiles.length} ${
failedFiles.length === 1 ? 'file' : 'files'
}`,
);
} else {
triggerToast(
selectedFiles.length === 1
? `The file was successfully deleted.`
: `${selectedFiles.length} files were successfully deleted.`,
);
}
toggleAllRowsSelected(false);
if (refetchData) {
@@ -110,9 +95,9 @@ export default function FilesDataGridControls({
}
} catch (error) {
triggerToast(error.message || 'Unknown error occurred');
} finally {
setDeleteLoading(false);
}
setDeleteLoading(false);
}
return (

View File

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

View File

@@ -0,0 +1,9 @@
import { useAuth } from '@/providers/Auth';
function useAccessToken() {
const { session } = useAuth();
return session?.accessToken;
}
export default useAccessToken;

View File

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

View File

@@ -0,0 +1,28 @@
import { useAccessToken } from '@/hooks/useAccessToken';
import { jwtDecode } from 'jwt-decode';
export interface JWTClaims {
sub?: string;
iat?: number;
exp?: number;
iss?: string;
'https://hasura.io/jwt/claims': JWTHasuraClaims;
}
export interface JWTHasuraClaims {
// ? does not work as expected: if the key does not start with `x-hasura-`, then it is typed as `any`
// [claim: `x-hasura-${string}`]: string | string[]
[claim: string]: string | string[] | null;
'x-hasura-allowed-roles': string[];
'x-hasura-default-role': string;
'x-hasura-user-id': string;
'x-hasura-user-is-anonymous': string;
'x-hasura-auth-elevated': string;
}
function useDecodedAccessToken() {
const token = useAccessToken();
return token ? jwtDecode<JWTClaims>(token) : null;
}
export default useDecodedAccessToken;

View File

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

View File

@@ -0,0 +1,26 @@
import { useUserData } from '@/hooks/useUserData';
import { isNotEmptyValue } from '@/lib/utils';
import { useNhostClient } from '@/providers/nhost';
import { startAuthentication } from '@simplewebauthn/browser';
function useElevateEmail() {
const nhost = useNhostClient();
const user = useUserData();
async function elevateEmail() {
const elevateResponse = await nhost.auth.elevateWebauthn();
const credential = await startAuthentication(elevateResponse.body);
const verifyResponse = await nhost.auth.verifyElevateWebauthn({
email: user?.email,
credential,
});
if (isNotEmptyValue(verifyResponse.body.session)) {
nhost.sessionStorage.set(verifyResponse.body.session);
}
}
return elevateEmail;
}
export default useElevateEmail;

View File

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

View File

@@ -0,0 +1,12 @@
import {
useDecodedAccessToken,
type JWTHasuraClaims,
} from '@/hooks/useDecodedAccessToken';
function useHasuraClaims(): JWTHasuraClaims | null {
const decodedToken = useDecodedAccessToken();
return decodedToken?.['https://hasura.io/jwt/claims'] || null;
}
export default useHasuraClaims;

View File

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

View File

@@ -1,72 +0,0 @@
import { ApolloClient, HttpLink, InMemoryCache, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { useAccessToken, useNhostClient } from '@nhost/nextjs';
import { createClient } from 'graphql-ws';
import { useMemo } from 'react';
/**
* It creates a new Apollo Client instance with a split property which recognizes the type of operation and uses a different
* link for queries/mutations (HttpLink -- our own application querying through remote schemas) and subscriptions (GraphQLWsLink connected to the bragi endpoint).
* @returns A function that returns a new ApolloClient instance with split functionality prepared for websockets connected to bragi.
*/
export default function useRemoteApplicationGQLClientWithSubscriptions() {
const client = useNhostClient();
const token = useAccessToken();
const userApplicationClient = useMemo(() => {
const httpLink = new HttpLink({
uri: client.graphql.getUrl(),
headers: {
authorization: `Bearer ${token}`,
},
});
const wsLink = new GraphQLWsLink(
createClient({
url: process.env.NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET,
connectionParams: {
headers: {
authorization: `Bearer ${token}`,
},
},
webSocketImpl: WebSocket,
}),
);
return new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Subscription: {
fields: {
logs: {
keyArgs: false,
},
},
},
Query: {
fields: {
logs: {
keyArgs: false,
},
},
},
},
}),
connectToDevTools: true,
link: split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
),
});
}, [client.graphql, token]);
return userApplicationClient;
}

View File

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

View File

@@ -0,0 +1,11 @@
import { useAuth } from '@/providers/Auth';
function useUserData() {
const authContext = useAuth();
const userData = authContext.user;
return userData;
}
export default useUserData;

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