Compare commits
34 Commits
@nhost/das
...
@nhost/apo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
262828f9a1 | ||
|
|
12f9726ad7 | ||
|
|
845937b552 | ||
|
|
f777a3380a | ||
|
|
5081372cab | ||
|
|
82212345c8 | ||
|
|
32d3f167c5 | ||
|
|
3d5f1ea922 | ||
|
|
97841ee5e8 | ||
|
|
4f3a615ebe | ||
|
|
8e8197691c | ||
|
|
e10389ecf6 | ||
|
|
cbdf6affec | ||
|
|
d19406e694 | ||
|
|
cffc5dc65b | ||
|
|
2b5cb58553 | ||
|
|
7459a9413e | ||
|
|
56871cc9f7 | ||
|
|
8f4d66e52d | ||
|
|
315a820073 | ||
|
|
ca57ad2cbd | ||
|
|
40259344eb | ||
|
|
4749f60a08 | ||
|
|
ac1888514d | ||
|
|
49b4af439b | ||
|
|
61e03d6c70 | ||
|
|
bec0fce497 | ||
|
|
c01568a7dd | ||
|
|
e934216a82 | ||
|
|
701d6b8c84 | ||
|
|
e158e2440a | ||
|
|
fbaa657001 | ||
|
|
559db6d0ec | ||
|
|
4c844930f1 |
@@ -5,6 +5,5 @@
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@nhost-examples/sveltekit"]
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
||||
10
.github/workflows/changesets.yaml
vendored
10
.github/workflows/changesets.yaml
vendored
@@ -141,16 +141,6 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
- name: Create GitHub Release
|
||||
uses: taiki-e/create-gh-release-action@v1
|
||||
with:
|
||||
changelog: dashboard/CHANGELOG.md
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: ${{ env.DASHBOARD_PACKAGE }}@
|
||||
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
- name: Remove tag on failure
|
||||
if: failure()
|
||||
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,5 +2,6 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"eslint.workingDirectories": ["./dashboard"]
|
||||
"eslint.workingDirectories": ["./dashboard"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.20.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.38
|
||||
|
||||
## 0.20.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e10389ecf: fix(dashboard): disable run tab when developing locally
|
||||
- @nhost/react-apollo@5.0.37
|
||||
|
||||
## 0.20.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c01568a7d: chore(dashboard): show alert to update oauth providers
|
||||
|
||||
## 0.20.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.20.22",
|
||||
"version": "0.20.25",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
|
||||
export default function DepricationNotice() {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
return (
|
||||
!currentProject?.providersUpdated && (
|
||||
<Alert severity="warning" className="grid place-content-center">
|
||||
<Text color="warning" className="max-w-3xl text-sm">
|
||||
On December 1st the old backend domain will cease to work. You need to
|
||||
make sure your client is instantiated using the subdomain and region
|
||||
and update your oauth2 settings. You can find more information{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
href="https://github.com/nhost/nhost/discussions/2303"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ContactUs } from './DepricationNotice';
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import type { SettingsSidebarProps } from '@/components/layout/SettingsSidebar';
|
||||
@@ -45,44 +46,47 @@ export default function SettingsLayout({
|
||||
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex flex-col flex-auto w-full overflow-scroll overflow-x-hidden"
|
||||
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
{hasGitRepo && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="grid grid-flow-row gap-2 place-content-center"
|
||||
>
|
||||
<Text color="warning" className="text-sm ">
|
||||
As you have a connected repository, make sure to synchronize
|
||||
your changes with{' '}
|
||||
<code
|
||||
className={twMerge(
|
||||
'rounded-md px-2 py-px',
|
||||
theme.palette.mode === 'dark'
|
||||
? 'bg-brown text-copper'
|
||||
: 'bg-slate-200 text-slate-700',
|
||||
)}
|
||||
>
|
||||
nhost config pull
|
||||
</code>{' '}
|
||||
or they may be reverted with the next push.
|
||||
<br />
|
||||
If there are multiple projects linked to the same repository and
|
||||
you only want these changes to apply to a subset of them, please
|
||||
check out{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
href="https://docs.nhost.io/cli/overlays"
|
||||
>
|
||||
docs.nhost.io/cli/overlays
|
||||
</a>{' '}
|
||||
for guidance.
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<DepricationNotice />
|
||||
{hasGitRepo && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="grid grid-flow-row place-content-center gap-2"
|
||||
>
|
||||
<Text color="warning" className="text-sm ">
|
||||
As you have a connected repository, make sure to synchronize
|
||||
your changes with{' '}
|
||||
<code
|
||||
className={twMerge(
|
||||
'rounded-md px-2 py-px',
|
||||
theme.palette.mode === 'dark'
|
||||
? 'bg-brown text-copper'
|
||||
: 'bg-slate-200 text-slate-700',
|
||||
)}
|
||||
>
|
||||
nhost config pull
|
||||
</code>{' '}
|
||||
or they may be reverted with the next push.
|
||||
<br />
|
||||
If there are multiple projects linked to the same repository
|
||||
and you only want these changes to apply to a subset of them,
|
||||
please check out{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
href="https://docs.nhost.io/cli/overlays"
|
||||
>
|
||||
docs.nhost.io/cli/overlays
|
||||
</a>{' '}
|
||||
for guidance.
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</RetryableErrorBoundary>
|
||||
</Box>
|
||||
|
||||
100
dashboard/src/components/settings/ProvidersUpdatedAlert.tsx
Normal file
100
dashboard/src/components/settings/ProvidersUpdatedAlert.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { useConfirmProvidersUpdatedMutation } from '@/utils/__generated__/graphql';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function ProvidersUpdatedAlert() {
|
||||
const theme = useTheme();
|
||||
const { openAlertDialog } = useDialog();
|
||||
const [confirmed, setConfirmed] = useState(true);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [confirmProvidersUpdated] = useConfirmProvidersUpdatedMutation({
|
||||
variables: { id: currentProject?.id },
|
||||
});
|
||||
|
||||
async function handleSubmitConfirmation() {
|
||||
const confirmProvidersUpdatedPromise = confirmProvidersUpdated();
|
||||
|
||||
await toast.promise(
|
||||
confirmProvidersUpdatedPromise,
|
||||
{
|
||||
loading: 'Confirming...',
|
||||
success: 'Your settings have been updated successfully.',
|
||||
error: 'An error occurred while trying to confirm the message.',
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
setConfirmed(false);
|
||||
}
|
||||
|
||||
function handleOpenConfirmationDialog() {
|
||||
openAlertDialog({
|
||||
title: 'Confirm all providers updated?',
|
||||
payload: (
|
||||
<Text variant="subtitle1" component="span">
|
||||
Please make sure to update all providers before continuing. Your
|
||||
sign-in flows might break if you don't.
|
||||
</Text>
|
||||
),
|
||||
props: {
|
||||
onPrimaryAction: handleSubmitConfirmation,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="grid items-center grid-flow-row gap-2 p-4 place-items-center lg:grid-flow-col lg:place-content-between"
|
||||
>
|
||||
<div className="grid grid-flow-row gap-1 text-left">
|
||||
<Text className="font-semibold">
|
||||
Please update the Redirect URL for all providers being used
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm+">
|
||||
We are deprecating your project's old DNS name in favor of
|
||||
individual DNS names for each service. Please make sure to update your
|
||||
providers to use the new auth specific URL under <b>Redirect URL</b>{' '}
|
||||
before the 1st of February 2023.{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/nhost/discussions/1319"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
className="font-medium"
|
||||
>
|
||||
Read the discussion here.
|
||||
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
|
||||
</Link>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
className={
|
||||
theme.palette.mode === 'dark'
|
||||
? 'text-white hover:bg-brown'
|
||||
: 'text-black hover:bg-orange-300'
|
||||
}
|
||||
onClick={handleOpenConfirmationDialog}
|
||||
>
|
||||
I have updated all Redirect URLs
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/components/settings/index.ts
Normal file
1
dashboard/src/components/settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ProvidersUpdatedAlert } from './ProvidersUpdatedAlert';
|
||||
@@ -142,6 +142,7 @@ export default function useProjectRoutes() {
|
||||
exact: false,
|
||||
label: 'Run',
|
||||
icon: <ServicesIcon />,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
...nhostRoutes,
|
||||
];
|
||||
|
||||
5
dashboard/src/gql/settings/confirmProvidersUpdated.gql
Normal file
5
dashboard/src/gql/settings/confirmProvidersUpdated.gql
Normal file
@@ -0,0 +1,5 @@
|
||||
mutation confirmProvidersUpdated($id: uuid!) {
|
||||
updateApp(pk_columns: { id: $id }, _set: { providersUpdated: true }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
@@ -67,5 +68,10 @@ export default function BackupsPage() {
|
||||
}
|
||||
|
||||
BackupsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
||||
import { DataBrowserEmptyState } from '@/features/database/dataGrid/components/DataBrowserEmptyState';
|
||||
import { DataBrowserLayout } from '@/features/database/dataGrid/components/DataBrowserLayout';
|
||||
@@ -35,5 +36,10 @@ export default function DataBrowserDatabaseDetailsPage() {
|
||||
DataBrowserDatabaseDetailsPage.getLayout = function getLayout(
|
||||
page: ReactElement,
|
||||
) {
|
||||
return <DataBrowserLayout>{page}</DataBrowserLayout>;
|
||||
return (
|
||||
<DataBrowserLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</DataBrowserLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import type { DeploymentStatus } from '@/components/presentational/StatusCircle';
|
||||
@@ -105,7 +106,7 @@ export default function DeploymentDetailsPage() {
|
||||
<Text color="secondary">{relativeDateOfDeployment}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" flex items-center">
|
||||
<div className="flex items-center ">
|
||||
<Link
|
||||
className="self-center font-mono font-medium"
|
||||
target="_blank"
|
||||
@@ -139,7 +140,7 @@ export default function DeploymentDetailsPage() {
|
||||
|
||||
{deployment.deploymentLogs.map((log) => (
|
||||
<div key={log.id} className="flex font-mono">
|
||||
<div className=" mr-2 flex-shrink-0">
|
||||
<div className="mr-2 flex-shrink-0 ">
|
||||
{format(parseISO(log.createdAt), 'HH:mm:ss')}:
|
||||
</div>
|
||||
<div className="break-all">{log.message}</div>
|
||||
@@ -152,5 +153,10 @@ export default function DeploymentDetailsPage() {
|
||||
}
|
||||
|
||||
DeploymentDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
@@ -67,5 +68,10 @@ export default function DeploymentsPage() {
|
||||
}
|
||||
|
||||
DeploymentsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
@@ -351,6 +352,7 @@ GraphQLPage.getLayout = function getLayout(page: ReactElement) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
@@ -114,5 +115,10 @@ export default function HasuraPage() {
|
||||
}
|
||||
|
||||
HasuraPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { ApplicationErrored } from '@/features/projects/common/components/ApplicationErrored';
|
||||
import { ApplicationLive } from '@/features/projects/common/components/ApplicationLive';
|
||||
@@ -49,5 +50,10 @@ export default function AppIndexPage() {
|
||||
}
|
||||
|
||||
AppIndexPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
@@ -134,5 +135,10 @@ export default function LogsPage() {
|
||||
}
|
||||
|
||||
LogsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
@@ -126,5 +127,10 @@ export default function MetricsPage() {
|
||||
}
|
||||
|
||||
MetricsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/
|
||||
import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
|
||||
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
|
||||
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
|
||||
import {
|
||||
ServiceForm,
|
||||
@@ -43,7 +44,7 @@ export default function ServicesPage() {
|
||||
const router = useRouter();
|
||||
const { openDrawer, openAlertDialog } = useDialog();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const isPlanFree = currentProject.plan.isFree;
|
||||
const isPlanFree = currentProject?.plan?.isFree;
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(
|
||||
parseInt(router.query.page as string, 10) || 1,
|
||||
@@ -258,5 +259,10 @@ export default function ServicesPage() {
|
||||
}
|
||||
|
||||
ServicesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { SettingsLayout } from '@/components/layout/SettingsLayout';
|
||||
import { ProvidersUpdatedAlert } from '@/components/settings';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { AnonymousSignInSettings } from '@/features/authentication/settings/components/AnonymousSignInSettings';
|
||||
import { AppleProviderSettings } from '@/features/authentication/settings/components/AppleProviderSettings';
|
||||
@@ -54,6 +55,7 @@ export default function SettingsSignInMethodsPage() {
|
||||
<WebAuthnSettings />
|
||||
<AnonymousSignInSettings />
|
||||
<SMSSettings />
|
||||
{!currentProject.providersUpdated && <ProvidersUpdatedAlert />}
|
||||
<AppleProviderSettings />
|
||||
<AzureADProviderSettings />
|
||||
<DiscordProviderSettings />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
@@ -44,6 +45,7 @@ StoragePage.getLayout = function getLayout(page: ReactElement) {
|
||||
<ProjectLayout
|
||||
mainContainerProps={{ sx: { backgroundColor: 'background.default' } }}
|
||||
>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Pagination } from '@/components/common/Pagination';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
@@ -390,6 +391,7 @@ export default function UsersPage() {
|
||||
UsersPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<ProjectLayout contentContainerProps={{ className: 'h-full' }}>
|
||||
<DepricationNotice />
|
||||
{page}
|
||||
</ProjectLayout>
|
||||
);
|
||||
|
||||
107
dashboard/src/utils/__generated__/graphql.ts
generated
107
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -1285,6 +1285,26 @@ export type ConfigHasuraUpdateInput = {
|
||||
webhookSecret?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigIngress = {
|
||||
__typename?: 'ConfigIngress';
|
||||
fqdn?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type ConfigIngressComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigIngressComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigIngressComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigIngressComparisonExp>>;
|
||||
fqdn?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigIngressInsertInput = {
|
||||
fqdn?: InputMaybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type ConfigIngressUpdateInput = {
|
||||
fqdn?: InputMaybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type ConfigInsertConfigResponse = {
|
||||
__typename?: 'ConfigInsertConfigResponse';
|
||||
config: ConfigConfig;
|
||||
@@ -1374,6 +1394,26 @@ export type ConfigLocaleComparisonExp = {
|
||||
_nin?: InputMaybe<Array<Scalars['ConfigLocale']>>;
|
||||
};
|
||||
|
||||
export type ConfigNetworking = {
|
||||
__typename?: 'ConfigNetworking';
|
||||
ingresses?: Maybe<Array<ConfigIngress>>;
|
||||
};
|
||||
|
||||
export type ConfigNetworkingComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigNetworkingComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigNetworkingComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigNetworkingComparisonExp>>;
|
||||
ingresses?: InputMaybe<ConfigIngressComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigNetworkingInsertInput = {
|
||||
ingresses?: InputMaybe<Array<ConfigIngressInsertInput>>;
|
||||
};
|
||||
|
||||
export type ConfigNetworkingUpdateInput = {
|
||||
ingresses?: InputMaybe<Array<ConfigIngressUpdateInput>>;
|
||||
};
|
||||
|
||||
export type ConfigObservability = {
|
||||
__typename?: 'ConfigObservability';
|
||||
grafana: ConfigGrafana;
|
||||
@@ -1551,6 +1591,7 @@ export type ConfigProviderUpdateInput = {
|
||||
export type ConfigResources = {
|
||||
__typename?: 'ConfigResources';
|
||||
compute: ConfigResourcesCompute;
|
||||
networking?: Maybe<ConfigNetworking>;
|
||||
/** Number of replicas for a service */
|
||||
replicas: Scalars['ConfigUint8'];
|
||||
};
|
||||
@@ -1560,6 +1601,7 @@ export type ConfigResourcesComparisonExp = {
|
||||
_not?: InputMaybe<ConfigResourcesComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigResourcesComparisonExp>>;
|
||||
compute?: InputMaybe<ConfigResourcesComputeComparisonExp>;
|
||||
networking?: InputMaybe<ConfigNetworkingComparisonExp>;
|
||||
replicas?: InputMaybe<ConfigUint8ComparisonExp>;
|
||||
};
|
||||
|
||||
@@ -1591,11 +1633,13 @@ export type ConfigResourcesComputeUpdateInput = {
|
||||
|
||||
export type ConfigResourcesInsertInput = {
|
||||
compute: ConfigResourcesComputeInsertInput;
|
||||
networking?: InputMaybe<ConfigNetworkingInsertInput>;
|
||||
replicas: Scalars['ConfigUint8'];
|
||||
};
|
||||
|
||||
export type ConfigResourcesUpdateInput = {
|
||||
compute?: InputMaybe<ConfigResourcesComputeUpdateInput>;
|
||||
networking?: InputMaybe<ConfigNetworkingUpdateInput>;
|
||||
replicas?: InputMaybe<Scalars['ConfigUint8']>;
|
||||
};
|
||||
|
||||
@@ -1674,6 +1718,7 @@ export type ConfigRunServiceNameComparisonExp = {
|
||||
|
||||
export type ConfigRunServicePort = {
|
||||
__typename?: 'ConfigRunServicePort';
|
||||
ingress?: Maybe<ConfigIngress>;
|
||||
port: Scalars['ConfigPort'];
|
||||
publish?: Maybe<Scalars['Boolean']>;
|
||||
type: Scalars['String'];
|
||||
@@ -1683,18 +1728,21 @@ export type ConfigRunServicePortComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigRunServicePortComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigRunServicePortComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigRunServicePortComparisonExp>>;
|
||||
ingress?: InputMaybe<ConfigIngressComparisonExp>;
|
||||
port?: InputMaybe<ConfigPortComparisonExp>;
|
||||
publish?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
type?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigRunServicePortInsertInput = {
|
||||
ingress?: InputMaybe<ConfigIngressInsertInput>;
|
||||
port: Scalars['ConfigPort'];
|
||||
publish?: InputMaybe<Scalars['Boolean']>;
|
||||
type: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ConfigRunServicePortUpdateInput = {
|
||||
ingress?: InputMaybe<ConfigIngressUpdateInput>;
|
||||
port?: InputMaybe<Scalars['ConfigPort']>;
|
||||
publish?: InputMaybe<Scalars['Boolean']>;
|
||||
type?: InputMaybe<Scalars['String']>;
|
||||
@@ -1926,7 +1974,10 @@ export type ConfigStandardOauthProviderWithScopeUpdateInput = {
|
||||
export type ConfigStorage = {
|
||||
__typename?: 'ConfigStorage';
|
||||
antivirus?: Maybe<ConfigStorageAntivirus>;
|
||||
/** Resources for the service */
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
resources?: Maybe<ConfigResources>;
|
||||
/**
|
||||
* Version of storage service, you can see available versions in the URL below:
|
||||
@@ -4408,6 +4459,7 @@ export type AuthRefreshTokens = {
|
||||
id: Scalars['uuid'];
|
||||
metadata?: Maybe<Scalars['jsonb']>;
|
||||
refreshTokenHash?: Maybe<Scalars['String']>;
|
||||
refresh_token?: Maybe<Scalars['uuid']>;
|
||||
type: AuthRefreshTokenTypes_Enum;
|
||||
/** An object relationship */
|
||||
user: Users;
|
||||
@@ -4482,6 +4534,7 @@ export type AuthRefreshTokens_Bool_Exp = {
|
||||
id?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
metadata?: InputMaybe<Jsonb_Comparison_Exp>;
|
||||
refreshTokenHash?: InputMaybe<String_Comparison_Exp>;
|
||||
refresh_token?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
type?: InputMaybe<AuthRefreshTokenTypes_Enum_Comparison_Exp>;
|
||||
user?: InputMaybe<Users_Bool_Exp>;
|
||||
userId?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
@@ -4515,6 +4568,7 @@ export type AuthRefreshTokens_Insert_Input = {
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
metadata?: InputMaybe<Scalars['jsonb']>;
|
||||
refreshTokenHash?: InputMaybe<Scalars['String']>;
|
||||
refresh_token?: InputMaybe<Scalars['uuid']>;
|
||||
type?: InputMaybe<AuthRefreshTokenTypes_Enum>;
|
||||
user?: InputMaybe<Users_Obj_Rel_Insert_Input>;
|
||||
userId?: InputMaybe<Scalars['uuid']>;
|
||||
@@ -4527,6 +4581,7 @@ export type AuthRefreshTokens_Max_Fields = {
|
||||
expiresAt?: Maybe<Scalars['timestamptz']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
refreshTokenHash?: Maybe<Scalars['String']>;
|
||||
refresh_token?: Maybe<Scalars['uuid']>;
|
||||
userId?: Maybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
@@ -4536,6 +4591,7 @@ export type AuthRefreshTokens_Max_Order_By = {
|
||||
expiresAt?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
refreshTokenHash?: InputMaybe<Order_By>;
|
||||
refresh_token?: InputMaybe<Order_By>;
|
||||
userId?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
@@ -4546,6 +4602,7 @@ export type AuthRefreshTokens_Min_Fields = {
|
||||
expiresAt?: Maybe<Scalars['timestamptz']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
refreshTokenHash?: Maybe<Scalars['String']>;
|
||||
refresh_token?: Maybe<Scalars['uuid']>;
|
||||
userId?: Maybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
@@ -4555,6 +4612,7 @@ export type AuthRefreshTokens_Min_Order_By = {
|
||||
expiresAt?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
refreshTokenHash?: InputMaybe<Order_By>;
|
||||
refresh_token?: InputMaybe<Order_By>;
|
||||
userId?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
@@ -4581,6 +4639,7 @@ export type AuthRefreshTokens_Order_By = {
|
||||
id?: InputMaybe<Order_By>;
|
||||
metadata?: InputMaybe<Order_By>;
|
||||
refreshTokenHash?: InputMaybe<Order_By>;
|
||||
refresh_token?: InputMaybe<Order_By>;
|
||||
type?: InputMaybe<Order_By>;
|
||||
user?: InputMaybe<Users_Order_By>;
|
||||
userId?: InputMaybe<Order_By>;
|
||||
@@ -4609,6 +4668,8 @@ export enum AuthRefreshTokens_Select_Column {
|
||||
/** column name */
|
||||
RefreshTokenHash = 'refreshTokenHash',
|
||||
/** column name */
|
||||
RefreshToken = 'refresh_token',
|
||||
/** column name */
|
||||
Type = 'type',
|
||||
/** column name */
|
||||
UserId = 'userId'
|
||||
@@ -4621,6 +4682,7 @@ export type AuthRefreshTokens_Set_Input = {
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
metadata?: InputMaybe<Scalars['jsonb']>;
|
||||
refreshTokenHash?: InputMaybe<Scalars['String']>;
|
||||
refresh_token?: InputMaybe<Scalars['uuid']>;
|
||||
type?: InputMaybe<AuthRefreshTokenTypes_Enum>;
|
||||
userId?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
@@ -4640,6 +4702,7 @@ export type AuthRefreshTokens_Stream_Cursor_Value_Input = {
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
metadata?: InputMaybe<Scalars['jsonb']>;
|
||||
refreshTokenHash?: InputMaybe<Scalars['String']>;
|
||||
refresh_token?: InputMaybe<Scalars['uuid']>;
|
||||
type?: InputMaybe<AuthRefreshTokenTypes_Enum>;
|
||||
userId?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
@@ -4657,6 +4720,8 @@ export enum AuthRefreshTokens_Update_Column {
|
||||
/** column name */
|
||||
RefreshTokenHash = 'refreshTokenHash',
|
||||
/** column name */
|
||||
RefreshToken = 'refresh_token',
|
||||
/** column name */
|
||||
Type = 'type',
|
||||
/** column name */
|
||||
UserId = 'userId'
|
||||
@@ -22549,6 +22614,13 @@ export type UpdateRunServiceConfigMutationVariables = Exact<{
|
||||
|
||||
export type UpdateRunServiceConfigMutation = { __typename?: 'mutation_root', updateRunServiceConfig: { __typename?: 'ConfigRunServiceConfig', name: any } };
|
||||
|
||||
export type ConfirmProvidersUpdatedMutationVariables = Exact<{
|
||||
id: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ConfirmProvidersUpdatedMutation = { __typename?: 'mutation_root', updateApp?: { __typename?: 'apps', id: any } | null };
|
||||
|
||||
export type GetFreeAndActiveProjectsQueryVariables = Exact<{
|
||||
userId: Scalars['uuid'];
|
||||
}>;
|
||||
@@ -26102,6 +26174,39 @@ export function useUpdateRunServiceConfigMutation(baseOptions?: Apollo.MutationH
|
||||
export type UpdateRunServiceConfigMutationHookResult = ReturnType<typeof useUpdateRunServiceConfigMutation>;
|
||||
export type UpdateRunServiceConfigMutationResult = Apollo.MutationResult<UpdateRunServiceConfigMutation>;
|
||||
export type UpdateRunServiceConfigMutationOptions = Apollo.BaseMutationOptions<UpdateRunServiceConfigMutation, UpdateRunServiceConfigMutationVariables>;
|
||||
export const ConfirmProvidersUpdatedDocument = gql`
|
||||
mutation confirmProvidersUpdated($id: uuid!) {
|
||||
updateApp(pk_columns: {id: $id}, _set: {providersUpdated: true}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ConfirmProvidersUpdatedMutationFn = Apollo.MutationFunction<ConfirmProvidersUpdatedMutation, ConfirmProvidersUpdatedMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useConfirmProvidersUpdatedMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useConfirmProvidersUpdatedMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useConfirmProvidersUpdatedMutation` 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 [confirmProvidersUpdatedMutation, { data, loading, error }] = useConfirmProvidersUpdatedMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useConfirmProvidersUpdatedMutation(baseOptions?: Apollo.MutationHookOptions<ConfirmProvidersUpdatedMutation, ConfirmProvidersUpdatedMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ConfirmProvidersUpdatedMutation, ConfirmProvidersUpdatedMutationVariables>(ConfirmProvidersUpdatedDocument, options);
|
||||
}
|
||||
export type ConfirmProvidersUpdatedMutationHookResult = ReturnType<typeof useConfirmProvidersUpdatedMutation>;
|
||||
export type ConfirmProvidersUpdatedMutationResult = Apollo.MutationResult<ConfirmProvidersUpdatedMutation>;
|
||||
export type ConfirmProvidersUpdatedMutationOptions = Apollo.BaseMutationOptions<ConfirmProvidersUpdatedMutation, ConfirmProvidersUpdatedMutationVariables>;
|
||||
export const GetFreeAndActiveProjectsDocument = gql`
|
||||
query GetFreeAndActiveProjects($userId: uuid!) {
|
||||
freeAndActiveProjects: apps(
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.21.3'
|
||||
|
||||
10
examples/quickstarts/sveltekit/CHANGELOG.md
Normal file
10
examples/quickstarts/sveltekit/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
|
||||
## 0.2.0
|
||||
### Minor Changes
|
||||
|
||||
- 4749f60a0: updated the SvelteKit quickstart project to demonstrate how to use the Nhost SDK on the server
|
||||
'@nhost-examples/sveltekit': minor
|
||||
---
|
||||
|
||||
feat: add nhost with sveltekit example project
|
||||
87
examples/quickstarts/sveltekit/README.md
Normal file
87
examples/quickstarts/sveltekit/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Using Nhost with SvelteKit quickstart
|
||||
|
||||
This is a quickstart that showcases how to use the Nhost SDK running on server `load` functions and API routes that run exclusively on the server.
|
||||
|
||||
## Authentication
|
||||
|
||||
1. **Saving the auth session**
|
||||
|
||||
In order to utilize the Nhost SDK with an authenticated session on the server, it is necessary to securely store this session within a cookie. This should be done right after any **signIn** or **signUp** operation. See example [here](https://github.com/nhost/nhost/blob/main/examples/quickstarts/sveltekit/src/routes/auth/sign-in/email-password/+page.server.js).
|
||||
|
||||
2. **Oauth & refresh session server hook**
|
||||
|
||||
Create a server hook `src/hook.server.js` that calls the helper method `manageAuthSession`. Feel free to copy paste the `src/lib/nhost.js` to your project. The second argument for `manageAuthSession` is for handling the case where there's an error refreshing the current session with the `refreshToken` stored in the cookie.
|
||||
|
||||
```typescript
|
||||
import { manageAuthSession } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
await manageAuthSession(event, () => {
|
||||
throw redirect(303, '/auth/sign-in')
|
||||
})
|
||||
|
||||
return resolve(event)
|
||||
}
|
||||
```
|
||||
|
||||
3. **Protected routes**
|
||||
|
||||
To make sure only authenticated users access certain pages, you need to create a server root layout file `src/routes/+layout.server.js`. Within that file you define your public routes, so any other route would redirect if there's no session.
|
||||
|
||||
```typescript
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
const publicRoutes = [
|
||||
'/auth/sign-in',
|
||||
'/auth/sign-up',
|
||||
...
|
||||
]
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies, route }) {
|
||||
const nhost = await getNhost(cookies)
|
||||
const session = nhost.auth.getSession()
|
||||
|
||||
if (!publicRoutes.includes(route.id ?? '') && !session) {
|
||||
throw redirect(303, '/auth/sign-in')
|
||||
}
|
||||
|
||||
return {
|
||||
user: session?.user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Get Started
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nhost/nhost
|
||||
cd nhost
|
||||
```
|
||||
|
||||
2. Install and build dependencies
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Terminal 1: Start the Nhost Backend
|
||||
|
||||
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
|
||||
|
||||
```sh
|
||||
cd examples/quickstarts/sveltekit
|
||||
nhost up
|
||||
```
|
||||
|
||||
4. Terminal 2: Start the SvelteKit dev server
|
||||
|
||||
```sh
|
||||
cd examples/quickstarts/sveltekit
|
||||
pnpm dev
|
||||
```
|
||||
17
examples/quickstarts/sveltekit/jsconfig.json
Normal file
17
examples/quickstarts/sveltekit/jsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/sveltekit",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -15,10 +15,11 @@
|
||||
"postinstall": "pnpm add-nhost-js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nhost/nhost-js": "2.2.13",
|
||||
"@nhost/nhost-js": "2.2.17",
|
||||
"@playwright/test": "^1.31.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -30,7 +31,7 @@
|
||||
"svelte-check": "^3.0.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.3.0",
|
||||
"vite": "^4.3.8",
|
||||
"vitest": "^0.25.3"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -40,6 +41,7 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
"playwright": "^1.37.1",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.0",
|
||||
"xstate": "^4.38.2"
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ dependencies:
|
||||
|
||||
devDependencies:
|
||||
'@nhost/nhost-js':
|
||||
specifier: 2.2.13
|
||||
version: 2.2.13(graphql@16.8.0)
|
||||
specifier: 2.2.17
|
||||
version: 2.2.17(graphql@16.8.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -36,8 +36,8 @@ packages:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/hasura-auth-js@2.1.7:
|
||||
resolution: {integrity: sha512-dbi8zrmuE3xSlA7WMNyrZzVPLKYSBUlKtUjob+axhts0+4TsqsB7NvdLXrhM0fYjRY5SFUtN2JLs+EWDGKAoLQ==}
|
||||
/@nhost/hasura-auth-js@2.1.9:
|
||||
resolution: {integrity: sha512-tZl6iArGBIuzaD9ZMCR4NUNxXneIj87c15nG0RWqzqScMAklQ0l9J52CLeE8NDPNFWFuwNLm2FBpnEz4YGJLVw==}
|
||||
dependencies:
|
||||
'@simplewebauthn/browser': 6.2.2
|
||||
fetch-ponyfill: 7.1.0
|
||||
@@ -48,8 +48,8 @@ packages:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/hasura-storage-js@2.2.2:
|
||||
resolution: {integrity: sha512-GMeB1m6YKZMZDSO6UtmgXYWxsFUNlphIZH9JeiDX+6UTmbd62UlDmhBcmx2OGy/tvv57D+HbohpV6AY9S6QL4w==}
|
||||
/@nhost/hasura-storage-js@2.2.5:
|
||||
resolution: {integrity: sha512-IW0IwHlvuo9PxVQTXoQUE8LOpuOnjp+XlqgAKeatg1rY0MjJokcm4Xd0yLInAXfYBkK0r9pdKz7+RW+GDiYQ5g==}
|
||||
dependencies:
|
||||
fetch-ponyfill: 7.1.0
|
||||
form-data: 4.0.0
|
||||
@@ -58,14 +58,14 @@ packages:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/nhost-js@2.2.13(graphql@16.8.0):
|
||||
resolution: {integrity: sha512-HU9eOpkVBGGMHsRThGyl654Y9W8bksSEnyEvDojUg76+3ngOn2ebrasXR/94YoR206soEzq3v4b0OKmn/1jlnQ==}
|
||||
/@nhost/nhost-js@2.2.17(graphql@16.8.0):
|
||||
resolution: {integrity: sha512-6KRzhqmx7JcOmbp91/YZaBavGKdyGdx7kDrzRLoP1RYYOAIMpdMhHzeIju9LQfujY/8nFARRq97vFpPSbpnhSg==}
|
||||
peerDependencies:
|
||||
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
dependencies:
|
||||
'@nhost/graphql-js': 0.1.4(graphql@16.8.0)
|
||||
'@nhost/hasura-auth-js': 2.1.7
|
||||
'@nhost/hasura-storage-js': 2.2.2
|
||||
'@nhost/hasura-auth-js': 2.1.9
|
||||
'@nhost/hasura-storage-js': 2.2.5
|
||||
graphql: 16.8.0
|
||||
isomorphic-unfetch: 3.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -1,5 +1,6 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
11
examples/quickstarts/sveltekit/src/hooks.server.js
Normal file
11
examples/quickstarts/sveltekit/src/hooks.server.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { manageAuthSession } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
await manageAuthSession(event, () => {
|
||||
throw redirect(303, '/auth/sign-in')
|
||||
})
|
||||
|
||||
return resolve(event)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
/** @type {("button" | "submit" | "reset" | null | undefined)} */
|
||||
export let type = 'button';
|
||||
export let disabled = false;
|
||||
|
||||
$: styles = `${$$restProps.class || ''} ${disabled
|
||||
? 'bg-indigo-200 hover:bg-grey-700'
|
||||
: 'bg-indigo-600 hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'} px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white focus:outline-none`;
|
||||
</script>
|
||||
|
||||
<button
|
||||
{disabled}
|
||||
{type}
|
||||
class={styles}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -4,8 +4,8 @@
|
||||
/** @type {string} */
|
||||
export let id;
|
||||
|
||||
/** @type {string} */
|
||||
export let label;
|
||||
/** @type {string | null} */
|
||||
export let label = null;
|
||||
|
||||
export let type = 'text';
|
||||
|
||||
@@ -23,14 +23,17 @@
|
||||
</script>
|
||||
|
||||
<div class={$$props.class}>
|
||||
<label for={id} class="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
{#if label}
|
||||
<label for={id} class="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1">
|
||||
<input
|
||||
use:setType
|
||||
{name}
|
||||
{id}
|
||||
{name}
|
||||
{required}
|
||||
bind:value
|
||||
bind:this={inputRef}
|
||||
@@ -1,6 +1,4 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
/** @type {import('@nhost/nhost-js').User | undefined} */
|
||||
export let user;
|
||||
|
||||
@@ -10,20 +8,18 @@
|
||||
name: 'Home'
|
||||
},
|
||||
{
|
||||
href: '/protected',
|
||||
name: `${user ? '🔓' : '🔒'} Protected`
|
||||
href: '/protected/todos',
|
||||
name: `${user ? '🔓' : '🔒'} Todos`
|
||||
},
|
||||
{
|
||||
href: '/protected/echo',
|
||||
name: `${user ? '🔓' : '🔒'} Echo`
|
||||
},
|
||||
{
|
||||
href: '/protected/pat',
|
||||
name: `${user ? '🔓' : '🔒'} PAT`
|
||||
}
|
||||
];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const handleSignOut = () => {
|
||||
user = undefined;
|
||||
|
||||
dispatch('signout', {
|
||||
signout: true
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<header class="bg-indigo-600">
|
||||
@@ -40,21 +36,23 @@
|
||||
</div>
|
||||
<div class="ml-10 space-x-4">
|
||||
{#if user}
|
||||
<form action=/auth/sign-out method=post>
|
||||
<button
|
||||
on:click={handleSignOut}
|
||||
type='submit'
|
||||
class="inline-block px-4 py-2 text-base font-medium text-white bg-indigo-500 border border-transparent rounded-md hover:bg-opacity-75"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
{:else}
|
||||
<a
|
||||
href="/sign-in"
|
||||
href="/auth/sign-in"
|
||||
class="inline-block px-4 py-2 text-base font-medium text-white bg-indigo-500 border border-transparent rounded-md hover:bg-opacity-75"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<a
|
||||
href="/sign-up"
|
||||
href="/auth/sign-up"
|
||||
class="inline-block px-4 py-2 text-base font-medium text-indigo-600 bg-white border border-transparent rounded-md hover:bg-indigo-50"
|
||||
>
|
||||
Sign up
|
||||
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
/** @type {import('../types').PersonalAccessToken}*/
|
||||
export let pat
|
||||
const handleDeletePAT = () => fetch(`/protected/pat/${pat.id}`, { method: 'DELETE' })
|
||||
.then(() => window.location.reload())
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row items-center justify-between p-2 bg-slate-100">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-lg">{pat.metadata?.name}</span>
|
||||
<span class="font-mono">{pat.id}</span>
|
||||
<span class="rounded text-slate-500">
|
||||
expires on {new Date(pat.expiresAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button on:click={handleDeletePAT}>
|
||||
<svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={1.5}
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script>
|
||||
import { env } from '$env/dynamic/public'
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
|
||||
/** @type {import('../types').Todo} */
|
||||
export let todo
|
||||
let done = todo.done
|
||||
|
||||
let nhost = new NhostClient({
|
||||
subdomain: env.PUBLIC_NHOST_SUBDOMAIN || 'local',
|
||||
region: env.PUBLIC_NHOST_REGION
|
||||
})
|
||||
|
||||
const handleChange = async () => {
|
||||
return fetch('/protected/todos/update', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: todo.id, done })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`flex flex-row items-center p-2 bg-slate-100 ${done ? 'bg-slate-200' : ''}`}>
|
||||
<label
|
||||
for={todo.id}
|
||||
class={`flex items-start justify-start w-full space-x-2 rounded select-none ${done ? 'bg-slate-200': ''}`}
|
||||
>
|
||||
<input type="checkbox" id={todo.id} bind:checked={done} on:change={handleChange} />
|
||||
<span class={done ? 'line-through' : ''}>{todo.title}</span>
|
||||
</label>
|
||||
|
||||
{#if todo.attachment}
|
||||
<a
|
||||
class="w-6 h-6"
|
||||
target=_blank
|
||||
href={nhost.storage.getPublicUrl({ fileId: todo.attachment.id })}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={1.5}
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<form action='/protected/todos/delete' method='post'>
|
||||
<input hidden name={'id'} value={todo.id} />
|
||||
|
||||
<button type='submit' class="w-6 h-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={1.5}
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
63
examples/quickstarts/sveltekit/src/lib/nhost.js
Normal file
63
examples/quickstarts/sveltekit/src/lib/nhost.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { NhostClient } from '@nhost/nhost-js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { waitFor } from 'xstate/lib/waitFor';
|
||||
|
||||
export const NHOST_SESSION_KEY = 'nhostSession';
|
||||
|
||||
/** @param {import('@sveltejs/kit').Cookies} cookies */
|
||||
export const getNhost = async (cookies) => {
|
||||
|
||||
const nhost = new NhostClient({
|
||||
subdomain: env.PUBLIC_NHOST_SUBDOMAIN || 'local',
|
||||
region: env.PUBLIC_NHOST_REGION,
|
||||
start: false
|
||||
})
|
||||
|
||||
const sessionCookieValue = cookies.get(NHOST_SESSION_KEY) || ''
|
||||
|
||||
/** @type {import('@nhost/nhost-js').NhostSession} */
|
||||
const initialSession = JSON.parse(atob(sessionCookieValue) || 'null')
|
||||
|
||||
nhost.auth.client.start({ initialSession })
|
||||
|
||||
/** @type {import('@nhost/nhost-js').NhostSession} */
|
||||
if (nhost.auth.client.interpreter) {
|
||||
await waitFor(nhost.auth.client.interpreter, (state) => !state.hasTag('loading'))
|
||||
}
|
||||
|
||||
return nhost
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {*} onError
|
||||
*/
|
||||
export const manageAuthSession = async (
|
||||
event,
|
||||
onError
|
||||
) => {
|
||||
const nhost = await getNhost(event.cookies)
|
||||
const session = nhost.auth.getSession()
|
||||
|
||||
const refreshToken = event.url.searchParams.get('refreshToken') || undefined
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
const tokenExpirationTime = nhost.auth.getDecodedAccessToken()?.exp
|
||||
const accessTokenExpired = session && tokenExpirationTime && currentTime > tokenExpirationTime
|
||||
|
||||
if (accessTokenExpired || refreshToken) {
|
||||
const { session: newSession, error } = await nhost.auth.refreshSession(refreshToken)
|
||||
|
||||
if (error) {
|
||||
return onError?.(error)
|
||||
}
|
||||
|
||||
event.cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(newSession)), { path: '/' })
|
||||
|
||||
if (refreshToken) {
|
||||
event.url.searchParams.delete('refreshToken')
|
||||
throw redirect(303, event.url.pathname)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
examples/quickstarts/sveltekit/src/lib/types.js
Normal file
22
examples/quickstarts/sveltekit/src/lib/types.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @typedef {Object} Attachment
|
||||
* @property {string} id
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Todo
|
||||
* @property {string} id
|
||||
* @property {string} title
|
||||
* @property {boolean} done
|
||||
* @property {Attachment} attachment
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PersonalAccessToken
|
||||
* @property {string} id
|
||||
* @property {Record<string, string>} metadata
|
||||
* @property {string} type
|
||||
* @property {string} expiresAt
|
||||
*/
|
||||
|
||||
export const Types = {}
|
||||
29
examples/quickstarts/sveltekit/src/routes/+layout.server.js
Normal file
29
examples/quickstarts/sveltekit/src/routes/+layout.server.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
const publicRoutes = [
|
||||
'/',
|
||||
'/auth/sign-in',
|
||||
'/auth/sign-in/email-password',
|
||||
'/auth/sign-in/magick-link',
|
||||
'/auth/sign-in/webauthn',
|
||||
'/auth/sign-in/google',
|
||||
'/auth/sign-in/pat',
|
||||
'/auth/sign-up',
|
||||
'/auth/sign-up/email-password',
|
||||
'/auth/sign-up/magick-link',
|
||||
'/auth/sign-up/webauthn'
|
||||
]
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies, route }) {
|
||||
const nhost = await getNhost(cookies)
|
||||
const session = nhost.auth.getSession()
|
||||
|
||||
if (!publicRoutes.includes(route.id ?? '') && !session) {
|
||||
throw redirect(303, '/auth/sign-in')
|
||||
}
|
||||
|
||||
return {
|
||||
user: session?.user
|
||||
}
|
||||
}
|
||||
14
examples/quickstarts/sveltekit/src/routes/+layout.svelte
Normal file
14
examples/quickstarts/sveltekit/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import Navigation from '$lib/components/Navigation.svelte'
|
||||
import './styles.css'
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
<Navigation user={data?.user} />
|
||||
|
||||
<div class="container p-4 mx-auto mt-8 antialiased">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="w-full mx-auto">
|
||||
<div class="flex flex-col w-full max-w-lg mx-auto space-y-5">
|
||||
<h1 class="text-2xl font-semibold text-center">Sign In</h1>
|
||||
|
||||
<a
|
||||
class="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
|
||||
href=/auth/sign-in/email-password
|
||||
>
|
||||
with email/password
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
|
||||
href=/auth/sign-in/webauthn
|
||||
>
|
||||
with a security key
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
|
||||
href=/auth/sign-in/magick-link
|
||||
>
|
||||
with a magick link
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
|
||||
href=/auth/sign-in/pat
|
||||
>
|
||||
with a Personal Access Token
|
||||
</a>
|
||||
|
||||
<form action="/auth/sign-in/google" method="post">
|
||||
<button
|
||||
class="text-white w-full bg-[#4285F4] hover:bg-[#4285F4]/90 focus:ring-4 focus:outline-none focus:ring-[#4285F4]/50 font-medium rounded-lg px-5 py-2.5 text-center inline-flex items-center justify-between dark:focus:ring-[#4285F4]/55 mr-2 mb-2"
|
||||
type=submit
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2 -ml-1"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="fab"
|
||||
data-icon="google"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 488 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
||||
/>
|
||||
</svg>
|
||||
with Google <span />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getNhost, NHOST_SESSION_KEY } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { request, cookies } = event
|
||||
|
||||
const formData = await request.formData()
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const email = String(formData.get('email'))
|
||||
const password = String(formData.get('password'))
|
||||
|
||||
const { session, error } = await nhost.auth.signIn({ email, password })
|
||||
|
||||
if (session) {
|
||||
cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { path: '/' })
|
||||
throw redirect(303, '/protected/todos')
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
error: error?.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import Input from '$lib/components/Input.svelte'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-semibold text-center">Sign In</h1>
|
||||
|
||||
{#if form?.error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{form?.error}</p>
|
||||
{/if}
|
||||
|
||||
<form class="max-w-xl mx-auto space-y-5" method='POST'>
|
||||
<Input label="Email" id="email" name="email" type="email" required />
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
|
||||
<Button class='w-full' type="submit">Sign In</Button>
|
||||
</form>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async ({ cookies }) => {
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const { providerUrl } = await nhost.auth.signIn({ provider: 'google' })
|
||||
|
||||
if (providerUrl) {
|
||||
throw redirect(307, providerUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { request, cookies } = event
|
||||
|
||||
const formData = await request.formData()
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const email = String(formData.get('email'))
|
||||
|
||||
const { error } = await nhost.auth.signIn({ email })
|
||||
|
||||
if (error) {
|
||||
return { error }
|
||||
} else {
|
||||
return {
|
||||
isSuccess: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import Input from '$lib/components/Input.svelte'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-semibold text-center">Sign in with a magick link</h1>
|
||||
|
||||
{#if form?.error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{form?.error}</p>
|
||||
{/if}
|
||||
|
||||
{#if form?.isSuccess}
|
||||
<p class="mt-3 font-semibold text-center text-green-500">
|
||||
Click the link in the email to finish the sign in process
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<form class="max-w-xl mx-auto space-y-5" method='POST'>
|
||||
<Input label="Email" id="email" name="email" type="email" required />
|
||||
<Button class='w-full' type="submit">Sign In</Button>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getNhost, NHOST_SESSION_KEY } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { request, cookies } = event
|
||||
|
||||
const formData = await request.formData()
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const pat = String(formData.get('pat'))
|
||||
|
||||
const { session, error } = await nhost.auth.signInPAT(pat)
|
||||
|
||||
if (session) {
|
||||
cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { path: '/' })
|
||||
throw redirect(303, '/protected/todos')
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
error: error?.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import Input from '$lib/components/Input.svelte'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-xl mx-auto space-y-4">
|
||||
<h1 class="text-2xl font-semibold text-center">Sign in with a Personal Access Token</h1>
|
||||
|
||||
{#if form?.error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{form?.error}</p>
|
||||
{/if}
|
||||
|
||||
<form class="max-w-xl mx-auto space-y-4" method='POST'>
|
||||
<Input label="PAT " id="pat" name="pat" required />
|
||||
<Button class='w-full' type="submit">Sign In</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation'
|
||||
import { env } from '$env/dynamic/public'
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import { NHOST_SESSION_KEY } from '$lib/nhost'
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
/** @type {string} */
|
||||
let email
|
||||
|
||||
/** @type {import('@nhost/nhost-js').AuthErrorPayload | null}*/
|
||||
let error
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const nhost = new NhostClient({
|
||||
subdomain: env.PUBLIC_NHOST_SUBDOMAIN,
|
||||
region: env.PUBLIC_NHOST_REGION
|
||||
})
|
||||
|
||||
const { session, error: signInError } = await nhost.auth.signIn({ email, securityKey: true })
|
||||
|
||||
if (session) {
|
||||
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { path: '/' })
|
||||
goto('/protected/todos')
|
||||
} else {
|
||||
error = signInError
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign Up</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-xl mx-auto space-y-4">
|
||||
<h1 class="text-2xl font-semibold text-center">Sign in with a security key</h1>
|
||||
|
||||
{#if error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{error.message}</p>
|
||||
{/if}
|
||||
|
||||
<form class="space-y-4" on:submit|preventDefault={handleSubmit}>
|
||||
<input
|
||||
bind:value={email}
|
||||
placeholder='email'
|
||||
class="block w-full p-3 border rounded-md border-slate-300 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
|
||||
<Button class='w-full' type="submit">Sign In</Button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getNhost, NHOST_SESSION_KEY } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const nhost = await getNhost(event.cookies)
|
||||
|
||||
await nhost.auth.signOut()
|
||||
event.cookies.set(NHOST_SESSION_KEY, '', { httpOnly: true, path: '/', maxAge: 0 })
|
||||
|
||||
throw redirect(303, '/auth/sign-in')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="w-full mx-auto">
|
||||
<div class="flex flex-col w-full max-w-lg mx-auto space-y-5">
|
||||
<h1 class="text-2xl font-semibold text-center">Sign Up</h1>
|
||||
|
||||
<a
|
||||
class="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
|
||||
href=/auth/sign-up/email-password
|
||||
>
|
||||
with email/password
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
|
||||
href=/auth/sign-up/webauthn
|
||||
>
|
||||
with a security key
|
||||
</a>
|
||||
|
||||
|
||||
<form action="/auth/sign-in/google" method="post">
|
||||
<button
|
||||
class="text-white w-full bg-[#4285F4] hover:bg-[#4285F4]/90 focus:ring-4 focus:outline-none focus:ring-[#4285F4]/50 font-medium rounded-lg px-5 py-2.5 text-center inline-flex items-center justify-between dark:focus:ring-[#4285F4]/55 mr-2 mb-2"
|
||||
type=submit
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2 -ml-1"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="fab"
|
||||
data-icon="google"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 488 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
||||
/>
|
||||
</svg>
|
||||
with Google <span />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getNhost, NHOST_SESSION_KEY } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { request, cookies } = event
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const formData = await request.formData()
|
||||
const email = String(formData.get('email'))
|
||||
const password = String(formData.get('password'))
|
||||
const firstName = String(formData.get('firstName'))
|
||||
const lastName = String(formData.get('lastName'))
|
||||
|
||||
const { session, error } = await nhost.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
displayName: `${firstName} ${lastName}`
|
||||
}
|
||||
})
|
||||
|
||||
if (session) {
|
||||
cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { path: '/' })
|
||||
throw redirect(303, '/protected/todos')
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
error: error?.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<script>
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import Input from '$lib/components/Input.svelte'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign Up</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-semibold text-center">Sign Up</h1>
|
||||
|
||||
{#if form?.error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{form?.error}</p>
|
||||
{/if}
|
||||
|
||||
<div class='max-w-xl mx-auto'>
|
||||
<form class="space-y-5" method='POST'>
|
||||
<Input
|
||||
label="First Name"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Last Name"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input label="Email" id="email" name="email" type="email" required />
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
|
||||
<Button class='block w-full' type="submit">Sign Up</Button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation'
|
||||
import { env } from '$env/dynamic/public'
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import { NHOST_SESSION_KEY } from '$lib/nhost'
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
/** @type {string} */
|
||||
let email
|
||||
|
||||
/** @type {import('@nhost/nhost-js').AuthErrorPayload | null}*/
|
||||
let error
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const nhost = new NhostClient({
|
||||
subdomain: env.PUBLIC_NHOST_SUBDOMAIN,
|
||||
region: env.PUBLIC_NHOST_REGION
|
||||
})
|
||||
|
||||
const { session, error: signInError } = await nhost.auth.signUp({ email, securityKey: true })
|
||||
|
||||
if (session) {
|
||||
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { path: '/' })
|
||||
goto('/protected/todos')
|
||||
} else {
|
||||
error = signInError
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign Up</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-xl mx-auto space-y-4">
|
||||
<h1 class="text-2xl font-semibold text-center">Sign up with a security key</h1>
|
||||
|
||||
{#if error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{error.message}</p>
|
||||
{/if}
|
||||
|
||||
<form class="space-y-4" on:submit|preventDefault={handleSubmit}>
|
||||
<input
|
||||
bind:value={email}
|
||||
placeholder='email'
|
||||
class="block w-full p-3 border rounded-md border-slate-300 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
|
||||
<Button class='w-full' type="submit">Sign Up</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export const load = async ({ cookies }) => {
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const { res } = await nhost.functions.call('echo')
|
||||
|
||||
return {
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
|
||||
let functionResponse = data.res?.data
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Echo</title>
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<pre class="overflow-auto">{JSON.stringify(functionResponse, null, 2)}</pre>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export const load = async ({ url, cookies }) => {
|
||||
const nhost = await getNhost(cookies)
|
||||
const page = parseInt(url.searchParams.get('page') || '0')
|
||||
|
||||
const {
|
||||
data: {
|
||||
authRefreshTokens,
|
||||
authRefreshTokensAggregate: {
|
||||
aggregate: { count }
|
||||
}
|
||||
}
|
||||
} = await nhost.graphql.request(
|
||||
gql`
|
||||
query getPersonalAccessTokens($offset: Int, $limit: Int) {
|
||||
authRefreshTokens(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: { createdAt: desc }
|
||||
where: { type: { _eq: pat } }
|
||||
) {
|
||||
id
|
||||
metadata
|
||||
type
|
||||
expiresAt
|
||||
}
|
||||
|
||||
authRefreshTokensAggregate(where: { type: { _eq: pat } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
offset: page * 10,
|
||||
limit: 10
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
/** @type {import('$lib/types').PersonalAccessToken[]} */
|
||||
personalAccessTokens: authRefreshTokens,
|
||||
|
||||
/** @type number */
|
||||
count,
|
||||
|
||||
/** @type number */
|
||||
page
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<script>
|
||||
import PatItem from '$lib/components/pat-item.svelte'
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data
|
||||
const { personalAccessTokens, count, page } = data
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Personal Access Tokens</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex items-center justify-between w-full mb-2">
|
||||
<h2 class="text-xl">Personal Access Tokens ({count})</h2>
|
||||
|
||||
<a
|
||||
href=/protected/pat/new
|
||||
class="px-4 py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
Add a PAT
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-1">
|
||||
{#each personalAccessTokens as pat}
|
||||
<li>
|
||||
<PatItem pat={pat} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if count > 10}
|
||||
<div class="flex justify-center space-x-2">
|
||||
{#if page > 0}
|
||||
<a
|
||||
href={`/protected/pat/${page - 1}`}
|
||||
class="px-4 py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
Previous
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if page + 1 < Math.ceil(count / 10)}
|
||||
<a
|
||||
href={`/protected/pat/${page + 1}`}
|
||||
class="px-4 py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getNhost } from '$lib/nhost.js'
|
||||
import { gql } from '@apollo/client'
|
||||
import { json, redirect } from '@sveltejs/kit'
|
||||
|
||||
export const DELETE = async ({ cookies, params }) => {
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const id = params.id
|
||||
|
||||
const { error } = await nhost.graphql.request(
|
||||
gql`
|
||||
mutation deletePersonalAccessToken($id: uuid!) {
|
||||
deleteAuthRefreshToken(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
id
|
||||
}
|
||||
)
|
||||
|
||||
if (!error) {
|
||||
throw redirect(303, '/protected/pat')
|
||||
}
|
||||
|
||||
return json({ error })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async ({ request, cookies }) => {
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const formData = await request.formData()
|
||||
|
||||
const name = String(formData.get('name'))
|
||||
const expiration = String(formData.get('expiration'))
|
||||
const expirationDate = new Date(expiration)
|
||||
|
||||
await nhost.auth.createPAT(expirationDate, { name })
|
||||
|
||||
throw redirect(303, '/protected/pat')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import Input from '$lib/components/Input.svelte'
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Create Personal Access Token</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col max-w-3xl mx-auto space-y-4">
|
||||
<h2 class="text-xl">New Personal Access Token</h2>
|
||||
<form class="space-y-4" action=/protected/pat/new method='post'>
|
||||
<Input id=name name=name label=Name required />
|
||||
<Input id=expiration name=expiration type=date required />
|
||||
<Button type=submit>Create PAT</Button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export const load = async ({ url, cookies }) => {
|
||||
const nhost = await getNhost(cookies)
|
||||
const page = parseInt(url.searchParams.get('page') || '0')
|
||||
|
||||
const {
|
||||
data: {
|
||||
todos,
|
||||
todos_aggregate: {
|
||||
aggregate: { count }
|
||||
}
|
||||
}
|
||||
} = await nhost.graphql.request(
|
||||
gql`
|
||||
query getTodos($limit: Int, $offset: Int) {
|
||||
todos(limit: $limit, offset: $offset, order_by: { createdAt: desc }) {
|
||||
id
|
||||
title
|
||||
done
|
||||
attachment {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
todos_aggregate {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
offset: page * 10,
|
||||
limit: 10
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
/** @type {import('$lib/types').Todo[]} */
|
||||
todos,
|
||||
|
||||
/** @type number */
|
||||
count,
|
||||
|
||||
/** @type number */
|
||||
page
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import TodoItem from '$lib/components/todo-item.svelte'
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
|
||||
const { todos, count, page } = data
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Todos - Protected</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h2 class="text-xl">Todos ({count})</h2>
|
||||
|
||||
<a
|
||||
href={`/protected/todos/new`}
|
||||
class="px-4 py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
Add Todo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-1">
|
||||
{#each todos as todo}
|
||||
<li>
|
||||
<TodoItem todo={todo} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if count > 10}
|
||||
<div class="flex justify-center space-x-2">
|
||||
{#if page > 0}
|
||||
<a
|
||||
href={`/protected/todos?page=${page - 1}`}
|
||||
data-sveltekit-reload
|
||||
class="px-4 py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
Previous
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if page + 1 < Math.ceil(count / 10)}
|
||||
<a
|
||||
href={`/protected/todos?page=${page + 1}`}
|
||||
data-sveltekit-reload
|
||||
class="px-4 py-2 text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { gql } from '@apollo/client'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { request, cookies } = event
|
||||
const nhost = await getNhost(cookies)
|
||||
|
||||
const data = await request.formData()
|
||||
const id = data.get('id')
|
||||
|
||||
const { error } = await nhost.graphql.request(
|
||||
gql`
|
||||
mutation deleteTodo($id: uuid!) {
|
||||
delete_todos_by_pk(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
id
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
throw redirect(303, '/protected/todos')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { gql } from '@apollo/client'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { request, cookies } = event
|
||||
|
||||
const nhost = await getNhost(cookies)
|
||||
const data = await request.formData()
|
||||
|
||||
const title = /** @type {string} */ data.get('title')
|
||||
const file = /** @type {File | null} */ (data.get('file'))
|
||||
|
||||
let payload = {}
|
||||
payload.title = title
|
||||
|
||||
if (file && file.size > 0) {
|
||||
const { fileMetadata, error: storageUploadError } = await nhost.storage.upload({
|
||||
formData: data
|
||||
})
|
||||
|
||||
if (storageUploadError) {
|
||||
return {
|
||||
error: storageUploadError.message
|
||||
}
|
||||
}
|
||||
|
||||
payload.file_id = fileMetadata?.processedFiles[0]?.id || null
|
||||
}
|
||||
|
||||
const { error } = await nhost.graphql.request(
|
||||
gql`
|
||||
mutation insertTodo($title: String!, $file_id: uuid) {
|
||||
insert_todos_one(object: { title: $title, file_id: $file_id }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
payload
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
throw redirect(303, '/protected/todos')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import Input from '$lib/components/Input.svelte'
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if form?.error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{form?.error}</p>
|
||||
{/if}
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="flex flex-col max-w-xl mx-auto space-y-4">
|
||||
<Input id='title' name='title' label='title' required placeholder='what needs to be done' class='w-full' />
|
||||
<input name='file' type="file" class="w-full p-3 border" />
|
||||
|
||||
<Button class='w-full' type="submit">Add</Button>
|
||||
</form>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { gql } from '@apollo/client'
|
||||
import { json } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function POST({ request, cookies }) {
|
||||
const nhost = await getNhost(cookies)
|
||||
const { id, done } = await request.json()
|
||||
|
||||
const { data } = await nhost.graphql.request(
|
||||
gql`
|
||||
mutation updateTodo($id: uuid!, $done: Boolean!) {
|
||||
update_todos_by_pk(pk_columns: { id: $id }, _set: { done: $done }) {
|
||||
id
|
||||
title
|
||||
done
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
id,
|
||||
done
|
||||
}
|
||||
)
|
||||
|
||||
return json(data)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -2,5 +2,8 @@ import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@nhost-examples/sveltekit': minor
|
||||
---
|
||||
|
||||
feat: add nhost with sveltekit example project
|
||||
@@ -1,44 +0,0 @@
|
||||
# Nhost with SvelteKit Example
|
||||
|
||||
## Get Started
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nhost/nhost
|
||||
cd nhost
|
||||
```
|
||||
|
||||
2. Install and build dependencies
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Go to the SvelteKit example folder
|
||||
|
||||
```sh
|
||||
cd examples/sveltekit
|
||||
```
|
||||
|
||||
4. Create a `.env` file and set the subdomain and region of your Nhost project. When running locally with the CLI, set the subdomain to `local`.
|
||||
|
||||
```sh
|
||||
PUBLIC_NHOST_SUBDOMAIN=
|
||||
PUBLIC_NHOST_REGION=
|
||||
```
|
||||
|
||||
5. Terminal 1: Start Nhost
|
||||
|
||||
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
|
||||
|
||||
```sh
|
||||
nhost up
|
||||
```
|
||||
|
||||
6. Terminal 2: Start the SvelteKit dev server
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<script>
|
||||
/** @type {("button" | "submit" | "reset" | null | undefined)} */
|
||||
export let type = 'button';
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<button
|
||||
{disabled}
|
||||
{type}
|
||||
class="{disabled == true
|
||||
? 'bg-indigo-200 hover:bg-grey-700'
|
||||
: 'bg-indigo-600 hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'} inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white focus:outline-none"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -1,62 +0,0 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { NhostClient } from '@nhost/nhost-js';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export const NHOST_SESSION_KEY = 'nhostSession';
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
/** @type {import('@nhost/nhost-js').NhostClient | null} */
|
||||
let nhost;
|
||||
|
||||
/** @param {import('@nhost/nhost-js').NhostSession | null} session */
|
||||
export const setNhostSessionInCookie = (session) => {
|
||||
if (!session) {
|
||||
Cookies.remove(NHOST_SESSION_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
const expires = new Date();
|
||||
|
||||
// * Expire the cookie 60 seconds before the token expires
|
||||
expires.setSeconds(expires.getSeconds() + session.accessTokenExpiresIn - 60);
|
||||
|
||||
Cookies.set(NHOST_SESSION_KEY, JSON.stringify(session), {
|
||||
sameSite: 'strict',
|
||||
expires
|
||||
});
|
||||
};
|
||||
|
||||
/** @param {import('@sveltejs/kit').Cookies} cookies */
|
||||
export const getNhostSessionFromCookie = (cookies) => {
|
||||
const nhostSessionCookie = cookies.get(NHOST_SESSION_KEY);
|
||||
return nhostSessionCookie ? JSON.parse(nhostSessionCookie) : null;
|
||||
};
|
||||
|
||||
/** @param {import('@nhost/nhost-js').NhostSession} session */
|
||||
export const getNhostLoadClient = async (session) => {
|
||||
if (isBrowser && nhost) {
|
||||
return nhost;
|
||||
}
|
||||
|
||||
nhost = new NhostClient({
|
||||
subdomain: env.PUBLIC_NHOST_SUBDOMAIN || 'local',
|
||||
region: env.PUBLIC_NHOST_REGION,
|
||||
start: false
|
||||
});
|
||||
|
||||
if (isBrowser) {
|
||||
nhost.auth.onAuthStateChanged((_, session) => {
|
||||
setNhostSessionInCookie(session);
|
||||
invalidate('nhost:auth');
|
||||
});
|
||||
|
||||
nhost.auth.onTokenChanged((session) => {
|
||||
setNhostSessionInCookie(session);
|
||||
});
|
||||
}
|
||||
|
||||
nhost.auth.client.start({ initialSession: session });
|
||||
|
||||
return nhost;
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { getNhostLoadClient } from '$lib/nhost-auth-sveltekit.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
const unProtectedRoutes = ['/', '/sign-in', '/sign-up'];
|
||||
|
||||
export const load = async ({ data, depends, route }) => {
|
||||
depends('nhost:auth');
|
||||
|
||||
const nhost = await getNhostLoadClient(data.nhostSession);
|
||||
const session = nhost.auth.getSession();
|
||||
|
||||
if (!unProtectedRoutes.includes(route.id ?? '')) {
|
||||
if (!session) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nhost: nhost,
|
||||
session: session
|
||||
};
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getNhostSessionFromCookie } from '$lib/nhost-auth-sveltekit';
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies }) {
|
||||
return {
|
||||
nhostSession: getNhostSessionFromCookie(cookies)
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import './styles.css';
|
||||
|
||||
export let data;
|
||||
let { nhost } = data;
|
||||
|
||||
/**
|
||||
* @param {{ detail: { signout: any; }; }} event
|
||||
*/
|
||||
async function handleSignOut(event) {
|
||||
await nhost.auth.signOut();
|
||||
await goto(`/`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
<Navigation user={data.session?.user} on:signout={handleSignOut} />
|
||||
|
||||
<div class="container p-4 mx-auto mt-8 antialiased">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script>
|
||||
export let data;
|
||||
let { session, nhost } = data;
|
||||
import { gql } from 'graphql-tag';
|
||||
|
||||
const getFiles = async () => {
|
||||
const response = await nhost.graphql.request(gql`
|
||||
{
|
||||
files {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return response.data.files;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Protected Page</title>
|
||||
<meta name="description" content="About this app" />
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-semibold text-center">
|
||||
Hi! You are registered with email: {session?.user.email}.
|
||||
</h1>
|
||||
|
||||
<h2>Files</h2>
|
||||
{#await getFiles()}
|
||||
<p>Loading...</p>
|
||||
{:then files}
|
||||
<p>Showing {files.length} files</p>
|
||||
|
||||
<ul>
|
||||
{#each files as file}
|
||||
<li>
|
||||
{file.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:catch error}
|
||||
<p>{error.message}</p>
|
||||
{/await}
|
||||
@@ -1,56 +0,0 @@
|
||||
<script>
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
let { nhost } = data;
|
||||
|
||||
/** @type {string}*/
|
||||
let email;
|
||||
|
||||
/** @type {string}*/
|
||||
let password;
|
||||
|
||||
/** @type {import('@nhost/nhost-js').AuthErrorPayload | null} */
|
||||
let error;
|
||||
|
||||
const handleSignIn = async () => {
|
||||
const { error: signInError } = await nhost.auth.signIn({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
error = signInError;
|
||||
|
||||
if (!error) {
|
||||
await invalidate('nhost:auth');
|
||||
await goto('/protected');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-semibold text-center">Sign In</h1>
|
||||
|
||||
{#if error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{error.message}</p>
|
||||
{/if}
|
||||
|
||||
<form class="space-y-5" on:submit={handleSignIn}>
|
||||
<Input label="Email" id="email" name="email" type="email" bind:value={email} required />
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
|
||||
<Button type="submit">Sign In</Button>
|
||||
</form>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script>
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
let { nhost } = data;
|
||||
|
||||
/** @type {string}*/
|
||||
let firstName;
|
||||
|
||||
/** @type {string}*/
|
||||
let lastName;
|
||||
|
||||
/** @type {string}*/
|
||||
let email;
|
||||
|
||||
/** @type {string}*/
|
||||
let password;
|
||||
|
||||
/** @type {import('@nhost/nhost-js').AuthErrorPayload | null} */
|
||||
let error;
|
||||
|
||||
const handleSignUp = async () => {
|
||||
const { error: signUpError } = await nhost.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
displayName: `${firstName} ${lastName}`
|
||||
}
|
||||
});
|
||||
|
||||
error = signUpError;
|
||||
|
||||
if (!error) {
|
||||
await goto('/sign-in');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign Up</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-semibold text-center">Sign Up</h1>
|
||||
|
||||
{#if error}
|
||||
<p class="mt-3 font-semibold text-center text-red-500">{error.message}</p>
|
||||
{/if}
|
||||
|
||||
<form class="space-y-5" on:submit={handleSignUp}>
|
||||
<Input
|
||||
label="First Name"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
bind:value={firstName}
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Last Name"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
bind:value={lastName}
|
||||
required
|
||||
/>
|
||||
|
||||
<Input label="Email" id="email" name="email" type="email" bind:value={email} required />
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
|
||||
<Button type="submit">Sign Up</Button>
|
||||
</form>
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 5.2.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f777a3380: fix: apollo-integration: correctly reset accessToken to null after sign-out
|
||||
|
||||
## 5.2.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7459a9413: fix: apollo-integration: reset accessToken to null after sign-out
|
||||
|
||||
## 5.2.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "5.2.19",
|
||||
"version": "5.2.21",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -187,7 +187,10 @@ export const createApolloClient = ({
|
||||
|
||||
interpreter?.onTransition(async (state, event) => {
|
||||
if (['SIGNOUT', 'SIGNED_IN', 'TOKEN_CHANGED'].includes(event.type)) {
|
||||
if (event.type === 'SIGNOUT') {
|
||||
if (
|
||||
event.type === 'SIGNOUT' ||
|
||||
(event.type === 'TOKEN_CHANGED' && state.context.accessToken.value === null)
|
||||
) {
|
||||
accessToken = null
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost/react-apollo
|
||||
|
||||
## 5.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [f777a3380]
|
||||
- @nhost/apollo@5.2.21
|
||||
|
||||
## 5.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [7459a9413]
|
||||
- @nhost/apollo@5.2.20
|
||||
|
||||
## 5.0.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-apollo",
|
||||
"version": "5.0.36",
|
||||
"version": "5.0.38",
|
||||
"description": "Nhost React Apollo client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
1067
pnpm-lock.yaml
generated
1067
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user