Compare commits
10 Commits
@nhost/das
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82728da100 | ||
|
|
2d68fee54c | ||
|
|
35010353c7 | ||
|
|
aff059ec71 | ||
|
|
713d53cfc0 | ||
|
|
e0ab6d9a37 | ||
|
|
7baee8a9cc | ||
|
|
3db2999f60 | ||
|
|
3c4dd55045 | ||
|
|
92b434e840 |
@@ -1,5 +1,26 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 1.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@9.0.2
|
||||
- @nhost/nextjs@2.1.4
|
||||
|
||||
## 1.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 713d53c: feat: add catch-all route for workspace/project - useful for documentation
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3db2999: fix: refresh table list after running SQL using the editor
|
||||
- 3c4dd55: fix: handle `Error` objects properly in the `ErrorToast` component
|
||||
- 92b434e: fix: resolve an issue where the checkbox in the data-grid header did not select all rows
|
||||
- @nhost/react-apollo@9.0.1
|
||||
- @nhost/nextjs@2.1.3
|
||||
|
||||
## 1.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -96,45 +96,52 @@ export default function DataGridHeader<T extends object>({
|
||||
}}
|
||||
key={column.id}
|
||||
>
|
||||
<Dropdown.Trigger
|
||||
className={twMerge(
|
||||
'focus:outline-none motion-safe:transition-colors',
|
||||
)}
|
||||
disabled={
|
||||
column.isDisabled ||
|
||||
column.id === 'selection' ||
|
||||
(column.disableSortBy && !onRemoveColumn)
|
||||
}
|
||||
hideChevron
|
||||
>
|
||||
{column.id === 'selection' ? (
|
||||
<span
|
||||
{...headerProps}
|
||||
className="relative grid w-full grid-flow-col items-center justify-between p-2"
|
||||
>
|
||||
{column.render('Header')}
|
||||
|
||||
{allowSort && (
|
||||
<Box component="span" sx={{ color: 'text.primary' }}>
|
||||
{column.isSorted && !column.isSortedDesc && (
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
)}
|
||||
|
||||
{column.isSorted && column.isSortedDesc && (
|
||||
<ArrowDownIcon className="h-3 w-3" />
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{allowResize && !column.disableResizing && (
|
||||
) : (
|
||||
<Dropdown.Trigger
|
||||
className={twMerge(
|
||||
'focus:outline-none motion-safe:transition-colors',
|
||||
)}
|
||||
disabled={
|
||||
column.isDisabled || (column.disableSortBy && !onRemoveColumn)
|
||||
}
|
||||
hideChevron
|
||||
>
|
||||
<span
|
||||
{...column.getResizerProps({
|
||||
onClick: (event: Event) => event.stopPropagation(),
|
||||
})}
|
||||
className="absolute top-0 bottom-0 -right-0.5 z-10 h-full w-1.5 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors"
|
||||
/>
|
||||
)}
|
||||
</Dropdown.Trigger>
|
||||
{...headerProps}
|
||||
className="relative grid w-full grid-flow-col items-center justify-between p-2"
|
||||
>
|
||||
{column.render('Header')}
|
||||
|
||||
{allowSort && (
|
||||
<Box component="span" sx={{ color: 'text.primary' }}>
|
||||
{column.isSorted && !column.isSortedDesc && (
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
)}
|
||||
|
||||
{column.isSorted && column.isSortedDesc && (
|
||||
<ArrowDownIcon className="h-3 w-3" />
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{allowResize && !column.disableResizing && (
|
||||
<span
|
||||
{...column.getResizerProps({
|
||||
onClick: (event: Event) => event.stopPropagation(),
|
||||
})}
|
||||
className="absolute -right-0.5 bottom-0 top-0 z-10 h-full w-1.5 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors"
|
||||
/>
|
||||
)}
|
||||
</Dropdown.Trigger>
|
||||
)}
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
|
||||
@@ -5,7 +5,7 @@ import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { getToastBackgroundColor } from '@/utils/constants/settings';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -20,6 +20,43 @@ interface ErrorDetails {
|
||||
error: any;
|
||||
}
|
||||
|
||||
const getInternalErrorMessage = (
|
||||
error: Error | ApolloError | undefined,
|
||||
): string | null => {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error instanceof ApolloError) {
|
||||
const internalError = error.graphQLErrors?.[0]?.extensions?.internal as
|
||||
| { error: { message: string } }
|
||||
| undefined;
|
||||
return internalError?.error?.message || null;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const errorToObject = (error: ApolloError | Error) => {
|
||||
if (error instanceof ApolloError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export default function ErrorToast({
|
||||
isVisible,
|
||||
errorMessage,
|
||||
@@ -28,7 +65,7 @@ export default function ErrorToast({
|
||||
}: {
|
||||
isVisible: boolean;
|
||||
errorMessage: string;
|
||||
error: ApolloError;
|
||||
error: ApolloError | Error;
|
||||
close: () => void;
|
||||
}) {
|
||||
const userData = useUserData();
|
||||
@@ -43,19 +80,10 @@ export default function ErrorToast({
|
||||
userId: userData?.id || 'local',
|
||||
url: asPath,
|
||||
},
|
||||
error,
|
||||
error: errorToObject(error),
|
||||
};
|
||||
|
||||
const internalError = error?.graphQLErrors?.at(0)?.extensions?.internal as {
|
||||
error: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
const msg =
|
||||
internalError?.error?.message ||
|
||||
error?.graphQLErrors?.at(0).message ||
|
||||
errorMessage;
|
||||
const msg = getInternalErrorMessage(error) || errorMessage;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useDatabaseQuery } from '@/features/database/dataGrid/hooks/useDatabaseQuery';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { parseIdentifiersFromSQL } from '@/utils/sql';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function useRunSQL(
|
||||
sqlCode: string,
|
||||
@@ -22,6 +24,14 @@ export default function useRunSQL(
|
||||
const [columns, setColumns] = useState<string[]>([]);
|
||||
const [rows, setRows] = useState<string[][]>([[]]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
query: { dataSourceSlug },
|
||||
} = router;
|
||||
|
||||
const { refetch } = useDatabaseQuery([dataSourceSlug as string]);
|
||||
|
||||
const appUrl = generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
@@ -269,6 +279,9 @@ export default function useRunSQL(
|
||||
}
|
||||
}
|
||||
|
||||
// refresh the table list after running the sql
|
||||
await refetch();
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
158
dashboard/src/pages/_/_/[...slug].tsx
Normal file
158
dashboard/src/pages/_/_/[...slug].tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
type GetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { Divider } from '@mui/material';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ChangeEvent, ReactElement } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type Workspace = Omit<
|
||||
GetAllWorkspacesAndProjectsQuery['workspaces'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function SelectWorkspaceAndProject() {
|
||||
const user = useUserData();
|
||||
const router = useRouter();
|
||||
|
||||
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const workspaces: Workspace[] = data?.workspaces || [];
|
||||
|
||||
const projects = workspaces.flatMap((workspace) =>
|
||||
workspace.projects.map((project) => ({
|
||||
workspaceName: workspace.name,
|
||||
projectName: project.name,
|
||||
value: `${workspace.slug}/${project.slug}`,
|
||||
})),
|
||||
);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const handleFilterChange = useMemo(
|
||||
() =>
|
||||
debounce((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, 200),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||
|
||||
const goToProjectPage = async (project: {
|
||||
workspaceName: string;
|
||||
projectName: string;
|
||||
value: string;
|
||||
}) => {
|
||||
const { slug } = router.query;
|
||||
|
||||
await router.push({
|
||||
pathname: `/${project.value}/${
|
||||
Array.isArray(slug) ? slug.join('/') : slug
|
||||
}`,
|
||||
});
|
||||
};
|
||||
|
||||
const projectsToDisplay = filter
|
||||
? projects.filter((project) =>
|
||||
project.projectName.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
: projects;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading workspaces and projects..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="mx-auto grid max-w-[760px] grid-flow-row gap-4 py-6 sm:py-14">
|
||||
<Text variant="h2" component="h1" className="">
|
||||
Select a Project
|
||||
</Text>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex w-full">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
onChange={handleFilterChange}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
{projectsToDisplay.length === 0 ? (
|
||||
<Box className="h-import py-2">
|
||||
<Text variant="subtitle2">No results found.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<List className="h-import overflow-y-auto">
|
||||
{projectsToDisplay.map((project, index) => (
|
||||
<Fragment key={project.value}>
|
||||
<ListItem.Root
|
||||
className="grid grid-flow-col justify-start gap-2 py-2.5"
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="primary"
|
||||
onClick={() => goToProjectPage(project)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItem.Avatar>
|
||||
<span className="inline-block h-6 w-6 overflow-hidden rounded-md">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
</ListItem.Avatar>
|
||||
<ListItem.Text
|
||||
primary={project.projectName}
|
||||
secondary={`${project.workspaceName} / ${project.projectName}`}
|
||||
/>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < projects.length - 1 && <Divider component="li" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
SelectWorkspaceAndProject.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="Select a Project">{page}</AuthenticatedLayout>
|
||||
);
|
||||
};
|
||||
@@ -29,6 +29,7 @@ import { Toaster } from 'react-hot-toast';
|
||||
const emotionCache = createEmotionCache();
|
||||
|
||||
process.env = {
|
||||
TEST_MODE: 'true',
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/cli
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.6
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/cli",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"main": "src/index.mjs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost-examples/codegen-react-apollo
|
||||
|
||||
## 0.1.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
- @nhost/react-apollo@9.0.2
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@9.0.1
|
||||
- @nhost/react@3.2.1
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-apollo",
|
||||
"version": "0.1.14",
|
||||
"version": "0.1.16",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/codegen-react-query
|
||||
|
||||
## 0.1.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
|
||||
## 0.1.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.1
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-query",
|
||||
"version": "0.1.15",
|
||||
"version": "0.1.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost-examples/react-urql
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
- @nhost/react-urql@6.0.2
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.1
|
||||
- @nhost/react-urql@6.0.1
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-urql",
|
||||
"private": true,
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.13",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost-examples/docker-compose
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- aff059e: fix: timers
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -42,6 +42,13 @@ services:
|
||||
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: public
|
||||
HASURA_GRAPHQL_LOG_LEVEL: debug
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: 'true'
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- curl http://localhost:8080/healthz > /dev/null 2>&1
|
||||
timeout: 60s
|
||||
interval: 30s
|
||||
start_period: 90s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.hasura.rule=Host(`${PROXY_HOST}`, `localhost`) && PathPrefix(`/`)"
|
||||
@@ -66,7 +73,7 @@ services:
|
||||
AUTH_SMTP_USER: user
|
||||
AUTH_SMTP_PASS: password
|
||||
AUTH_SMTP_SENDER: mail@example.com
|
||||
expose:
|
||||
expose:
|
||||
- 4000
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -112,7 +119,7 @@ services:
|
||||
- "traefik.http.routers.functions.middlewares=strip-functions@docker"
|
||||
- "traefik.http.routers.functions.entrypoints=web"
|
||||
restart: always
|
||||
expose:
|
||||
expose:
|
||||
- 3000
|
||||
volumes:
|
||||
- .:/opt/project
|
||||
@@ -140,7 +147,7 @@ services:
|
||||
SMTP_SECURE: "${AUTH_SMTP_SECURE:-false}"
|
||||
SMTP_SENDER: ${AUTH_SMTP_SENDER:-hbp@hbp.com}
|
||||
ports:
|
||||
- ${AUTH_SMTP_PORT:-1025}:1025
|
||||
- ${AUTH_SMTP_PORT:-1025}:1025
|
||||
- 8025:8025
|
||||
volumes:
|
||||
- ./data/mailhog:/maildir
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/docker-compose",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"e2e": "vitest run"
|
||||
|
||||
@@ -11,8 +11,12 @@ describe(
|
||||
beforeAll(async () => {
|
||||
// * Start docker compose
|
||||
await promisifiedExec(
|
||||
'docker compose -f docker-compose.yaml --env-file .env.example up --wait --quiet-pull'
|
||||
'docker compose -f docker-compose.yaml --env-file .env.example up --wait --wait-timeout 300 --quiet-pull'
|
||||
)
|
||||
|
||||
// we wait a bit extra because sometimes traefik takes a bit to configure the services
|
||||
setTimeout(() => {}, 30000);
|
||||
|
||||
}, 5 * 60 * 1000)
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/multi-tenant-one-to-many
|
||||
|
||||
## 2.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
|
||||
## 2.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.6
|
||||
|
||||
## 2.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/multi-tenant-one-to-many",
|
||||
"private": true,
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.6",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @nhost-examples/nextjs
|
||||
|
||||
## 0.1.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
- @nhost/react-apollo@9.0.2
|
||||
- @nhost/nextjs@2.1.4
|
||||
|
||||
## 0.1.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@9.0.1
|
||||
- @nhost/react@3.2.1
|
||||
- @nhost/nextjs@2.1.3
|
||||
|
||||
## 0.1.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs",
|
||||
"version": "0.1.16",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/node-storage
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.6
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/node-storage",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.10",
|
||||
"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.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.6
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs-server-components",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost-examples/react-apollo
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
- @nhost/react-apollo@9.0.2
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@9.0.1
|
||||
- @nhost/react@3.2.1
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-apollo",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.4",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/react-gqty
|
||||
|
||||
## 1.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.1
|
||||
|
||||
## 1.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-gqty",
|
||||
"private": true,
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @nhost-examples/vue-apollo
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
- @nhost/apollo@6.0.7
|
||||
- @nhost/vue@2.2.2
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e0ab6d9]
|
||||
- @nhost/apollo@6.0.6
|
||||
- @nhost/nhost-js@3.0.6
|
||||
- @nhost/vue@2.2.1
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-apollo",
|
||||
"private": true,
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.3",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @nhost-examples/vue-quickstart
|
||||
|
||||
## 0.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/apollo@6.0.7
|
||||
- @nhost/vue@2.2.2
|
||||
|
||||
## 0.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e0ab6d9]
|
||||
- @nhost/apollo@6.0.6
|
||||
- @nhost/vue@2.2.1
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-quickstart",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.15",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 6.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
|
||||
## 6.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e0ab6d9: fix: add extra logic to check and wait for a valid JWT
|
||||
- @nhost/nhost-js@3.0.6
|
||||
|
||||
## 6.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "6.0.5",
|
||||
"version": "6.0.7",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -65,7 +65,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"graphql": "16.8.1",
|
||||
"graphql-ws": "^5.14.3"
|
||||
"graphql-ws": "^5.14.3",
|
||||
"jwt-decode": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/client": "^3.9.4",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { setContext } from '@apollo/client/link/context'
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { AuthContext, NhostClient } from '@nhost/nhost-js'
|
||||
import jwtDecode, { JwtPayload } from 'jwt-decode'
|
||||
|
||||
import { createRestartableClient } from './ws'
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
@@ -58,25 +59,41 @@ export const createApolloClient = ({
|
||||
|
||||
let accessToken: AuthContext['accessToken'] | null = null
|
||||
|
||||
const isJwtValid = () => {
|
||||
if (!accessToken?.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const marginInSeconds = 3
|
||||
const marginInMilliseconds = marginInSeconds * 1000
|
||||
|
||||
let decodedToken: JwtPayload = jwtDecode(accessToken.value)
|
||||
return decodedToken.exp! * 1000 > Date.now() - marginInMilliseconds
|
||||
}
|
||||
|
||||
const isTokenValid = () =>
|
||||
!!accessToken?.value && !!accessToken?.expiresAt && accessToken?.expiresAt > new Date()
|
||||
!!accessToken?.value &&
|
||||
!!accessToken?.expiresAt &&
|
||||
accessToken?.expiresAt > new Date() &&
|
||||
isJwtValid()
|
||||
|
||||
const isTokenValidOrNull = () => !accessToken || isTokenValid()
|
||||
|
||||
const awaitValidTokenOrNull = () => {
|
||||
if (isTokenValidOrNull()) {
|
||||
return
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// doing this as an interval to avoid race conditions.
|
||||
const interval = setInterval(() => {
|
||||
if (isTokenValidOrNull()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
const waitForValidToken = () => {
|
||||
if (isTokenValidOrNull()) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => waitForValidToken().then(resolve), 100)
|
||||
})
|
||||
}
|
||||
|
||||
return waitForValidToken()
|
||||
}
|
||||
|
||||
const getAuthHeaders = async () => {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @nhost/react-apollo
|
||||
|
||||
## 9.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/apollo@6.0.7
|
||||
- @nhost/react@3.2.2
|
||||
|
||||
## 9.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e0ab6d9]
|
||||
- @nhost/apollo@6.0.6
|
||||
- @nhost/react@3.2.1
|
||||
|
||||
## 9.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-apollo",
|
||||
"version": "9.0.0",
|
||||
"version": "9.0.2",
|
||||
"description": "Nhost React Apollo client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/react-urql
|
||||
|
||||
## 6.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
|
||||
## 6.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.1
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-urql",
|
||||
"version": "6.0.0",
|
||||
"version": "6.0.2",
|
||||
"description": "Nhost React URQL client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/graphql-js
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2d68fee: fix: resolve an issue where unauthenticated graphql requests are not sent
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e0ab6d9: fix: add extra logic to check and wait for a valid JWT
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/graphql-js",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"description": "Nhost GraphQL client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -54,7 +54,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"isomorphic-unfetch": "^3.1.0"
|
||||
"isomorphic-unfetch": "^3.1.0",
|
||||
"jwt-decode": "^3.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
Variables
|
||||
} from './types'
|
||||
|
||||
import jwtDecode, { JwtPayload } from 'jwt-decode'
|
||||
|
||||
/**
|
||||
* @alias GraphQL
|
||||
*/
|
||||
@@ -28,6 +30,37 @@ export class NhostGraphqlClient {
|
||||
this.adminSecret = adminSecret
|
||||
}
|
||||
|
||||
private isAccessTokenValidOrNull = () => {
|
||||
if (!this.accessToken) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedToken: JwtPayload = jwtDecode(this.accessToken)
|
||||
return decodedToken.exp != null && decodedToken.exp * 1000 > Date.now()
|
||||
} catch (error) {
|
||||
console.error('Error decoding token:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private awaitForValidAccessTokenOrNull = async () => {
|
||||
if (this.isAccessTokenValidOrNull()) {
|
||||
return true
|
||||
}
|
||||
|
||||
const waitForValidTokenOrNull = () => {
|
||||
if (this.isAccessTokenValidOrNull()) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => waitForValidTokenOrNull().then(resolve), 100)
|
||||
})
|
||||
}
|
||||
|
||||
return waitForValidTokenOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.graphql.request` to send a GraphQL request. For more serious GraphQL usage we recommend using a GraphQL client such as Apollo Client (https://www.apollographql.com/docs/react).
|
||||
*
|
||||
@@ -70,6 +103,12 @@ export class NhostGraphqlClient {
|
||||
|
||||
const { headers, ...otherOptions } = config || {}
|
||||
const { query, operationName } = resolveRequestDocument(requestOptions.document)
|
||||
|
||||
if (!process.env.TEST_MODE) {
|
||||
// We skip this while running unit tests because the accessToken is generated using faker
|
||||
await this.awaitForValidAccessTokenOrNull()
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(this.httpUrl, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost/hasura-auth-js
|
||||
|
||||
## 2.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7baee8a: fix(hasura-auth-js): replace `jwt-decode` with `jose` for decoding access tokens that works on both the browser and Node.js
|
||||
- e0ab6d9: fix: add extra logic to check and wait for a valid JWT
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/hasura-auth-js",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"description": "Hasura-auth client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -66,8 +66,8 @@
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^6.2.2",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"jose": "^5.2.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"xstate": "^4.38.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import jwt_decode from 'jwt-decode'
|
||||
import * as jose from 'jose'
|
||||
import { interpret } from 'xstate'
|
||||
import {
|
||||
EMAIL_NEEDS_VERIFICATION,
|
||||
@@ -625,7 +625,7 @@ export class HasuraAuthClient {
|
||||
public getDecodedAccessToken(): JWTClaims | null {
|
||||
const jwt = this.getAccessToken()
|
||||
if (!jwt) return null
|
||||
return jwt_decode<JWTClaims>(jwt)
|
||||
return jose.decodeJwt<JWTClaims>(jwt)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -687,9 +687,16 @@ export const createAuthMachine = ({
|
||||
isAutoRefreshDisabled: () => !autoRefreshToken,
|
||||
refreshTimerShouldRefresh: (ctx) => {
|
||||
const { expiresAt } = ctx.accessToken
|
||||
|
||||
if (!expiresAt) {
|
||||
return false
|
||||
}
|
||||
|
||||
// This happens when either the computer goes to sleep or when Chrome descides to suspend the tab
|
||||
if (expiresAt.getTime() < Date.now()) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (ctx.refreshTimer.lastAttempt) {
|
||||
// * If the refresh timer reached the maximum number of attempts, we should not try again
|
||||
if (ctx.refreshTimer.attempts > REFRESH_TOKEN_MAX_ATTEMPTS) {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/nextjs
|
||||
|
||||
## 2.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.2
|
||||
|
||||
## 2.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.2.1
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/nextjs",
|
||||
"version": "2.1.2",
|
||||
"version": "2.1.4",
|
||||
"description": "Nhost NextJS library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @nhost/nhost-js
|
||||
|
||||
## 3.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2d68fee]
|
||||
- @nhost/graphql-js@0.1.7
|
||||
|
||||
## 3.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [7baee8a]
|
||||
- Updated dependencies [e0ab6d9]
|
||||
- @nhost/hasura-auth-js@2.3.1
|
||||
- @nhost/graphql-js@0.1.6
|
||||
|
||||
## 3.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/nhost-js",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.7",
|
||||
"description": "Nhost JavaScript SDK",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/react
|
||||
|
||||
## 3.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
|
||||
## 3.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.6
|
||||
|
||||
## 3.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.2",
|
||||
"description": "Nhost React library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/vue
|
||||
|
||||
## 2.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.7
|
||||
|
||||
## 2.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.6
|
||||
|
||||
## 2.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/vue",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"description": "Nhost Vue library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -1407,6 +1407,9 @@ importers:
|
||||
graphql-ws:
|
||||
specifier: ^5.14.3
|
||||
version: 5.14.3(graphql@16.8.1)
|
||||
jwt-decode:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
devDependencies:
|
||||
'@apollo/client':
|
||||
specifier: ^3.9.4
|
||||
@@ -1575,6 +1578,9 @@ importers:
|
||||
isomorphic-unfetch:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
jwt-decode:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
devDependencies:
|
||||
'@nhost/docgen':
|
||||
specifier: workspace:*
|
||||
@@ -1591,12 +1597,12 @@ importers:
|
||||
fetch-ponyfill:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
jose:
|
||||
specifier: ^5.2.2
|
||||
version: 5.2.2
|
||||
js-cookie:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
jwt-decode:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
xstate:
|
||||
specifier: ^4.38.3
|
||||
version: 4.38.3
|
||||
@@ -5938,7 +5944,7 @@ packages:
|
||||
/@graphql-tools/utils@8.13.1(graphql@16.8.1):
|
||||
resolution: {integrity: sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==}
|
||||
peerDependencies:
|
||||
graphql: '>=16.8.1'
|
||||
graphql: 16.8.1
|
||||
dependencies:
|
||||
graphql: 16.8.1
|
||||
tslib: 2.6.2
|
||||
@@ -19441,6 +19447,10 @@ packages:
|
||||
resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==}
|
||||
dev: true
|
||||
|
||||
/jose@5.2.2:
|
||||
resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==}
|
||||
dev: false
|
||||
|
||||
/jpeg-js@0.4.4:
|
||||
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
|
||||
dev: true
|
||||
|
||||
Reference in New Issue
Block a user