Compare commits

..

12 Commits

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


# Releases
## @nhost/dashboard@1.27.0

### Minor Changes

-   a7cd02c: fix: resolve rate limit query

## @nhost/docs@2.16.0

### Minor Changes

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

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


___

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



___



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


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

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


</details>


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

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


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

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



</details>


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

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

.changeset/smooth-bears-confess.md

- Added a changeset for the rate limit query fix.



</details>


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

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

___

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

---------

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-08-27 14:45:08 +01:00
David Barroso
3d70c63d1b feat (docs): added docs about rate-limits (#2812) 2024-08-27 15:17:03 +02:00
David Barroso
ba55c1b779 feat (docs): run: added a guide on using a private registry (#2843) 2024-08-27 12:36:09 +02:00
github-actions[bot]
852f13b273 chore: update versions (#2824)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@7.1.5

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost/react-apollo@12.0.5

### Patch Changes

-   @nhost/apollo@7.1.5
-   @nhost/react@3.5.5

## @nhost/react-urql@9.0.5

### Patch Changes

-   @nhost/react@3.5.5

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

### Patch Changes

- caa8bd7: fix: add error handling logic to transition to the signedOut
state when the token is invalid or expired

## @nhost/nextjs@2.1.19

### Patch Changes

-   @nhost/react@3.5.5

## @nhost/nhost-js@3.1.8

### Patch Changes

-   Updated dependencies [caa8bd7]
    -   @nhost/hasura-auth-js@2.5.5

## @nhost/react@3.5.5

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost/vue@2.6.5

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost/dashboard@1.26.0

### Minor Changes

-   3773ad7: chore: update pricing information
- b63250d: fix: not allow run service creation form resubmission while
creating a run service
-   a44a1d4: feat: add rate limits settings page

### Patch Changes

-   @nhost/react-apollo@12.0.5
-   @nhost/nextjs@2.1.19

## @nhost/docs@2.15.0

### Minor Changes

-   40c0d7b: │feat: added subdomain/region information
-   a18b545: feat: added postgres upgrade docs

## @nhost-examples/cli@0.3.10

### Patch Changes

-   @nhost/nhost-js@3.1.8

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5

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

### Patch Changes

-   @nhost/react@3.5.5

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-urql@9.0.5

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

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost-examples/nextjs@0.3.10

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5
-   @nhost/nextjs@2.1.19

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

### Patch Changes

-   @nhost/nhost-js@3.1.8

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

### Patch Changes

-   @nhost/nhost-js@3.1.8

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5

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

### Patch Changes

-   @nhost/react@3.5.5

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5

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

### Patch Changes

-   @nhost/nhost-js@3.1.8
-   @nhost/apollo@7.1.5
-   @nhost/vue@2.6.5

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

### Patch Changes

-   @nhost/apollo@7.1.5
-   @nhost/vue@2.6.5

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-27 10:48:43 +01:00
David BM
a44a1d48d6 feat (dashboard): change rate limits from dashboard (#2832) 2024-08-26 18:47:06 +02:00
David BM
b63250d1cb fix (dashboard): not allow run service creation form resubmission while creating a run service (#2838) 2024-08-23 15:20:27 +02:00
Hassan Ben Jobrane
caa8bd75ec fix(hasura-auth-js): transition to the signedOut state when the token is invalid or expired (#2835)
### **User description**
fixes https://github.com/nhost/nhost/issues/2817


___

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


___

### **Description**
- Added error handling logic to transition to the `signedOut` state when
the token is invalid or expired.
- Updated the authentication machine to handle 401 errors by signing out
the user.
- Enhanced test cases to verify the new behavior of signing out on
unauthorized errors.
- Updated Hasura page teardown logic to ensure the first matching
element is clicked.
- Added `micromatch` to the audit-ci allowlist for dependency
management.



___



### **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>machine.ts</strong><dd><code>Add error handling for
unauthorized token refresh</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

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

<li>Added error handling logic to transition to <code>signedOut</code>
state on <br>unauthorized error.<br> <li> Introduced a new condition
<code>isUnauthorizedError</code> to check for 401 <br>status.<br> <li>
Reordered imports for better organization.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>refreshToken.test.ts</strong><dd><code>Update token
refresh test for unauthorized error handling</code></dd></summary>
<hr>

packages/hasura-auth-js/tests/refreshToken.test.ts

<li>Updated test to expect sign out on unauthorized error during token
<br>refresh.<br> <li> Adjusted test logic to match new authentication
state transitions.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-271b5a8899ade50e4876f5a50f06da16954125f50d16f28219598cff4e39344b">+3/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>global-teardown.ts</strong><dd><code>Update Hasura
locator to click first matching element</code>&nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

dashboard/global-teardown.ts

<li>Updated locator to click the first matching element for Hasura page
<br>teardown.<br>


</details>


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

</tr>                    

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

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

- Added `isUnauthorizedError` to type definitions.



</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>silent-lies-smoke.md</strong><dd><code>Document bug fix
for invalid token handling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/silent-lies-smoke.md

<li>Documented the bug fix for transitioning to <code>signedOut</code>
state on invalid <br>token.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-f8d41906481f17db7208e2c154075e8679f222536c7958000e6f50f1f019aa01">+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
allowlist with micromatch</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

audit-ci.jsonc

- Added `micromatch` to the audit-ci allowlist.



</details>


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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-08-23 09:29:33 +01:00
David Barroso
40c0d7b914 feat (docs): added subdomain/region information (#2820) 2024-08-19 14:07:53 +02:00
David BM
3773ad7cca chore (dashboard): update pricing information (#2827)
Resolves #2822
2024-08-15 14:04:10 +02:00
Hassan Ben Jobrane
6f122521e9 fix: eval vulnerabilities (#2828)
### **PR Type**
enhancement, dependencies


___

### **Description**
- Removed `trim-newlines` from the `audit-ci` allowlist to address
potential vulnerabilities.
- Added `axios` version 1.7.4 to `package.json` resolutions to fix a
vulnerability.
- Updated `axios`, `@nhost/hasura-auth-js`, and `@nhost/nhost-js`
versions in `pnpm-lock.yaml` to ensure compatibility and security.



___



### **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>audit-ci.jsonc</strong><dd><code>Update audit-ci
allowlist configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

audit-ci.jsonc

- Removed `trim-newlines` from the allowlist.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2828/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>Add axios version to
package resolutions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

package.json

- Added `axios` version 1.7.4 to resolutions.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>pnpm-lock.yaml</strong><dd><code>Update dependencies in
pnpm-lock file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

pnpm-lock.yaml

<li>Updated <code>axios</code> to version 1.7.4.<br> <li> Updated
<code>@nhost/hasura-auth-js</code> to version 2.5.4.<br> <li> Updated
<code>@nhost/nhost-js</code> to version 3.1.7.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2828/files#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bb">+11/-10</a>&nbsp;
</td>

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-08-15 11:39:09 +01:00
David Barroso
a18b545d2a feat (docs): added postgres upgrade docs (#2823) 2024-08-13 10:42:08 +02:00
98 changed files with 2309 additions and 193 deletions

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": ["trim-newlines", "vue-template-compiler"]
"allowlist": ["vue-template-compiler", "micromatch"]
}

View File

@@ -1,5 +1,24 @@
# @nhost/dashboard
## 1.27.0
### Minor Changes
- a7cd02c: fix: resolve rate limit query
## 1.26.0
### Minor Changes
- 3773ad7: chore: update pricing information
- b63250d: fix: not allow run service creation form resubmission while creating a run service
- a44a1d4: feat: add rate limits settings page
### Patch Changes
- @nhost/react-apollo@12.0.5
- @nhost/nextjs@2.1.19
## 1.25.0
### Minor Changes

View File

@@ -43,7 +43,7 @@ async function globalTeardown() {
await adminSecretInput.press('Enter');
// note: getByRole doesn't work here
await hasuraPage.locator('a', { hasText: /data/i }).click();
await hasuraPage.locator('a', { hasText: /data/i }).nth(0).click();
await hasuraPage.locator('[data-test="sql-link"]').click();
// Set the value of the Ace code editor using JavaScript evaluation in the browser context

View File

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

View File

@@ -221,6 +221,13 @@ export default function SettingsSidebar({
>
Custom Domains
</SettingsNavLink>
<SettingsNavLink
href="/rate-limiting"
exact={false}
onClick={handleSelect}
>
Rate Limiting
</SettingsNavLink>
<SettingsNavLink href="/ai" exact={false} onClick={handleSelect}>
AI
</SettingsNavLink>

View File

@@ -2,8 +2,8 @@ import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { BaseDialog } from '@/components/ui/v2/Dialog';
import { Radio } from '@/components/ui/v2/Radio';
import { Text } from '@/components/ui/v2/Text';
import { useAppState } from '@/features/projects/common/hooks/useAppState';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
@@ -31,7 +31,7 @@ function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
>
<div className="grid grid-flow-row gap-y-0.5">
<div className="grid grid-flow-col items-center justify-start gap-2">
<Checkbox
<Radio
onChange={setPlan}
checked={selectedPlanId === planId}
aria-label={planName}
@@ -241,7 +241,21 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
))}
</div>
<div className="mt-2 grid grid-flow-row gap-2">
<div className="mt-0">
<Text variant="subtitle2" className="w-full px-1">
For a complete list of features, visit our{' '}
<a
href="https://nhost.io/pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
pricing page
</a>
</Text>
</div>
<div className="mt-6 grid grid-flow-row gap-2">
<Button
onClick={handleChangePlanClick}
disabled={!selectedPlan}

View File

@@ -1,6 +1,6 @@
const planDescriptions = {
Starter: '1 GB database, 5 GB of file storage, 10 GB of network traffic.',
Pro: '10 GB database, 25 GB of file storage, 50 GB of network traffic, and backups.',
Pro: '10 GB database, 50 GB of file storage, 50 GB of network traffic, and backups.',
Team: 'Reach out to us at support@nhost.io to have your private channel set up.',
};

View File

@@ -0,0 +1,303 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Divider } from '@/components/ui/v2/Divider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { rateLimitingItemValidationSchema } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useUpdateRateLimitConfigMutation } from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { RateLimitField } from 'features/projects/rate-limiting/settings/components/RateLimitField';
import { useGetRateLimits } from 'features/projects/rate-limiting/settings/hooks/useGetRateLimits';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
enabled: Yup.boolean().label('Enabled'),
bruteForce: rateLimitingItemValidationSchema,
emails: rateLimitingItemValidationSchema,
global: rateLimitingItemValidationSchema,
signups: rateLimitingItemValidationSchema,
sms: rateLimitingItemValidationSchema,
});
export type AuthLimitingFormValues = Yup.InferType<typeof validationSchema>;
export default function AuthLimitingForm() {
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
const [updateRateLimitConfig] = useUpdateRateLimitConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { authRateLimit, loading } = useGetRateLimits();
const {
bruteForce,
emails,
global,
signups,
sms,
enabled: authRateEnabled,
} = authRateLimit;
const {
limit: bruteForceLimit,
interval: bruteForceInterval,
intervalUnit: bruteForceIntervalUnit,
} = bruteForce;
const {
limit: emailsLimit,
interval: emailsInterval,
intervalUnit: emailsIntervalUnit,
} = emails;
const {
limit: globalLimit,
interval: globalInterval,
intervalUnit: globalIntervalUnit,
} = global;
const {
limit: signupsLimit,
interval: signupsInterval,
intervalUnit: signupsIntervalUnit,
} = signups;
const {
limit: smsLimit,
interval: smsInterval,
intervalUnit: smsIntervalUnit,
} = sms;
const form = useForm<AuthLimitingFormValues>({
defaultValues: {
enabled: authRateEnabled,
bruteForce: {
limit: bruteForceLimit,
interval: bruteForceInterval,
intervalUnit: bruteForceIntervalUnit,
},
emails: {
limit: emailsLimit,
interval: emailsInterval,
intervalUnit: emailsIntervalUnit,
},
global: {
limit: globalLimit,
interval: globalInterval,
intervalUnit: globalIntervalUnit,
},
signups: {
limit: signupsLimit,
interval: signupsInterval,
intervalUnit: signupsIntervalUnit,
},
sms: {
limit: smsLimit,
interval: smsInterval,
intervalUnit: smsIntervalUnit,
},
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && authRateEnabled) {
form.reset({
enabled: authRateEnabled,
bruteForce: {
limit: bruteForceLimit,
interval: bruteForceInterval,
intervalUnit: bruteForceIntervalUnit,
},
emails: {
limit: emailsLimit,
interval: emailsInterval,
intervalUnit: emailsIntervalUnit,
},
global: {
limit: globalLimit,
interval: globalInterval,
intervalUnit: globalIntervalUnit,
},
signups: {
limit: signupsLimit,
interval: signupsInterval,
intervalUnit: signupsIntervalUnit,
},
sms: {
limit: smsLimit,
interval: smsInterval,
intervalUnit: smsIntervalUnit,
},
});
}
}, [
loading,
form,
authRateEnabled,
bruteForceLimit,
bruteForceInterval,
bruteForceIntervalUnit,
emailsLimit,
emailsInterval,
emailsIntervalUnit,
globalLimit,
globalInterval,
globalIntervalUnit,
signupsLimit,
signupsInterval,
signupsIntervalUnit,
smsLimit,
smsInterval,
smsIntervalUnit,
]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading rate limits..."
className="justify-center"
/>
);
}
const {
register,
formState: { errors },
formState,
watch,
} = form;
const enabled = watch('enabled');
const handleSubmit = async (formValues: AuthLimitingFormValues) => {
const updateConfigPromise = updateRateLimitConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
rateLimit: formValues.enabled
? {
bruteForce: {
limit: formValues.bruteForce.limit,
interval: `${formValues.bruteForce.interval}${formValues.bruteForce.intervalUnit}`,
},
emails: {
limit: formValues.emails.limit,
interval: `${formValues.emails.interval}${formValues.emails.intervalUnit}`,
},
global: {
limit: formValues.global.limit,
interval: `${formValues.global.interval}${formValues.global.intervalUnit}`,
},
signups: {
limit: formValues.signups.limit,
interval: `${formValues.signups.interval}${formValues.signups.intervalUnit}`,
},
sms: {
limit: formValues.sms.limit,
interval: `${formValues.sms.interval}${formValues.sms.intervalUnit}`,
},
}
: null,
},
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating Auth rate limit settings...',
successMessage: 'Auth rate limit settings updated successfully',
errorMessage: 'Failed to update Auth rate limit settings',
},
);
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<SettingsContainer
title="Auth"
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col px-0"
>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.bruteForce}
id="bruteForce"
title="Brute Force"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.emails}
id="emails"
title="Emails"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.global}
id="global"
title="Global"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.signups}
id="signups"
title="Signups"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.sms}
id="sms"
title="SMS"
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,88 @@
import { ControlledSelect } from '@/components/form/ControlledSelect';
import { Box } from '@/components/ui/v2/Box';
import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text';
import { intervalUnitOptions } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import type {
FieldError,
FieldErrorsImpl,
Merge,
UseFormRegister,
} from 'react-hook-form';
interface RateLimitFieldProps {
register: UseFormRegister<any>;
errors: Merge<
FieldError,
FieldErrorsImpl<{
limit: number;
interval: number;
intervalUnit: string;
}>
>;
disabled?: boolean;
title?: string;
id: string;
}
export default function RateLimitField({
register,
disabled,
id,
errors,
title,
}: RateLimitFieldProps) {
return (
<Box className="px-4">
{title ? <Text className="py-4 font-semibold">{title}</Text> : null}
<div className="flex flex-col gap-8 lg:flex-row">
<div className="flex flex-row items-center gap-2">
<Text>Limit</Text>
<Input
{...register(`${id}.limit`)}
disabled={disabled}
id={`${id}.limit`}
type="number"
placeholder=""
className="max-w-60"
hideEmptyHelperText
error={!!errors?.limit}
helperText={errors?.limit?.message}
autoComplete="off"
/>
</div>
<div className="flex flex-row items-center gap-2">
<Text>Interval</Text>
<Input
{...register(`${id}.interval`)}
disabled={disabled}
id={`${id}.interval`}
type="number"
placeholder=""
hideEmptyHelperText
className="max-w-32"
error={!!errors?.interval}
helperText={errors?.interval?.message}
autoComplete="off"
/>
<ControlledSelect
{...register(`${id}.intervalUnit`)}
disabled={disabled}
variant="normal"
id={`${id}.intervalUnit`}
className="w-27"
defaultValue="m"
hideEmptyHelperText
>
{intervalUnitOptions.map(({ value, label }) => (
<Option key={`${id}.intervalUnit.${value}`} value={value}>
{label}
</Option>
))}
</ControlledSelect>
</div>
</div>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,169 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Divider } from '@/components/ui/v2/Divider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { rateLimitingItemValidationSchema } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
useUpdateRateLimitConfigMutation,
type ConfigConfigUpdateInput,
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { RateLimitField } from 'features/projects/rate-limiting/settings/components/RateLimitField';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
enabled: Yup.boolean().label('Enabled'),
rateLimit: rateLimitingItemValidationSchema,
});
export interface RateLimitDefaultValues {
enabled: boolean;
rateLimit: { limit: number; interval: number; intervalUnit: string };
}
export interface RateLimitingFormProps {
defaultValues: RateLimitDefaultValues;
serviceName: keyof ConfigConfigUpdateInput;
title: string;
loading: boolean;
}
export type RateLimitingFormValues = Yup.InferType<typeof validationSchema>;
export default function RateLimitingForm({
defaultValues,
serviceName,
title,
loading,
}: RateLimitingFormProps) {
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
const [updateRateLimitConfig] = useUpdateRateLimitConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<RateLimitingFormValues>({
defaultValues: defaultValues.enabled
? defaultValues
: {
enabled: false,
rateLimit: {
limit: 0,
interval: 0,
intervalUnit: 's',
},
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && defaultValues.enabled) {
form.reset(defaultValues);
}
}, [loading, defaultValues, form]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading rate limits..."
className="justify-center"
/>
);
}
const {
register,
formState: { errors },
formState,
watch,
} = form;
const enabled = watch('enabled');
const handleSubmit = async (formValues: RateLimitingFormValues) => {
const updateConfigPromise = updateRateLimitConfig({
variables: {
appId: currentProject.id,
config: {
[serviceName]: {
rateLimit: formValues.enabled
? {
limit: formValues.rateLimit.limit,
interval: `${formValues.rateLimit.interval}${formValues.rateLimit.intervalUnit}`,
}
: null,
},
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: `Updating ${title} rate limit settings...`,
successMessage: `${title} rate limit settings updated successfully`,
errorMessage: `Failed to update ${title} rate limit settings`,
},
);
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<SettingsContainer
title={title}
switchId="enabled"
showSwitch
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col px-0"
>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.rateLimit}
id="rateLimit"
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,194 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Divider } from '@/components/ui/v2/Divider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { rateLimitingItemValidationSchema } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useUpdateRunServiceConfigMutation } from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { RateLimitField } from 'features/projects/rate-limiting/settings/components/RateLimitField';
import type { UseGetRunServiceRateLimitsReturn } from 'features/projects/rate-limiting/settings/hooks/useGetRunServiceRateLimits/useGetRunServiceRateLimits';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
enabled: Yup.boolean().label('Enabled'),
ports: Yup.array().of(rateLimitingItemValidationSchema),
});
export type RunServiceLimitingFormValues = Yup.InferType<
typeof validationSchema
>;
export interface RunServiceLimitingFormProps {
title?: string;
serviceId?: string;
loading?: boolean;
enabledDefault?: boolean;
ports?: UseGetRunServiceRateLimitsReturn['services'][0]['ports'];
}
export default function RunServiceLimitingForm({
title,
serviceId,
ports,
loading,
enabledDefault,
}: RunServiceLimitingFormProps) {
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
const [updateRunServiceRateLimit] = useUpdateRunServiceConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<RunServiceLimitingFormValues>({
defaultValues: {
enabled: enabledDefault,
ports: [
...ports.map((port) => ({
limit: port?.rateLimit?.limit,
interval: port?.rateLimit?.interval,
intervalUnit: port?.rateLimit?.intervalUnit,
})),
],
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && enabledDefault) {
form.reset({
enabled: enabledDefault,
ports: [
...ports.map((port) => ({
limit: port?.rateLimit?.limit,
interval: port?.rateLimit?.interval,
intervalUnit: port?.rateLimit?.intervalUnit,
})),
],
});
}
}, [loading, enabledDefault, ports, form]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading rate limits..."
className="justify-center"
/>
);
}
const {
register,
formState: { errors },
formState,
watch,
} = form;
const enabled = watch('enabled');
const handleSubmit = async (formValues: RunServiceLimitingFormValues) => {
const updateConfigPromise = updateRunServiceRateLimit({
variables: {
appID: currentProject?.id,
serviceID: serviceId,
config: {
ports: ports.map((port, index) => {
const rateLimit = formValues.ports[index];
return {
...port,
rateLimit: enabled
? {
limit: rateLimit.limit,
interval: `${rateLimit.interval}${rateLimit.intervalUnit}`,
}
: null,
};
}),
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating Run service rate limit settings...',
successMessage: 'Run service rate limit settings updated successfully',
errorMessage: 'Failed to update Run service rate limit settings',
},
);
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<SettingsContainer
title={title}
switchId="enabled"
showSwitch
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col px-0"
>
<Divider />
{ports.map((port, index) => {
if (port?.type !== 'http' || !port?.publish) {
return null;
}
const fieldTitle = `${port.type} <-> ${port.port}`.toUpperCase();
const showDivider = index < ports.length - 1;
return (
<div key={`ports.${port.port}`}>
<RateLimitField
title={fieldTitle}
disabled={!enabled}
register={register}
errors={errors.ports}
id={`ports.${index}`}
/>
{showDivider && <Divider />}
</div>
);
})}
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,23 @@
import * as Yup from 'yup';
export const rateLimitingItemValidationSchema = Yup.object({
limit: Yup.number()
.required('Limit is required.')
.min(1)
.positive('Limit must be a positive number')
.typeError('Limit must be a number.'),
interval: Yup.number()
.required('Interval is required.')
.min(1)
.positive('Interval must be a positive number')
.typeError('Interval must be a number.'),
intervalUnit: Yup.string()
.required('Interval unit is required.')
.oneOf(['s', 'm', 'h']),
});
export const intervalUnitOptions = [
{ value: 's', label: 'seconds' },
{ value: 'm', label: 'minutes' },
{ value: 'h', label: 'hours' },
];

View File

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

View File

@@ -0,0 +1,120 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { useGetRateLimitConfigQuery } from '@/utils/__generated__/graphql';
import { DEFAULT_RATE_LIMITS } from 'features/projects/rate-limiting/settings/utils/constants';
import { parseIntervalNameUnit } from 'features/projects/rate-limiting/settings/utils/parseIntervalNameUnit';
export default function useGetRateLimits() {
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { data, loading } = useGetRateLimitConfigQuery({
variables: {
appId: currentProject?.id,
resolve: true,
},
skip: !currentProject,
...(!isPlatform ? { client: localMimirClient } : {}),
});
const authRateLimit = data?.config?.auth?.rateLimit;
const hasuraRateLimit = data?.config?.hasura?.rateLimit;
const storageRateLimit = data?.config?.storage?.rateLimit;
const functionsRateLimit = data?.config?.functions?.rateLimit;
const { bruteForce, emails, global, signups, sms } = authRateLimit || {};
const { limit: bruteForceLimit, interval: bruteForceIntervalStr } =
bruteForce || {};
const { interval: bruteForceInterval, intervalUnit: bruteForceIntervalUnit } =
parseIntervalNameUnit(bruteForceIntervalStr);
const { limit: emailsLimit, interval: emailsIntervalStr } = emails || {};
const { interval: emailsInterval, intervalUnit: emailsIntervalUnit } =
parseIntervalNameUnit(emailsIntervalStr);
const { limit: globalLimit, interval: globalIntervalStr } = global || {};
const { interval: globalInterval, intervalUnit: globalIntervalUnit } =
parseIntervalNameUnit(globalIntervalStr);
const { limit: signupsLimit, interval: signupsIntervalStr } = signups || {};
const { interval: signupsInterval, intervalUnit: signupsIntervalUnit } =
parseIntervalNameUnit(signupsIntervalStr);
const { limit: smsLimit, interval: smsIntervalStr } = sms || {};
const { interval: smsInterval, intervalUnit: smsIntervalUnit } =
parseIntervalNameUnit(smsIntervalStr);
const { limit: hasuraLimit, interval: hasuraIntervalStr } =
hasuraRateLimit || {};
const { interval: hasuraInterval, intervalUnit: hasuraIntervalUnit } =
parseIntervalNameUnit(hasuraIntervalStr);
const { limit: storageLimit, interval: storageIntervalStr } =
storageRateLimit || {};
const { interval: storageInterval, intervalUnit: storageIntervalUnit } =
parseIntervalNameUnit(storageIntervalStr);
const { limit: functionsLimit, interval: functionsIntervalStr } =
functionsRateLimit || {};
const { interval: functionsInterval, intervalUnit: functionsIntervalUnit } =
parseIntervalNameUnit(functionsIntervalStr);
return {
authRateLimit: {
enabled: !!authRateLimit,
bruteForce: {
limit: bruteForceLimit || DEFAULT_RATE_LIMITS.limit,
interval: bruteForceInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit:
bruteForceIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
emails: {
limit: emailsLimit || DEFAULT_RATE_LIMITS.limit,
interval: emailsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: emailsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
global: {
limit: globalLimit || DEFAULT_RATE_LIMITS.limit,
interval: globalInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: globalIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
signups: {
limit: signupsLimit || DEFAULT_RATE_LIMITS.limit,
interval: signupsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: signupsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
sms: {
limit: smsLimit || DEFAULT_RATE_LIMITS.limit,
interval: smsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: smsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
hasuraDefaultValues: {
enabled: !!hasuraRateLimit,
rateLimit: {
limit: hasuraLimit || DEFAULT_RATE_LIMITS.limit,
interval: hasuraInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: hasuraIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
storageDefaultValues: {
enabled: !!storageRateLimit,
rateLimit: {
limit: storageLimit || DEFAULT_RATE_LIMITS.limit,
interval: storageInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: storageIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
functionsDefaultValues: {
enabled: !!functionsRateLimit,
rateLimit: {
limit: functionsLimit || DEFAULT_RATE_LIMITS.limit,
interval: functionsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: functionsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
loading,
};
}

View File

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

View File

@@ -0,0 +1,107 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import {
useGetLocalRunServiceRateLimitQuery,
useGetRunServicesRateLimitQuery,
type GetRunServicesRateLimitQuery,
} from '@/utils/__generated__/graphql';
import { DEFAULT_RATE_LIMITS } from 'features/projects/rate-limiting/settings/utils/constants';
import { parseIntervalNameUnit } from 'features/projects/rate-limiting/settings/utils/parseIntervalNameUnit';
import { useMemo } from 'react';
type RunService = Pick<
GetRunServicesRateLimitQuery['app']['runServices'][0],
'config'
> & {
id?: string;
serviceID?: string;
createdAt?: string;
updatedAt?: string;
subdomain?: string;
};
export interface UseGetRunServiceRateLimitsReturn {
services: {
name?: string;
id?: string;
enabled?: boolean;
ports?: {
type?: string;
port?: string;
publish?: boolean;
rateLimit?: {
limit?: number;
interval?: number;
intervalUnit?: string;
};
}[];
}[];
loading: boolean;
}
export default function useGetRunServiceRateLimits(): UseGetRunServiceRateLimitsReturn {
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { data, loading: loadingPlatformServices } =
useGetRunServicesRateLimitQuery({
variables: {
appID: currentProject?.id,
resolve: false,
},
skip: !isPlatform,
});
const { loading: loadingLocalServices, data: localServicesData } =
useGetLocalRunServiceRateLimitQuery({
variables: { appID: currentProject?.id, resolve: false },
skip: isPlatform,
client: localMimirClient,
});
const platformServices = useMemo(
() => data?.app?.runServices.map((service) => service) ?? [],
[data],
);
const localServices = useMemo(
() => localServicesData?.runServiceConfigs.map((service) => service) ?? [],
[localServicesData],
);
const services: RunService[] = isPlatform ? platformServices : localServices;
const loading = isPlatform ? loadingPlatformServices : loadingLocalServices;
const servicesInfo = services.map((service) => {
const enabled = service?.config?.ports?.some(
(port) => port?.rateLimit && port?.type === 'http' && port?.publish,
);
const ports = service?.config?.ports?.map((port) => {
const { interval, intervalUnit } = parseIntervalNameUnit(
port?.rateLimit?.interval,
);
const rateLimit = {
limit: port?.rateLimit?.limit || DEFAULT_RATE_LIMITS.limit,
interval: interval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: intervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
};
return {
type: port?.type,
publish: port?.publish,
port: port?.port,
rateLimit,
};
});
return {
enabled,
name: service.config?.name,
id: service.id ?? service.serviceID,
ports,
};
});
return { services: servicesInfo, loading };
}

View File

@@ -0,0 +1,5 @@
export const DEFAULT_RATE_LIMITS = {
limit: 1000,
interval: 5,
intervalUnit: 'm',
};

View File

@@ -0,0 +1 @@
export * from './constants';

View File

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

View File

@@ -0,0 +1,18 @@
export default function parseIntervalNameUnit(interval: string) {
if (!interval) {
return {};
}
const regex = /^(\d+)([a-zA-Z])$/;
const match = interval.match(regex);
if (!match) {
return {};
}
const [, intervalValue, intervalUnit] = match;
return {
interval: parseInt(intervalValue, 10),
intervalUnit,
};
}

View File

@@ -102,6 +102,9 @@ export default function ServiceForm({
const getFormattedConfig = (values: ServiceFormValues) => {
// Remove any __typename property from the values
const sanitizedValues = removeTypename(values) as ServiceFormValues;
const sanitizedInitialDataPorts = initialData?.ports
? removeTypename(initialData.ports)
: [];
const config: ConfigRunServiceConfigInsertInput = {
name: sanitizedValues.name,
@@ -130,6 +133,10 @@ export default function ServiceForm({
type: item.type,
publish: item.publish,
ingresses: item.ingresses,
rateLimit:
sanitizedInitialDataPorts.find(
(port) => port.port === item.port && port.type === item.type,
)?.rateLimit ?? null,
})),
healthCheck: sanitizedValues.healthCheck
? {
@@ -309,7 +316,7 @@ export default function ServiceForm({
<Tooltip title="Name of the service, must be unique per project.">
<InfoIcon
aria-label="Info"
className="w-4 h-4"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
@@ -349,7 +356,7 @@ export default function ServiceForm({
>
<InfoIcon
aria-label="Info"
className="w-4 h-4"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
@@ -380,7 +387,7 @@ export default function ServiceForm({
<Tooltip title="Command to run when to start the service. This is optional as the image may already have a baked-in command.">
<InfoIcon
aria-label="Info"
className="w-4 h-4"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
@@ -428,7 +435,7 @@ export default function ServiceForm({
{createServiceFormError && (
<Alert
severity="error"
className="grid items-center justify-between grid-flow-col px-4 py-3"
className="grid grid-flow-col items-center justify-between px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {createServiceFormError.message}

View File

@@ -69,7 +69,16 @@ export interface ServiceFormProps extends DialogFormProps {
/**
* if there is initialData then it's an update operation
*/
initialData?: ServiceFormValues & { subdomain?: string }; // subdomain is only set on the backend
initialData?: Omit<ServiceFormValues, 'ports'> & {
subdomain?: string;
ports: {
port: number;
type: PortTypes;
publish: boolean;
ingresses?: { fqdn?: string[] }[] | null;
rateLimit?: { limit: number; interval: string } | null;
}[];
}; // subdomain is only set on the backend
/**
* Function to be called when the operation is cancelled.

View File

@@ -7,6 +7,7 @@ import { Tooltip } from '@/components/ui/v2/Tooltip';
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import type { ServiceFormValues } from '@/features/services/components/ServiceForm/ServiceFormTypes';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { useState } from 'react';
export interface ServiceConfirmationDialogProps {
/**
@@ -28,10 +29,21 @@ export default function ServiceConfirmationDialog({
onCancel,
onSubmit,
}: ServiceConfirmationDialogProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const approximatePriceForService = parseFloat(
(formValues.compute.cpu * formValues.replicas * COST_PER_VCPU).toFixed(2),
);
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await onSubmit();
} finally {
setIsSubmitting(false);
}
};
return (
<div className="grid grid-flow-row gap-6 px-6 pb-6">
<Box className="grid grid-flow-row gap-4">
@@ -74,7 +86,12 @@ export default function ServiceConfirmationDialog({
</Box>
<Box className="grid grid-flow-row gap-2">
<Button color="primary" onClick={onSubmit} autoFocus>
<Button
loading={isSubmitting}
color="primary"
onClick={handleSubmit}
autoFocus
>
Confirm
</Button>

View File

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

View File

@@ -0,0 +1,46 @@
query getRateLimitConfig($appId: uuid!, $resolve: Boolean!) {
config(appID: $appId, resolve: $resolve) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}

View File

@@ -0,0 +1,49 @@
mutation UpdateRateLimitConfig(
$appId: uuid!
$config: ConfigConfigUpdateInput!
) {
updateConfig(appID: $appId, config: $config) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}

View File

@@ -27,6 +27,10 @@ fragment RunServiceConfig on ConfigRunServiceConfig {
ingresses {
fqdn
}
rateLimit {
limit
interval
}
}
healthCheck {
port

View File

@@ -0,0 +1,38 @@
fragment RunServiceRateLimit on ConfigRunServiceConfig {
name
ports {
port
type
publish
rateLimit {
limit
interval
}
ingresses {
fqdn
}
}
}
query getRunServicesRateLimit($appID: uuid!, $resolve: Boolean!) {
app(id: $appID) {
runServices {
id
createdAt
updatedAt
subdomain
config(resolve: $resolve) {
...RunServiceRateLimit
}
}
}
}
query getLocalRunServiceRateLimit($appID: uuid!, $resolve: Boolean!) {
runServiceConfigs(appID: $appID, resolve: $resolve) {
serviceID
config {
...RunServiceRateLimit
}
}
}

View File

@@ -0,0 +1,90 @@
import { Container } from '@/components/layout/Container';
import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { Box } from '@/components/ui/v2/Box';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { AuthLimitingForm } from '@/features/projects/rate-limiting/settings/components/AuthLimitingForm';
import { RateLimitingForm } from '@/features/projects/rate-limiting/settings/components/RateLimitingForm';
import { RunServiceLimitingForm } from '@/features/projects/rate-limiting/settings/components/RunServiceLimitingForm';
import { useGetRateLimits } from '@/features/projects/rate-limiting/settings/hooks/useGetRateLimits';
import { useGetRunServiceRateLimits } from '@/features/projects/rate-limiting/settings/hooks/useGetRunServiceRateLimits';
import { type ReactElement } from 'react';
export default function RateLimiting() {
const { services, loading } = useGetRunServiceRateLimits();
const {
hasuraDefaultValues,
functionsDefaultValues,
storageDefaultValues,
loading: loadingBaseServices,
} = useGetRateLimits();
return (
<Container
className="grid max-w-5xl grid-flow-row gap-6 bg-transparent"
rootClassName="bg-transparent"
>
<Box className="flex flex-row items-center gap-4 overflow-hidden rounded-lg border-1 p-4">
<div className="flex flex-col space-y-2">
<Text className="text-lg font-semibold">Rate Limiting</Text>
<Text color="secondary">
Learn more about
<Link
href="https://docs.nhost.io/platform/rate-limits"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="ml-1 font-medium"
>
Rate Limiting
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</Text>
</div>
</Box>
<AuthLimitingForm />
<RateLimitingForm
defaultValues={hasuraDefaultValues}
loading={loadingBaseServices}
serviceName="hasura"
title="Hasura"
/>
<RateLimitingForm
defaultValues={storageDefaultValues}
loading={loadingBaseServices}
serviceName="storage"
title="Storage"
/>
<RateLimitingForm
defaultValues={functionsDefaultValues}
loading={loadingBaseServices}
serviceName="functions"
title="Functions"
/>
{services?.map((service) => {
if (
service?.ports?.some((port) => port?.type === 'http' && port?.publish)
) {
return (
<RunServiceLimitingForm
enabledDefault={service.enabled}
key={service.id}
title={service.name}
serviceId={service.id}
ports={service.ports}
loading={loading}
/>
);
}
return null;
})}
</Container>
);
}
RateLimiting.getLayout = function getLayout(page: ReactElement) {
return <SettingsLayout>{page}</SettingsLayout>;
};

View File

@@ -199,6 +199,7 @@ export type ConfigAuth = {
__typename?: 'ConfigAuth';
elevatedPrivileges?: Maybe<ConfigAuthElevatedPrivileges>;
method?: Maybe<ConfigAuthMethod>;
rateLimit?: Maybe<ConfigAuthRateLimit>;
redirections?: Maybe<ConfigAuthRedirections>;
/** Resources for the service */
resources?: Maybe<ConfigResources>;
@@ -223,6 +224,7 @@ export type ConfigAuthComparisonExp = {
_or?: InputMaybe<Array<ConfigAuthComparisonExp>>;
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesComparisonExp>;
method?: InputMaybe<ConfigAuthMethodComparisonExp>;
rateLimit?: InputMaybe<ConfigAuthRateLimitComparisonExp>;
redirections?: InputMaybe<ConfigAuthRedirectionsComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>;
session?: InputMaybe<ConfigAuthSessionComparisonExp>;
@@ -255,6 +257,7 @@ export type ConfigAuthElevatedPrivilegesUpdateInput = {
export type ConfigAuthInsertInput = {
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesInsertInput>;
method?: InputMaybe<ConfigAuthMethodInsertInput>;
rateLimit?: InputMaybe<ConfigAuthRateLimitInsertInput>;
redirections?: InputMaybe<ConfigAuthRedirectionsInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>;
session?: InputMaybe<ConfigAuthSessionInsertInput>;
@@ -684,6 +687,42 @@ export type ConfigAuthMethodWebauthnUpdateInput = {
relyingParty?: InputMaybe<ConfigAuthMethodWebauthnRelyingPartyUpdateInput>;
};
export type ConfigAuthRateLimit = {
__typename?: 'ConfigAuthRateLimit';
bruteForce?: Maybe<ConfigRateLimit>;
emails?: Maybe<ConfigRateLimit>;
global?: Maybe<ConfigRateLimit>;
signups?: Maybe<ConfigRateLimit>;
sms?: Maybe<ConfigRateLimit>;
};
export type ConfigAuthRateLimitComparisonExp = {
_and?: InputMaybe<Array<ConfigAuthRateLimitComparisonExp>>;
_not?: InputMaybe<ConfigAuthRateLimitComparisonExp>;
_or?: InputMaybe<Array<ConfigAuthRateLimitComparisonExp>>;
bruteForce?: InputMaybe<ConfigRateLimitComparisonExp>;
emails?: InputMaybe<ConfigRateLimitComparisonExp>;
global?: InputMaybe<ConfigRateLimitComparisonExp>;
signups?: InputMaybe<ConfigRateLimitComparisonExp>;
sms?: InputMaybe<ConfigRateLimitComparisonExp>;
};
export type ConfigAuthRateLimitInsertInput = {
bruteForce?: InputMaybe<ConfigRateLimitInsertInput>;
emails?: InputMaybe<ConfigRateLimitInsertInput>;
global?: InputMaybe<ConfigRateLimitInsertInput>;
signups?: InputMaybe<ConfigRateLimitInsertInput>;
sms?: InputMaybe<ConfigRateLimitInsertInput>;
};
export type ConfigAuthRateLimitUpdateInput = {
bruteForce?: InputMaybe<ConfigRateLimitUpdateInput>;
emails?: InputMaybe<ConfigRateLimitUpdateInput>;
global?: InputMaybe<ConfigRateLimitUpdateInput>;
signups?: InputMaybe<ConfigRateLimitUpdateInput>;
sms?: InputMaybe<ConfigRateLimitUpdateInput>;
};
export type ConfigAuthRedirections = {
__typename?: 'ConfigAuthRedirections';
/** AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS */
@@ -834,6 +873,7 @@ export type ConfigAuthTotpUpdateInput = {
export type ConfigAuthUpdateInput = {
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesUpdateInput>;
method?: InputMaybe<ConfigAuthMethodUpdateInput>;
rateLimit?: InputMaybe<ConfigAuthRateLimitUpdateInput>;
redirections?: InputMaybe<ConfigAuthRedirectionsUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>;
session?: InputMaybe<ConfigAuthSessionUpdateInput>;
@@ -1233,6 +1273,7 @@ export type ConfigFloatComparisonExp = {
export type ConfigFunctions = {
__typename?: 'ConfigFunctions';
node?: Maybe<ConfigFunctionsNode>;
rateLimit?: Maybe<ConfigRateLimit>;
resources?: Maybe<ConfigFunctionsResources>;
};
@@ -1241,11 +1282,13 @@ export type ConfigFunctionsComparisonExp = {
_not?: InputMaybe<ConfigFunctionsComparisonExp>;
_or?: InputMaybe<Array<ConfigFunctionsComparisonExp>>;
node?: InputMaybe<ConfigFunctionsNodeComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
resources?: InputMaybe<ConfigFunctionsResourcesComparisonExp>;
};
export type ConfigFunctionsInsertInput = {
node?: InputMaybe<ConfigFunctionsNodeInsertInput>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
resources?: InputMaybe<ConfigFunctionsResourcesInsertInput>;
};
@@ -1291,6 +1334,7 @@ export type ConfigFunctionsResourcesUpdateInput = {
export type ConfigFunctionsUpdateInput = {
node?: InputMaybe<ConfigFunctionsNodeUpdateInput>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
resources?: InputMaybe<ConfigFunctionsResourcesUpdateInput>;
};
@@ -1415,6 +1459,7 @@ export type ConfigHasura = {
/** JWT Secrets configuration */
jwtSecrets?: Maybe<Array<ConfigJwtSecret>>;
logs?: Maybe<ConfigHasuraLogs>;
rateLimit?: Maybe<ConfigRateLimit>;
/** Resources for the service */
resources?: Maybe<ConfigResources>;
/**
@@ -1477,6 +1522,7 @@ export type ConfigHasuraComparisonExp = {
events?: InputMaybe<ConfigHasuraEventsComparisonExp>;
jwtSecrets?: InputMaybe<ConfigJwtSecretComparisonExp>;
logs?: InputMaybe<ConfigHasuraLogsComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>;
settings?: InputMaybe<ConfigHasuraSettingsComparisonExp>;
version?: InputMaybe<ConfigStringComparisonExp>;
@@ -1510,6 +1556,7 @@ export type ConfigHasuraInsertInput = {
events?: InputMaybe<ConfigHasuraEventsInsertInput>;
jwtSecrets?: InputMaybe<Array<ConfigJwtSecretInsertInput>>;
logs?: InputMaybe<ConfigHasuraLogsInsertInput>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>;
settings?: InputMaybe<ConfigHasuraSettingsInsertInput>;
version?: InputMaybe<Scalars['String']>;
@@ -1602,6 +1649,7 @@ export type ConfigHasuraUpdateInput = {
events?: InputMaybe<ConfigHasuraEventsUpdateInput>;
jwtSecrets?: InputMaybe<Array<ConfigJwtSecretUpdateInput>>;
logs?: InputMaybe<ConfigHasuraLogsUpdateInput>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>;
settings?: InputMaybe<ConfigHasuraSettingsUpdateInput>;
version?: InputMaybe<Scalars['String']>;
@@ -2013,6 +2061,30 @@ export type ConfigProviderUpdateInput = {
smtp?: InputMaybe<ConfigSmtpUpdateInput>;
};
export type ConfigRateLimit = {
__typename?: 'ConfigRateLimit';
interval: Scalars['String'];
limit: Scalars['ConfigUint32'];
};
export type ConfigRateLimitComparisonExp = {
_and?: InputMaybe<Array<ConfigRateLimitComparisonExp>>;
_not?: InputMaybe<ConfigRateLimitComparisonExp>;
_or?: InputMaybe<Array<ConfigRateLimitComparisonExp>>;
interval?: InputMaybe<ConfigStringComparisonExp>;
limit?: InputMaybe<ConfigUint32ComparisonExp>;
};
export type ConfigRateLimitInsertInput = {
interval: Scalars['String'];
limit: Scalars['ConfigUint32'];
};
export type ConfigRateLimitUpdateInput = {
interval?: InputMaybe<Scalars['String']>;
limit?: InputMaybe<Scalars['ConfigUint32']>;
};
/** Resource configuration for a service */
export type ConfigResources = {
__typename?: 'ConfigResources';
@@ -2155,6 +2227,7 @@ export type ConfigRunServicePort = {
ingresses?: Maybe<Array<ConfigIngress>>;
port: Scalars['ConfigPort'];
publish?: Maybe<Scalars['Boolean']>;
rateLimit?: Maybe<ConfigRateLimit>;
type: Scalars['String'];
};
@@ -2165,6 +2238,7 @@ export type ConfigRunServicePortComparisonExp = {
ingresses?: InputMaybe<ConfigIngressComparisonExp>;
port?: InputMaybe<ConfigPortComparisonExp>;
publish?: InputMaybe<ConfigBooleanComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
type?: InputMaybe<ConfigStringComparisonExp>;
};
@@ -2172,6 +2246,7 @@ export type ConfigRunServicePortInsertInput = {
ingresses?: InputMaybe<Array<ConfigIngressInsertInput>>;
port: Scalars['ConfigPort'];
publish?: InputMaybe<Scalars['Boolean']>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
type: Scalars['String'];
};
@@ -2179,6 +2254,7 @@ export type ConfigRunServicePortUpdateInput = {
ingresses?: InputMaybe<Array<ConfigIngressUpdateInput>>;
port?: InputMaybe<Scalars['ConfigPort']>;
publish?: InputMaybe<Scalars['Boolean']>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
type?: InputMaybe<Scalars['String']>;
};
@@ -2386,6 +2462,7 @@ export type ConfigStandardOauthProviderWithScopeUpdateInput = {
export type ConfigStorage = {
__typename?: 'ConfigStorage';
antivirus?: Maybe<ConfigStorageAntivirus>;
rateLimit?: Maybe<ConfigRateLimit>;
/**
* Networking (custom domains at the moment) are not allowed as we need to do further
* configurations in the CDN. We will enable it again in the future.
@@ -2427,18 +2504,21 @@ export type ConfigStorageComparisonExp = {
_not?: InputMaybe<ConfigStorageComparisonExp>;
_or?: InputMaybe<Array<ConfigStorageComparisonExp>>;
antivirus?: InputMaybe<ConfigStorageAntivirusComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>;
version?: InputMaybe<ConfigStringComparisonExp>;
};
export type ConfigStorageInsertInput = {
antivirus?: InputMaybe<ConfigStorageAntivirusInsertInput>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>;
version?: InputMaybe<Scalars['String']>;
};
export type ConfigStorageUpdateInput = {
antivirus?: InputMaybe<ConfigStorageAntivirusUpdateInput>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>;
version?: InputMaybe<Scalars['String']>;
};
@@ -23010,6 +23090,22 @@ export type GetConfigRawJsonQueryVariables = Exact<{
export type GetConfigRawJsonQuery = { __typename?: 'query_root', configRawJSON: string };
export type GetRateLimitConfigQueryVariables = Exact<{
appId: Scalars['uuid'];
resolve: Scalars['Boolean'];
}>;
export type GetRateLimitConfigQuery = { __typename?: 'query_root', config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }, storage?: { __typename?: 'ConfigStorage', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, functions?: { __typename?: 'ConfigFunctions', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, auth?: { __typename?: 'ConfigAuth', rateLimit?: { __typename?: 'ConfigAuthRateLimit', bruteForce?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, emails?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, global?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, signups?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, sms?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null } | null } | null };
export type UpdateRateLimitConfigMutationVariables = Exact<{
appId: Scalars['uuid'];
config: ConfigConfigUpdateInput;
}>;
export type UpdateRateLimitConfigMutation = { __typename?: 'mutation_root', updateConfig: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }, storage?: { __typename?: 'ConfigStorage', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, functions?: { __typename?: 'ConfigFunctions', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, auth?: { __typename?: 'ConfigAuth', rateLimit?: { __typename?: 'ConfigAuthRateLimit', bruteForce?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, emails?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, global?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, signups?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, sms?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null } | null } };
export type ReplaceConfigRawJsonMutationVariables = Exact<{
appID: Scalars['uuid'];
rawJSON: Scalars['String'];
@@ -23393,7 +23489,7 @@ export type GetRunServiceQueryVariables = Exact<{
export type GetRunServiceQuery = { __typename?: 'query_root', runService?: { __typename?: 'run_service', id: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null }> | null } | null } | null };
export type RunServiceConfigFragment = { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null };
export type RunServiceConfigFragment = { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null };
export type GetRunServicesQueryVariables = Exact<{
appID: Scalars['uuid'];
@@ -23403,7 +23499,7 @@ export type GetRunServicesQueryVariables = Exact<{
}>;
export type GetRunServicesQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', runServices: Array<{ __typename?: 'run_service', id: any, createdAt: any, updatedAt: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } | null }>, runServices_aggregate: { __typename?: 'run_service_aggregate', aggregate?: { __typename?: 'run_service_aggregate_fields', count: number } | null } } | null };
export type GetRunServicesQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', runServices: Array<{ __typename?: 'run_service', id: any, createdAt: any, updatedAt: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } | null }>, runServices_aggregate: { __typename?: 'run_service_aggregate', aggregate?: { __typename?: 'run_service_aggregate_fields', count: number } | null } } | null };
export type GetLocalRunServiceConfigsQueryVariables = Exact<{
appID: Scalars['uuid'];
@@ -23411,7 +23507,25 @@ export type GetLocalRunServiceConfigsQueryVariables = Exact<{
}>;
export type GetLocalRunServiceConfigsQuery = { __typename?: 'query_root', runServiceConfigs: Array<{ __typename?: 'ConfigRunServiceConfigWithID', serviceID: any, config: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } }> };
export type GetLocalRunServiceConfigsQuery = { __typename?: 'query_root', runServiceConfigs: Array<{ __typename?: 'ConfigRunServiceConfigWithID', serviceID: any, config: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } }> };
export type RunServiceRateLimitFragment = { __typename?: 'ConfigRunServiceConfig', name: any, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null };
export type GetRunServicesRateLimitQueryVariables = Exact<{
appID: Scalars['uuid'];
resolve: Scalars['Boolean'];
}>;
export type GetRunServicesRateLimitQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', runServices: Array<{ __typename?: 'run_service', id: any, createdAt: any, updatedAt: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null } | null }> } | null };
export type GetLocalRunServiceRateLimitQueryVariables = Exact<{
appID: Scalars['uuid'];
resolve: Scalars['Boolean'];
}>;
export type GetLocalRunServiceRateLimitQuery = { __typename?: 'query_root', runServiceConfigs: Array<{ __typename?: 'ConfigRunServiceConfigWithID', serviceID: any, config: { __typename?: 'ConfigRunServiceConfig', name: any, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null } }> };
export type InsertRunServiceMutationVariables = Exact<{
object: Run_Service_Insert_Input;
@@ -23874,6 +23988,10 @@ export const RunServiceConfigFragmentDoc = gql`
ingresses {
fqdn
}
rateLimit {
limit
interval
}
}
healthCheck {
port
@@ -23882,6 +24000,23 @@ export const RunServiceConfigFragmentDoc = gql`
}
}
`;
export const RunServiceRateLimitFragmentDoc = gql`
fragment RunServiceRateLimit on ConfigRunServiceConfig {
name
ports {
port
type
publish
rateLimit {
limit
interval
}
ingresses {
fqdn
}
}
}
`;
export const GetWorkspaceMembersWorkspaceMemberFragmentDoc = gql`
fragment getWorkspaceMembersWorkspaceMember on workspaceMembers {
id
@@ -25371,6 +25506,161 @@ export type GetConfigRawJsonQueryResult = Apollo.QueryResult<GetConfigRawJsonQue
export function refetchGetConfigRawJsonQuery(variables: GetConfigRawJsonQueryVariables) {
return { query: GetConfigRawJsonDocument, variables: variables }
}
export const GetRateLimitConfigDocument = gql`
query getRateLimitConfig($appId: uuid!, $resolve: Boolean!) {
config(appID: $appId, resolve: $resolve) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}
`;
/**
* __useGetRateLimitConfigQuery__
*
* To run a query within a React component, call `useGetRateLimitConfigQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRateLimitConfigQuery` 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 } = useGetRateLimitConfigQuery({
* variables: {
* appId: // value for 'appId'
* resolve: // value for 'resolve'
* },
* });
*/
export function useGetRateLimitConfigQuery(baseOptions: Apollo.QueryHookOptions<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>(GetRateLimitConfigDocument, options);
}
export function useGetRateLimitConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>(GetRateLimitConfigDocument, options);
}
export type GetRateLimitConfigQueryHookResult = ReturnType<typeof useGetRateLimitConfigQuery>;
export type GetRateLimitConfigLazyQueryHookResult = ReturnType<typeof useGetRateLimitConfigLazyQuery>;
export type GetRateLimitConfigQueryResult = Apollo.QueryResult<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>;
export function refetchGetRateLimitConfigQuery(variables: GetRateLimitConfigQueryVariables) {
return { query: GetRateLimitConfigDocument, variables: variables }
}
export const UpdateRateLimitConfigDocument = gql`
mutation UpdateRateLimitConfig($appId: uuid!, $config: ConfigConfigUpdateInput!) {
updateConfig(appID: $appId, config: $config) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}
`;
export type UpdateRateLimitConfigMutationFn = Apollo.MutationFunction<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>;
/**
* __useUpdateRateLimitConfigMutation__
*
* To run a mutation, you first call `useUpdateRateLimitConfigMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateRateLimitConfigMutation` 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 [updateRateLimitConfigMutation, { data, loading, error }] = useUpdateRateLimitConfigMutation({
* variables: {
* appId: // value for 'appId'
* config: // value for 'config'
* },
* });
*/
export function useUpdateRateLimitConfigMutation(baseOptions?: Apollo.MutationHookOptions<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>(UpdateRateLimitConfigDocument, options);
}
export type UpdateRateLimitConfigMutationHookResult = ReturnType<typeof useUpdateRateLimitConfigMutation>;
export type UpdateRateLimitConfigMutationResult = Apollo.MutationResult<UpdateRateLimitConfigMutation>;
export type UpdateRateLimitConfigMutationOptions = Apollo.BaseMutationOptions<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>;
export const ReplaceConfigRawJsonDocument = gql`
mutation ReplaceConfigRawJSON($appID: uuid!, $rawJSON: String!) {
replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON)
@@ -27572,6 +27862,95 @@ export type GetLocalRunServiceConfigsQueryResult = Apollo.QueryResult<GetLocalRu
export function refetchGetLocalRunServiceConfigsQuery(variables: GetLocalRunServiceConfigsQueryVariables) {
return { query: GetLocalRunServiceConfigsDocument, variables: variables }
}
export const GetRunServicesRateLimitDocument = gql`
query getRunServicesRateLimit($appID: uuid!, $resolve: Boolean!) {
app(id: $appID) {
runServices {
id
createdAt
updatedAt
subdomain
config(resolve: $resolve) {
...RunServiceRateLimit
}
}
}
}
${RunServiceRateLimitFragmentDoc}`;
/**
* __useGetRunServicesRateLimitQuery__
*
* To run a query within a React component, call `useGetRunServicesRateLimitQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRunServicesRateLimitQuery` 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 } = useGetRunServicesRateLimitQuery({
* variables: {
* appID: // value for 'appID'
* resolve: // value for 'resolve'
* },
* });
*/
export function useGetRunServicesRateLimitQuery(baseOptions: Apollo.QueryHookOptions<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>(GetRunServicesRateLimitDocument, options);
}
export function useGetRunServicesRateLimitLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>(GetRunServicesRateLimitDocument, options);
}
export type GetRunServicesRateLimitQueryHookResult = ReturnType<typeof useGetRunServicesRateLimitQuery>;
export type GetRunServicesRateLimitLazyQueryHookResult = ReturnType<typeof useGetRunServicesRateLimitLazyQuery>;
export type GetRunServicesRateLimitQueryResult = Apollo.QueryResult<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>;
export function refetchGetRunServicesRateLimitQuery(variables: GetRunServicesRateLimitQueryVariables) {
return { query: GetRunServicesRateLimitDocument, variables: variables }
}
export const GetLocalRunServiceRateLimitDocument = gql`
query getLocalRunServiceRateLimit($appID: uuid!, $resolve: Boolean!) {
runServiceConfigs(appID: $appID, resolve: $resolve) {
serviceID
config {
...RunServiceRateLimit
}
}
}
${RunServiceRateLimitFragmentDoc}`;
/**
* __useGetLocalRunServiceRateLimitQuery__
*
* To run a query within a React component, call `useGetLocalRunServiceRateLimitQuery` and pass it any options that fit your needs.
* When your component renders, `useGetLocalRunServiceRateLimitQuery` 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 } = useGetLocalRunServiceRateLimitQuery({
* variables: {
* appID: // value for 'appID'
* resolve: // value for 'resolve'
* },
* });
*/
export function useGetLocalRunServiceRateLimitQuery(baseOptions: Apollo.QueryHookOptions<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>(GetLocalRunServiceRateLimitDocument, options);
}
export function useGetLocalRunServiceRateLimitLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>(GetLocalRunServiceRateLimitDocument, options);
}
export type GetLocalRunServiceRateLimitQueryHookResult = ReturnType<typeof useGetLocalRunServiceRateLimitQuery>;
export type GetLocalRunServiceRateLimitLazyQueryHookResult = ReturnType<typeof useGetLocalRunServiceRateLimitLazyQuery>;
export type GetLocalRunServiceRateLimitQueryResult = Apollo.QueryResult<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>;
export function refetchGetLocalRunServiceRateLimitQuery(variables: GetLocalRunServiceRateLimitQueryVariables) {
return { query: GetLocalRunServiceRateLimitDocument, variables: variables }
}
export const InsertRunServiceDocument = gql`
mutation insertRunService($object: run_service_insert_input!) {
insertRunService(object: $object) {

View File

@@ -1,5 +1,19 @@
# @nhost/docs
## 2.16.0
### Minor Changes
- ba55c1b: feat: run: added a guide on using a private registry
- 3d70c63: feat: added rate-limiter guide for auth service
## 2.15.0
### Minor Changes
- 40c0d7b: │feat: added subdomain/region information
- a18b545: feat: added postgres upgrade docs
## 2.14.3
### Patch Changes

View File

@@ -1,121 +0,0 @@
---
title: Connect Devices to Local Nhost Project
description: Configuring dnsmasq for network device connectivity to a local Nhost project
icon: ethernet
---
## Introduction
If you want to connect to your local environment from other devices on the same network, such as Android emulators
or iPhone devices, you can use **dnsmasq**. Follow this guide for the necessary configuration steps to enable this
functionality for your local Nhost project running on your machine.
<Note>
Make sure to install **dnsmasq**. If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
<Tabs>
<Tab title="macOS">
```shell Terminal
brew install dnsmasq
```
</Tab>
<Tab title="Debian">
```shell Terminal
apt-get install dnsmasq
```
</Tab>
<Tab title="Nix">
```shell Terminal
nix-env -iA nixpkgs.dnsmasq
```
</Tab>
</Tabs>
</Note>
# Configure dnsmasq for Android
<Warning>These steps are necessary when running on both an **Android emulator** or **physical Android device**</Warning>
<Steps>
<Step title="Configure dnsmasq">
Configure `dnsmasq` to resolve nhost service urls to your machine's special [loopback address](https://developer.android.com/studio/run/emulator-networking) `10.0.2.2`
```shell Terminal
sudo dnsmasq -d \
--address=/local.auth.nhost.run/10.0.2.2 \
--address=/local.graphql.nhost.run/10.0.2.2 \
--address=/local.storage.nhost.run/10.0.2.2 \
--address=/local.functions.nhost.run/10.0.2.2
```
</Step>
<Step title="Restart dnsmasq">
If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
<Tabs>
<Tab title="macOS">
```shell Terminal
sudo brew services restart dnsmasq
```
</Tab>
<Tab title="Debian">
```shell Terminal
sudo systemctl restart dnsmasq
```
</Tab>
</Tabs>
</Step>
<Step title="Configure the android device/emulator's DNS settings">
1. Edit your network settings: Settings > Network & Internet > Internet > AndroidWifi
2. set `IP settings` to `Static`
3. set `DNS 1` and `DNS 2` to `10.0.2.2`
4. Save
</Step>
</Steps>
# Configure dnsmasq for iOS
<Warning>These steps are only necessary when running on physical iOS device, the iOS simulator uses the host machine's network, so no additional configuration is typically needed.</Warning>
<Steps>
<Step title="Inspect your machine's IP address on your network">
<Tabs>
<Tab title="macOS">
```shell Terminal
ipconfig getifaddr en0
```
</Tab>
<Tab title="Debian">
```shell Terminal
ip addr show dev en0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1
```
</Tab>
</Tabs>
</Step>
<Step title="Configure dnsmasq">
Configure `dnsmasq` to resolve nhost service urls to your machine's ip address.
<Warning>Make sure to replace every occurrence of **[your-machine-s-up-address]** with the address printed in Step `1`</Warning>
```shell Terminal
sudo dnsmasq -d \
--address=/local.auth.nhost.run/[your-machine-s-up-address] \
--address=/local.graphql.nhost.run/[your-machine-s-up-address] \
--address=/local.storage.nhost.run/[your-machine-s-up-address] \
--address=/local.functions.nhost.run/[your-machine-s-up-address]
```
</Step>
<Step title="Restart dnsmasq">
If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
<Tabs>
<Tab title="macOS">
```shell Terminal
sudo brew services restart dnsmasq
```
</Tab>
<Tab title="Debian">
```shell Terminal
sudo systemctl restart dnsmasq
```
</Tab>
</Tabs>
</Step>
<Step title="Configure the iPhone's DNS settings">
1. Select the wifi you're connected and select `Configure DNS`
2. Select `Manual`
3. Click on `Add server` and type your local machine's IP address printed in Step `1`
4. Save
</Step>
</Steps>

View File

@@ -0,0 +1,119 @@
---
title: Subdomain/Region
description: Connecting to your local environment
icon: compass
---
When you start the CLI the services are exposed similarly to the way they are exposed in the [cloud](/platform/subdomain). For instance, the following information is shown on your terminal after running `nhost up`
```
> nhost up
...
URLs:
- Postgres: postgres://postgres:postgres@localhost:5432/local
- Hasura: https://local.hasura.local.nhost.run
- GraphQL: https://local.graphql.local.nhost.run
- Auth: https://local.auth.local.nhost.run
- Storage: https://local.storage.local.nhost.run
- Functions: https://local.functions.local.nhost.run
- Dashboard: https://local.dashboard.local.nhost.run
- Mailhog: https://local.mailhog.local.nhost.run
SDK Configuration:
Subdomain: local
Region: local
```
There you can see the various URLs you can use to access each service plus the region and subdomain you can use to configure the SDK:
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient(
{ region: 'local', subdomain: 'local' }
)
```
The domains in the URLs above will all return the IP address for localhost, `127.0.0.1`, which should suffice for most development environments. For instance:
```
> host local.auth.local.nhost.run
local.auth.local.nhost.run has address 127.0.0.1
```
However, those URLs are powered by a dynamic DNS that can return any IPv4 address you need, you just need to replace the subdomain `local` with a `subdomain` that contains the 4 octets of the IPv4 adress you want separated by `-`. For instance:
```
> host 192-168-100-1.auth.local.nhost.run
192-168-100-1.auth.local.nhost.run has address 192.168.100.1
> host 10-10-1-108.auth.local.nhost.run
10-10-1-108.auth.local.nhost.run has address 10.10.1.108
```
This is useful if you need to connect to your environment from a different device, a VM or a mobile device emulator.
To make use of this functionality you can start your development environment after setting the environment variable `NHOST_LOCAL_SUBDOMAIN` or passing the flag `--local-subdomain` :
```
> export NHOST_LOCAL_SUBDOMAIN=192-168-1-1-8 # either this or --local-subdomain 192-168-1-108
> nhost --local-subdomain 192-168-1-108 up
...
Nhost development environment started.
URLs:
- Postgres: postgres://postgres:postgres@localhost:5432/local
- Hasura: https://192-168-1-108.hasura.local.nhost.run
- GraphQL: https://192-168-1-108.graphql.local.nhost.run
- Auth: https://192-168-1-108.auth.local.nhost.run
- Storage: https://192-168-1-108.storage.local.nhost.run
- Functions: https://192-168-1-108.functions.local.nhost.run
- Dashboard: https://192-168-1-108.dashboard.local.nhost.run
- Mailhog: https://192-168-1-108.mailhog.local.nhost.run
SDK Configuration:
Subdomain: 192-168-1-108
Region: local
Run `nhost up` to reload the development environment
Run `nhost down` to stop the development environment
Run `nhost logs` to watch the logs
```
Now you can configure the SDK with:
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient(
{ region: 'local', subdomain: '192-168-1-108' }
)
```
<Warning>
If you are trying to connect to your local environment from an external device or VM make sure that:
- The IP address you are using is reachable from this device/VM
- That your firewall isn't blocking requests
</Warning>
<Warning>
If you are testing a social provider don't forget you will need to configure the callback URL to match the subdomain/region you are using. The dashboard should be able to provide this information in settings page.
</Warning>
## Offline access
All the URLs in this document are resolved by a public DNS, which means you need Internet access to resolve them. If you need to use any of those URLs without Internet access you can add them to your `/etc/hosts` file. For instance:
```
> cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
# ::1 localhost
127.0.0.1 local.auth.local.nhost.run local.storage.local.nhost.run ...
```
Just start with the IP you want to resolve followed by all the entries you need separated by spaces.

View File

@@ -0,0 +1,37 @@
---
title: "Upgrade Major Version"
description: Upgrade to Postgres 15.x or 16.y
icon: circle-up
---
# Upgrade process
<Info>
This document only applies when changing Postgres major version (i.e. from 14 to 15/16 or from 15 to 16). It doesn'e apply when upgrading minor versions (i.e. from 14.5 to 14.11).
</Info>
While new cloud projects ship with Postgres 14 by default, versions 15 and 16 are also supported. To change your major version you can go to Settings -> Database, select the new major version and start the process:
![dashboard settings](/images/guides/database/upgrade_01.png)
<Warning>
Keep in mind that the upgrade process requires downtime. Pay attention to all the information provided to you in the settings page.
</Warning>
After starting the process you can follow it on the same page:
![logs](/images/guides/database/upgrade_02.png)
Finally, you can confirm the upgrade by executing the SQL query `SELECT version();`
![select version()](/images/guides/database/upgrade_03.png)
## Projects with connected repos
This process can only be triggered from the dashboard. If you have a project with a connected repository and want to upgrade postgres to either 15 or 16 you will have to follow the steps below:
1. Upgrade the major version using the dashboard
2. Run `nhost config pull` or edit the `nhost.toml` by hand.
3. Push to git (this step should be a NOOP and can be skipped)
If you attempt to change major versions via a deployment the deployment will fail. This is done on purpose to avoid unintended upgrades which can lead to downtime.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

View File

@@ -73,6 +73,7 @@
{
"group": "Platform",
"pages": [
"platform/subdomain",
"platform/compute-resources",
"platform/service-replicas",
{
@@ -83,7 +84,8 @@
"platform/environment-variables",
"platform/secrets",
"platform/deployments",
"platform/custom-domains"
"platform/custom-domains",
"platform/rate-limits"
]
},
{
@@ -117,7 +119,8 @@
"guides/database/configuring-postgres",
"guides/database/access",
"guides/database/extensions",
"guides/database/performance"
"guides/database/performance",
"guides/database/upgrade-major"
]
},
{
@@ -197,11 +200,11 @@
"group": "CLI",
"pages": [
"guides/cli/local-development",
"guides/cli/subdomain",
"guides/cli/migrate-config",
"guides/cli/multiple-projects",
"guides/cli/configuration-overlays",
"guides/cli/seeds",
"guides/cli/connect-devices-to-local-nhost-project"
"guides/cli/seeds"
]
},
{

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
---
title: Subdomain/Region
description: Connecting to your Cloud project
icon: compass
---
Services in the Nhost Cloud adhere to the following format:
- https://&lt;subdomain&gt;.&lt;service&gt;.&lt;region&gt;.nhost.run
When you set up a new project in Nhost Cloud, you're assigned a subdomain that, along with the region where the project was created, allows you to generate the different URLs for your services. For example:
![dashboard](/images/platform/subdomain.png)
With that information at hand generating the URLs for the various services is trivial:
- https://xglwkhjnufblhgtwtfwz.auth.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.db.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.functions.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.graphql.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.hasura.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.storage.us-west-2.nhost.run
If you are using our SDK you can just specify the region and subdomain and the SDK will automatically generate these URLs for you. For instance:
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient(
{ region: 'xglwkhjnufblhgtwtfwz', subdomain: 'us-west-2' }
)
```
<Note>
If you want to use your own domain you can head to the [custom domains documentation](/platform/custom-domains)
</Note>
<Note>
For information on how to access your local environment check the [cli documentation](/guides/cli/subdomain)
</Note>

View File

@@ -22,7 +22,7 @@ const nhost = new NhostClient({
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient({ subdomain: 'local' })
const nhost = new NhostClient({ region: 'local', subdomain: 'local' })
```
## Parameters

View File

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

View File

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

View File

@@ -1,5 +1,12 @@
# @nhost-examples/codegen-react-apollo
## 0.4.10
### Patch Changes
- @nhost/react@3.5.5
- @nhost/react-apollo@12.0.5
## 0.4.9
### Patch Changes

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,12 @@
# @nhost-examples/react-urql
## 0.3.10
### Patch Changes
- @nhost/react@3.5.5
- @nhost/react-urql@9.0.5
## 0.3.9
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/codegen-react-urql",
"private": true,
"version": "0.3.9",
"version": "0.3.10",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",

View File

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

View File

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

View File

@@ -1,5 +1,13 @@
# @nhost-examples/nextjs
## 0.3.10
### Patch Changes
- @nhost/react@3.5.5
- @nhost/react-apollo@12.0.5
- @nhost/nextjs@2.1.19
## 0.3.9
### Patch Changes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,12 @@
# @nhost-examples/react-apollo
## 0.8.11
### Patch Changes
- @nhost/react@3.5.5
- @nhost/react-apollo@12.0.5
## 0.8.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/react-apollo",
"version": "0.8.10",
"version": "0.8.11",
"private": true,
"dependencies": {
"@apollo/client": "^3.9.9",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/react-gqty
## 1.2.10
### Patch Changes
- @nhost/react@3.5.5
## 1.2.9
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/react-gqty",
"private": true,
"version": "1.2.9",
"version": "1.2.10",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,12 @@
# @nhost-examples/react-native
## 0.0.4
### Patch Changes
- @nhost/react@3.5.5
- @nhost/react-apollo@12.0.5
## 0.0.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/react-native",
"version": "0.0.3",
"version": "0.0.4",
"private": true,
"scripts": {
"android": "react-native run-android",

View File

@@ -1,5 +1,13 @@
# @nhost-examples/vue-apollo
## 0.6.10
### Patch Changes
- @nhost/nhost-js@3.1.8
- @nhost/apollo@7.1.5
- @nhost/vue@2.6.5
## 0.6.9
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/vue-apollo",
"private": true,
"version": "0.6.9",
"version": "0.6.10",
"scripts": {
"dev": "vite",
"build": "vite build",

View File

@@ -1,5 +1,12 @@
# @nhost-examples/vue-quickstart
## 0.2.10
### Patch Changes
- @nhost/apollo@7.1.5
- @nhost/vue@2.6.5
## 0.2.9
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/vue-quickstart",
"version": "0.2.9",
"version": "0.2.10",
"private": true,
"scripts": {
"build": "vite build",

View File

@@ -1,5 +1,11 @@
# @nhost/apollo
## 7.1.5
### Patch Changes
- @nhost/nhost-js@3.1.8
## 7.1.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/apollo",
"version": "7.1.4",
"version": "7.1.5",
"description": "Nhost Apollo Client library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,12 @@
# @nhost/react-apollo
## 12.0.5
### Patch Changes
- @nhost/apollo@7.1.5
- @nhost/react@3.5.5
## 12.0.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-apollo",
"version": "12.0.4",
"version": "12.0.5",
"description": "Nhost React Apollo client",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/react-urql
## 9.0.5
### Patch Changes
- @nhost/react@3.5.5
## 9.0.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-urql",
"version": "9.0.4",
"version": "9.0.5",
"description": "Nhost React URQL client",
"license": "MIT",
"keywords": [

View File

@@ -149,7 +149,8 @@
"@grpc/grpc-js@>=1.10.0 <1.10.9": ">=1.10.9",
"undici@>=6.0.0 <6.11.1": "6.11.1",
"undici@<5.28.4": "5.28.4",
"fast-xml-parser@<4.4.1": ">=4.4.1"
"fast-xml-parser@<4.4.1": ">=4.4.1",
"axios": "1.7.4"
}
}
}

View File

@@ -1,5 +1,11 @@
# @nhost/hasura-auth-js
## 2.5.5
### Patch Changes
- caa8bd7: fix: add error handling logic to transition to the signedOut state when the token is invalid or expired
## 2.5.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/hasura-auth-js",
"version": "2.5.4",
"version": "2.5.5",
"description": "Hasura-auth client",
"license": "MIT",
"keywords": [

View File

@@ -5,7 +5,7 @@ import type {
PublicKeyCredentialRequestOptionsJSON,
RegistrationCredentialJSON
} from '@simplewebauthn/typescript-types'
import { InterpreterFrom, assign, createMachine, send } from 'xstate'
import { assign, createMachine, InterpreterFrom, send } from 'xstate'
import {
NHOST_JWT_EXPIRES_AT_KEY,
NHOST_REFRESH_TOKEN_ID_KEY,
@@ -341,7 +341,13 @@ export const createAuthMachine = ({
actions: ['saveSession', 'resetTimer', 'reportTokenChanged'],
target: 'pending'
},
onError: [{ actions: 'saveRefreshAttempt', target: 'pending' }]
onError: [
{
cond: 'isUnauthorizedError',
target: '#nhost.authentication.signedOut'
},
{ actions: 'saveRefreshAttempt', target: 'pending' }
]
}
}
}
@@ -755,7 +761,8 @@ export const createAuthMachine = ({
// * Event guards
hasSession: (_, e) => !!e.data?.session,
hasMfaTicket: (_, e) => !!e.data?.mfa
hasMfaTicket: (_, e) => !!e.data?.mfa,
isUnauthorizedError: (_, { data: { error } }: any) => error.status === 401
},
services: {

View File

@@ -213,6 +213,7 @@ export interface Typegen0 {
| 'error.platform.authenticateWithPAT'
| 'error.platform.authenticateWithToken'
| 'error.platform.importRefreshToken'
| 'error.platform.refreshToken'
| 'error.platform.signInMfaTotp'
reportTokenChanged:
| 'SESSION_UPDATE'
@@ -305,6 +306,7 @@ export interface Typegen0 {
isAutoRefreshDisabled: ''
isRefreshTokenPAT: ''
isSignedIn: '' | 'error.platform.authenticateWithToken'
isUnauthorizedError: 'error.platform.refreshToken'
noToken: ''
refreshTimerShouldRefresh: ''
shouldRetryImportToken: 'error.platform.importRefreshToken'

View File

@@ -87,21 +87,17 @@ describe(`Time based token refresh`, () => {
server.resetHandlers()
})
test(`token refresh should fail if the signed-in user's refresh token was invalid`, async () => {
test(`token refresh should fail and sign out the user when the server returns an unauthorized error`, async () => {
server.use(authTokenUnauthorizedHandler)
// Fast forwarding to initial expiration date
vi.setSystemTime(initialExpiration)
await waitFor(authServiceWithInitialSession, (state) =>
const state = await waitFor(authServiceWithInitialSession, (state) =>
state.matches({ authentication: { signedIn: { refreshTimer: { running: 'refreshing' } } } })
)
const state = await waitFor(authServiceWithInitialSession, (state) =>
state.matches({ authentication: { signedIn: { refreshTimer: { running: 'pending' } } } })
)
expect(state.context.refreshTimer.attempts).toBeGreaterThan(0)
expect(state.matches({ authentication: 'signedOut' }))
})
test(`access token should always be refreshed when reaching the expiration margin`, async () => {

View File

@@ -1,5 +1,11 @@
# @nhost/nextjs
## 2.1.19
### Patch Changes
- @nhost/react@3.5.5
## 2.1.18
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nextjs",
"version": "2.1.18",
"version": "2.1.19",
"description": "Nhost NextJS library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,12 @@
# @nhost/nhost-js
## 3.1.8
### Patch Changes
- Updated dependencies [caa8bd7]
- @nhost/hasura-auth-js@2.5.5
## 3.1.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nhost-js",
"version": "3.1.7",
"version": "3.1.8",
"description": "Nhost JavaScript SDK",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/react
## 3.5.5
### Patch Changes
- @nhost/nhost-js@3.1.8
## 3.5.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react",
"version": "3.5.4",
"version": "3.5.5",
"description": "Nhost React library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/vue
## 2.6.5
### Patch Changes
- @nhost/nhost-js@3.1.8
## 2.6.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/vue",
"version": "2.6.4",
"version": "2.6.5",
"description": "Nhost Vue library",
"license": "MIT",
"keywords": [

21
pnpm-lock.yaml generated
View File

@@ -55,6 +55,7 @@ overrides:
undici@>=6.0.0 <6.11.1: 6.11.1
undici@<5.28.4: 5.28.4
fast-xml-parser@<4.4.1: '>=4.4.1'
axios: 1.7.4
importers:
@@ -1035,7 +1036,7 @@ importers:
devDependencies:
'@nhost/nhost-js':
specifier: ^3.1.5
version: 3.1.6(graphql@16.8.1)
version: 3.1.7(graphql@16.8.1)
'@playwright/test':
specifier: ^1.42.1
version: 1.42.1
@@ -9783,7 +9784,7 @@ packages:
resolution: {integrity: sha512-MMAdhT6DrylDg1doi2oK2Zw0b7gMspr3Cq8stFGTLl8qOGJo9RT8KCNEWUDzR5eurSGcwE1660rq3Qibe4bOag==}
engines: {node: '>=18.0.0'}
dependencies:
axios: 1.7.2
axios: 1.7.4
openapi-types: 12.1.3
transitivePeerDependencies:
- debug
@@ -10299,8 +10300,8 @@ packages:
- encoding
dev: true
/@nhost/hasura-auth-js@2.5.3:
resolution: {integrity: sha512-WPDmF7vMU32I/G4Ytlo+ZUE+INzcjp1Av5KNLH/iBWDw/0HB6Vj5/+AWq+IjxQm0+HmKE+hvnc5Hc5rn0oPreg==}
/@nhost/hasura-auth-js@2.5.4:
resolution: {integrity: sha512-w9DVBDWamV6KSgO2q6mhA8MEhhFnne4c7fzj2JNEIDUfvc/QMaLpL93ZJII2s4Dc8QeeJM4Vcsjx7zDXI+RnsQ==}
dependencies:
'@simplewebauthn/browser': 9.0.1
fetch-ponyfill: 7.1.0
@@ -10322,13 +10323,13 @@ packages:
- encoding
dev: true
/@nhost/nhost-js@3.1.6(graphql@16.8.1):
resolution: {integrity: sha512-5DPm3vvsiMzJxYzMSxHwqIsn1GfDsZ1LHq5vndHILL8OH5LHPh3tVlUc4EcElbkqN81yzZ++umcDxxH+w8pkAg==}
/@nhost/nhost-js@3.1.7(graphql@16.8.1):
resolution: {integrity: sha512-9ifZ2qvlJFp+Xk/Frm8do3nkGg5L7s2K11Rr9iZG92LnJP4InqtELwhSxxfN80Gt/aQkgEOxyQ21bJXW3XCSxg==}
peerDependencies:
graphql: '>=16.8.1'
dependencies:
'@nhost/graphql-js': 0.3.0(graphql@16.8.1)
'@nhost/hasura-auth-js': 2.5.3
'@nhost/hasura-auth-js': 2.5.4
'@nhost/hasura-storage-js': 2.5.1
graphql: 16.8.1
isomorphic-unfetch: 3.1.0
@@ -15953,7 +15954,7 @@ packages:
engines: {node: '>=4'}
dependencies:
'@segment/loosely-validate-event': 2.0.0
axios: 1.7.2
axios: 1.7.4
axios-retry: 3.2.0
lodash.isstring: 4.0.1
md5: 2.3.0
@@ -16433,8 +16434,8 @@ packages:
is-retry-allowed: 1.2.0
dev: false
/axios@1.7.2:
resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
/axios@1.7.4:
resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==}
dependencies:
follow-redirects: 1.15.6
form-data: 4.0.0