Compare commits

...

22 Commits

Author SHA1 Message Date
github-actions[bot]
70433187cc chore: update versions (#3355)
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-auth-js@2.12.0

### Minor Changes

-   39b10a2: feat (dashboard): Add multi-factor authentication

## @nhost/react@3.11.0

### Minor Changes

-   39b10a2: feat (dashboard): Add multi-factor authentication
-   4b84780: feat (dashboard): Add Webauthn to dashboard

### Patch Changes

-   @nhost/nhost-js@3.2.9

## @nhost/apollo@8.0.9

### Patch Changes

-   @nhost/nhost-js@3.2.9

## @nhost/react-apollo@18.0.0

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0
    -   @nhost/apollo@8.0.9

## @nhost/react-urql@15.0.0

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0

## @nhost/nextjs@2.2.8

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0

## @nhost/nhost-js@3.2.9

### Patch Changes

-   Updated dependencies [39b10a2]
    -   @nhost/hasura-auth-js@2.12.0

## @nhost/vue@2.9.6

### Patch Changes

-   @nhost/nhost-js@3.2.9

## @nhost/dashboard@2.31.0

### Minor Changes

-   39b10a2: feat (dashboard): Add multi-factor authentication
-   4b84780: feat (dashboard): Add Webauthn to dashboard

### Patch Changes

-   61eb6cd: fix (dashboard): Fix update project e2e test
    -   @nhost/react-apollo@18.0.0
    -   @nhost/nextjs@2.2.8

## @nhost-examples/cli@0.3.22

### Patch Changes

-   @nhost/nhost-js@3.2.9

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

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0
    -   @nhost/react-apollo@18.0.0

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

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0

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

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0
    -   @nhost/react-urql@15.0.0

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

### Patch Changes

-   @nhost/nhost-js@3.2.9

## @nhost-examples/nextjs@0.4.8

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0
    -   @nhost/react-apollo@18.0.0
    -   @nhost/nextjs@2.2.8

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

### Patch Changes

-   @nhost/nhost-js@3.2.9

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

### Patch Changes

-   @nhost/nhost-js@3.2.9

## @nhost-examples/sveltekit@0.8.1

### Patch Changes

-   @nhost/nhost-js@3.2.9

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

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0
    -   @nhost/react-apollo@18.0.0

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

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0

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

### Patch Changes

-   Updated dependencies [39b10a2]
-   Updated dependencies [4b84780]
    -   @nhost/react@3.11.0
    -   @nhost/react-apollo@18.0.0

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

### Patch Changes

-   @nhost/nhost-js@3.2.9
-   @nhost/apollo@8.0.9
-   @nhost/vue@2.9.6

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

### Patch Changes

-   @nhost/apollo@8.0.9
-   @nhost/vue@2.9.6

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-05 12:39:48 +02:00
robertkasza
39b10a2e9f feat (dashboard): Add mfa (#3342)
### **PR Type**
Enhancement


___

### **Description**
- Add multi-factor authentication (MFA) to dashboard

- Implement MFA OTP form and QR code generation

- Create MFA settings and activation components

- Update sign-in process to support MFA


___



### **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>19
files</summary><table>
<tr>
<td><strong>MfaOtpForm.tsx</strong><dd><code>Create reusable MFA OTP
form component</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/3342/files#diff-88ee3610a0658d5eead85db025a5e91e74a4d2f2a836adf7eb44ff80888a613b">+61/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>index.ts</strong><dd><code>Export MfaOtpForm
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-9c1deb50c3a92ca5494be705635984a97e1b41b07cd0847168a4eeddf0e375d0">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>AccountMfaSettings.tsx</strong><dd><code>Implement MFA
settings component</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/3342/files#diff-4eb33e0f23780eaf93fd7d86850b263d83b05dc2d7a3f6ed9e30d1ca811f17af">+32/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>GenerateMfaQRCodeButton.tsx</strong><dd><code>Create button
to generate MFA QR code</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/3342/files#diff-7310648a5e879bb76ba6c3136fe555ed3bbdacddc33eef4ce8fc9c21a547ec82">+50/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>MfaQRCode.tsx</strong><dd><code>Implement MFA QR code
generation component</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/3342/files#diff-0c60d61f12b47e421c67c389c66399da76af4b32241610fe94c6635353e57da2">+49/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useActivateMfa.ts</strong><dd><code>Create hook for MFA
activation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-0ae70fc9df5a3a6828f7a266db8036107ce9ea705cd318d3a1c4b7304d8522ba">+46/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useMfaEnabled.ts</strong><dd><code>Create hook to check MFA
status</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/3342/files#diff-31d2af339a8dd32beff8cce79962fa0dd23b6c89687b21aa75663ebeccb0b154">+17/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>index.ts</strong><dd><code>Export AccountMfaSettings
component</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/3342/files#diff-8c530fc016dd3569f2b7ec7e9085b99c99922ed077357bec562b8c9acaead24a">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>SecurityKeyList.tsx</strong><dd><code>Update import path for
useGetSecurityKeys</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/3342/files#diff-966a157d381be33bc876e76b28f804e80cae6edb1aa088e78f883063966be3ba">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useOnAddNewSecurityKeyHandler.ts</strong><dd><code>Update
import paths for hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-3514a6d1514269a83f37fc25e9cb24add9d5d74f9cf3341293c0e0f2a4c2e286">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useRemoveSecurityKey.ts</strong><dd><code>Update import
paths for hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-5683e00a14f39018d8fe58a3116c2a8ea6d2f2a83abb2177bbf0ee8ddf0f97b5">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useElevatedPermissions.ts</strong><dd><code>Create hook for
elevated permissions</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/3342/files#diff-c1e4f573300c771149cc2e59918c9acf2ae5f8a6680800a899707c70800ba144">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useGetSecurityKeys.ts</strong><dd><code>Create hook to fetch
security keys</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/3342/files#diff-1f9fed870cab61f15e304342e4913edab0f5537eeb6230070de4b4f7173fa138">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>MfaSignInOtpForm.tsx</strong><dd><code>Create MFA sign-in
OTP form component</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/3342/files#diff-91eba232beb0543b1e972ed9a21a0be797ed94b720487834bb3316a5dbd732f5">+26/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>SignInWithEmailAndPassword.tsx</strong><dd><code>Update
sign-in component to support MFA</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/3342/files#diff-a2b70644663baf4f6f2cdffd846d4d743a5ca1f2a64c4b278b6f04c6c5c92161">+16/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>

<td><strong>SignInWithEmailAndPasswordForm.tsx</strong><dd><code>Implement
sign-in form with MFA support</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/3342/files#diff-a07fd6bd20c97d0c9c875e690cd3a80068fc58f74d3579feb210e189d32f5031">+91/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>

<td><strong>useOnSignInWithEmailAndPasswordHandler.ts</strong><dd><code>Create
hook for sign-in with MFA</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/3342/files#diff-1a253bfc02c3267ab1c6b58c07aa06142b7e711d613b672c8420ff2861b12d27">+56/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>

<td><strong>useSignInWithEmailAndPasswordForm.ts</strong><dd><code>Create
hook for sign-in form validation</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/3342/files#diff-b908e474c0fb54db9c922d9fef7cf1ef6c4ccb0dd7519da0c45a18e5bb26ed40">+30/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>index.ts</strong><dd><code>Export SignInWithEmailAndPassword
component</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/3342/files#diff-d3fd195b5ca8ece9eac446129e8501793e5bd6e5c167ed36c8c6d0adc1723fda">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>8 files</summary><table>
<tr>
  <td><strong>mighty-onions-crash.md</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-834d585225de297c20c9e325a231c6d3a72227fc1d8cc84b0c1f8fe0dbb1c523">+7/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>getActiveMfaType.gql</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-ac5aa6c409363b550d15aace147448c5e267a3cf0fb7f86faf5060f8cbe35302">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>index.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-61a48d15d3a2e29160a6d91cd01501ac94cf9f70995c6a84fbb6d6e2c2d4fca1">+4/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>email.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-b5d7db4460066bc114cb766771612d6f908bd6e440f40de98e4ac311a26b50cd">+16/-152</a></td>

</tr>

<tr>
  <td><strong>graphql.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-fbd5db84b560b1c91675004448c6c7fa0dcbfb28b9eb05d53b03e6cb7b83ebac">+80/-35</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>enable-mfa.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-8ed0174991b707a5c54f54ec881656403b4409cd0e3d7004045a80dbeb7b4444">+1/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-1cee8646d2cfba37d6ce6a6e9a8d16f8caba0b99fc3a1ad0cb997ed8c7384d2e">+7/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>useSignInEmailPassword.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3342/files#diff-107884d4022cd6c01459f001fa97d2b2ce11566a2c88c8deaec4727c1af44aba">+6/-8</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-05 11:38:49 +02:00
robertkasza
4b8478004e feat (dashboard): Add Webauthn to dashboard (#3320) 2025-06-05 10:28:42 +02:00
robertkasza
61eb6cdc2d fix (dashboard): fix e2e test (#3354)
### **PR Type**
Bug fix, Tests


___

### **Description**
- Refactored e2e test for project upgrade

- Removed unnecessary 'await' keywords

- Added step to save payment information

- Commented out local dashboard image build and tests


___



### **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>upgrade-project.test.ts</strong><dd><code>Refactor and
enhance upgrade project e2e test</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

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

<li>Removed redundant 'await' keywords<br> <li> Added step to save
payment information<br> <li> Simplified assertions and variable
assignments<br> <li> Minor code cleanup and formatting


</details>


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

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ci.yaml</strong><dd><code>Modify CI workflow to focus
on specific tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.github/workflows/ci.yaml

<li>Commented out local dashboard image build step<br> <li> Removed
Nhost CLI installation and local tests<br> <li> Disabled general e2e
tests run


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3354/files#diff-944291df2c9c06359d37cc8833d182d705c9e8c3108e7cfe132d61a06e9133dd">+21/-21</a>&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-05 09:17:46 +02:00
github-actions[bot]
14187d381f chore: update versions (#3332)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@2.30.0

### Minor Changes

-   f6947a2: fix: fetch job-backup services logs using Live filter
- 44a3e6b: fix: collapsed main navigation sidebar overlaps mobile navbar
-   99b78f1: feat: dashboard: add download button for soc2 report
-   9acae7d: fix: e2e tests, stop on error when refreshing metadata

### Patch Changes

- 31e636a: fix (dashboard): Use the correct payload to reset metadata
before the e2e tests

## @nhost/docs@2.32.0

### Minor Changes

-   df51c3e: fix: added installation instructions for the CLI

### Patch Changes

-   4d835c4: fix: remove nodejs18
-   2aa81a6: fix (docs): fix audit

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

### Minor Changes

-   6ee2d1f: fix: proper use of onError in middleware

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-03 18:33:21 +00:00
Nuno Pato
99b78f147e feat: dashboard: add download button for soc2 report (#3349)
### **PR Type**
Enhancement


___

### **Description**
- Add SOC2 report download button for eligible organizations

- Implement Soc2Download component with error handling

- Update settings page to include Soc2Download component

- Add environment variable for SOC2 report file ID


___



### **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>Soc2Download.tsx</strong><dd><code>Implement
Soc2Download Component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>


dashboard/src/features/orgs/components/general/components/Soc2Download/Soc2Download.tsx

<li>Create Soc2Download component with download functionality<br> <li>
Implement eligibility check for Team and Enterprise plans<br> <li> Add
error handling and user feedback for download process<br> <li> Design UI
for SOC2 report download section


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3349/files#diff-3768eb3fc718d4780028c34b5c76388e8d93cbbac94868f82c1a262fb9cc1100">+88/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>index.ts</strong><dd><code>Export Soc2Download
Component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>


dashboard/src/features/orgs/components/general/components/Soc2Download/index.ts

- Export Soc2Download component as default


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>settings.tsx</strong><dd><code>Add Soc2Download to
Settings 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; </dd></summary>
<hr>

dashboard/src/pages/orgs/[orgSlug]/settings.tsx

<li>Import Soc2Download component<br> <li> Add Soc2Download component to
OrgSettings page


</details>


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

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>spicy-walls-joke.md</strong><dd><code>Add Changeset for
SOC2 Download Feature</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

.changeset/spicy-walls-joke.md

<li>Add changeset for minor version bump<br> <li> Describe feature
addition for SOC2 report download


</details>


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

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>.env.example</strong><dd><code>Add SOC2 Report File ID
Environment Variable</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/.env.example

- Add NEXT_PUBLIC_SOC2_REPORT_FILE_ID environment variable


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3349/files#diff-b47cf46119af2f0298d96e5657e53e57161833e8b02d87526ac5c1ed9393d477">+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>
2025-06-03 15:04:05 +00:00
robertkasza
2aa81a6cb9 fix (docs): fix audit (#3351)
### **PR Type**
Bug fix, Documentation


___

### **Description**
- Update audit-ci.jsonc to allow CVE-2025-48068 for 'next'

- Add new security patches in package.json

- Create changeset for @nhost/docs patch

- Improve documentation and audit compliance


___



### **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>yellow-drinks-sort.md</strong><dd><code>Add changeset
for @nhost/docs patch</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/yellow-drinks-sort.md

<li>Add new changeset file for @nhost/docs<br> <li> Specify patch
version update<br> <li> Include fix message for documentation audit


</details>


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

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>audit-ci.jsonc</strong><dd><code>Update audit-ci.jsonc
allowlist for security exception</code>&nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

audit-ci.jsonc

- Add allowlist entry for CVE-2025-48068 in 'next' path


</details>


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

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update package.json with
new security patches</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

package.json

<li>Add new security patches for undici and tar-fs<br> <li> Update
existing security patch versions<br> <li> Adjust formatting for
consistency


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3351/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+4/-2</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-03 16:01:52 +02:00
David BM
a1edaf18ea fix: update requirements in DEVELOPERS.md (#3345)
### **User description**
Update requirements for development to Node.js 20


___

### **PR Type**
Documentation


___

### **Description**
- Update Node.js requirement to v20 or later

- Remove mention of Node.js v16 support

- Simplify Node.js version requirement section


___



### **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>DEVELOPERS.md</strong><dd><code>Update Node.js version
requirement</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></summary>
<hr>

DEVELOPERS.md

<li>Updated Node.js requirement to v20 or later<br> <li> Removed mention
of Node.js v16 support<br> <li> Simplified Node.js version requirement
section


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3345/files#diff-bd017515eb79a7fb7569b1d15e8963ea380123d4fdf779978dd4b3ab7500fd10">+1/-3</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 Murcia <davidbm@air-m4.local>
2025-05-22 12:14:15 +02:00
David Barroso
4d835c4b9c fix (docs): remove nodejs18 (#3343)
### **PR Type**
Documentation


___

### **Description**
- Remove Node.js 18 runtime from supported versions

- Update documentation to reflect current runtimes

- Remove Node.js 18 configuration example

- Add changeset for patch update to @nhost/docs


___



### **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>brave-garlics-act.md</strong><dd><code>Add changeset
for removing Node.js 18</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/brave-garlics-act.md

<li>Add new changeset file for @nhost/docs patch update<br> <li>
Describe fix to remove Node.js 18


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>runtimes.mdx</strong><dd><code>Update runtimes
documentation to remove Node.js 18</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

docs/products/functions/runtimes.mdx

<li>Remove Node.js 18 from list of supported runtimes<br> <li> Delete
Node.js 18 configuration example<br> <li> Update supported versions to
Node.js 20 and 22


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3343/files#diff-41cc586838cadca39a91bf32878fb7cc5473d5815dec595547a4089684b5d489">+0/-9</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-05-20 22:19:18 +02:00
David BM
44a3e6bd41 fix (dashboard): collapsed main navigation sidebar overlaps mobile navbar (#3341) 2025-05-20 22:05:58 +02:00
Dimitri POSTOLOV
6ee2d1f5bf fix (examples/nextjs-server-components): proper use of onError in middleware (#3330)
onError should be returned since it can contain `NextResponse.redirect`
like in the example
e87505c564/examples/quickstarts/nextjs-server-components/src/middleware.ts (L5-L7)

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-05-19 11:21:06 +02:00
Calvin
df51c3e64e fix (docs): added installation instructions for the CLI (#3329)
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-05-16 09:01:45 +02:00
David BM
9acae7d1c4 fix (dashboard ci): stop playwright e2e tests run after first failure (#3334)
### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Reload metadata for Graphite's remote schemas in e2e tests

- Add 'reload_remote_schemas' parameter to metadata reload

- Include Changeset for version tracking


___



### **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>refresh-metadata.setup.ts</strong><dd><code>Add
Graphite remote schema to metadata reload</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/e2e/setup/refresh-metadata.setup.ts

<li>Add 'reload_remote_schemas' parameter with value ['graphite']<br>
<li> Include Graphite remote schema in metadata reload


</details>


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

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>wild-stingrays-help.md</strong><dd><code>Add changeset
for dashboard version update</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/wild-stingrays-help.md

<li>Create new changeset file<br> <li> Specify minor version bump for
'@nhost/dashboard'<br> <li> Add description of the fix


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3334/files#diff-b5a0c57a4f2bd08515f9feef3c38804e7f948ffb4b76418f6549131f61cf20fd">+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-05-15 18:03:29 +02:00
David BM
f6947a2194 fix (dashboard): logs page, fetch job-backup services logs using Live filter (#3333)
### **User description**
Adds `job-backup.+` regex pattern to the service filter in the logs
subscription, to match any job-backup services


___

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


___

### **Description**
- Fix logs fetching for job-backup services

- Implement regex pattern for job-backup service filter

- Improve live log subscription functionality

- Add changeset for version tracking


___



### **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>logs.tsx</strong><dd><code>Improve log fetching for
job-backup services</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/pages/orgs/[orgSlug]/projects/[appSubdomain]/logs.tsx

<li>Modify service filter in logs subscription<br> <li> Implement regex
pattern 'job-backup.+' for job-backup services<br> <li> Enhance
conditional logic for service filtering


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3333/files#diff-77489a68a7526d74f06d59019ad68c44728b7620637308d70fba38d6649b73fa">+4/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>hungry-waves-destroy.md</strong><dd><code>Add changeset
for version tracking</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></summary>
<hr>

.changeset/hungry-waves-destroy.md

<li>Add new changeset file<br> <li> Specify minor version bump for
@nhost/dashboard<br> <li> Document fix for job-backup services logs
fetching


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3333/files#diff-45dd073b01febac00c055bb63f51182ff043ee7a5df2c12058232caf2d2a31ca">+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-05-14 13:55:11 +02:00
robertkasza
31e636a9c8 fix (dashboard): Use the correct payload to reset metadata before e2 tests (#3331)
### **PR Type**
Bug fix, Tests


___

### **Description**
- Fix metadata reset payload in e2e tests

- Improve error handling and logging for metadata consistency

- Add changeset for patch version bump


___



### **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>refresh-metadata.setup.ts</strong><dd><code>Refactor
metadata reset and improve consistency checks</code>&nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/e2e/setup/refresh-metadata.setup.ts

<li>Updated payload for metadata reset (removed
<code>reload_remote_schemas</code>, set <br><code>reload_sources</code>
to false)<br> <li> Enhanced error handling and logging for metadata
consistency<br> <li> Added detailed console logging for inconsistent
metadata


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3331/files#diff-e32371f0138a80b833384d09a2b1fcc0de848b555823c16c0e9a96ed441edc8a">+10/-3</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>lemon-flies-live.md</strong><dd><code>Add changeset for
dashboard patch update</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/lemon-flies-live.md

<li>Added changeset file for patch version bump<br> <li> Described fix
for metadata reset in e2e tests


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3331/files#diff-25789650a83994d924e29e67aafc8011da8f98e2178211207de1ef049d53e4f6">+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-05-13 13:18:29 +02:00
github-actions[bot]
0fdff345ac chore: update versions (#3327)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@2.29.0

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit
-   a0931e2: fix: improve logs time range and filter selection
- c0635ae: feat (dashboard): Add information about that free
organization cannot be upgraded.
- e87505c: fix: can downsize postgres storage capacity using local
dashboard

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

## @nhost-examples/sveltekit@0.8.0

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

### Patch Changes

-   97db637: fix: fix settings

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-12 20:05:18 +00:00
David Barroso
97db63791b fix (examples/react-apollo): fix settings (#3301)
### **PR Type**
Enhancement, Configuration changes


___

### **Description**
- Update auth settings in nhost.toml

- Remove rate limiting configurations

- Remove SMTP provider settings

- Add changeset for patch version bump


___



### **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>lazy-years-kneel.md</strong><dd><code>Add changeset for
patch version bump</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/lazy-years-kneel.md

<li>Add new changeset file for patch version bump<br> <li> Specify
'@nhost-examples/react-apollo' package<br> <li> Include fix description


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3301/files#diff-019ff1fe3f65a591a01a6cbc0fa05c44c065e2611417ac0b3dcf6bd5a6eda7c2">+5/-0</a>&nbsp;
&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>Update Nhost configuration
settings</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/nhost/nhost.toml

<li>Change auth.elevatedPrivileges.mode to 'recommended'<br> <li> Remove
auth.rateLimit configurations<br> <li> Remove provider.smtp settings<br>
<li> Update allowed URLs for auth redirections


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3301/files#diff-268d6c8dddd6990d60d62c1c923955c4e0e7549a80f0f5856192f889378416a0">+1/-30</a>&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-05-12 19:30:59 +02:00
David BM
a0931e282f fix (dashboard): logs persist time range selector, fix validation (#3300)
### **User description**
Resolves #3154


___

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


___

### **Description**
- Fix logs search functionality with selected service filter

- Implement persistent time range selector

- Add interval-based date range selection

- Improve form validation and submission logic


___



### **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>LogsDatePicker.tsx</strong><dd><code>Enhance
LogsDatePicker with form context integration</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/logs/components/LogsDatePicker/LogsDatePicker.tsx

<li>Import LogsFilterFormValues type and useFormContext hook<br> <li>
Add setValue function from form context<br> <li> Implement
handleDateChange to update selected date and reset interval<br> <li>
Update DatePicker onChange to use new handleDateChange function


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3300/files#diff-0768cb2a5cee4ab57a64580c34213950a042a9893b5da51b8886e166cb7a9060">+9/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>LogsHeader.tsx</strong><dd><code>Improve LogsHeader
with interval support and form handling</code></dd></summary>
<hr>


dashboard/src/features/orgs/projects/logs/components/LogsHeader/LogsHeader.tsx

<li>Add interval to validation schema and form default values<br> <li>
Implement interval-based date recalculation in handleSubmit<br> <li>
Update form mode to 'onChange' for real-time validation<br> <li> Modify
useEffect to trigger submission on service change


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3300/files#diff-ebb3285aa776c9c5ea8b72672c4aafd55994c6c694998bbf56ca9c56d1e77664">+27/-3</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>LogsRangeSelector.tsx</strong><dd><code>Enhance
LogsRangeSelector with interval selection and UI
updates</code></dd></summary>
<hr>


dashboard/src/features/orgs/projects/logs/components/LogsRangeSelector/LogsRangeSelector.tsx

<li>Add interval handling in LogsToDatePickerLiveButton<br> <li>
Implement interval setting in handleIntervalChange function<br> <li>
Update Button variant based on selected interval<br> <li> Add interval
to useWatch hook for reactive updates


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3300/files#diff-46dd7c795a79e4b443213ed10089651423d13e5c776ca72e3a95ae5e0f7f63c8">+9/-2</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-05-12 18:31:58 +02:00
David BM
e87505c564 fix (dashboard): downsize postgres using local dashboard (#3292)
### **User description**
Fixes #3265


___

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


___

### **Description**
- Fix PostgreSQL version parsing for empty strings

- Enable downsizing Postgres in local dashboard

- Improve handling of free project and platform checks

- Update refetch queries with optional chaining


___



### **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>useGetPostgresVersion.ts</strong><dd><code>Fix
PostgreSQL version parsing for empty strings</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/database/common/hooks/useGetPostgresVersion/useGetPostgresVersion.ts

<li>Add fallback to empty string for <code>version</code> in
<br><code>splitPostgresMajorMinorVersions</code>


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>index.tsx</strong><dd><code>Update refetch queries with
optional chaining</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/pages/orgs/[orgSlug]/projects/[appSubdomain]/settings/index.tsx

- Add optional chaining for `userData.id` in refetch queries


</details>


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

</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>DatabaseStorageCapacity.tsx</strong><dd><code>Improve
database storage capacity handling and UI</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/database/settings/components/DatabaseStorageCapacity/DatabaseStorageCapacity.tsx

<li>Add <code>isEmptyValue</code> check for <code>org</code> object<br>
<li> Introduce <code>shouldShowUpdateCapacityWarning</code> variable<br>
<li> Modify <code>submitDisabled</code> logic for non-platform
environments<br> <li> Update conditional rendering of
<code>DatabaseStorageCapacityWarning</code>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3292/files#diff-097a59d13b44816051386182a444eadfe2dcacd69b88c121af6733d7eca3ee43">+16/-3</a>&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-05-08 14:02:09 +02:00
robertkasza
c0635ae1c7 feat (dashboard): Add information about that free organization cannot be upgraded (#3316)
### **PR Type**
Enhancement


___

### **Description**
- Add info about free org upgrade limitations

- Introduce new 'NewOrgButton' component

- Update UI for subscription plan section

- Improve text link component functionality


___



### **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>InfoAlert.tsx</strong><dd><code>Enhance AlertTitle
styling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/features/orgs/components/InfoAlert/InfoAlert.tsx

- Added 'font-semibold' class to AlertTitle


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>SubscriptionPlan.tsx</strong><dd><code>Update
subscription plan UI and add free org info</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/components/billing/components/SubscriptionPlan/SubscriptionPlan.tsx

<li>Added InfoAlert for free organizations<br> <li> Introduced
NewOrgButton component<br> <li> Updated layout and styling of
subscription plan section<br> <li> Replaced Link component with TextLink


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3316/files#diff-2a5f070869055286b669e382b18d656935752803b9a1ef13390ac028c2a48ac4">+32/-30</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>TextLink.tsx</strong><dd><code>Enhance TextLink
component with optional icon</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/common/components/TextLink/TextLink.tsx

<li>Added optional icon to TextLink component<br> <li> Introduced
withIcon prop for flexibility


</details>


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

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>good-frogs-share.md</strong><dd><code>Add changeset for
dashboard feature</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/good-frogs-share.md

- Added changeset file for version bump


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3316/files#diff-ff1c12916da9254a5d59fef39d5220a0ccdd20a7e66e1436a860da9a014d31ee">+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-05-07 13:31:01 +02:00
robertkasza
d2a9a9ae1d fix: update labeler config (#3328)
### **PR Type**
Enhancement


___

### **Description**
- Update labeler configuration for all categories

- Standardize format using 'any' key for all rules

- Remove redundant 'any' key for documentation category

- Maintain existing category definitions and file patterns


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>labeler.yml</strong><dd><code>Standardize and simplify
labeler configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.github/labeler.yml

<li>Standardized all rules using 'any' key<br> <li> Removed redundant
'any' key for documentation<br> <li> Maintained existing category
definitions and file patterns<br> <li> Simplified overall configuration
structure


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3328/files#diff-a22c263686553013feaeb0677d26eeb0b8778a756c4311c1fce13384258026aa">+8/-9</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-05-07 10:00:34 +02:00
David BM
c97b43f149 fix (ci): update vite to solve vulnerability audit (#3323)
### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Update Vite to address vulnerability audit

- Upgrade Vite in multiple project dependencies

- Update package overrides for security

- Add changeset for minor version bumps


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>chatty-kids-exist.md</strong><dd><code>Add changeset for
minor version updates</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/3323/files#diff-22d27113acb695bcdab878d71e0e553a23f87070faeb4672ce09bf2108c56064">+13/-0</a>&nbsp;
&nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Dependencies</strong></td><td><details><summary>10
files</summary><table>
<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-2d8d55c799cd71f1b35e831f075f8178ed1734c4820a2ad548b4dd24d6938d7c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-83675898dc6ed88838763232d022f6e100e07d71681cc8a1f02aee99ee3f229b">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-9fb3a23f389ab1d192d7e018d2acbe512bd8792278662101401caa98692735db">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-cb7094614884e8cd2c8fb67dadedb1887c46c31b888840def0b7042273bfbb28">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v6.2.7</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-d95dc3391741287366ea2e61f70e9ccc64452e0d22b1db91d6bf524f5aa4331c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-8a3e5ed0f618f15211c31f700e0da998e2eae58f60353624b7a7e637bd63b153">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-fc4298d3512fdd9a3d871f9f182fe871c8beccd1580f864a271ddfb32005feef">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-85166d1137e29a5275f991e1e94a0c9d5b83ac7504463ba76f9187b2b750c895">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite and adjust
overrides</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/3323/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+4/-4</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>

---------

Co-authored-by: robertkasza <robert.kasza@bishop-co.com>
2025-05-06 14:39:39 +02:00
167 changed files with 3102 additions and 1051 deletions

19
.github/labeler.yml vendored
View File

@@ -1,24 +1,25 @@
dashboard:
- dashboard/**/*
- any:
- changed-files:
- any-glob-to-any-file: ['dashboard/**/*']
documentation:
- any:
- docs/**/*
- any: ['docs/**/*']
examples:
- examples/**/*
- any: ['examples/**/*']
sdk:
- packages/**/*
- any: ['packages/**/*']
integrations:
- integrations/**/*
- any: ['integrations/**/*']
react:
- '{packages,examples,integrations}/*react*/**/*'
- any: ['{packages,examples,integrations}/*react*/**/*']
nextjs:
- '{packages,examples}/*next*/**/*'
- any: ['{packages,examples}/*next*/**/*']
vue:
- '{packages,examples,integrations}/*vue*/**/*'
- any: ['{packages,examples,integrations}/*vue*/**/*']

View File

@@ -29,7 +29,7 @@ env:
NHOST_PRO_TEST_PROJECT_NAME: ${{ vars.NHOST_PRO_TEST_PROJECT_NAME }}
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
NHOST_TEST_PROJECT_ADMIN_SECRET: '${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}'
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
jobs:

View File

@@ -3,13 +3,12 @@ on:
- pull_request_target
jobs:
triage:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5
with:
repo-token: '${{ secrets.GH_PAT }}'
sync-labels: ''
repo-token: ${{ secrets.GH_PAT }}

View File

@@ -2,9 +2,7 @@
## Requirements
### Node.js v18
_⚠️ Node.js v16 is also supported for the time being but support will be dropped in the near future_.
### Node.js v20 or later
### [pnpm](https://pnpm.io/) package manager

View File

@@ -2,5 +2,5 @@
// $schema provides code completion hints to IDEs.
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"moderate": true,
"allowlist": ["vue-template-compiler"]
"allowlist": ["vue-template-compiler", { "id": "CVE-2025-48068", "path": "next" }]
}

View File

@@ -25,4 +25,6 @@ NEXT_PUBLIC_ZENDESK_USER_EMAIL=
CODEGEN_GRAPHQL_URL=https://local.graphql.local.nhost.run/v1
CODEGEN_HASURA_ADMIN_SECRET=nhost-admin-secret
NEXT_PUBLIC_TURNSTILE_SITE_KEY=FIXME
NEXT_PUBLIC_TURNSTILE_SITE_KEY=FIXME
NEXT_PUBLIC_SOC2_REPORT_FILE_ID=

View File

@@ -76,6 +76,13 @@ module.exports = {
],
},
],
'jsx-a11y/label-has-associated-control': [
2,
{
controlComponents: ['Input'],
depth: 3,
},
],
},
overrides: [
{

View File

@@ -1,5 +1,40 @@
# @nhost/dashboard
## 2.31.0
### Minor Changes
- 39b10a2: feat (dashboard): Add multi-factor authentication
- 4b84780: feat (dashboard): Add Webauthn to dashboard
### Patch Changes
- 61eb6cd: fix (dashboard): Fix update project e2e test
- @nhost/react-apollo@18.0.0
- @nhost/nextjs@2.2.8
## 2.30.0
### Minor Changes
- f6947a2: fix: fetch job-backup services logs using Live filter
- 44a3e6b: fix: collapsed main navigation sidebar overlaps mobile navbar
- 99b78f1: feat: dashboard: add download button for soc2 report
- 9acae7d: fix: e2e tests, stop on error when refreshing metadata
### Patch Changes
- 31e636a: fix (dashboard): Use the correct payload to reset metadata before the e2e tests
## 2.29.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
- a0931e2: fix: improve logs time range and filter selection
- c0635ae: feat (dashboard): Add information about that free organization cannot be upgraded.
- e87505c: fix: can downsize postgres storage capacity using local dashboard
## 2.28.0
### Minor Changes

View File

@@ -1,9 +1,10 @@
/* eslint-disable no-console */
import { TEST_PROJECT_ADMIN_SECRET, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { test as setup } from '@playwright/test';
setup('refresh metadata', async () => {
try {
await fetch(
const response = await fetch(
`https://${TEST_PROJECT_SUBDOMAIN}.hasura.eu-central-1.staging.nhost.run/v1/metadata`,
{
method: 'POST',
@@ -15,8 +16,7 @@ setup('refresh metadata', async () => {
{
type: 'reload_metadata',
args: {
reload_remote_schemas: [],
reload_sources: [],
reload_sources: false,
},
},
{
@@ -29,6 +29,21 @@ setup('refresh metadata', async () => {
}),
},
);
const body = await response.json();
if (!response.ok) {
const message = `[${body.code}]:${body.error}`;
throw new Error(message);
} else {
const isConsistent = body[0].is_consistent;
if (isConsistent) {
console.log('Metadata is consistent.');
} else {
console.error('Metadata is not consistent.');
console.error(body[0].inconsistent_objects);
throw new Error('Metadata is not consistent');
}
}
} catch (error) {
// Log safe error information
console.error(

View File

@@ -24,23 +24,20 @@ test.beforeAll(async ({ browser }) => {
});
test('should create a new project', async () => {
await await gotoUrl(
page,
`/orgs/${getFreeUserStarterOrgSlug()}/projects/new`,
);
await gotoUrl(page, `/orgs/${getFreeUserStarterOrgSlug()}/projects/new`);
const projectName = faker.lorem.words(3);
await page.getByLabel('Project Name').fill(projectName);
await page.getByText('Create Project').click();
expect(await page.getByText('Creating the project...')).toBeVisible();
expect(await page.getByText('Internal info')).toBeVisible();
expect(page.getByText('Creating the project...')).toBeVisible();
expect(page.getByText('Internal info')).toBeVisible();
await page.waitForSelector('button:has-text("Upgrade project")', {
timeout: 120000,
});
const newProjectSlug = getProjectSlugFromUrl(await page.url());
const newProjectSlug = getProjectSlugFromUrl(page.url());
setNewProjectSlug(newProjectSlug);
setNewProjectName(projectName);
});
@@ -50,7 +47,7 @@ test('should upgrade the project', async () => {
page,
`/orgs/${getFreeUserStarterOrgSlug()}/projects/${getNewProjectSlug()}`,
);
const upgradeProject = await page.getByText('Upgrade project');
const upgradeProject = page.getByText('Upgrade project');
expect(upgradeProject).toBeVisible();
await upgradeProject.click();
@@ -67,10 +64,10 @@ test('should upgrade the project', async () => {
await page.waitForSelector('button:has-text("Create organization")', {
state: 'hidden',
});
const stripeFrame = await page
const stripeFrame = page
.frameLocator('iframe[name="embedded-checkout"]')
.first();
await stripeFrame.getByText('Subscribe to Nhost');
stripeFrame.getByText('Subscribe to Nhost');
await stripeFrame.getByLabel('Email').fill(faker.internet.email());
await stripeFrame
@@ -85,7 +82,7 @@ test('should upgrade the project', async () => {
await stripeFrame.locator('#billingCountry').scrollIntoViewIfNeeded();
// Need to comment out for local testing START
await stripeFrame.getByPlaceholder('Address', { exact: true }).click();
await stripeFrame.locator('span:has-text("Enter address manually")');
stripeFrame.locator('span:has-text("Enter address manually")');
await stripeFrame.getByText('Enter address manually').click();
await stripeFrame
.getByPlaceholder('Address line 1', { exact: true })
@@ -94,6 +91,7 @@ test('should upgrade the project', async () => {
.getByPlaceholder('City', { exact: true })
.fill('Springfield');
await stripeFrame.getByPlaceholder('ZIP', { exact: true }).fill('62701');
await stripeFrame.locator('#enableStripePass').click({ force: true });
// local Comment end
await stripeFrame
.getByTestId('hosted-payment-submit-button')
@@ -110,12 +108,12 @@ test('should upgrade the project', async () => {
'div:has-text("Project has been upgraded successfully!")',
);
await page.getByRole('button', { name: 'Create project' });
page.getByRole('button', { name: 'Create project' });
await page.waitForSelector(`div:has-text("${newOrgName}")`);
await page.waitForSelector(`p:has-text("${getNewProjectName()}")`);
setNewOrgSlug(getOrgSlugFromUrl(await page.url()));
setNewOrgSlug(getOrgSlugFromUrl(page.url()));
});
test('should delete the new organization', async () => {
@@ -126,12 +124,12 @@ test('should delete the new organization', async () => {
await page.getByRole('button', { name: 'Delete' }).click();
await page.waitForSelector('h2:has-text("Delete Organization")');
expect(await page.getByTestId('deleteOrgButton')).toBeDisabled();
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel("I'm sure I want to delete this Organization").click();
expect(await page.getByTestId('deleteOrgButton')).toBeDisabled();
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel('I understand this action cannot be undone').click();
expect(await page.getByTestId('deleteOrgButton')).not.toBeDisabled();
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
await page.getByTestId('deleteOrgButton').click();

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "2.28.0",
"version": "2.31.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -16,7 +16,7 @@
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook",
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
"e2e:tests": "pnpm install-browsers && pnpm playwright test --config=playwright.config.ts",
"e2e:tests": "pnpm install-browsers && pnpm playwright test --config=playwright.config.ts -x",
"e2e": "pnpm e2e:tests --project=main",
"e2e:local": "pnpm e2e:tests --project=local",
"e2e:upgrade-project": "pnpm e2e:tests --project=upgrade-project"
@@ -39,6 +39,7 @@
"@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^3.9.0",
"@iarna/toml": "^2.2.5",
"@icons-pack/react-simple-icons": "^9.6.0",
"@marsidev/react-turnstile": "^1.0.2",
"@mui/base": "5.0.0-beta.31",
"@mui/material": "^5.15.14",
@@ -196,7 +197,7 @@
"tailwindcss": "^3.4.12",
"ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"vite": "^5.4.18",
"vite": "^5.4.19",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^0.32.4"
},

View File

@@ -0,0 +1,61 @@
import { Button } from '@/components/ui/v3/button';
import { Input } from '@/components/ui/v3/input';
import { type ChangeEvent, useEffect, useRef, useState } from 'react';
interface Props {
sendMfaOtp: (code: string) => Promise<any>;
loading: boolean;
}
function MfaOtpForm({ sendMfaOtp, loading }: Props) {
const [otpValue, setOtpValue] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
async function sendMfa(code: string) {
if (code.length === 6 && !isSubmitting) {
setIsSubmitting(true);
const result = await sendMfaOtp(code);
if (!result) {
setTimeout(() => {
inputRef.current?.focus();
}, 10);
}
}
setIsSubmitting(false);
}
async function handleChange(event: ChangeEvent<HTMLInputElement>) {
const code = event.target.value.replace(/[^0-9]/g, '').slice(0, 6);
setOtpValue(code);
sendMfa(code);
}
const isInputDisabled = loading || isSubmitting;
const isButtonDisabled = isInputDisabled || otpValue.length !== 6;
return (
<div className="relative grid w-full grid-flow-row gap-4 bg-transparent">
<Input
ref={inputRef}
value={otpValue}
placeholder="Enter TOTP"
className="!bg-transparent"
disabled={isInputDisabled}
onChange={handleChange}
/>
<Button disabled={isButtonDisabled}>
{loading ? 'Verifying...' : 'Verify'}
</Button>
</div>
);
}
export default MfaOtpForm;

View File

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

View File

@@ -0,0 +1,59 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import { Input } from '@/components/ui/v3/input';
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
const inputClasses =
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
interface FormInputProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
control: Control<TFieldValues>;
name: TName;
label: string;
placeholder?: string;
className?: string;
type?: string;
}
function FormInput<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
control,
name,
label,
placeholder,
className = '',
type = 'text',
}: FormInputProps<TFieldValues, TName>) {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input
type={type}
placeholder={placeholder || label}
{...field}
className={`${inputClasses} ${className}`}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
export default FormInput;

View File

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

View File

@@ -41,7 +41,7 @@ export default function MainNav({ container }: MainNavProps) {
return (
<Sheet open={open} onOpenChange={setOpen}>
<div
className="min- absolute left-0 z-50 flex h-full w-6 justify-center border-r-[1px] bg-background pt-1 hover:bg-accent"
className="min- absolute left-0 z-[39] flex h-full w-6 justify-center border-r-[1px] bg-background pt-1 hover:bg-accent"
onMouseEnter={() => setOpen(true)}
>
<Menu className="h-4 w-4" />

View File

@@ -6,8 +6,8 @@ import {
type ReactElement,
} from 'react';
import { CopyToClipboardButton as CopyToClipboardButtonOriginal } from '@/components/presentational/CopyToClipboardButton';
import { Box } from '@/components/ui/v2/Box';
import { CopyToClipboardButton as CopyToClipboardButtonOriginal } from './CopyToClipboardButton';
import { getNodeText } from './getNodeText';
export interface CodeBlockPropsBase {

View File

@@ -1,14 +1,11 @@
import { Button, type ButtonProps } from '@/components/ui/v3/button';
import { isNotEmptyValue } from '@/lib/utils';
import { copy } from '@/utils/copy';
import { clsx } from 'clsx';
import { Copy } from 'lucide-react';
import { useEffect, useState } from 'react';
import {
IconButton,
type IconButtonProps,
} from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { copy } from '@/utils/copy';
export function CopyToClipboardButton({
function CopyToClipboardButton({
textToCopy,
className,
title,
@@ -16,7 +13,7 @@ export function CopyToClipboardButton({
}: {
textToCopy: string;
title: string;
} & IconButtonProps) {
} & ButtonProps) {
const [disabled, setDisabled] = useState(true);
useEffect(() => {
@@ -35,12 +32,18 @@ export function CopyToClipboardButton({
if (!textToCopy || disabled) {
return null;
}
const hasChildren = isNotEmptyValue(props.children);
return (
<IconButton
variant="borderless"
color="secondary"
className={clsx('group', className)}
<Button
type="button"
size="icon"
variant="outline"
className={clsx(
'group h-fit w-fit border-0 bg-transparent p-[2px] hover:bg-[#d6eefb] dark:hover:bg-[#1e2942]',
className,
{ 'gap-3': hasChildren },
)}
onClick={(event) => {
event.stopPropagation();
@@ -49,7 +52,9 @@ export function CopyToClipboardButton({
aria-label={textToCopy}
{...props}
>
<CopyIcon className="top-5 h-4 w-4" />
</IconButton>
{props.children}
<Copy className="top-5 h-4 w-4" />
</Button>
);
}
export default CopyToClipboardButton;

View File

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

View File

@@ -0,0 +1,44 @@
import { Badge } from '@/components/ui/v3/badge';
import useMfaEnabled from '@/features/account/settings/components/AccountMfaSettings/hooks/useMfaEnabled';
import DisableMfaButton from './DisableMfaButton/DisableMfaButton';
import EnableMfaButton from './EnableMfaButton/EnableMfaButton';
function MFaEnabledBadge() {
return (
<Badge variant="outline" className="border-green-400 text-green-400">
Enabled
</Badge>
);
}
function MFaDisabledBadge() {
return (
<Badge
variant="outline"
className="text- border-destructive text-destructive"
>
Disabled
</Badge>
);
}
function AccountMfaSettings() {
const { isMfaEnabled } = useMfaEnabled();
return (
<div className="rounded-lg border border-[#EAEDF0] bg-white font-['Inter_var'] dark:border-[#2F363D] dark:bg-paper">
<div className="flex w-full flex-col items-start gap-6 p-4">
<div className="flex w-full items-center justify-between">
<h3 className="flex items-center text-[1.125rem] font-semibold leading-[1.75]">
<span className="mr-4">Multi-Factor Authentication </span>
{isMfaEnabled ? <MFaEnabledBadge /> : <MFaDisabledBadge />}
</h3>
</div>
</div>
<div className="flex w-full items-center border-t border-[#EAEDF0] px-4 py-2 dark:border-[#2F363D]">
{isMfaEnabled ? <DisableMfaButton /> : <EnableMfaButton />}
</div>
</div>
);
}
export default AccountMfaSettings;

View File

@@ -0,0 +1,70 @@
import { MfaOtpForm } from '@/components/common/MfaOtpForm';
import { Button } from '@/components/ui/v3/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/v3/dialog';
import useMfaEnabled from '@/features/account/settings/components/AccountMfaSettings/hooks/useMfaEnabled';
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 [open, setOpen] = 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,
getToastStyleProps(),
);
return false;
}
toast.success(
'Multi-factor authentication has been disabled.',
getToastStyleProps(),
);
await refetch();
setOpen(false);
return true;
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
disabled={buttonDisabled}
className="p-y[0.375rem] h-9 gap-2 border-destructive px-2 text-destructive hover:bg-destructive"
>
Disable multi-factor authentication
</Button>
</DialogTrigger>
<DialogContent className="z-[9999] max-w-[28rem] text-foreground">
<DialogHeader>
<DialogTitle>Disable multi-factor authentication</DialogTitle>
</DialogHeader>
<DialogDescription className="hidden">
Disable multi-factor authentication
</DialogDescription>
<MfaOtpForm loading={isDisabling} sendMfaOtp={onSendMfaOtp} />
</DialogContent>
</Dialog>
);
}
export default DisableMfaButton;

View File

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

View File

@@ -0,0 +1,19 @@
import { CopyToClipboardButton } from '@/components/presentational/CopyToClipboardButton';
interface Props {
totpSecret: string | null;
}
function CopyMfaTOTPSecret({ totpSecret }: Props) {
return (
<CopyToClipboardButton
className="p-2"
textToCopy={totpSecret}
title="TOTP secret"
>
OR Copy TOTP secret
</CopyToClipboardButton>
);
}
export default CopyMfaTOTPSecret;

View File

@@ -0,0 +1,48 @@
import { Button } from '@/components/ui/v3/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/v3/dialog';
import useMfaEnabled from '@/features/account/settings/components/AccountMfaSettings/hooks/useMfaEnabled';
import { useState } from 'react';
import MfaQRCodeAndTOTPSecret from './MfaQRCodeAndTOTPSecret';
function EnableMfaButton() {
const [open, setOpen] = useState(false);
const { isMfaEnabled, loading, refetch } = useMfaEnabled();
const buttonDisabled = loading || isMfaEnabled;
async function handleOnSuccess() {
setOpen(false);
await refetch();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
disabled={buttonDisabled}
className="p-y[0.375rem] h-9 gap-2 border-green-600 px-2 text-green-600 hover:bg-destructive hover:bg-green-600"
>
Enable multi-factor authentication
</Button>
</DialogTrigger>
<DialogContent className="z-[9999] max-w-[28rem] text-foreground">
<DialogHeader>
<DialogTitle>Enable multi-factor authentication</DialogTitle>
</DialogHeader>
<DialogDescription className="hidden">
Enable multi-factor authentication
</DialogDescription>
<MfaQRCodeAndTOTPSecret onSuccess={handleOnSuccess} />
</DialogContent>
</Dialog>
);
}
export default EnableMfaButton;

View File

@@ -0,0 +1,74 @@
/* eslint-disable @next/next/no-img-element */
import { MfaOtpForm } from '@/components/common/MfaOtpForm';
import { Spinner } from '@/components/ui/v3/spinner';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useConfigMfa } from '@nhost/nextjs';
import { useEffect } 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();
async function onSendMfaOtp(code: string) {
const result = await activateMfa(code);
if (result.error) {
toast.error(
result.error.message || defaultErrorMessage,
getToastStyleProps(),
);
return 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());
}
}
generate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="flex w-full flex-col items-center justify-center gap-4">
{isGenerating && <Spinner />}
{isGenerated && qrCodeDataUrl && (
<>
<div className="flex flex-col justify-center gap-4">
<p className="text-base">
Scan the QR Code with your authenticator app
</p>
<img alt="qrcode" src={qrCodeDataUrl} className="mx-auto w-64" />
</div>
<CopyMfaTOTPSecret totpSecret={totpSecret} />
<MfaOtpForm loading={isActivating} sendMfaOtp={onSendMfaOtp} />
</>
)}
</div>
);
}
export default MfaQRCodeAndTOTPSecret;

View File

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

View File

@@ -0,0 +1,17 @@
import { isNotEmptyValue } from '@/lib/utils';
import { useGetActiveMfaTypeQuery } from '@/utils/__generated__/graphql';
import { useUserId } from '@nhost/nextjs';
function useMfaEnabled() {
const userId = useUserId();
const { data, loading, refetch } = useGetActiveMfaTypeQuery({
variables: { id: userId },
fetchPolicy: 'cache-first',
});
const isMfaEnabled = isNotEmptyValue(data?.user.activeMfaType);
return { loading, isMfaEnabled, refetch };
}
export default useMfaEnabled;

View File

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

View File

@@ -10,9 +10,9 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
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 type { DialogFormProps } from '@/types/common';
import { GetPersonalAccessTokensDocument } from '@/utils/__generated__/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import { getDateComponents } from '@/utils/getDateComponents';
import { useApolloClient } from '@apollo/client';
@@ -20,7 +20,6 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { useNhostClient } from '@nhost/nextjs';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
export const createPATFormValidationSchema = Yup.object({
@@ -84,6 +83,25 @@ export default function CreatePATForm({
resolver: yupResolver(createPATFormValidationSchema),
});
const createPAT = useActionWithElevatedPermissions({
actionFn: async (
expiresAt: Date,
metadata?: Record<string, string | number>,
) => {
const result = await nhostClient.auth.createPAT(expiresAt, metadata);
return result;
},
successMessage: 'The personal access token has been created successfully.',
onSuccess: ({ data }) => {
setPersonalAccessToken(data?.personalAccessToken);
apolloClient.refetchQueries({
include: [GetPersonalAccessTokensDocument],
});
form.reset();
},
});
const { register, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
@@ -93,44 +111,11 @@ export default function CreatePATForm({
}, [isDirty, location, onDirtyStateChange]);
async function handleSubmit(formValues: CreatePATFormValues) {
try {
const { error, data } = await nhostClient.auth.createPAT(
new Date(formValues.expiresAt),
{
name: formValues.name,
application: 'dashboard',
userAgent: window.navigator.userAgent,
},
);
const toastStyle = getToastStyleProps();
if (error) {
toast.error(error.message, {
style: toastStyle.style,
...toastStyle.error,
});
return;
}
toast.success(
'The personal access token has been created successfully.',
{
style: toastStyle.style,
...toastStyle.success,
},
);
setPersonalAccessToken(data?.personalAccessToken);
apolloClient.refetchQueries({
include: [GetPersonalAccessTokensDocument],
});
form.reset();
} catch {
// Note: This error is handled by the toast.
}
await createPAT(new Date(formValues.expiresAt), {
name: formValues.name,
application: 'dashboard',
userAgent: window.navigator.userAgent,
});
}
if (personalAccessToken) {

View File

@@ -19,7 +19,7 @@ export type DisplayNameSettingFormValues = Yup.InferType<
>;
export default function DisplayNameSetting() {
const { id: userID, displayName } = useUserData();
const { id: userID, displayName } = useUserData() || {};
const [updateUserDisplayName] = useUpdateUserDisplayNameMutation();

View File

@@ -1,7 +1,7 @@
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 useActionWithElevatedPermissions from '@/features/account/settings/hooks/useActionWithElevatedPermissions';
import { yupResolver } from '@hookform/resolvers/yup';
import { useNhostClient, useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
@@ -15,7 +15,7 @@ export type EmailSettingFormValues = Yup.InferType<typeof validationSchema>;
export default function EmailSetting() {
const nhost = useNhostClient();
const { email } = useUserData();
const { email } = useUserData() || {};
const form = useForm<EmailSettingFormValues>({
reValidateMode: 'onSubmit',
@@ -26,25 +26,23 @@ export default function EmailSetting() {
const { register, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const changeEmail = useActionWithElevatedPermissions({
actionFn: async (newEmail: string) => {
const result = await nhost.auth.changeEmail({
newEmail,
options: {
redirectTo: `${window.location.origin}/account`,
},
});
return result;
},
successMessage:
'Please check your inbox. Follow the link to finalize changing your email.',
onSuccess: () => form.reset(),
});
async function handleSubmit(formValues: EmailSettingFormValues) {
await execPromiseWithErrorToast(
async () => {
await nhost.auth.changeEmail({
newEmail: formValues.email,
options: {
redirectTo: `${window.location.origin}/account`,
},
});
form.reset({ email: formValues.email });
},
{
loadingMessage: 'Updating your email...',
successMessage:
'Please check your inbox. Follow the link to finalize changing your email.',
errorMessage:
'An error occurred while trying to update your email. Please try again.',
},
);
await changeEmail(formValues.email);
}
return (

View File

@@ -1,93 +0,0 @@
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 { yupResolver } from '@hookform/resolvers/yup';
import { useChangePassword } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const validationSchema = Yup.object({
newPassword: Yup.string()
.label('New Password')
.nullable()
.required('This field is required.'),
confirmPassword: Yup.string()
.label('Confirm Password')
.nullable()
.required('This field is required.')
.test(
'passwords-match',
'Passwords must match.',
(value, ctx) => ctx.parent.newPassword === value,
),
});
export type PasswordSettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function PasswordSettings() {
const { changePassword } = useChangePassword();
const form = useForm<PasswordSettingsFormValues>({
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
const { register, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
async function handleSubmit(formValues: PasswordSettingsFormValues) {
await execPromiseWithErrorToast(
async () => {
// TODO fix changePassword should throw an error if something happens
await changePassword(formValues.newPassword);
form.reset();
},
{
loadingMessage: 'Changing password...',
successMessage: 'The password has been changed successfully.',
errorMessage:
'An error occurred while trying to update the password. Please try again.',
},
);
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Change Password"
description="Update your account password."
slotProps={{
submitButton: {
disabled: !isDirty,
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row lg:grid-cols-5"
>
<Input
{...register('newPassword')}
className="col-span-2"
type="password"
id="new-password"
label="New Password"
fullWidth
helperText={formState.errors.newPassword?.message}
error={Boolean(formState.errors.newPassword)}
/>
<Input
{...register('confirmPassword')}
className="col-span-2 row-start-2"
type="password"
id="confirm-password"
label="Confirm Password"
fullWidth
helperText={formState.errors.confirmPassword?.message}
error={Boolean(formState.errors.confirmPassword)}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1,50 @@
import { FormInput } from '@/components/form/FormInput';
import { Button } from '@/components/ui/v3/button';
import { Form } from '@/components/ui/v3/form';
import useChangePasswordForm from '@/features/account/settings/components/PasswordSettings/hooks/useChangePasswordForm';
import useOnChangePasswordHandler from '@/features/account/settings/components/PasswordSettings/hooks/useOnChangePasswordHandler';
export default function PasswordSettings() {
const form = useChangePasswordForm();
const onSubmit = useOnChangePasswordHandler({
onSuccess: () => form.reset(),
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="rounded-lg border border-[#EAEDF0] bg-white font-['Inter_var'] dark:border-[#2F363D] dark:bg-paper">
<div className="flex w-full flex-col items-start gap-4 p-4">
<div className="flex w-full flex-col items-start">
<h3 className="text-[1.125rem] font-semibold leading-[1.75]">
Change Password
</h3>
<p className="text-[#556378] dark:text-[#A2B3BE]">
Update your account password.
</p>
</div>
<div className="flex w-[370px] flex-col gap-4">
<FormInput
control={form.control}
name="newPassword"
type="password"
label="New Password"
/>
<FormInput
control={form.control}
name="confirmPassword"
type="password"
label="Confirm Password"
/>
</div>
</div>
<div className="flex w-full items-center justify-end border-t border-[#EAEDF0] px-4 py-2 dark:border-[#2F363D]">
<Button type="submit" variant="outline">
Save
</Button>
</div>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,39 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const validationSchema = z
.object({
newPassword: z
.string({
required_error: 'This field is required.',
})
.min(1, 'This field is required.'),
confirmPassword: z
.string({
required_error: 'This field is required.',
})
.min(1, 'This field is required.'),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: 'Passwords must match.',
path: ['confirmPassword'],
});
export type ChangePasswordFormValues = z.infer<typeof validationSchema>;
function useChangePasswordForm() {
const form = useForm<ChangePasswordFormValues>({
mode: 'onTouched',
reValidateMode: 'onBlur',
defaultValues: {
newPassword: '',
confirmPassword: '',
},
resolver: zodResolver(validationSchema),
});
return form;
}
export default useChangePasswordForm;

View File

@@ -0,0 +1,27 @@
import useActionWithElevatedPermissions from '@/features/account/settings/hooks/useActionWithElevatedPermissions';
import { useChangePassword } from '@nhost/nextjs';
import { type ChangePasswordFormValues } from './useChangePasswordForm';
interface Props {
onSuccess: () => void;
}
function useOnChangePasswordHandler({ onSuccess }: Props) {
const { changePassword: actionFn } = useChangePassword();
const changePassword = useActionWithElevatedPermissions({
actionFn,
onSuccess,
successMessage: 'The password has been changed successfully.',
});
async function onSubmit(values: ChangePasswordFormValues) {
const { newPassword } = values;
await changePassword(newPassword);
}
return onSubmit;
}
export default useOnChangePasswordHandler;

View File

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

View File

@@ -0,0 +1,45 @@
import { Button } from '@/components/ui/v3/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/v3/dialog';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import SecurityKeyForm from './NewSecurityKeyForm';
function AddSecurityKeyButton() {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
className="h-9 gap-2 px-2 py-[0.375rem] hover:bg-[#d6eefb] dark:hover:bg-[#1e2942]"
>
<Plus className="h-5 w-5" />
Add New Security Key
</Button>
</DialogTrigger>
<DialogContent
className="sr z-[9999] text-foreground sm:max-w-xl"
aria-describedby="Add a Security Key"
>
<DialogHeader>
<DialogTitle>Add a Security Key</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Add a Security Key
</DialogDescription>
<SecurityKeyForm onSuccess={() => setOpen(false)} />
</DialogContent>
</Dialog>
);
}
export default AddSecurityKeyButton;

View File

@@ -0,0 +1,56 @@
import { Button } from '@/components/ui/v3/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import { Input } from '@/components/ui/v3/input';
import useNewSecurityKeyForm from '@/features/account/settings/components/SecurityKeysSettings/hooks/useNewSecurityKeyForm';
import useOnAddNewSecurityKeyHandler from '@/features/account/settings/components/SecurityKeysSettings/hooks/useOnAddNewSecurityKeyHandler';
interface Props {
onSuccess: () => void;
}
function NewSecurityKeyForm({ onSuccess }: Props) {
const form = useNewSecurityKeyForm();
const onSubmit = useOnAddNewSecurityKeyHandler({ onSuccess });
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-flow-row gap-4 bg-transparent"
>
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Name"
{...field}
className="!bg-transparent"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant="outline"
className="w-full !bg-transparent"
>
Add new security key
</Button>
</form>
</Form>
);
}
export default NewSecurityKeyForm;

View File

@@ -0,0 +1,34 @@
import { Button } from '@/components/ui/v3/button';
import useRemoveSecurityKey from '@/features/account/settings/components/SecurityKeysSettings/hooks/useRemoveSecurityKey';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { Trash } from 'lucide-react';
import { memo } from 'react';
interface Props {
id: string;
}
function RemoveSecurityKeyButton({ id }: Props) {
const removeSecurityKey = useRemoveSecurityKey();
function handleClick() {
execPromiseWithErrorToast(async () => removeSecurityKey(id), {
loadingMessage: 'Removing security key...',
successMessage: 'Security key has been removed successfully.',
errorMessage:
'An error occurred while trying to remove security key. Please try again.',
});
}
return (
<Button
variant="ghost"
onClick={handleClick}
aria-label={`Remove security key ${id}`}
>
<Trash />
</Button>
);
}
export default memo(RemoveSecurityKeyButton);

View File

@@ -0,0 +1,43 @@
import { Spinner } from '@/components/ui/v3/spinner';
import useGetSecurityKeys from '@/features/account/settings/hooks/useGetSecurityKeys';
import { InfoAlert } from '@/features/orgs/components/InfoAlert';
import { Fingerprint } from 'lucide-react';
import { memo } from 'react';
import RemoveSecurityKeyButton from './RemoveSecurityKeyButton';
type SecurityKeyProps = {
id: string;
nickname?: string;
};
function SecurityKey({ id, nickname }: SecurityKeyProps) {
return (
<div className="flex w-full items-center justify-between rounded-lg border border-[#EAEDF0] px-2 py-2 dark:border-[#2F363D]">
<div className="flex justify-start gap-3">
<Fingerprint />
<span>{nickname || id}</span>
</div>
<RemoveSecurityKeyButton id={id} />
</div>
);
}
const MemoizedSecurityKey = memo(SecurityKey);
function SecurityKeyList() {
const { data, loading } = useGetSecurityKeys();
return (
<div>
{loading && <Spinner />}
{!loading && data?.authUserSecurityKeys.length === 0 && (
<InfoAlert>No security keys have been added yet!</InfoAlert>
)}
{data?.authUserSecurityKeys.map(({ id, nickname }) => (
<MemoizedSecurityKey key={id} id={id} nickname={nickname} />
))}
</div>
);
}
export default SecurityKeyList;

View File

@@ -0,0 +1,24 @@
import AddSecurityKeyButton from './AddSecurityKeyButton';
import SecurityKeyList from './SecurityKeyList';
function SecurityKeysSettings() {
return (
<div className="rounded-lg border border-[#EAEDF0] bg-white font-display dark:border-[#2F363D] dark:bg-paper">
<div className="flex w-full flex-col items-start gap-6 p-4">
<div className="flex w-full items-center justify-between">
<h3 className="text-[1.125rem] font-semibold leading-[1.75]">
Manage your security keys
</h3>
</div>
<div className="flex w-full flex-col gap-4">
<SecurityKeyList />
</div>
</div>
<div className="flex w-full items-center border-t border-[#EAEDF0] px-4 py-2 dark:border-[#2F363D]">
<AddSecurityKeyButton />
</div>
</div>
);
}
export default SecurityKeysSettings;

View File

@@ -0,0 +1,25 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const validationSchema = z
.object({
nickname: z.string().min(1, { message: 'Nickname is required' }),
})
.required();
export type NewSecurityKeyFormValues = z.infer<typeof validationSchema>;
function useNewSecurityKeyForm() {
const form = useForm<NewSecurityKeyFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
nickname: '',
},
resolver: zodResolver(validationSchema),
});
return form;
}
export default useNewSecurityKeyForm;

View File

@@ -0,0 +1,30 @@
import useActionWithElevatedPermissions from '@/features/account/settings/hooks/useActionWithElevatedPermissions';
import useGetSecurityKeys from '@/features/account/settings/hooks/useGetSecurityKeys';
import { useAddSecurityKey } from '@nhost/nextjs';
import { type NewSecurityKeyFormValues } from './useNewSecurityKeyForm';
interface Props {
onSuccess: () => void;
}
function useOnAddNewSecurityKeyHandler({ onSuccess }: Props) {
const { refetch } = useGetSecurityKeys();
const { add: actionFn } = useAddSecurityKey();
const addSecurityKey = useActionWithElevatedPermissions({
actionFn,
onSuccess: async () => {
await refetch();
onSuccess();
},
successMessage: 'Security key has been added.',
});
async function onSubmit(values: NewSecurityKeyFormValues) {
const { nickname } = values;
await addSecurityKey(nickname);
}
return onSubmit;
}
export default useOnAddNewSecurityKeyHandler;

View File

@@ -0,0 +1,22 @@
import useElevatedPermissions from '@/features/account/settings/hooks/useElevatedPermissions';
import useGetSecurityKeys from '@/features/account/settings/hooks/useGetSecurityKeys';
import { useRemoveSecurityKeyMutation } from '@/utils/__generated__/graphql';
function useRemoveSecurityKey() {
const [removeSecurityKeyMutation] = useRemoveSecurityKeyMutation();
const { elevatePermissions } = useElevatedPermissions();
const { refetch: refetchSecurityKeys } = useGetSecurityKeys();
async function removeSecurityKey(id: string) {
const permissionGranted = await elevatePermissions(true);
if (permissionGranted) {
await removeSecurityKeyMutation({ variables: { id } });
await refetchSecurityKeys();
}
}
return removeSecurityKey;
}
export default useRemoveSecurityKey;

View File

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

View File

@@ -0,0 +1,5 @@
query getActiveMfaType($id: uuid!) {
user(id: $id) {
activeMfaType
}
}

View File

@@ -0,0 +1,6 @@
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}

View File

@@ -0,0 +1,5 @@
mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id
}
}

View File

@@ -0,0 +1,61 @@
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 ActionResult = {
isError?: boolean;
error: AuthErrorPayload;
};
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
interface Props<Fn extends Action> {
actionFn: Fn;
onSuccess?: (result: UnwrapPromise<ReturnType<Fn>>) => void;
onError?: () => void;
successMessage?: string;
}
function useActionWithElevatedPermissions<F extends Action>({
actionFn,
onSuccess,
onError,
successMessage,
}: Props<F>) {
const { elevated, elevatePermissions } = useElevatedPermissions();
const { data } = useGetSecurityKeys();
async function requestPermissions() {
if (elevated || data?.authUserSecurityKeys.length === 0) {
return true;
}
const isPermissionsElevated = await elevatePermissions();
return isPermissionsElevated;
}
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');
onSuccess?.(response as UnwrapPromise<ReturnType<F>>);
isSuccess = true;
}
return isSuccess;
}
return actionWithElevatedPermissions;
}
export default useActionWithElevatedPermissions;

View File

@@ -0,0 +1,36 @@
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();
async function elevatePermissions(shouldThrowError = 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);
}
return true;
} catch (e) {
if (shouldThrowError) {
throw e;
} else {
const message = e?.message || 'Could not elevate permissions';
toast.error(message, getToastStyleProps());
return false;
}
}
}
return { elevated, elevatePermissions };
}
export default useElevatedPermissions;

View File

@@ -0,0 +1,15 @@
import { useSecurityKeysQuery } from '@/utils/__generated__/graphql';
import { useUserId } from '@nhost/nextjs';
function useGetSecurityKeys() {
const currentUserId = useUserId();
const query = useSecurityKeysQuery({
variables: {
userId: currentUserId,
},
});
return query;
}
export default useGetSecurityKeys;

View File

@@ -0,0 +1,35 @@
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import {
useGithubAuthentication,
type UseGithubAuthenticationHookProps,
} from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication';
import { SiGithub } from '@icons-pack/react-simple-icons';
interface Props extends UseGithubAuthenticationHookProps {
buttonText?: string;
withAnonId?: boolean;
redirectTo?: string;
}
function GithubAuthButton({
buttonText = 'Continue with GitHub',
withAnonId = false,
redirectTo,
}: Props) {
const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({
withAnonId,
redirectTo,
});
return (
<Button
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"
disabled={isLoading}
loading={isLoading}
onClick={() => signInWithGithub()}
>
<SiGithub size={14} /> {buttonText}
</Button>
);
}
export default GithubAuthButton;

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
import { getAnonId } from '@/lib/segment';
import { isNotEmptyValue } from '@/lib/utils';
import { getToastStyleProps } from '@/utils/constants/settings';
import { nhost } from '@/utils/nhost';
import { useMutation } from '@tanstack/react-query';
import { toast } from 'react-hot-toast';
export interface UseGithubAuthenticationHookProps {
withAnonId?: boolean;
redirectTo?: string;
errorText?: string;
}
function useGithubAuthentication({
withAnonId = false,
redirectTo,
errorText,
}: UseGithubAuthenticationHookProps) {
const githubAuthenticationMutation = useMutation(
async () => {
const options = {
...(isNotEmptyValue(redirectTo) && { redirectTo }),
...(withAnonId && { metadata: { anonId: await getAnonId() } }),
};
return nhost.auth.signIn({
provider: 'github',
...(isNotEmptyValue(options) && { options }),
});
},
{
onError: () => {
toast.error(errorText, getToastStyleProps());
},
},
);
return githubAuthenticationMutation;
}
export default useGithubAuthentication;

View File

@@ -0,0 +1,25 @@
import { Button } from '@/components/ui/v3/button';
import { useSignInWithSecurityKey } from '@/features/auth/SignIn/SecurityKey/hooks/useSignInWithSecurityKey';
import { Fingerprint } from 'lucide-react';
import { VerifyEmailDialog } from './VerifyEmailDialog';
function SignInWithSecurityKey() {
const { disabled, signInWithSecurityKey, needsEmailVerification } =
useSignInWithSecurityKey();
return (
<>
<VerifyEmailDialog needsEmailVerification={needsEmailVerification} />
<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"
disabled={disabled}
onClick={signInWithSecurityKey}
>
<Fingerprint size={14} />
Continue with a security key
</Button>
</>
);
}
export default SignInWithSecurityKey;

View File

@@ -0,0 +1,34 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/v3/dialog';
import { useEffect, useState } from 'react';
interface Props {
needsEmailVerification: boolean;
}
export function VerifyEmailDialog({ needsEmailVerification }: Props) {
const [open, setOpen] = useState(needsEmailVerification);
useEffect(() => {
setOpen(needsEmailVerification);
}, [needsEmailVerification, open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Email verification required</DialogTitle>
<DialogDescription>
You need to verify your email first. Please check your mailbox and
follow the confirmation link to complete the registration.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}

View File

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

View File

@@ -0,0 +1,34 @@
import { getToastStyleProps } from '@/utils/constants/settings';
import { useSignInSecurityKey } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
function useSignInWithSecurityKey() {
const { signInSecurityKey } = useSignInSecurityKey();
const [needsEmailVerification, setNeedsEmailVerification] = useState(false);
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('/');
}
setDisabled(false);
}
return { disabled, signInWithSecurityKey, needsEmailVerification };
}
export default useSignInWithSecurityKey;

View File

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

View File

@@ -0,0 +1,26 @@
import { MfaOtpForm } from '@/components/common/MfaOtpForm';
import type { SendMfaOtpHandler } from '@nhost/nextjs';
import { Smartphone } from 'lucide-react';
interface Props {
sendMfaOtp: SendMfaOtpHandler;
loading: boolean;
}
function MfaSignInOtpForm({ sendMfaOtp, loading }: 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} />
<p className="text-center">
Open your authenticator app or browser extension to view your
authentication code.
</p>
</div>
);
}
export default MfaSignInOtpForm;

View File

@@ -0,0 +1,16 @@
import useOnSignUpWithPasswordHandler from '@/features/auth/SignIn/SignInWithEmailAndPassword/hooks/useOnSignInWithEmailAndPasswordHandler';
import MfaSignInOtpForm from './MfaSignInOtpForm';
import SignInWithEmailAndPasswordForm from './SignInWithEmailAndPasswordForm';
function SignInWithEmailAndPassword() {
const { onSignIWithEmailAndPassword, sendMfaOtp, isLoading, needsMfaOtp } =
useOnSignUpWithPasswordHandler();
return needsMfaOtp ? (
<MfaSignInOtpForm sendMfaOtp={sendMfaOtp} loading={isLoading} />
) : (
<SignInWithEmailAndPasswordForm onSubmit={onSignIWithEmailAndPassword} />
);
}
export default SignInWithEmailAndPassword;

View File

@@ -0,0 +1,57 @@
import { FormInput } from '@/components/form/FormInput';
import { Button } from '@/components/ui/v3/button';
import { Form } from '@/components/ui/v3/form';
import useSignInWithEmailAndPasswordForm, {
type SignInWithEmailAndPasswordFormValues,
} from '@/features/auth/SignIn/SignInWithEmailAndPassword/hooks/useSignInWithEmailAndPasswordForm';
import NextLink from 'next/link';
interface Props {
onSubmit: (values: SignInWithEmailAndPasswordFormValues) => void;
}
function SignInWithEmailAndPassword({ onSubmit }: Props) {
const form = useSignInWithEmailAndPasswordForm();
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-flow-row gap-4 bg-transparent"
>
<FormInput
control={form.control}
label="Email"
name="email"
type="email"
/>
<FormInput
control={form.control}
label="Password"
name="password"
type="password"
/>
<NextLink
href="/password/new"
className="justify-self-start font-semibold"
>
Forgot password?
</NextLink>
<Button
type="submit"
variant="outline"
className="w-full !bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
>
Sign In
</Button>
<p color="secondary" className="text-center">
<span className="text-[#A2B3BE]">or </span>
<NextLink className="font-semibold" href="/signin">
sign in with GitHub
</NextLink>
</p>
</form>
</Form>
);
}
export default SignInWithEmailAndPassword;

View File

@@ -0,0 +1,50 @@
import { getToastStyleProps } from '@/utils/constants/settings';
import {
useSendVerificationEmail,
useSignInEmailPassword,
} from '@nhost/nextjs';
import { useRouter } from 'next/router';
import toast from 'react-hot-toast';
import type { SignInWithEmailAndPasswordFormValues } from './useSignInWithEmailAndPasswordForm';
function useOnSignInWithEmailAndPasswordHandler() {
const router = useRouter();
const { signInEmailPassword, needsMfaOtp, sendMfaOtp, isLoading } =
useSignInEmailPassword();
const { sendEmail } = useSendVerificationEmail();
async function onSignIWithEmailAndPassword({
email,
password,
}: SignInWithEmailAndPasswordFormValues) {
try {
const { needsEmailVerification, error } = await 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)}`);
}
} catch {
toast.error(
'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
}
}
return { onSignIWithEmailAndPassword, needsMfaOtp, sendMfaOtp, isLoading };
}
export default useOnSignInWithEmailAndPasswordHandler;

View File

@@ -0,0 +1,30 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const validationSchema = z
.object({
email: z.string().email({ message: 'Invalid email address' }),
password: z.string().min(1, { message: 'Password is required' }),
})
.required();
export type SignInWithEmailAndPasswordFormValues = z.infer<
typeof validationSchema
>;
function useSignInWithEmailAndPasswordForm() {
const form = useForm<SignInWithEmailAndPasswordFormValues>({
mode: 'onTouched',
reValidateMode: 'onBlur',
defaultValues: {
email: '',
password: '',
},
resolver: zodResolver(validationSchema),
});
return form;
}
export default useSignInWithEmailAndPasswordForm;

View File

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

View File

@@ -0,0 +1,15 @@
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
function SignInWithGithub() {
const redirectTo = useHostName();
return (
<GithubAuthButton
redirectTo={redirectTo}
buttonText="Continue with GitHub"
errorText="An error occurred while trying to sign in using GitHub. Please try again later."
/>
);
}
export default SignInWithGithub;

View File

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

View File

@@ -0,0 +1,39 @@
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/v3/tabs';
import { useState } from 'react';
import { SignUpWithEmailAndPasswordForm } from './SignUpWithEmailAndPassword';
import { SignUpWithSecurityKeyForm } from './SignUpWithSecurityKey';
function SignUpTabs() {
const [tab, setTab] = useState<string>('password');
return (
<Tabs value={tab} onValueChange={setTab} className="w-full">
<TabsList className="w-full">
<TabsTrigger value="password" className="w-full">
Sign Up with a Password
</TabsTrigger>
<TabsTrigger value="security-key" className="w-full">
Sign Up with a Security key
</TabsTrigger>
</TabsList>
<div className="pt-7">
{tab === 'password' && (
<TabsContent value="password">
<SignUpWithEmailAndPasswordForm />
</TabsContent>
)}
{tab === 'security-key' && (
<TabsContent value="security-key">
<SignUpWithSecurityKeyForm />
</TabsContent>
)}
</div>
</Tabs>
);
}
export default SignUpTabs;

View File

@@ -0,0 +1,81 @@
import { FormInput } from '@/components/form/FormInput';
import { Button } from '@/components/ui/v3/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import useOnSignUpWithPasswordHandler from '@/features/auth/SignUp/SignUpTabs/SignUpWithEmailAndPassword/hooks/useOnSignUpWithPasswordHandler';
import useSignUpWithEmailAndPasswordForm from '@/features/auth/SignUp/SignUpTabs/SignUpWithEmailAndPassword/hooks/useSignUpWithEmailAndPasswordForm';
import { Turnstile } from '@marsidev/react-turnstile';
function SignUpWithEmailAndPasswordForm() {
const form = useSignUpWithEmailAndPasswordForm();
const onSignUpWithPassword = useOnSignUpWithPasswordHandler();
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSignUpWithPassword)}
className="grid grid-flow-row gap-4 bg-transparent"
>
<FormInput control={form.control} label="Name" name="displayName" />
<FormInput
control={form.control}
label="Email"
name="email"
type="email"
/>
<FormInput
control={form.control}
label="Password"
name="password"
type="password"
/>
<FormField
control={form.control}
name="turnstileToken"
render={() => (
<FormItem>
<FormLabel>Verification</FormLabel>
<FormControl>
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
options={{ theme: 'dark', size: 'flexible' }}
onSuccess={(token) => {
form.setValue('turnstileToken', token, {
shouldValidate: true,
});
}}
onError={() => {
form.setValue('turnstileToken', '', {
shouldValidate: true,
});
}}
onExpire={() => {
form.setValue('turnstileToken', '', {
shouldValidate: true,
});
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant="outline"
className="w-full !bg-transparent"
>
Sign Up
</Button>
</form>
</Form>
);
}
export default SignUpWithEmailAndPasswordForm;

View File

@@ -0,0 +1,56 @@
import { getAnonId } from '@/lib/segment';
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 router = useRouter();
async function onSignUpWithPassword({
email,
password,
displayName,
turnstileToken,
}: SignUpWithEmailAndPasswordFormValues) {
try {
const { needsEmailVerification, error } = await signUpEmailPassword(
email,
password,
{
displayName,
metadata: { anonId: await getAnonId() },
},
{
headers: {
'x-cf-turnstile-response': turnstileToken,
},
},
);
if (error) {
toast.error(
error.message ||
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
return;
}
if (needsEmailVerification) {
router.push(`/email/verify?email=${encodeURIComponent(email)}`);
}
} catch {
toast.error(
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}
}
return onSignUpWithPassword;
}
export default useOnSignUpWithPasswordHandler;

View File

@@ -0,0 +1,43 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const validationSchema = z
.object({
email: z.string().email({ message: 'Invalid email address' }),
password: z.string().min(1, { message: 'Password is required' }),
displayName: z
.string()
.regex(
/^[\p{L}\p{N}\p{S} ,.'-]+$/u,
'Use only letters, numbers, symbols and basic punctuation',
)
.min(1, { message: 'Name is required' })
.max(32, { message: 'Name must be 32 characters or less' }),
turnstileToken: z
.string()
.min(1, { message: 'Please complete the CAPTCHA' }),
})
.required();
export type SignUpWithEmailAndPasswordFormValues = z.infer<
typeof validationSchema
>;
function useSignUpWithEmailAndPasswordForm() {
const form = useForm<SignUpWithEmailAndPasswordFormValues>({
mode: 'onTouched',
reValidateMode: 'onBlur',
defaultValues: {
email: '',
password: '',
displayName: '',
turnstileToken: '',
},
resolver: zodResolver(validationSchema),
});
return form;
}
export default useSignUpWithEmailAndPasswordForm;

View File

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

View File

@@ -0,0 +1,75 @@
import { FormInput } from '@/components/form/FormInput';
import { Button } from '@/components/ui/v3/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import useSignupWithSecurityKeyForm from '@/features/auth/SignUp/SignUpTabs/SignUpWithSecurityKey/hooks/useSignupWithSecurityKeyForm';
import useSignupWithSecurityKeyHandler from '@/features/auth/SignUp/SignUpTabs/SignUpWithSecurityKey/hooks/useSignupWithSecurityKeyHandler';
import { Turnstile } from '@marsidev/react-turnstile';
function SignUpWithSecurityKeyForm() {
const form = useSignupWithSecurityKeyForm();
const onSignUpWithSecurityKey = useSignupWithSecurityKeyHandler();
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSignUpWithSecurityKey)}
className="grid grid-flow-row gap-4 bg-transparent"
>
<FormInput control={form.control} label="Name" name="displayName" />
<FormInput
control={form.control}
label="Email"
name="email"
type="email"
/>
<FormField
control={form.control}
name="turnstileToken"
render={() => (
<FormItem>
<FormLabel>Verification</FormLabel>
<FormControl>
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
options={{ theme: 'dark', size: 'flexible' }}
onSuccess={(token) => {
form.setValue('turnstileToken', token, {
shouldValidate: true,
});
}}
onError={() => {
form.setValue('turnstileToken', '', {
shouldValidate: true,
});
}}
onExpire={() => {
form.setValue('turnstileToken', '', {
shouldValidate: true,
});
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant="outline"
className="w-full !bg-transparent"
>
Sign Up
</Button>
</form>
</Form>
);
}
export default SignUpWithSecurityKeyForm;

View File

@@ -0,0 +1,42 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const validationSchema = z
.object({
email: z
.string()
.email({ message: 'Invalid email address' })
.min(1, { message: 'Email is required' }),
displayName: z
.string()
.regex(
/^[\p{L}\p{N}\p{S} ,.'-]+$/u,
'Use only letters, numbers, symbols and basic punctuation',
)
.min(1, { message: 'Name is required' })
.max(32, { message: 'Name must be 32 characters or less' }),
turnstileToken: z
.string()
.min(1, { message: 'Please complete the CAPTCHA' }),
})
.required();
export type SignUpWithSecurityKeyFormValues = z.infer<typeof validationSchema>;
function useSignUpWithSecurityKey() {
const form = useForm<SignUpWithSecurityKeyFormValues>({
mode: 'onTouched',
reValidateMode: 'onBlur',
defaultValues: {
email: '',
displayName: '',
turnstileToken: '',
},
resolver: zodResolver(validationSchema),
});
return form;
}
export default useSignUpWithSecurityKey;

View File

@@ -0,0 +1,54 @@
import { getAnonId } from '@/lib/segment';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useSignUpEmailSecurityKeyEmail } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import toast from 'react-hot-toast';
import type { SignUpWithSecurityKeyFormValues } from './useSignupWithSecurityKeyForm';
function useOnSignUpWithSecurityKeyHandler() {
const { signUpEmailSecurityKey } = useSignUpEmailSecurityKeyEmail();
const router = useRouter();
async function onSignUpWithSecurityKey({
email,
displayName,
turnstileToken,
}: SignUpWithSecurityKeyFormValues) {
try {
const { needsEmailVerification, error } = await signUpEmailSecurityKey(
email,
{
displayName,
metadata: { anonId: await getAnonId() },
},
{
headers: {
'x-cf-turnstile-response': turnstileToken,
},
},
);
if (error) {
toast.error(
error.message ||
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
return;
}
if (needsEmailVerification) {
router.push(`/email/verify?email=${encodeURIComponent(email)}`);
}
} catch {
toast.error(
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}
}
return onSignUpWithSecurityKey;
}
export default useOnSignUpWithSecurityKeyHandler;

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
function SignUpWithGithub() {
return (
<GithubAuthButton
withAnonId
buttonText="Sign Up with GitHub"
errorText="An error occurred while trying to sign up using GitHub. Please try again."
/>
);
}
export default SignUpWithGithub;

View File

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

View File

@@ -26,7 +26,7 @@ function InfoAlert({
<Alert className={alertClassNames}>
{icon && <div>{icon}</div>}
<div>
{title && <AlertTitle>{title}</AlertTitle>}
{title && <AlertTitle className="font-semibold">{title}</AlertTitle>}
{children && (
<AlertDescription className={descClassNames}>
{children}

View File

@@ -1,7 +1,6 @@
import { useUI } from '@/components/common/UIProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Button } from '@/components/ui/v3/button';
import {
Dialog,
@@ -21,6 +20,8 @@ import {
FormMessage,
} from '@/components/ui/v3/form';
import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
import { InfoAlert } from '@/features/orgs/components/InfoAlert';
import TextLink from '@/features/orgs/projects/common/components/TextLink/TextLink';
import { planDescriptions } from '@/features/orgs/projects/common/utils/planDescriptions';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
@@ -35,6 +36,14 @@ import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
function NewOrgButton() {
return (
<strong className="inline-flex items-center justify-center gap-2 px-1">
<span>+ New Organization</span>
</strong>
);
}
const changeOrgPlanForm = z.object({
plan: z.string(),
});
@@ -48,6 +57,8 @@ export default function SubscriptionPlan() {
const [fetchOrganizationCustomePortalLink, { loading }] =
useBillingOrganizationCustomePortalLazyQuery();
const isFreeOrg = org?.plan.isFree;
const form = useForm<z.infer<typeof changeOrgPlanForm>>({
resolver: zodResolver(changeOrgPlanForm),
defaultValues: {
@@ -125,7 +136,7 @@ export default function SubscriptionPlan() {
<div className="flex w-full flex-col gap-1 border-b p-4">
<h4 className="font-medium">Subscription plan</h4>
</div>
<div className="flex w-full flex-col justify-between gap-8 border-b p-4 md:flex-row">
<div className="flex w-full flex-col justify-between gap-8 p-4 md:flex-row">
<div className="flex basis-1/2 flex-col gap-4">
<span className="font-medium">Organization name</span>
<span className="font-medium">{org?.name}</span>
@@ -152,31 +163,26 @@ export default function SubscriptionPlan() {
</div>
</div>
</div>
<div className="flex w-full flex-col-reverse items-end justify-between gap-2 p-4 md:flex-row md:items-center md:gap-0">
{isFreeOrg && (
<div className="flex w-full flex-col justify-between gap-8 p-4 md:flex-row">
<InfoAlert title="Personal Organizations can not be upgraded.">
You may create a new organization with premium features by
clicking on the <NewOrgButton /> button in the left sidebar.
</InfoAlert>
</div>
)}
<div className="flex w-full flex-col-reverse items-end justify-between gap-2 border-t p-4 md:flex-row md:items-center md:gap-0">
<div>
<span>For a complete list of features, visit our </span>
<Link
href="https://nhost.io/pricing"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
<TextLink href="https://nhost.io/pricing">
pricing
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
</Link>
</TextLink>
<span> You can also visit our </span>
<Link
href="https://docs.nhost.io/platform/cloud/billing"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
<TextLink href="https://docs.nhost.io/platform/cloud/billing">
documentation
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
</Link>
</TextLink>
<span> for billing information</span>
</div>
<div className="flex w-full flex-row items-center justify-end gap-2">
@@ -245,7 +251,7 @@ export default function SubscriptionPlan() {
</div>
<div className="mt-0 flex h-full items-center text-xl font-semibold">
{plan.isFree ? 'Free' : `${plan.price}/mo`}
{isFreeOrg ? 'Free' : `${plan.price}/mo`}
</div>
</FormLabel>
</FormItem>
@@ -264,16 +270,10 @@ export default function SubscriptionPlan() {
</div>
</div>
<Link
href="mailto:hello@nhost.io"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
<TextLink href="mailto:hello@nhost.io">
Contact us
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</TextLink>
</div>
</div>
</RadioGroup>

View File

@@ -0,0 +1,95 @@
import { Button } from '@/components/ui/v3/button';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
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 [downloading, setDownloading] = useState(false);
const showSoc2Download =
(org?.plan?.name === 'Team' || org?.plan?.name?.startsWith('Enterprise')) &&
org?.status === Organization_Status_Enum.Ok;
const handleDownload = async () => {
if (!org || !showSoc2Download) {
return;
}
setDownloading(true);
await execPromiseWithErrorToast(
async () => {
const fileId = process.env.NEXT_PUBLIC_SOC2_REPORT_FILE_ID;
if (!fileId) {
throw new Error('SOC2 report file ID not configured');
}
const { file, error } = await nhost.storage.download({
fileId,
});
if (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...',
successMessage: 'SOC2 report downloaded successfully',
errorMessage:
'Failed to download SOC2 report. Please try again or contact support.',
},
);
setDownloading(false);
};
if (!showSoc2Download) {
return null;
}
return (
<div className="flex w-full flex-col rounded-md border bg-background">
<div className="w-full border-b p-4 font-medium">
SOC2 Compliance Report
</div>
<div className="flex w-full flex-col gap-4 p-4">
<div className="flex flex-col gap-2">
<p className="text-sm text-muted-foreground">
Download Nhost&apos;s SOC2 Type II compliance report. This report
demonstrates our commitment to security controls.
</p>
</div>
<div className="flex justify-start">
<Button
onClick={handleDownload}
disabled={downloading}
variant="outline"
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
{downloading ? 'Downloading...' : 'Download SOC2 Report'}
</Button>
</div>
</div>
</div>
);
}

View File

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

View File

@@ -1,3 +1,4 @@
import { SquareArrowUpRightIcon } from 'lucide-react';
import Link from 'next/link';
import type { PropsWithChildren } from 'react';
@@ -5,7 +6,8 @@ function TextLink({
href,
children,
target = '_blank',
}: PropsWithChildren<{ href: string; target?: string }>) {
withIcon = false,
}: PropsWithChildren<{ href: string; target?: string; withIcon?: boolean }>) {
return (
<Link
href={href}
@@ -14,6 +16,7 @@ function TextLink({
rel="noopener noreferrer"
>
{children}
{withIcon && <SquareArrowUpRightIcon className="h-4 w-4" />}
</Link>
);
}

View File

@@ -23,7 +23,7 @@ export default function useGetPostgresVersion() {
});
const { version } = postgresSettingsData?.config?.postgres || {};
const { major, minor } = splitPostgresMajorMinorVersions(version);
const { major, minor } = splitPostgresMajorMinorVersions(version || '');
return {
version,

View File

@@ -21,6 +21,7 @@ import {
useGetPostgresSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { isEmptyValue } from '@/lib/utils';
import { ApplicationStatus } from '@/types/application';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useMemo } from 'react';
@@ -46,7 +47,9 @@ export default function DatabaseStorageCapacity() {
const localMimirClient = useLocalMimirClient();
const { project } = useProject();
const isFreeProject = !!org?.plan?.isFree;
const isFreeProject = isEmptyValue(org) ? false : org.plan.isFree;
const shouldShowUpdateCapacityWarning = !isFreeProject && isPlatform;
const {
data,
@@ -98,6 +101,10 @@ export default function DatabaseStorageCapacity() {
return true;
}
if (!isPlatform) {
return false;
}
if (maintenanceActive) {
return true;
}
@@ -107,7 +114,13 @@ export default function DatabaseStorageCapacity() {
}
return false;
}, [isDirty, maintenanceActive, decreasingSize, applicationPause]);
}, [
isDirty,
maintenanceActive,
decreasingSize,
applicationPause,
isPlatform,
]);
useEffect(() => {
if (data && !loading) {
@@ -195,7 +208,7 @@ export default function DatabaseStorageCapacity() {
helperText={formState.errors.capacity?.message}
/>
</Box>
{!isFreeProject && (
{shouldShowUpdateCapacityWarning && (
<DatabaseStorageCapacityWarning
state={state}
decreasingSize={decreasingSize}

View File

@@ -6,11 +6,13 @@ import { Dropdown } from '@/components/ui/v2/Dropdown';
import { CalendarIcon } from '@/components/ui/v2/icons/CalendarIcon';
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
import { Text } from '@/components/ui/v2/Text';
import type { LogsFilterFormValues } from '@/features/orgs/projects/logs/components/LogsHeader';
import { LogsTimePicker } from '@/features/orgs/projects/logs/components/LogsTimePicker';
import { DATEPICKER_DISPLAY_FORMAT } from '@/features/orgs/projects/logs/utils/constants/datePicker';
import { usePreviousData } from '@/hooks/usePreviousData';
import { format } from 'date-fns';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface LogsDatePickerProps extends DatePickerProps {
@@ -36,6 +38,7 @@ function LogsDatePicker({
value,
}: LogsDatePickerProps) {
const [selectedDate, setSelectedDate] = useState<Date | null>(value);
const { setValue } = useFormContext<LogsFilterFormValues>();
const { button: buttonSlotProps } = {
button: componentsProps?.button || {},
};
@@ -45,6 +48,11 @@ function LogsDatePicker({
// going to display the last state set.
const previousDate = usePreviousData(selectedDate);
const handleDateChange = (newValue: Date) => {
setSelectedDate(new Date(newValue));
setValue('interval', null);
};
return (
<Dropdown.Root>
<div className="grid grid-flow-col gap-x-3">
@@ -84,9 +92,7 @@ function LogsDatePicker({
<Dropdown.Content>
<DatePicker
value={disabled ? previousDate : selectedDate}
onChange={(newValue) => {
setSelectedDate(new Date(newValue));
}}
onChange={handleDateChange}
minDate={minDate}
maxDate={maxDate}
/>

View File

@@ -17,7 +17,7 @@ import {
} from '@/features/orgs/projects/logs/utils/constants/services';
import { isEmptyValue } from '@/lib/utils';
import { useGetServiceLabelValuesQuery } from '@/utils/__generated__/graphql';
import { MINUTES_TO_DECREASE_FROM_CURRENT_DATE } from '@/utils/constants/common';
import { DEFAULT_LOG_INTERVAL } from '@/utils/constants/common';
import { yupResolver } from '@hookform/resolvers/yup';
import { subMinutes } from 'date-fns';
import { useEffect, useMemo } from 'react';
@@ -28,6 +28,7 @@ import LogsServiceFilter from './LogsServiceFilter';
export const validationSchema = Yup.object({
from: Yup.date(),
to: Yup.date().nullable(),
interval: Yup.number().nullable(), // in minutes
service: Yup.string().oneOf(Object.values(AvailableLogsService)),
regexFilter: Yup.string(),
});
@@ -44,11 +45,17 @@ interface LogsHeaderProps extends Omit<BoxProps, 'children'> {
* Function to be called when the user submits the filters form
*/
onSubmitFilterValues: (value: LogsFilterFormValues) => void;
/**
*
* Function to be called to force a refetch of the logs when the form is not dirty and the user submits the form
*/
onRefetch: () => void;
}
export default function LogsHeader({
loading,
onSubmitFilterValues,
onRefetch,
...props
}: LogsHeaderProps) {
const { project } = useProject();
@@ -83,16 +90,20 @@ export default function LogsHeader({
const form = useForm<LogsFilterFormValues>({
defaultValues: {
from: subMinutes(new Date(), MINUTES_TO_DECREASE_FROM_CURRENT_DATE),
from: subMinutes(new Date(), DEFAULT_LOG_INTERVAL),
to: new Date(),
regexFilter: '',
service: AvailableLogsService.ALL,
interval: DEFAULT_LOG_INTERVAL,
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
reValidateMode: 'onSubmit',
});
const { formState } = form;
const { register, watch, getValues } = form;
const isNotDirty = Object.keys(formState.dirtyFields).length === 0;
const { register, watch, getValues, setValue } = form;
const service = watch('service');
@@ -100,8 +111,33 @@ export default function LogsHeader({
onSubmitFilterValues(getValues());
}, [service, getValues, onSubmitFilterValues]);
const handleSubmit = (values: LogsFilterFormValues) =>
const handleSubmit = (values: LogsFilterFormValues) => {
// If there's an interval set, recalculate the dates
if (values.interval) {
const now = new Date();
const newValues = {
...values,
from: subMinutes(now, values.interval),
to: now,
interval: values.interval,
};
// Update form values before submitting, to ensure the dates have the current date if selected an interval
setValue('from', newValues.from);
setValue('to', newValues.to);
setValue('interval', newValues.interval);
onSubmitFilterValues(newValues);
return;
}
// If the form is not dirty, force a refetch of the logs
if (isNotDirty) {
onRefetch();
}
onSubmitFilterValues(values);
};
return (
<Box
@@ -198,11 +234,13 @@ export default function LogsHeader({
type="submit"
className="h-10"
startIcon={
loading ? (
<ActivityIndicator className="h-4 w-4" />
) : (
<SearchIcon />
)
<div className="flex h-5 w-5 items-center justify-center">
{loading ? (
<ActivityIndicator className="h-5 w-5" />
) : (
<SearchIcon className="h-5 w-5" />
)}
</div>
}
disabled={loading}
>

View File

@@ -27,11 +27,18 @@ function LogsToDatePickerLiveButton() {
if (isLive) {
setValue('from', subMinutes(new Date(), 20));
setValue('to', new Date());
setValue('interval', null);
return;
}
setValue('to', null);
setCurrentTime(new Date());
setValue('interval', null);
}
function handleChangeToDate(date: Date) {
setValue('to', date);
setValue('interval', null);
}
useInterval(() => setCurrentTime(new Date()), isLive ? 1000 : 0);
@@ -43,7 +50,7 @@ function LogsToDatePickerLiveButton() {
label="To"
value={!isLive ? to : currentTime}
disabled={isLive}
onChange={(date: Date) => setValue('to', date)}
onChange={handleChangeToDate}
minDate={from}
maxDate={new Date()}
componentsProps={{
@@ -84,7 +91,7 @@ function LogsRangeSelectorIntervalPickers({
const applicationCreationDate = new Date(project.createdAt);
const { setValue, getValues } = useFormContext<LogsFilterFormValues>();
const { from } = useWatch<LogsFilterFormValues>();
const { from, interval } = useWatch<LogsFilterFormValues>();
const { handleClose } = useDropdown();
@@ -101,6 +108,12 @@ function LogsRangeSelectorIntervalPickers({
}: LogsCustomInterval) {
setValue('from', subMinutes(new Date(), minutesToDecreaseFromCurrentDate));
setValue('to', new Date());
setValue('interval', minutesToDecreaseFromCurrentDate);
}
function handleChangeFromDate(date: Date) {
setValue('from', date);
setValue('interval', null);
}
return (
@@ -109,7 +122,7 @@ function LogsRangeSelectorIntervalPickers({
<LogsDatePicker
label="From"
value={from}
onChange={(date) => setValue('from', date)}
onChange={handleChangeFromDate}
minDate={applicationCreationDate}
maxDate={new Date()}
/>
@@ -122,7 +135,11 @@ function LogsRangeSelectorIntervalPickers({
<Button
key={logInterval.label}
variant="outlined"
color="secondary"
color={
interval === logInterval.minutesToDecreaseFromCurrentDate
? 'primary'
: 'secondary'
}
className="self-center"
onClick={() => handleIntervalChange(logInterval)}
>

View File

@@ -1,6 +1,25 @@
import { AnalyticsBrowser } from "@segment/analytics-next";
import { isDevOrStaging } from '@/utils/helpers';
import { AnalyticsBrowser } from '@segment/analytics-next';
import { isPlatform } from '@/utils/env';
export const analytics = AnalyticsBrowser.load({
cdnURL: process.env.NEXT_PUBLIC_SEGMENT_CDN_URL!,
writeKey: process.env.NEXT_PUBLIC_ANALYTICS_WRITE_KEY!,
export const analytics = AnalyticsBrowser.load(
{
cdnURL: process.env.NEXT_PUBLIC_SEGMENT_CDN_URL!,
writeKey: process.env.NEXT_PUBLIC_ANALYTICS_WRITE_KEY!,
},
{
disable: !isPlatform() || isDevOrStaging()
});
export async function getAnonId() {
let anonId: string;
try {
const user = await analytics.user();
anonId = user.anonymousId();
} catch (err) {
console.error('Failed to get anonymous ID:', err);
}
return anonId;
}

View File

@@ -5,8 +5,6 @@ import { TreeNavStateProvider } from '@/components/layout/MainNav/TreeNavStateCo
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { ThemeProvider } from '@/components/ui/v2/ThemeProvider';
import { TooltipProvider } from '@/components/ui/v3/tooltip';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { isDevOrStaging } from '@/utils/helpers';
// eslint-disable-next-line import/extensions
import '@/styles/fonts.css';
// eslint-disable-next-line import/extensions
@@ -59,7 +57,6 @@ function MyApp({
pageProps,
emotionCache = clientSideEmotionCache,
}: MyAppProps) {
const isPlatform = useIsPlatform();
const router = useRouter();
useEffect(() => {
@@ -89,7 +86,7 @@ function MyApp({
<UIProvider>
<Toaster position="bottom-center" />
{isPlatform && !isDevOrStaging() && <Analytics />}
<Analytics />
<ThemeProvider
colorPreferenceStorageKey={COLOR_PREFERENCE_STORAGE_KEY}

View File

@@ -1,11 +1,13 @@
import { Container } from '@/components/layout/Container';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { AccountMfaSettings } from '@/features/account/settings/components/AccountMfaSettings';
import { AccountSettingsLayout } from '@/features/account/settings/components/AccountSettingsLayout';
import { DeleteAccount } from '@/features/account/settings/components/DeleteAccount';
import { DisplayNameSetting } from '@/features/account/settings/components/DisplayNameSetting';
import { EmailSetting } from '@/features/account/settings/components/EmailSetting';
import { PasswordSettings } from '@/features/account/settings/components/PasswordSettings';
import { PATSettings } from '@/features/account/settings/components/PATSettings';
import { SecurityKeysSettings } from '@/features/account/settings/components/SecurityKeysSettings';
import { SocialProvidersSettings } from '@/features/account/settings/components/SocialProvidersSettings';
import type { ReactElement } from 'react';
@@ -26,7 +28,12 @@ export default function AccountSettingsPage() {
<RetryableErrorBoundary>
<PasswordSettings />
</RetryableErrorBoundary>
<RetryableErrorBoundary>
<AccountMfaSettings />
</RetryableErrorBoundary>
<RetryableErrorBoundary>
<SecurityKeysSettings />
</RetryableErrorBoundary>
<RetryableErrorBoundary>
<SocialProvidersSettings />
</RetryableErrorBoundary>

View File

@@ -12,7 +12,7 @@ import {
GetLogsSubscriptionDocument,
useGetProjectLogsQuery,
} from '@/utils/__generated__/graphql';
import { MINUTES_TO_DECREASE_FROM_CURRENT_DATE } from '@/utils/constants/common';
import { DEFAULT_LOG_INTERVAL } from '@/utils/constants/common';
import { subMinutes } from 'date-fns';
import {
useCallback,
@@ -37,7 +37,7 @@ export default function LogsPage() {
const subscriptionReturn = useRef(null);
const [filters, setFilters] = useState<LogsFilters>({
from: subMinutes(new Date(), MINUTES_TO_DECREASE_FROM_CURRENT_DATE),
from: subMinutes(new Date(), DEFAULT_LOG_INTERVAL),
to: new Date(),
regexFilter: '',
service: AvailableLogsService.ALL,
@@ -48,6 +48,7 @@ export default function LogsPage() {
error,
subscribeToMore,
client,
refetch,
loading: loadingLogs,
} = useGetProjectLogsQuery({
variables: { appID: project?.id, ...filters },
@@ -63,7 +64,10 @@ export default function LogsPage() {
document: GetLogsSubscriptionDocument,
variables: {
appID: project?.id,
service: filters.service,
service:
filters.service === AvailableLogsService.JOB_BACKUP
? 'job-backup.+' // Use regex pattern to match any job-backup services
: filters.service,
from: filters.from,
regexFilter: filters.regexFilter,
},
@@ -147,6 +151,7 @@ export default function LogsPage() {
<LogsHeader
loading={loading}
onSubmitFilterValues={onSubmitFilterValues}
onRefetch={refetch}
/>
<LogsBody error={error} loading={loading} logsData={data} />
</RetryableErrorBoundary>

View File

@@ -78,7 +78,10 @@ export default function SettingsGeneralPage() {
usePauseApplicationMutation({
variables: { appId: project?.id },
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{
query: GetOrganizationsDocument,
variables: { userId: userData?.id },
},
],
});
@@ -86,7 +89,10 @@ export default function SettingsGeneralPage() {
useUnpauseApplicationMutation({
variables: { appId: project?.id },
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{
query: GetOrganizationsDocument,
variables: { userId: userData?.id },
},
],
});

View File

@@ -1,5 +1,6 @@
import { DeleteOrg } from '@/features/orgs/components/general/components/DeleteOrg';
import { GeneralSettings } from '@/features/orgs/components/general/components/GeneralSettings';
import { Soc2Download } from '@/features/orgs/components/general/components/Soc2Download';
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
import type { ReactElement } from 'react';
@@ -7,6 +8,7 @@ export default function OrgSettings() {
return (
<div className="flex h-full flex-col gap-4 overflow-auto bg-accent p-4">
<GeneralSettings />
<Soc2Download />
<DeleteOrg />
</div>
);

View File

@@ -1,106 +1,68 @@
import { NavLink } from '@/components/common/NavLink';
import { UnauthenticatedLayout } from '@/components/layout/UnauthenticatedLayout';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
import { Text } from '@/components/ui/v2/Text';
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
import { getToastStyleProps } from '@/utils/constants/settings';
import { nhost } from '@/utils/nhost';
import { Button } from '@/components/ui/v3/button';
import NextLink from 'next/link';
import type { ReactElement } from 'react';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
export default function SignUpPage() {
const [loading, setLoading] = useState(false);
const redirectTo = useHostName();
import { SignInWithSecurityKey } from '@/features/auth/SignIn/SecurityKey';
import { SignInWithGithub } from '@/features/auth/SignIn/SignInWithGithub';
export default function SigninPage() {
return (
<>
<Text
variant="h2"
component="h1"
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
>
<div className="grid gap-12 font-[Inter]">
<h2 className="text-center text-3.5xl font-semibold lg:text-4.5xl">
It&apos;s time to build
</Text>
</h2>
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<div className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<SignInWithGithub />
<SignInWithSecurityKey />
<div className="relative py-2">
<p className="absolute left-0 right-0 top-1/2 mx-auto w-12 -translate-y-1/2 bg-black px-2 text-center text-sm text-[#68717A]">
OR
</p>
<Divider className="!my-2" />
</div>
<Button
className="!bg-white !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
startIcon={<GitHubIcon />}
size="large"
disabled={loading}
loading={loading}
onClick={async () => {
setLoading(true);
try {
await nhost.auth.signIn({
provider: 'github',
options: { redirectTo },
});
} catch {
toast.error(
`An error occurred while trying to sign in using GitHub. Please try again later.`,
getToastStyleProps(),
);
} finally {
setLoading(false);
}
}}
>
Continue with GitHub
</Button>
<Button
variant="borderless"
variant="ghost"
className="!text-white hover:!bg-white hover:!bg-opacity-10 focus:!bg-white focus:!bg-opacity-10"
size="large"
href="/signin/email"
LinkComponent={NavLink}
>
Continue with Email
<NextLink href="/signin/email">Continue with Email</NextLink>
</Button>
<Divider className="!my-2" />
<Text color="secondary" className="text-center text-sm">
<p className="text-center text-sm">
By clicking continue, you agree to our{' '}
<NavLink
<NextLink
href="https://nhost.io/legal/terms-of-service"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
className="font-semibold text-white"
>
Terms of Service
</NavLink>{' '}
</NextLink>{' '}
and{' '}
<NavLink
<NextLink
href="https://nhost.io/legal/privacy-policy"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
className="font-semibold text-white"
>
Privacy Policy
</NavLink>
</Text>
</Box>
</NextLink>
</p>
</div>
<Text color="secondary" className="text-center lg:text-lg">
<p className="text-center lg:text-lg">
Don&apos;t have an account?{' '}
<NavLink href="/signup" color="white" className="font-medium">
<NextLink href="/signup" className="font-medium text-white">
Sign Up
</NavLink>
</Text>
</>
</NextLink>
</p>
</div>
);
}
SignUpPage.getLayout = function getLayout(page: ReactElement) {
SigninPage.getLayout = function getLayout(page: ReactElement) {
return <UnauthenticatedLayout title="Sign In">{page}</UnauthenticatedLayout>;
};

View File

@@ -1,165 +1,29 @@
import { NavLink } from '@/components/common/NavLink';
import { Form } from '@/components/form/Form';
import { UnauthenticatedLayout } from '@/components/layout/UnauthenticatedLayout';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { getToastStyleProps } from '@/utils/constants/settings';
import { yupResolver } from '@hookform/resolvers/yup';
import { styled } from '@mui/material';
import { useNhostClient, useSignInEmailPassword } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { useEffect, type ReactElement } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
email: Yup.string().label('Email').email().required(),
password: Yup.string().label('Password').required(),
});
export type EmailSignUpFormValues = Yup.InferType<typeof validationSchema>;
const StyledInput = styled(Input)({
backgroundColor: 'transparent',
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent !important',
},
});
export default function EmailSignUpPage() {
const router = useRouter();
const nhost = useNhostClient();
const { signInEmailPassword, error } = useSignInEmailPassword();
const form = useForm<EmailSignUpFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
email: '',
password: '',
},
resolver: yupResolver(validationSchema),
});
const { register, formState } = form;
useEffect(() => {
if (!error) {
return;
}
toast.error(
error?.message || 'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
}, [error]);
async function handleSubmit({ email, password }: EmailSignUpFormValues) {
try {
const { needsEmailVerification } = await signInEmailPassword(
email,
password,
);
if (needsEmailVerification) {
await nhost.auth.sendVerificationEmail({ email: email as string });
router.push(`/email/verify?email=${email}`);
}
} catch {
toast.error(
'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
}
}
import { SignInWithEmailAndPassword } from '@/features/auth/SignIn/SignInWithEmailAndPassword';
import type { ReactElement } from 'react';
function SigninPage() {
return (
<>
<Text
variant="h2"
component="h1"
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
>
<div className="grid gap-12 font-[Inter]">
<h1 className="text-center text-3.5xl font-semibold lg:text-4.5xl">
Sign In
</Text>
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="grid grid-flow-row gap-4 bg-transparent"
>
<StyledInput
{...register('email')}
id="email"
placeholder="Email"
inputProps={{ min: 2, max: 128 }}
spellCheck="false"
autoCapitalize="none"
type="email"
label="Email"
hideEmptyHelperText
fullWidth
error={!!formState.errors.email}
helperText={formState.errors.email?.message}
autoFocus
/>
<StyledInput
{...register('password')}
id="password"
placeholder="Password"
inputProps={{ min: 2, max: 128 }}
spellCheck="false"
autoCapitalize="none"
type="password"
label="Password"
hideEmptyHelperText
fullWidth
error={!!formState.errors.password}
helperText={formState.errors.password?.message}
/>
<NavLink
href="/password/new"
color="white"
className="justify-self-start font-semibold"
>
Forgot password?
</NavLink>
<Button
className="!bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
size="large"
disabled={formState.isSubmitting}
loading={formState.isSubmitting}
type="submit"
>
Sign In
</Button>
<Text color="secondary" className="text-center">
or{' '}
<NavLink color="white" className="font-semibold" href="/signin">
sign in with GitHub
</NavLink>
</Text>
</Form>
</FormProvider>
</Box>
<Text color="secondary" className="text-center text-base lg:text-lg">
</h1>
<div className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<SignInWithEmailAndPassword />
</div>
<p className="text-center text-base lg:text-lg">
Don&apos;t have an account?{' '}
<NavLink href="/signup" color="white">
Sign Up
</NavLink>
</Text>
</>
</p>
</div>
);
}
EmailSignUpPage.getLayout = function getLayout(page: ReactElement) {
return <UnauthenticatedLayout title="Sign In">{page}</UnauthenticatedLayout>;
SigninPage.getLayout = function getLayout(page: ReactElement) {
return <UnauthenticatedLayout title="Sign Up">{page}</UnauthenticatedLayout>;
};
export default SigninPage;

View File

@@ -1,268 +1,60 @@
import { NavLink } from '@/components/common/NavLink';
import { Form } from '@/components/form/Form';
import { UnauthenticatedLayout } from '@/components/layout/UnauthenticatedLayout';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { analytics } from '@/lib/segment';
import { getToastStyleProps } from '@/utils/constants/settings';
import { nhost } from '@/utils/nhost';
import { yupResolver } from '@hookform/resolvers/yup';
import { Turnstile } from '@marsidev/react-turnstile';
import { styled } from '@mui/material';
import { useSignUpEmailPassword } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { SignUpTabs } from '@/features/auth/SignUp/SignUpTabs';
import { SignUpWithEmailAndPasswordForm } from '@/features/auth/SignUp/SignUpTabs/SignUpWithEmailAndPassword';
import { SignUpWithGithub } from '@/features/auth/SignUp/SignUpWithGithub';
import NextLink from 'next/link';
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
email: Yup.string().label('Email').email().required(),
password: Yup.string().label('Password').required(),
displayName: Yup.string().label('Name').required(),
});
export type SignUpFormValues = Yup.InferType<typeof validationSchema>;
const StyledInput = styled(Input)({
backgroundColor: 'transparent',
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent !important',
},
});
export default function SignUpPage() {
const { signUpEmailPassword, error } = useSignUpEmailPassword();
const [loading, setLoading] = useState(false);
const router = useRouter();
const [anonId, setAnonId] = useState<string | null>(null);
useEffect(() => {
const getAnonId = async () => {
try {
const user = await analytics.user();
setAnonId(user.anonymousId());
} catch (err) {
console.error('Failed to get anonymous ID:', err);
}
};
getAnonId();
}, []);
// x-cf-turnstile-response
const [turnstileResponse, setTurnstileResponse] = useState(null);
const form = useForm<SignUpFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
email: '',
password: '',
},
resolver: yupResolver(validationSchema),
});
const { register, formState } = form;
useEffect(() => {
if (!error) {
return;
}
toast.error(
error?.message || 'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}, [error]);
async function handleSubmit({
email,
password,
displayName,
}: SignUpFormValues) {
if (!turnstileResponse) {
toast.error(
'Please complete the signup verification challenge to continue.',
getToastStyleProps(),
);
return;
}
try {
const { needsEmailVerification } = await signUpEmailPassword(
email,
password,
{
displayName,
metadata: { anonId },
},
{
headers: {
'x-cf-turnstile-response': turnstileResponse,
},
},
);
if (needsEmailVerification) {
router.push(`/email/verify?email=${email}`);
}
} catch {
toast.error(
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}
}
return (
<>
<Text
variant="h2"
component="h1"
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
>
<div className="flex flex-col gap-12 font-[Inter]">
<h1 className="text-center text-3.5xl font-semibold lg:text-4.5xl">
Sign Up
</Text>
</h1>
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<Button
variant="borderless"
className="!bg-white !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
startIcon={<GitHubIcon />}
disabled={loading}
loading={loading}
size="large"
onClick={async () => {
setLoading(true);
try {
await nhost.auth.signIn({
provider: 'github',
options: { metadata: { anonId } },
});
} catch {
toast.error(
`An error occurred while trying to sign up using GitHub. Please try again.`,
getToastStyleProps(),
);
} finally {
setLoading(false);
}
}}
>
Sign Up with GitHub
</Button>
<div className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<SignUpWithGithub />
<div className="relative py-2">
<Text
className="absolute left-0 right-0 top-1/2 mx-auto w-12 -translate-y-1/2 bg-black px-2 text-center text-sm"
color="disabled"
>
<p className="absolute left-0 right-0 top-1/2 mx-auto w-12 -translate-y-1/2 bg-black px-2 text-center text-sm text-[#68717A]">
OR
</Text>
</p>
<Divider />
</div>
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="grid grid-flow-row gap-4 bg-transparent"
>
<StyledInput
{...register('displayName')}
id="displayName"
label="Name"
placeholder="Name"
fullWidth
autoFocus
inputProps={{ min: 2, max: 128 }}
error={!!formState.errors.email}
helperText={formState.errors.email?.message}
/>
<StyledInput
{...register('email')}
type="email"
id="email"
label="Email"
placeholder="Email"
fullWidth
inputProps={{ min: 2, max: 128 }}
error={!!formState.errors.email}
helperText={formState.errors.email?.message}
/>
<StyledInput
{...register('password')}
type="password"
id="password"
label="Password"
placeholder="Password"
fullWidth
inputProps={{ min: 2, max: 128 }}
error={!!formState.errors.password}
helperText={formState.errors.password?.message}
/>
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
options={{ theme: 'dark', size: 'flexible' }}
onSuccess={setTurnstileResponse}
/>
<Button
variant="outlined"
color="secondary"
className="hover:!bg-white hover:!bg-opacity-10 focus:ring-0"
size="large"
type="submit"
disabled={formState.isSubmitting}
loading={formState.isSubmitting}
>
Sign Up
</Button>
</Form>
</FormProvider>
{/* TODO: https://github.com/nhost/nhost/issues/3340 */}
<SignUpTabs />
{false && <SignUpWithEmailAndPasswordForm />}
<Divider className="!my-2" />
<Text color="secondary" className="text-center text-sm">
<p className="text-center text-sm text-[#A2B3BE]">
By signing up, you agree to our{' '}
<NavLink
<NextLink
href="https://nhost.io/legal/terms-of-service"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
className="font-semibold text-white"
>
Terms of Service
</NavLink>{' '}
</NextLink>{' '}
and{' '}
<NavLink
<NextLink
href="https://nhost.io/legal/privacy-policy"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
className="font-semibold text-white"
>
Privacy Policy
</NavLink>
</Text>
</Box>
</NextLink>
</p>
</div>
<Text color="secondary" className="text-center text-base lg:text-lg">
<p className="text-center text-base text-[#A2B3BE] lg:text-lg">
Already have an account?{' '}
<NavLink href="/signin" color="white" className="font-medium">
<NextLink href="/signin" className="font-medium text-white">
Sign In
</NavLink>
</Text>
</>
</NextLink>
</p>
</div>
);
}

View File

@@ -6307,9 +6307,7 @@ export enum AuthUserProviders_Constraint {
/** unique or primary key constraint on columns "id" */
UserProvidersPkey = 'user_providers_pkey',
/** unique or primary key constraint on columns "provider_user_id", "provider_id" */
UserProvidersProviderIdProviderUserIdKey = 'user_providers_provider_id_provider_user_id_key',
/** unique or primary key constraint on columns "user_id", "provider_id" */
UserProvidersUserIdProviderIdKey = 'user_providers_user_id_provider_id_key'
UserProvidersProviderIdProviderUserIdKey = 'user_providers_provider_id_provider_user_id_key'
}
/** input type for inserting data into table "auth.user_providers" */
@@ -21937,7 +21935,7 @@ export type Regions_Allowed_Organization_Bool_Exp = {
export enum Regions_Allowed_Organization_Constraint {
/** unique or primary key constraint on columns "id" */
RegionsAllowedOrganizationPkey = 'regions_allowed_organization_pkey',
/** unique or primary key constraint on columns "region_id", "organization_id" */
/** unique or primary key constraint on columns "organization_id", "region_id" */
RegionsAllowedOrganizationRegionIdOrganizationIdKey = 'regions_allowed_organization_region_id_organization_id_key'
}
@@ -26663,7 +26661,7 @@ export type WorkspaceMemberInvites_Bool_Exp = {
/** unique or primary key constraints on table "workspace_member_invites" */
export enum WorkspaceMemberInvites_Constraint {
/** unique or primary key constraint on columns "workspace_id", "email" */
/** unique or primary key constraint on columns "email", "workspace_id" */
WorkspaceMemberInvitesEmailWorkspaceIdKey = 'workspace_member_invites_email_workspace_id_key',
/** unique or primary key constraint on columns "id" */
WorkspaceMemberInvitesPkey = 'workspace_member_invites_pkey'
@@ -26946,7 +26944,7 @@ export type WorkspaceMembers_Bool_Exp = {
export enum WorkspaceMembers_Constraint {
/** unique or primary key constraint on columns "id" */
WorkspaceMembersPkey = 'workspace_members_pkey',
/** unique or primary key constraint on columns "workspace_id", "user_id" */
/** unique or primary key constraint on columns "user_id", "workspace_id" */
WorkspaceMembersUserIdWorkspaceIdKey = 'workspace_members_user_id_workspace_id_key'
}
@@ -27716,6 +27714,13 @@ export type DeleteUserAccountMutationVariables = Exact<{
export type DeleteUserAccountMutation = { __typename?: 'mutation_root', deleteUser?: { __typename: 'users' } | null };
export type GetActiveMfaTypeQueryVariables = Exact<{
id: Scalars['uuid'];
}>;
export type GetActiveMfaTypeQuery = { __typename?: 'query_root', user?: { __typename?: 'users', activeMfaType?: string | null } | null };
export type GetAuthUserProvidersQueryVariables = Exact<{ [key: string]: never; }>;
@@ -27726,6 +27731,20 @@ export type GetPersonalAccessTokensQueryVariables = Exact<{ [key: string]: never
export type GetPersonalAccessTokensQuery = { __typename?: 'query_root', personalAccessTokens: Array<{ __typename?: 'authRefreshTokens', id: any, metadata?: any | null, createdAt: any, expiresAt: any }> };
export type SecurityKeysQueryVariables = Exact<{
userId: Scalars['uuid'];
}>;
export type SecurityKeysQuery = { __typename?: 'query_root', authUserSecurityKeys: Array<{ __typename?: 'authUserSecurityKeys', id: any, nickname?: string | null }> };
export type RemoveSecurityKeyMutationVariables = Exact<{
id: Scalars['uuid'];
}>;
export type RemoveSecurityKeyMutation = { __typename?: 'mutation_root', deleteAuthUserSecurityKey?: { __typename?: 'authUserSecurityKeys', id: any } | null };
export type DeletePersonalAccessTokenMutationVariables = Exact<{
patId: Scalars['uuid'];
}>;
@@ -28999,6 +29018,44 @@ export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOp
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>;
export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const GetActiveMfaTypeDocument = gql`
query getActiveMfaType($id: uuid!) {
user(id: $id) {
activeMfaType
}
}
`;
/**
* __useGetActiveMfaTypeQuery__
*
* To run a query within a React component, call `useGetActiveMfaTypeQuery` and pass it any options that fit your needs.
* When your component renders, `useGetActiveMfaTypeQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetActiveMfaTypeQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useGetActiveMfaTypeQuery(baseOptions: Apollo.QueryHookOptions<GetActiveMfaTypeQuery, GetActiveMfaTypeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetActiveMfaTypeQuery, GetActiveMfaTypeQueryVariables>(GetActiveMfaTypeDocument, options);
}
export function useGetActiveMfaTypeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetActiveMfaTypeQuery, GetActiveMfaTypeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetActiveMfaTypeQuery, GetActiveMfaTypeQueryVariables>(GetActiveMfaTypeDocument, options);
}
export type GetActiveMfaTypeQueryHookResult = ReturnType<typeof useGetActiveMfaTypeQuery>;
export type GetActiveMfaTypeLazyQueryHookResult = ReturnType<typeof useGetActiveMfaTypeLazyQuery>;
export type GetActiveMfaTypeQueryResult = Apollo.QueryResult<GetActiveMfaTypeQuery, GetActiveMfaTypeQueryVariables>;
export function refetchGetActiveMfaTypeQuery(variables: GetActiveMfaTypeQueryVariables) {
return { query: GetActiveMfaTypeDocument, variables: variables }
}
export const GetAuthUserProvidersDocument = gql`
query getAuthUserProviders {
authUserProviders {
@@ -29080,6 +29137,78 @@ export type GetPersonalAccessTokensQueryResult = Apollo.QueryResult<GetPersonalA
export function refetchGetPersonalAccessTokensQuery(variables?: GetPersonalAccessTokensQueryVariables) {
return { query: GetPersonalAccessTokensDocument, variables: variables }
}
export const SecurityKeysDocument = gql`
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: {userId: {_eq: $userId}}) {
id
nickname
}
}
`;
/**
* __useSecurityKeysQuery__
*
* To run a query within a React component, call `useSecurityKeysQuery` and pass it any options that fit your needs.
* When your component renders, `useSecurityKeysQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useSecurityKeysQuery({
* variables: {
* userId: // value for 'userId'
* },
* });
*/
export function useSecurityKeysQuery(baseOptions: Apollo.QueryHookOptions<SecurityKeysQuery, SecurityKeysQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SecurityKeysQuery, SecurityKeysQueryVariables>(SecurityKeysDocument, options);
}
export function useSecurityKeysLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SecurityKeysQuery, SecurityKeysQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<SecurityKeysQuery, SecurityKeysQueryVariables>(SecurityKeysDocument, options);
}
export type SecurityKeysQueryHookResult = ReturnType<typeof useSecurityKeysQuery>;
export type SecurityKeysLazyQueryHookResult = ReturnType<typeof useSecurityKeysLazyQuery>;
export type SecurityKeysQueryResult = Apollo.QueryResult<SecurityKeysQuery, SecurityKeysQueryVariables>;
export function refetchSecurityKeysQuery(variables: SecurityKeysQueryVariables) {
return { query: SecurityKeysDocument, variables: variables }
}
export const RemoveSecurityKeyDocument = gql`
mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id
}
}
`;
export type RemoveSecurityKeyMutationFn = Apollo.MutationFunction<RemoveSecurityKeyMutation, RemoveSecurityKeyMutationVariables>;
/**
* __useRemoveSecurityKeyMutation__
*
* To run a mutation, you first call `useRemoveSecurityKeyMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemoveSecurityKeyMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [removeSecurityKeyMutation, { data, loading, error }] = useRemoveSecurityKeyMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useRemoveSecurityKeyMutation(baseOptions?: Apollo.MutationHookOptions<RemoveSecurityKeyMutation, RemoveSecurityKeyMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveSecurityKeyMutation, RemoveSecurityKeyMutationVariables>(RemoveSecurityKeyDocument, options);
}
export type RemoveSecurityKeyMutationHookResult = ReturnType<typeof useRemoveSecurityKeyMutation>;
export type RemoveSecurityKeyMutationResult = Apollo.MutationResult<RemoveSecurityKeyMutation>;
export type RemoveSecurityKeyMutationOptions = Apollo.BaseMutationOptions<RemoveSecurityKeyMutation, RemoveSecurityKeyMutationVariables>;
export const DeletePersonalAccessTokenDocument = gql`
mutation DeletePersonalAccessToken($patId: uuid!) {
deletePersonalAccessToken: deleteAuthRefreshToken(id: $patId) {

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