Compare commits
20 Commits
@nhost/nex
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58dec6e7b2 | ||
|
|
526183ab88 | ||
|
|
435b65a65a | ||
|
|
35a2f1203c | ||
|
|
be3b85bbc8 | ||
|
|
cffdec585c | ||
|
|
8b12426157 | ||
|
|
4cf6677284 | ||
|
|
fdaaf19057 | ||
|
|
a7cd02c965 | ||
|
|
3d70c63d1b | ||
|
|
ba55c1b779 | ||
|
|
852f13b273 | ||
|
|
a44a1d48d6 | ||
|
|
b63250d1cb | ||
|
|
caa8bd75ec | ||
|
|
40c0d7b914 | ||
|
|
3773ad7cca | ||
|
|
6f122521e9 | ||
|
|
a18b545d2a |
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
link-workspace-packages = false
|
||||
link-workspace-packages = false
|
||||
auto-install-peers = false
|
||||
resolution-mode=highest
|
||||
@@ -1,5 +1,37 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 1.28.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 526183a: feat: allow filtering users in "make request as" in graphql section
|
||||
- be3b85b: feat: add conceal errors toggle on auth settings page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 35a2f12: fix: prevent run service details from opening when attempting to delete
|
||||
- @nhost/react-apollo@12.0.6
|
||||
- @nhost/nextjs@2.1.20
|
||||
|
||||
## 1.27.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a7cd02c: fix: resolve rate limit query
|
||||
|
||||
## 1.26.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
@@ -12,9 +12,9 @@ test('should be able to ban and unban a user', async ({ page }) => {
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
@@ -22,7 +22,9 @@ test('should be able to ban and unban a user', async ({ page }) => {
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
||||
);
|
||||
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
@@ -19,9 +19,9 @@ test.beforeEach(async () => {
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
@@ -29,7 +29,9 @@ test.beforeEach(async () => {
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
||||
);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
@@ -19,9 +19,9 @@ test.beforeEach(async () => {
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
@@ -29,7 +29,9 @@ test.beforeEach(async () => {
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
||||
);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
@@ -19,9 +19,9 @@ test.beforeEach(async () => {
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
@@ -29,7 +29,9 @@ test.beforeEach(async () => {
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
||||
);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject, prepareTable } from '@/e2e/utils';
|
||||
@@ -20,9 +20,9 @@ test.beforeEach(async () => {
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
@@ -55,7 +55,7 @@ test('should create a simple table', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -84,7 +84,7 @@ test('should create a table with unique constraints', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -113,7 +113,7 @@ test('should create a table with nullable columns', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -146,7 +146,7 @@ test('should create a table with an identity column', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -174,7 +174,7 @@ test('should create table with foreign key constraint', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
@@ -219,7 +219,7 @@ test('should create table with foreign key constraint', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -247,7 +247,7 @@ test('should not be able to create a table with a name that already exists', asy
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { deleteTable, openProject, prepareTable } from '@/e2e/utils';
|
||||
@@ -20,9 +20,9 @@ test.beforeEach(async () => {
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
@@ -53,7 +53,7 @@ test('should delete a table', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await deleteTable({
|
||||
@@ -63,7 +63,7 @@ test('should delete a table', async () => {
|
||||
|
||||
// navigate to next URL
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/**`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/**`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -91,7 +91,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
@@ -138,7 +138,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -61,12 +61,8 @@ test('should create and delete a run service', async () => {
|
||||
page.getByRole('heading', { name: /confirm resources/i }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: /confirm/i }).click();
|
||||
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /service details/i }),
|
||||
).toBeVisible();
|
||||
@@ -74,16 +70,14 @@ test('should create and delete a run service', async () => {
|
||||
await page.getByRole('button', { name: /ok/i }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: /test/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/more options/i).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /delete service/i }).click();
|
||||
|
||||
await page.getByLabel(/confirm delete project #/i).check();
|
||||
await page
|
||||
.getByText(/delete service/i)
|
||||
.nth(2)
|
||||
.click();
|
||||
|
||||
await page.getByLabel('Close').click();
|
||||
await page.getByRole('button', { name: /delete service/i }).click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "1.25.0",
|
||||
"version": "1.28.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -10,10 +10,10 @@ export default defineConfig({
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
globalTeardown: require.resolve('./global-teardown'),
|
||||
use: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type ToggleConcealErrorsFormValues = Yup.InferType<
|
||||
typeof validationSchema
|
||||
>;
|
||||
|
||||
export default function ConcealErrorsSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const form = useForm<ToggleConcealErrorsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: data?.config?.auth?.misc?.concealErrors,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({
|
||||
enabled: data?.config?.auth?.misc?.concealErrors,
|
||||
});
|
||||
}
|
||||
}, [loading, data, form]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading conceal error settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState } = form;
|
||||
|
||||
const handleToggleConcealErrors = async (
|
||||
values: ToggleConcealErrorsFormValues,
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
auth: {
|
||||
misc: {
|
||||
concealErrors: values.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset(values);
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Updating conceal error settings...',
|
||||
successMessage: 'Conceal error settings updated successfully.',
|
||||
errorMessage:
|
||||
'Failed to update conceal error settings. Please try again.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleToggleConcealErrors}>
|
||||
<SettingsContainer
|
||||
title="Conceal errors"
|
||||
description="If set, conceals sensitive error messages to prevent leaking information about user accounts."
|
||||
switchId="enabled"
|
||||
showSwitch
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ConcealErrorsSettings } from './ConcealErrorsSettings';
|
||||
@@ -50,6 +50,9 @@ query GetAuthenticationSettings($appId: uuid!) {
|
||||
default
|
||||
}
|
||||
}
|
||||
misc {
|
||||
concealErrors
|
||||
}
|
||||
version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { Autocomplete } from '@/components/ui/v2/Autocomplete';
|
||||
import { DEFAULT_ROLES } from '@/features/graphql/common/utils/constants';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import type { RemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
useRemoteAppGetUsersCustomLazyQuery,
|
||||
type RemoteAppGetUsersCustomQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { debounce } from '@mui/material/utils';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export interface UserSelectProps {
|
||||
/**
|
||||
@@ -13,7 +14,7 @@ export interface UserSelectProps {
|
||||
*/
|
||||
onUserChange: (userId: string, availableRoles?: string[]) => void;
|
||||
/**
|
||||
* Class name to be applied to the `<Select />` element.
|
||||
* Class name to be applied to the `<Autocomplete />` element.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
@@ -22,35 +23,87 @@ export default function UserSelect({
|
||||
onUserChange,
|
||||
...props
|
||||
}: UserSelectProps) {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [users, setUsers] = useState([]);
|
||||
const [active, setActive] = useState(true);
|
||||
|
||||
const userApplicationClient = useRemoteApplicationGQLClient();
|
||||
const { data, loading, error } = useRemoteAppGetUsersCustomQuery({
|
||||
|
||||
const [fetchAppUsers, { loading }] = useRemoteAppGetUsersCustomLazyQuery({
|
||||
client: userApplicationClient,
|
||||
variables: { where: {}, limit: 250, offset: 0 },
|
||||
skip: !currentProject,
|
||||
variables: {
|
||||
where: {},
|
||||
limit: 250,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<ActivityIndicator label="Loading users..." delay={500} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const fetchUsers = useCallback(
|
||||
async (
|
||||
request: { input: string },
|
||||
callback: (results?: RemoteAppGetUsersCustomQuery['users']) => void,
|
||||
) => {
|
||||
const ilike = `%${request.input === 'Admin' ? '' : request.input}%`;
|
||||
const { data } = await fetchAppUsers({
|
||||
client: userApplicationClient,
|
||||
variables: {
|
||||
where: {
|
||||
displayName: { _ilike: ilike },
|
||||
},
|
||||
limit: 250,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
callback(data?.users);
|
||||
},
|
||||
[fetchAppUsers, userApplicationClient],
|
||||
);
|
||||
|
||||
const fetchOptions = useMemo(() => debounce(fetchUsers, 1000), [fetchUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOptions({ input: inputValue }, (results) => {
|
||||
if (active || inputValue === '') {
|
||||
setUsers(results);
|
||||
}
|
||||
});
|
||||
}, [inputValue, fetchOptions, active]);
|
||||
|
||||
const autocompleteOptions = [
|
||||
{
|
||||
value: 'admin',
|
||||
label: 'Admin',
|
||||
group: 'Admin',
|
||||
},
|
||||
...users.map((user) => ({
|
||||
value: user.id,
|
||||
label: user.displayName,
|
||||
group: 'Users',
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
<Autocomplete
|
||||
{...props}
|
||||
id="user-select"
|
||||
label="Make Request As"
|
||||
hideEmptyHelperText
|
||||
defaultValue="admin"
|
||||
slotProps={{ root: { className: 'truncate' } }}
|
||||
onChange={(_event, userId) => {
|
||||
label="Make request as"
|
||||
options={autocompleteOptions}
|
||||
defaultValue={{
|
||||
value: 'admin',
|
||||
label: 'Admin',
|
||||
group: 'Admin',
|
||||
}}
|
||||
autoComplete
|
||||
fullWidth
|
||||
autoSelect
|
||||
groupBy={(option) => option.group}
|
||||
autoHighlight
|
||||
includeInputInList
|
||||
loading={loading}
|
||||
onChange={(_event, _value, reason, details) => {
|
||||
setActive(false);
|
||||
const userId = details.option.value;
|
||||
if (typeof userId !== 'string') {
|
||||
return;
|
||||
}
|
||||
@@ -61,22 +114,23 @@ export default function UserSelect({
|
||||
return;
|
||||
}
|
||||
|
||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = data?.users.find(
|
||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
|
||||
({ id }) => id === userId,
|
||||
);
|
||||
|
||||
const roles = user?.roles.map(({ role }) => role);
|
||||
const roles = user?.roles?.map(({ role }) => role);
|
||||
|
||||
onUserChange(user.id, roles);
|
||||
onUserChange(userId, roles ?? DEFAULT_ROLES);
|
||||
|
||||
fetchUsers({ input: '' }, (results) => {
|
||||
if (results) {
|
||||
setUsers(results);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="admin">Admin</Option>
|
||||
|
||||
{data?.users.map(({ id, displayName, email, phoneNumber }) => (
|
||||
<Option key={id} value={id}>
|
||||
{displayName || email || phoneNumber || id}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
onInputChange={(event, newInputValue) => {
|
||||
setInputValue(newInputValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
1
dashboard/src/features/graphql/common/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useGetAppUsers } from './useGetAppUsers';
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import type {
|
||||
RemoteAppGetUsersCustomQuery,
|
||||
RemoteAppGetUsersCustomQueryVariables,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import type { QueryHookOptions } from '@apollo/client';
|
||||
|
||||
export type UseFilesOptions = {
|
||||
searchString?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
/**
|
||||
* Custom options for the query.
|
||||
*/
|
||||
options?: QueryHookOptions<
|
||||
RemoteAppGetUsersCustomQuery,
|
||||
RemoteAppGetUsersCustomQueryVariables
|
||||
>;
|
||||
};
|
||||
|
||||
export default function useGetAppUsers({
|
||||
searchString,
|
||||
limit = 250,
|
||||
offset = 0,
|
||||
options = {},
|
||||
}: UseFilesOptions) {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const userApplicationClient = useRemoteApplicationGQLClient();
|
||||
const { data, error, loading } = useRemoteAppGetUsersCustomQuery({
|
||||
...options,
|
||||
client: userApplicationClient,
|
||||
variables: {
|
||||
...options.variables,
|
||||
where: searchString
|
||||
? {
|
||||
displayName: { _ilike: `%${searchString}%` },
|
||||
}
|
||||
: {},
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
skip: !currentProject,
|
||||
});
|
||||
|
||||
const users = data?.users || [];
|
||||
|
||||
return {
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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.',
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AuthLimitingForm } from './AuthLimitingForm';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RateLimitField } from './RateLimitField';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RateLimitingForm } from './RateLimitingForm';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RunServiceLimitingForm } from './RunServiceLimitingForm';
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useGetRateLimits } from './useGetRateLimits';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useGetRunServiceRateLimits } from './useGetRunServiceRateLimits';
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const DEFAULT_RATE_LIMITS = {
|
||||
limit: 1000,
|
||||
interval: 5,
|
||||
intervalUnit: 'm',
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './constants';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as parseIntervalNameUnit } from './parseIntervalNameUnit';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -179,7 +180,10 @@ export default function ServicesList({
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
sx={{ color: 'error.main' }}
|
||||
onClick={() => deleteService(service)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteService(service);
|
||||
}}
|
||||
disabled={!isPlatform}
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,10 @@ fragment RunServiceConfig on ConfigRunServiceConfig {
|
||||
ingresses {
|
||||
fqdn
|
||||
}
|
||||
rateLimit {
|
||||
limit
|
||||
interval
|
||||
}
|
||||
}
|
||||
healthCheck {
|
||||
port
|
||||
|
||||
38
dashboard/src/gql/services/getRunServicesRateLimit.gql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { AllowedRedirectURLsSettings } from '@/features/authentication/settings/
|
||||
import { AuthServiceVersionSettings } from '@/features/authentication/settings/components/AuthServiceVersionSettings';
|
||||
import { BlockedEmailSettings } from '@/features/authentication/settings/components/BlockedEmailSettings';
|
||||
import { ClientURLSettings } from '@/features/authentication/settings/components/ClientURLSettings';
|
||||
import { ConcealErrorsSettings } from '@/features/authentication/settings/components/ConcealErrorsSettings';
|
||||
import { DisableNewUsersSettings } from '@/features/authentication/settings/components/DisableNewUsersSettings';
|
||||
import { GravatarSettings } from '@/features/authentication/settings/components/GravatarSettings';
|
||||
import { MFASettings } from '@/features/authentication/settings/components/MFASettings';
|
||||
@@ -43,7 +44,7 @@ export default function SettingsAuthenticationPage() {
|
||||
|
||||
return (
|
||||
<Container
|
||||
className="grid max-w-5xl grid-flow-row bg-transparent gap-y-6"
|
||||
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
|
||||
rootClassName="bg-transparent"
|
||||
>
|
||||
<AuthServiceVersionSettings />
|
||||
@@ -55,6 +56,7 @@ export default function SettingsAuthenticationPage() {
|
||||
<SessionSettings />
|
||||
<GravatarSettings />
|
||||
<DisableNewUsersSettings />
|
||||
<ConcealErrorsSettings />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
419
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -199,6 +199,8 @@ export type ConfigAuth = {
|
||||
__typename?: 'ConfigAuth';
|
||||
elevatedPrivileges?: Maybe<ConfigAuthElevatedPrivileges>;
|
||||
method?: Maybe<ConfigAuthMethod>;
|
||||
misc?: Maybe<ConfigAuthMisc>;
|
||||
rateLimit?: Maybe<ConfigAuthRateLimit>;
|
||||
redirections?: Maybe<ConfigAuthRedirections>;
|
||||
/** Resources for the service */
|
||||
resources?: Maybe<ConfigResources>;
|
||||
@@ -223,6 +225,8 @@ export type ConfigAuthComparisonExp = {
|
||||
_or?: InputMaybe<Array<ConfigAuthComparisonExp>>;
|
||||
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesComparisonExp>;
|
||||
method?: InputMaybe<ConfigAuthMethodComparisonExp>;
|
||||
misc?: InputMaybe<ConfigAuthMiscComparisonExp>;
|
||||
rateLimit?: InputMaybe<ConfigAuthRateLimitComparisonExp>;
|
||||
redirections?: InputMaybe<ConfigAuthRedirectionsComparisonExp>;
|
||||
resources?: InputMaybe<ConfigResourcesComparisonExp>;
|
||||
session?: InputMaybe<ConfigAuthSessionComparisonExp>;
|
||||
@@ -255,6 +259,8 @@ export type ConfigAuthElevatedPrivilegesUpdateInput = {
|
||||
export type ConfigAuthInsertInput = {
|
||||
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesInsertInput>;
|
||||
method?: InputMaybe<ConfigAuthMethodInsertInput>;
|
||||
misc?: InputMaybe<ConfigAuthMiscInsertInput>;
|
||||
rateLimit?: InputMaybe<ConfigAuthRateLimitInsertInput>;
|
||||
redirections?: InputMaybe<ConfigAuthRedirectionsInsertInput>;
|
||||
resources?: InputMaybe<ConfigResourcesInsertInput>;
|
||||
session?: InputMaybe<ConfigAuthSessionInsertInput>;
|
||||
@@ -684,6 +690,62 @@ export type ConfigAuthMethodWebauthnUpdateInput = {
|
||||
relyingParty?: InputMaybe<ConfigAuthMethodWebauthnRelyingPartyUpdateInput>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMisc = {
|
||||
__typename?: 'ConfigAuthMisc';
|
||||
concealErrors?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMiscComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigAuthMiscComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigAuthMiscComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigAuthMiscComparisonExp>>;
|
||||
concealErrors?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMiscInsertInput = {
|
||||
concealErrors?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMiscUpdateInput = {
|
||||
concealErrors?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ConfigAuthRateLimit = {
|
||||
__typename?: 'ConfigAuthRateLimit';
|
||||
bruteForce?: Maybe<ConfigRateLimit>;
|
||||
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 +896,8 @@ export type ConfigAuthTotpUpdateInput = {
|
||||
export type ConfigAuthUpdateInput = {
|
||||
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesUpdateInput>;
|
||||
method?: InputMaybe<ConfigAuthMethodUpdateInput>;
|
||||
misc?: InputMaybe<ConfigAuthMiscUpdateInput>;
|
||||
rateLimit?: InputMaybe<ConfigAuthRateLimitUpdateInput>;
|
||||
redirections?: InputMaybe<ConfigAuthRedirectionsUpdateInput>;
|
||||
resources?: InputMaybe<ConfigResourcesUpdateInput>;
|
||||
session?: InputMaybe<ConfigAuthSessionUpdateInput>;
|
||||
@@ -1233,6 +1297,7 @@ export type ConfigFloatComparisonExp = {
|
||||
export type ConfigFunctions = {
|
||||
__typename?: 'ConfigFunctions';
|
||||
node?: Maybe<ConfigFunctionsNode>;
|
||||
rateLimit?: Maybe<ConfigRateLimit>;
|
||||
resources?: Maybe<ConfigFunctionsResources>;
|
||||
};
|
||||
|
||||
@@ -1241,11 +1306,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 +1358,7 @@ export type ConfigFunctionsResourcesUpdateInput = {
|
||||
|
||||
export type ConfigFunctionsUpdateInput = {
|
||||
node?: InputMaybe<ConfigFunctionsNodeUpdateInput>;
|
||||
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
|
||||
resources?: InputMaybe<ConfigFunctionsResourcesUpdateInput>;
|
||||
};
|
||||
|
||||
@@ -1415,6 +1483,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 +1546,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 +1580,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 +1673,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 +2085,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';
|
||||
@@ -2126,6 +2222,8 @@ export type ConfigRunServiceConfigWithId = {
|
||||
export type ConfigRunServiceImage = {
|
||||
__typename?: 'ConfigRunServiceImage';
|
||||
image: Scalars['String'];
|
||||
/** content of "auths", i.e., { "auths": $THIS } */
|
||||
pullCredentials?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigRunServiceImageComparisonExp = {
|
||||
@@ -2133,14 +2231,17 @@ export type ConfigRunServiceImageComparisonExp = {
|
||||
_not?: InputMaybe<ConfigRunServiceImageComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigRunServiceImageComparisonExp>>;
|
||||
image?: InputMaybe<ConfigStringComparisonExp>;
|
||||
pullCredentials?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigRunServiceImageInsertInput = {
|
||||
image: Scalars['String'];
|
||||
pullCredentials?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigRunServiceImageUpdateInput = {
|
||||
image?: InputMaybe<Scalars['String']>;
|
||||
pullCredentials?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigRunServiceNameComparisonExp = {
|
||||
@@ -2155,6 +2256,7 @@ export type ConfigRunServicePort = {
|
||||
ingresses?: Maybe<Array<ConfigIngress>>;
|
||||
port: Scalars['ConfigPort'];
|
||||
publish?: Maybe<Scalars['Boolean']>;
|
||||
rateLimit?: Maybe<ConfigRateLimit>;
|
||||
type: Scalars['String'];
|
||||
};
|
||||
|
||||
@@ -2165,6 +2267,7 @@ export type ConfigRunServicePortComparisonExp = {
|
||||
ingresses?: InputMaybe<ConfigIngressComparisonExp>;
|
||||
port?: InputMaybe<ConfigPortComparisonExp>;
|
||||
publish?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
|
||||
type?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
@@ -2172,6 +2275,7 @@ export type ConfigRunServicePortInsertInput = {
|
||||
ingresses?: InputMaybe<Array<ConfigIngressInsertInput>>;
|
||||
port: Scalars['ConfigPort'];
|
||||
publish?: InputMaybe<Scalars['Boolean']>;
|
||||
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
|
||||
type: Scalars['String'];
|
||||
};
|
||||
|
||||
@@ -2179,6 +2283,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 +2491,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 +2533,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']>;
|
||||
};
|
||||
@@ -22805,7 +22914,7 @@ export type GetAuthenticationSettingsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAuthenticationSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', auth?: { __typename: 'ConfigAuth', version?: string | null, id: 'ConfigAuth', redirections?: { __typename?: 'ConfigAuthRedirections', clientUrl?: any | null, allowedUrls?: Array<string> | null } | null, totp?: { __typename?: 'ConfigAuthTotp', enabled?: boolean | null, issuer?: string | null } | null, signUp?: { __typename?: 'ConfigAuthSignUp', enabled?: boolean | null } | null, session?: { __typename?: 'ConfigAuthSession', accessToken?: { __typename?: 'ConfigAuthSessionAccessToken', expiresIn?: any | null } | null, refreshToken?: { __typename?: 'ConfigAuthSessionRefreshToken', expiresIn?: any | null } | null } | null, resources?: { __typename?: 'ConfigResources', networking?: { __typename?: 'ConfigNetworking', ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null } | null } | null, user?: { __typename?: 'ConfigAuthUser', email?: { __typename?: 'ConfigAuthUserEmail', allowed?: Array<any> | null, blocked?: Array<any> | null } | null, emailDomains?: { __typename?: 'ConfigAuthUserEmailDomains', allowed?: Array<string> | null, blocked?: Array<string> | null } | null, gravatar?: { __typename?: 'ConfigAuthUserGravatar', enabled?: boolean | null, default?: string | null, rating?: string | null } | null, locale?: { __typename?: 'ConfigAuthUserLocale', allowed?: Array<any> | null, default?: any | null } | null } | null } | null } | null };
|
||||
export type GetAuthenticationSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', auth?: { __typename: 'ConfigAuth', version?: string | null, id: 'ConfigAuth', redirections?: { __typename?: 'ConfigAuthRedirections', clientUrl?: any | null, allowedUrls?: Array<string> | null } | null, totp?: { __typename?: 'ConfigAuthTotp', enabled?: boolean | null, issuer?: string | null } | null, signUp?: { __typename?: 'ConfigAuthSignUp', enabled?: boolean | null } | null, session?: { __typename?: 'ConfigAuthSession', accessToken?: { __typename?: 'ConfigAuthSessionAccessToken', expiresIn?: any | null } | null, refreshToken?: { __typename?: 'ConfigAuthSessionRefreshToken', expiresIn?: any | null } | null } | null, resources?: { __typename?: 'ConfigResources', networking?: { __typename?: 'ConfigNetworking', ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null } | null } | null, user?: { __typename?: 'ConfigAuthUser', email?: { __typename?: 'ConfigAuthUserEmail', allowed?: Array<any> | null, blocked?: Array<any> | null } | null, emailDomains?: { __typename?: 'ConfigAuthUserEmailDomains', allowed?: Array<string> | null, blocked?: Array<string> | null } | null, gravatar?: { __typename?: 'ConfigAuthUserGravatar', enabled?: boolean | null, default?: string | null, rating?: string | null } | null, locale?: { __typename?: 'ConfigAuthUserLocale', allowed?: Array<any> | null, default?: any | null } | null } | null, misc?: { __typename?: 'ConfigAuthMisc', concealErrors?: boolean | null } | null } | null } | null };
|
||||
|
||||
export type GetPostgresSettingsQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
@@ -23010,6 +23119,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 +23518,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 +23528,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 +23536,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 +24017,10 @@ export const RunServiceConfigFragmentDoc = gql`
|
||||
ingresses {
|
||||
fqdn
|
||||
}
|
||||
rateLimit {
|
||||
limit
|
||||
interval
|
||||
}
|
||||
}
|
||||
healthCheck {
|
||||
port
|
||||
@@ -23882,6 +24029,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
|
||||
@@ -24194,6 +24358,9 @@ export const GetAuthenticationSettingsDocument = gql`
|
||||
default
|
||||
}
|
||||
}
|
||||
misc {
|
||||
concealErrors
|
||||
}
|
||||
version
|
||||
}
|
||||
}
|
||||
@@ -25371,6 +25538,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 +27894,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) {
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 2.17.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cffdec5: feat: update react quickstart guide to use the nhost react apollo template
|
||||
- 4cf6677: feat: update list of postgres extensions
|
||||
|
||||
## 2.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ba55c1b: feat: run: added a guide on using a private registry
|
||||
- 3d70c63: feat: added rate-limiter guide for auth service
|
||||
|
||||
## 2.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 40c0d7b: │feat: added subdomain/region information
|
||||
- a18b545: feat: added postgres upgrade docs
|
||||
|
||||
## 2.14.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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>
|
||||
119
docs/guides/cli/subdomain.mdx
Normal 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.
|
||||
@@ -4,6 +4,57 @@ description: List of available extensions with Nhost Postgres.
|
||||
icon: grid
|
||||
---
|
||||
|
||||
## hypopg
|
||||
|
||||
HypoPG is a PostgreSQL extension adding support for hypothetical indexes.
|
||||
|
||||
An hypothetical -- or virtual -- index is an index that doesn't really exists, and thus doesn't cost CPU, disk or any resource to create. They're useful to know if specific indexes can increase performance for problematic queries, since you can know if PostgreSQL will use these indexes or not without having to spend resources to create them.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION hypopg;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION hypopg;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [GitHub](https://github.com/HypoPG/hypopg)
|
||||
- [Documentation](https://hypopg.readthedocs.io/)
|
||||
|
||||
## http
|
||||
|
||||
HTTP client for PostgreSQL, retrieve a web page from inside the database.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION http;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION http;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [GitHub](https://github.com/pramsey/pgsql-http)
|
||||
|
||||
## postgis
|
||||
|
||||
PostGIS extends the capabilities of the PostgreSQL relational database by adding support storing, indexing and querying geographic data.
|
||||
@@ -84,11 +135,9 @@ DROP EXTENSION pg_cron;
|
||||
|
||||
- [GitHub](https://github.com/citusdata/pg_cron)
|
||||
|
||||
## hypopg
|
||||
## pg_hashids
|
||||
|
||||
HypoPG is a PostgreSQL extension adding support for hypothetical indexes.
|
||||
|
||||
An hypothetical -- or virtual -- index is an index that doesn't really exists, and thus doesn't cost CPU, disk or any resource to create. They're useful to know if specific indexes can increase performance for problematic queries, since you can know if PostgreSQL will use these indexes or not without having to spend resources to create them.
|
||||
Hashids is a small open-source library that generates short, unique, non-sequential ids from numbers. It converts numbers like 347 into strings like “yr8”. You can also decode those ids back. This is useful in bundling several parameters into one or simply using them as short UIDs.
|
||||
|
||||
### Managing
|
||||
|
||||
@@ -96,20 +145,68 @@ To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION hypopg;
|
||||
CREATE EXTENSION pg_hashids;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION hypopg;
|
||||
DROP EXTENSION pg_hashids;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [GitHub](https://github.com/HypoPG/hypopg)
|
||||
- [Documentation](https://hypopg.readthedocs.io/)
|
||||
- [GitHub](https://github.com/iCyberon/pg_hashids)
|
||||
|
||||
## pg_squeeze
|
||||
|
||||
PostgreSQL extension that removes unused space from a table and optionally sorts tuples according to particular index (as if CLUSTER command was executed concurrently with regular reads / writes). In fact we try to replace pg_repack extension.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION pg_squeeze;
|
||||
```
|
||||
In addition, you may need to configure the WAL level and replication slots. Check the official documentation for details.
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION pg_squeeze;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [GitHub](https://github.com/cybertec-postgresql/pg_squeeze)
|
||||
|
||||
## pg_stat_statements
|
||||
|
||||
The pg_stat_statements module provides a means for tracking planning and execution statistics of all SQL statements executed by a server.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION pg_stat_statements;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION pg_stat_statements;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [Documentation](https://www.postgresql.org/docs/14/pgstatstatements.html)
|
||||
|
||||
## timescaledb
|
||||
|
||||
@@ -140,50 +237,3 @@ DROP EXTENSION timescaledb;
|
||||
- [Documentation](https://docs.timescale.com/)
|
||||
- [Website](https://www.timescale.com/)
|
||||
|
||||
## pg_stat_statements
|
||||
|
||||
The pg_stat_statements module provides a means for tracking planning and execution statistics of all SQL statements executed by a server.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION pg_stat_statements;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION pg_stat_statements;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [Documentation](https://www.postgresql.org/docs/14/pgstatstatements.html)
|
||||
|
||||
## http
|
||||
|
||||
HTTP client for PostgreSQL, retrieve a web page from inside the database.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION http;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION http;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [GitHub](https://github.com/pramsey/pgsql-http)
|
||||
|
||||
37
docs/guides/database/upgrade-major.mdx
Normal 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:
|
||||
|
||||

|
||||
|
||||
<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:
|
||||
|
||||

|
||||
|
||||
Finally, you can confirm the upgrade by executing the SQL query `SELECT version();`
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -1,149 +1,301 @@
|
||||
---
|
||||
title: Setup Nhost with React
|
||||
title: Get up and running with Nhost and React
|
||||
sidebarTitle: React
|
||||
description: Get up and running with Nhost and React
|
||||
icon: react
|
||||
---
|
||||
|
||||
<Steps>
|
||||
<Step title="Create Project">
|
||||
If you haven't, please create a project through the [Nhost Dashboard](https://app.nhost.io/new).
|
||||
<Step title="Create Nhost Project">
|
||||
Create your project through the [Nhost Dashboard](https://app.nhost.io/new).
|
||||
</Step>
|
||||
|
||||
<Step title="Setup Database">
|
||||
Navigate to the **SQL Editor** of the database and run the following SQL to create a new table `movies` with some great movies.
|
||||
|
||||
```sql SQL Editor
|
||||
CREATE TABLE movies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
director VARCHAR(255),
|
||||
release_year INTEGER,
|
||||
genre VARCHAR(100),
|
||||
rating FLOAT
|
||||
);
|
||||
|
||||
INSERT INTO movies (title, director, release_year, genre, rating) VALUES
|
||||
('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8),
|
||||
('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2),
|
||||
('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8),
|
||||
('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7);
|
||||
```
|
||||
Navigate to the **SQL Editor** of the database and run the following SQL to create a new table `todos`.
|
||||
|
||||
<Warning>Make sure the option `Track this` is enabled</Warning>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="permissions">
|
||||
Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action.
|
||||
|
||||

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

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
BIN
docs/images/guides/database/upgrade_01.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/guides/database/upgrade_02.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/guides/database/upgrade_03.png
Normal file
|
After Width: | Height: | Size: 892 KiB |
BIN
docs/images/guides/run/registry_8.png
Normal file
|
After Width: | Height: | Size: 429 KiB |
BIN
docs/images/platform/rate-limiting/auth.png
Normal file
|
After Width: | Height: | Size: 365 KiB |
BIN
docs/images/platform/rate-limiting/misc.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
docs/images/platform/subdomain.png
Normal file
|
After Width: | Height: | Size: 971 KiB |
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "2.14.3",
|
||||
"version": "2.17.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "mintlify dev"
|
||||
|
||||
112
docs/platform/rate-limits.mdx
Normal 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**
|
||||
|
||||

|
||||
|
||||
</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**
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
40
docs/platform/subdomain.mdx
Normal 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://<subdomain>.<service>.<region>.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:
|
||||
|
||||

|
||||
|
||||
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>
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/cli
|
||||
|
||||
## 0.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.9
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.8
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/cli",
|
||||
"version": "0.3.9",
|
||||
"version": "0.3.11",
|
||||
"main": "src/index.mjs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost-examples/codegen-react-apollo
|
||||
|
||||
## 0.4.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.6
|
||||
- @nhost/react-apollo@12.0.6
|
||||
|
||||
## 0.4.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.5
|
||||
- @nhost/react-apollo@12.0.5
|
||||
|
||||
## 0.4.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-apollo",
|
||||
"version": "0.4.9",
|
||||
"version": "0.4.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/codegen-react-query
|
||||
|
||||
## 0.4.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.6
|
||||
|
||||
## 0.4.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.5
|
||||
|
||||
## 0.4.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-query",
|
||||
"version": "0.4.9",
|
||||
"version": "0.4.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost-examples/react-urql
|
||||
|
||||
## 0.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.6
|
||||
- @nhost/react-urql@9.0.6
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.5
|
||||
- @nhost/react-urql@9.0.5
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-urql",
|
||||
"private": true,
|
||||
"version": "0.3.9",
|
||||
"version": "0.3.11",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@@ -15,7 +15,7 @@
|
||||
"graphql": "16.8.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"urql": "^3.0.4"
|
||||
"urql": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/multi-tenant-one-to-many
|
||||
|
||||
## 2.2.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.9
|
||||
|
||||
## 2.2.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.8
|
||||
|
||||
## 2.2.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/multi-tenant-one-to-many",
|
||||
"private": true,
|
||||
"version": "2.2.9",
|
||||
"version": "2.2.11",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @nhost-examples/nextjs
|
||||
|
||||
## 0.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.6
|
||||
- @nhost/react-apollo@12.0.6
|
||||
- @nhost/nextjs@2.1.20
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.5
|
||||
- @nhost/react-apollo@12.0.5
|
||||
- @nhost/nextjs@2.1.19
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs",
|
||||
"version": "0.3.9",
|
||||
"version": "0.3.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/node-storage
|
||||
|
||||
## 0.2.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.9
|
||||
|
||||
## 0.2.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.8
|
||||
|
||||
## 0.2.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/node-storage",
|
||||
"version": "0.2.9",
|
||||
"version": "0.2.11",
|
||||
"private": true,
|
||||
"description": "This is an example of how to use the Storage with Node.js",
|
||||
"main": "src/index.mjs",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/nextjs-server-components
|
||||
|
||||
## 0.4.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.9
|
||||
|
||||
## 0.4.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.1.8
|
||||
|
||||
## 0.4.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs-server-components",
|
||||
"version": "0.4.10",
|
||||
"version": "0.4.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1 +1 @@
|
||||
hoist-pattern=[]
|
||||
hoist-pattern[]=!@nhost/nhost-js
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nhost/nhost-js": "^3.1.5",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@playwright/test": "^1.41.0",
|
||||
"@sveltejs/adapter-auto": "^2.1.1",
|
||||
"@sveltejs/kit": "^1.30.4",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
@@ -25,7 +25,7 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^3.59.2",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.6.8",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
|
||||
49
examples/react-apollo/.gitignore
vendored
@@ -1,32 +1,27 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
.nhost
|
||||
functions/node_modules
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
storageState.json
|
||||
.secrets
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
test-results
|
||||
playwright-report
|
||||
@@ -1,5 +1,23 @@
|
||||
# @nhost-examples/react-apollo
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- cffdec5: feat: rewrite example using shadcn ui components
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.6
|
||||
- @nhost/react-apollo@12.0.6
|
||||
|
||||
## 0.8.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.5.5
|
||||
- @nhost/react-apollo@12.0.5
|
||||
|
||||
## 0.8.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
17
examples/react-apollo/components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,13 @@ test('should add an item to the todo list when authenticated with email and pass
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /apollo/i }).click()
|
||||
await expect(newPage.getByText(/todo list/i)).toBeVisible()
|
||||
|
||||
await newPage.getByRole('link', { name: /todos/i }).click()
|
||||
await expect(newPage.getByRole('heading', { name: /todos/i })).toBeVisible()
|
||||
|
||||
await newPage.getByRole('textbox').fill(sentence)
|
||||
await newPage.getByRole('button', { name: /add/i }).click()
|
||||
await expect(newPage.getByRole('listitem').first()).toHaveText(sentence)
|
||||
await expect(newPage.getByRole('main')).toContainText(sentence)
|
||||
})
|
||||
|
||||
test('should add an item to the todo list when authenticated anonymously', async ({ page }) => {
|
||||
@@ -28,11 +30,13 @@ test('should add an item to the todo list when authenticated anonymously', async
|
||||
await page.goto('/')
|
||||
|
||||
await signInAnonymously({ page })
|
||||
await page.getByRole('button', { name: /apollo/i }).click()
|
||||
await expect(page.getByText(/todo list/i)).toBeVisible()
|
||||
|
||||
await page.getByRole('link', { name: /todos/i }).click()
|
||||
await expect(page.getByRole('heading', { name: /todos/i })).toBeVisible()
|
||||
|
||||
await page.getByRole('textbox').fill(sentence)
|
||||
await page.getByRole('button', { name: /add/i }).click()
|
||||
await expect(page.getByRole('listitem').first()).toHaveText(sentence)
|
||||
await expect(page.getByRole('main')).toContainText(sentence)
|
||||
})
|
||||
|
||||
test('should fail when network is not available', async ({ page }) => {
|
||||
@@ -41,12 +45,12 @@ test('should fail when network is not available', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
await signInAnonymously({ page })
|
||||
await page.getByRole('button', { name: /apollo/i }).click()
|
||||
await expect(page.getByText(/todo list/i)).toBeVisible()
|
||||
await page.getByRole('link', { name: /todos/i }).click()
|
||||
await expect(page.getByRole('heading', { name: /todos/i })).toBeVisible()
|
||||
|
||||
await page.route('**', (route) => route.abort('internetdisconnected'))
|
||||
await page.getByRole('textbox').fill(sentence)
|
||||
await page.getByRole('button', { name: /add/i }).click()
|
||||
|
||||
await expect(page.getByText(/network error/i)).toBeVisible()
|
||||
await expect(page.getByText(/failed to fetch/i)).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -12,18 +12,28 @@ test('should be able to change email', async ({ page, browser }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /profile/i }).click()
|
||||
await newPage.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const newEmail = faker.internet.email()
|
||||
|
||||
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
|
||||
await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
||||
|
||||
await expect(
|
||||
newPage.getByText(/please check your inbox and follow the link to confirm the email change/i)
|
||||
).toBeVisible()
|
||||
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
||||
await newPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Change emailChange$/ })
|
||||
.getByRole('button')
|
||||
.click()
|
||||
|
||||
await newPage.getByRole('button', { name: /sign out/i }).click()
|
||||
// await expect(
|
||||
// newPage.getByText(/please check your inbox and follow the link to confirm the email change./i)
|
||||
// ).toBeVisible()
|
||||
|
||||
await expect(newPage.getByRole('status')).toContainText(
|
||||
'Please check your inbox and follow the link to confirm the email change.'
|
||||
)
|
||||
|
||||
await newPage.getByRole('link', { name: /sign out/i }).click()
|
||||
|
||||
const mailhogPage = await browser.newPage()
|
||||
|
||||
@@ -35,7 +45,8 @@ test('should be able to change email', async ({ page, browser }) => {
|
||||
requestType: 'email-confirm-change'
|
||||
})
|
||||
|
||||
await expect(updatedEmailPage.getByText(/profile page/i)).toBeVisible()
|
||||
// await expect(updatedEmailPage.getByText(/profile page/i)).toBeVisible()
|
||||
await expect(updatedEmailPage.getByRole('heading', { name: /profile/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should not accept an invalid email', async ({ page }) => {
|
||||
@@ -48,12 +59,18 @@ test('should not accept an invalid email', async ({ page }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /profile/i }).click()
|
||||
await newPage.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const newEmail = faker.random.alphaNumeric()
|
||||
|
||||
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
|
||||
await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
||||
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
||||
|
||||
await newPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Change emailChange$/ })
|
||||
.getByRole('button')
|
||||
.click()
|
||||
|
||||
await expect(newPage.getByText(/email is incorrectly formatted/i)).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -12,15 +12,22 @@ test('should be able to change password', async ({ page }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /profile/i }).click()
|
||||
await newPage.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const newPassword = faker.internet.password()
|
||||
|
||||
await newPage.getByPlaceholder(/new password/i).fill(newPassword)
|
||||
await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
|
||||
await expect(newPage.getByText(/password changed successfully/i)).toBeVisible()
|
||||
|
||||
await newPage.getByRole('button', { name: /sign out/i }).click()
|
||||
// await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
|
||||
await newPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Change passwordChange$/ })
|
||||
.getByRole('button')
|
||||
.click()
|
||||
|
||||
await expect(newPage.getByText(/password changed successfully./i)).toBeVisible()
|
||||
|
||||
await newPage.getByRole('link', { name: 'Sign out' }).click()
|
||||
|
||||
await signInWithEmailAndPassword({ page: newPage, email, password: newPassword })
|
||||
await expect(newPage.getByText(/you are authenticated/i)).toBeVisible()
|
||||
@@ -36,12 +43,18 @@ test('should not accept an invalid email', async ({ page }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /profile/i }).click()
|
||||
await newPage.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const newPassword = faker.internet.password(2)
|
||||
|
||||
await newPage.getByPlaceholder(/new password/i).fill(newPassword)
|
||||
await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
|
||||
// await newPage.locator('h1:has-text("Change password") + div button:has-text("Change")').click()
|
||||
|
||||
await newPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Change passwordChange$/ })
|
||||
.getByRole('button')
|
||||
.click()
|
||||
|
||||
await expect(newPage.getByText(/password is incorrectly formatted/i)).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -12,10 +12,12 @@ test('should upload a single file', async ({ page }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /storage/i }).click()
|
||||
await newPage.getByRole('link', { name: /storage/i }).click()
|
||||
|
||||
await newPage
|
||||
.getByRole('button', { name: /drag a file here or click to select/i })
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Drag a file here or click to select$/ })
|
||||
.nth(1)
|
||||
.locator('input[type=file]')
|
||||
.setInputFiles({
|
||||
buffer: Buffer.from('file contents', 'utf-8'),
|
||||
@@ -23,7 +25,7 @@ test('should upload a single file', async ({ page }) => {
|
||||
mimeType: 'text/plain'
|
||||
})
|
||||
|
||||
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
|
||||
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should upload two files using the same single file uploader', async ({ page }) => {
|
||||
@@ -36,10 +38,12 @@ test('should upload two files using the same single file uploader', async ({ pag
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /storage/i }).click()
|
||||
await newPage.getByRole('link', { name: /storage/i }).click()
|
||||
|
||||
await newPage
|
||||
.getByRole('button', { name: /drag a file here or click to select/i })
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Drag a file here or click to select$/ })
|
||||
.nth(1)
|
||||
.locator('input[type=file]')
|
||||
.setInputFiles({
|
||||
buffer: Buffer.from('file contents 1', 'utf-8'),
|
||||
@@ -47,10 +51,12 @@ test('should upload two files using the same single file uploader', async ({ pag
|
||||
mimeType: 'text/plain'
|
||||
})
|
||||
|
||||
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
|
||||
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
|
||||
|
||||
await newPage
|
||||
.getByRole('button', { name: /successfully uploaded/i })
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Uploaded successfully$/ })
|
||||
.nth(1)
|
||||
.locator('input[type=file]')
|
||||
.setInputFiles({
|
||||
buffer: Buffer.from('file contents 2', 'utf-8'),
|
||||
@@ -58,7 +64,7 @@ test('should upload two files using the same single file uploader', async ({ pag
|
||||
mimeType: 'text/plain'
|
||||
})
|
||||
|
||||
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
|
||||
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should upload multiple files at once', async ({ page }) => {
|
||||
@@ -71,10 +77,12 @@ test('should upload multiple files at once', async ({ page }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await newPage.getByRole('button', { name: /storage/i }).click()
|
||||
await newPage.getByRole('link', { name: /storage/i }).click()
|
||||
|
||||
await newPage
|
||||
.getByRole('button', { name: /drag files here or click to select/i })
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Drag a file here or click to select$/ })
|
||||
.nth(3)
|
||||
.locator('input[type=file]')
|
||||
.setInputFiles([
|
||||
{
|
||||
@@ -89,9 +97,9 @@ test('should upload multiple files at once', async ({ page }) => {
|
||||
}
|
||||
])
|
||||
|
||||
await expect(newPage.getByRole('row').nth(0)).toHaveText('file1.txt')
|
||||
await expect(newPage.getByRole('row').nth(1)).toHaveText('file2.txt')
|
||||
await expect(newPage.getByText('file1.txt')).toBeVisible()
|
||||
await expect(newPage.getByText('file2.txt')).toBeVisible()
|
||||
await newPage.getByRole('button', { name: /upload/i }).click()
|
||||
|
||||
await expect(newPage.getByText(/successfully uploaded/i)).toBeVisible()
|
||||
await expect(newPage.getByText(/Uploaded successfully/i)).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -31,11 +31,11 @@ test('should sign in automatically with a refresh token', async ({ page }) => {
|
||||
await clearStorage({ page: newPage })
|
||||
await newPage.reload()
|
||||
|
||||
await expect(newPage.getByText(/sign in to the application/i)).toBeVisible()
|
||||
await expect(newPage.getByText(/sign in/i).nth(1)).toBeVisible()
|
||||
|
||||
// User should be signed in automatically
|
||||
await newPage.goto(`${baseURL}/profile#refreshToken=${refreshToken}`)
|
||||
await expect(newPage.getByText(/profile page/i)).toBeVisible()
|
||||
await newPage.goto(`${baseURL}/profile?refreshToken=${refreshToken}`)
|
||||
await expect(newPage.getByRole('heading', { name: 'Profile' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should fail automatic sign-in when network is not available', async ({ page }) => {
|
||||
@@ -61,11 +61,11 @@ test('should fail automatic sign-in when network is not available', async ({ pag
|
||||
await clearStorage({ page: newPage })
|
||||
await newPage.reload()
|
||||
|
||||
await expect(newPage.getByText(/sign in to the application/i)).toBeVisible()
|
||||
await expect(newPage.getByText(/sign in/i).nth(1)).toBeVisible()
|
||||
await newPage.route(`${authBackendURL}/**`, (route) => route.abort('internetdisconnected'))
|
||||
|
||||
// User should be signed in automatically
|
||||
await newPage.goto(`${baseURL}/profile#refreshToken=${refreshToken}`)
|
||||
await newPage.goto(`${baseURL}/profile?refreshToken=${refreshToken}`)
|
||||
await expect(
|
||||
newPage.getByText(/could not sign in automatically. retrying to get user information/i)
|
||||
).toBeVisible()
|
||||
|
||||
@@ -26,13 +26,13 @@ test.beforeAll(async ({ browser }) => {
|
||||
|
||||
const newPage = await verifyEmail({ page, email, context: page.context() })
|
||||
await expect(newPage.getByText(/you are authenticated/i)).toBeVisible()
|
||||
await newPage.getByRole('button', { name: /sign out/i }).click()
|
||||
await newPage.getByRole('link', { name: /sign out/i }).click()
|
||||
|
||||
page = newPage
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
await page.getByRole('button', { name: /sign out/i }).click()
|
||||
await page.getByRole('link', { name: /sign out/i }).click()
|
||||
})
|
||||
|
||||
test.afterAll(() => {
|
||||
@@ -53,7 +53,7 @@ test('should activate and sign in with MFA', async () => {
|
||||
|
||||
await signInWithEmailAndPassword({ page, email, password })
|
||||
await page.waitForURL(baseURL)
|
||||
await page.getByRole('button', { name: /profile/i }).click()
|
||||
await page.getByRole('link', { name: /profile/i }).click()
|
||||
await page.getByRole('button', { name: /generate/i }).click()
|
||||
|
||||
const image = page.getByAltText(/qrcode/i)
|
||||
@@ -70,9 +70,8 @@ test('should activate and sign in with MFA', async () => {
|
||||
await page.getByPlaceholder(/enter activation code/i).fill(code)
|
||||
await page.getByRole('button', { name: /activate/i }).click()
|
||||
await expect(page.getByText(/mfa has been activated/i)).toBeVisible()
|
||||
await page.getByRole('button', { name: /sign out/i }).click()
|
||||
await page.getByRole('link', { name: /sign out/i }).click()
|
||||
|
||||
await page.getByRole('button', { name: /continue with email \+ password/i }).click()
|
||||
await signInWithEmailAndPassword({ page, email, password })
|
||||
await expect(page.getByText(/send 2-step verification code/i)).toBeVisible()
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ test('should deanonymize with email and password', async ({ page, context }) =>
|
||||
await page.goto('/')
|
||||
|
||||
await signInAnonymously({ page })
|
||||
await page.getByRole('button', { name: /profile/i }).click()
|
||||
await page.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const userData = await getUserData(page)
|
||||
|
||||
@@ -24,7 +24,7 @@ test('should deanonymize with email and password', async ({ page, context }) =>
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const authenticatedPage = await verifyEmail({ page, context, email })
|
||||
await authenticatedPage.getByRole('button', { name: /profile/i }).click()
|
||||
await authenticatedPage.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const updatedUserData = await getUserData(authenticatedPage)
|
||||
expect(updatedUserData.id).toBe(userData.id)
|
||||
@@ -37,7 +37,7 @@ test('should deanonymize with a magic link', async ({ page, context }) => {
|
||||
await page.goto('/')
|
||||
|
||||
await signInAnonymously({ page })
|
||||
await page.getByRole('button', { name: /profile/i }).click()
|
||||
await page.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const userData = await getUserData(page)
|
||||
|
||||
@@ -45,7 +45,7 @@ test('should deanonymize with a magic link', async ({ page, context }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const authenticatedPage = await verifyMagicLink({ page, context, email })
|
||||
await authenticatedPage.getByRole('button', { name: /profile/i }).click()
|
||||
await authenticatedPage.getByRole('link', { name: /profile/i }).click()
|
||||
|
||||
const updatedUserData = await getUserData(authenticatedPage)
|
||||
expect(updatedUserData.id).toBe(userData.id)
|
||||
|
||||
@@ -16,7 +16,7 @@ test('should sign up with email and password', async ({ page, context }) => {
|
||||
})
|
||||
|
||||
test('should raise an error when trying to sign up with an existing email', async ({ page }) => {
|
||||
page.goto('/')
|
||||
await page.goto('/')
|
||||
|
||||
const email = faker.internet.email()
|
||||
const password = faker.internet.password()
|
||||
@@ -27,6 +27,8 @@ test('should raise an error when trying to sign up with an existing email', asyn
|
||||
// close modal
|
||||
await page.getByRole('dialog').getByRole('button').click()
|
||||
|
||||
await page.goto('/')
|
||||
|
||||
await signUpWithEmailAndPassword({ page, email, password })
|
||||
await expect(page.getByText(/email already in use/i)).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -11,8 +11,12 @@ test('should sign up with a magic link', async ({ page, context }) => {
|
||||
await expect(page.getByText(/verification email sent/i)).toBeVisible()
|
||||
|
||||
const authenticatedPage = await verifyMagicLink({ page, context, email })
|
||||
await authenticatedPage.getByRole('button', { name: /home/i }).click()
|
||||
await expect(authenticatedPage.getByText(/you are authenticated/i)).toBeVisible()
|
||||
await authenticatedPage.getByRole('link', { name: /home/i }).click()
|
||||
await expect(
|
||||
authenticatedPage.getByText(
|
||||
/You are authenticated. You have now access to the authorised part of the application./i
|
||||
)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should fail when network is not available', async ({ page }) => {
|
||||
|
||||