Compare commits
49 Commits
@nhost/das
...
@nhost/apo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3ef503ff81 | ||
|
|
bfcfd236ea | ||
|
|
bfa7033506 | ||
|
|
78c29fcf0e | ||
|
|
f1b934ed22 | ||
|
|
914369c53f | ||
|
|
af379b967e | ||
|
|
c3efb7ec84 | ||
|
|
27cbd48c8c | ||
|
|
236996a903 | ||
|
|
5d0936bb93 | ||
|
|
733c212f2d | ||
|
|
8b47549189 | ||
|
|
3c9c1025ce | ||
|
|
3e46d3873c | ||
|
|
4cf8820d72 | ||
|
|
02a11184fb | ||
|
|
7214d47cc7 | ||
|
|
238b77baad | ||
|
|
81b8e538b4 |
@@ -5,6 +5,5 @@
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@nhost-examples/sveltekit"]
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
||||
12
.github/workflows/changesets.yaml
vendored
12
.github/workflows/changesets.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
commit: 'chore: update versions'
|
||||
title: 'chore: update versions'
|
||||
publish: pnpm run release
|
||||
createGithubReleases: true
|
||||
createGithubReleases: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -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,30 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 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
|
||||
|
||||
- c3efb7ec8: feat(dashboard): query latest announcement from platform
|
||||
|
||||
## 0.20.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3e46d3873: chore: update link to node18 announcement
|
||||
|
||||
## 0.20.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.20.20",
|
||||
"version": "0.20.24",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { forwardRef, type ForwardedRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import AnnouncementContainer, {
|
||||
type AnnouncementContainerProps,
|
||||
} from './AnnouncementContainer';
|
||||
|
||||
export interface AnnouncementProps extends AnnouncementContainerProps {
|
||||
/**
|
||||
* Function called when the announcement is closed.
|
||||
*/
|
||||
onClose?: VoidFunction;
|
||||
/**
|
||||
* The href to use for the announcement link.
|
||||
*/
|
||||
href: string;
|
||||
}
|
||||
|
||||
function Announcement(
|
||||
{ children, slotProps, onClose, href, ...props }: AnnouncementProps,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) {
|
||||
return (
|
||||
<AnnouncementContainer
|
||||
{...props}
|
||||
ref={ref}
|
||||
className="grid grid-flow-col justify-between gap-4"
|
||||
slotProps={{
|
||||
root: {
|
||||
...(slotProps?.root || {}),
|
||||
className: twMerge('w-full py-1.5', slotProps?.root?.className),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span />
|
||||
|
||||
<div className="flex items-center self-center truncate">
|
||||
<a href={href}>
|
||||
<Text className="cursor-pointer truncate hover:underline">
|
||||
{children}
|
||||
</Text>
|
||||
</a>
|
||||
<ArrowRightIcon className="ml-1 h-4 w-4 text-white" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={onClose}
|
||||
aria-label="Close announcement"
|
||||
size="small"
|
||||
className="rounded-sm p-1"
|
||||
>
|
||||
<XIcon className="opacity-65 h-4 w-4" />
|
||||
</Button>
|
||||
</AnnouncementContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(Announcement);
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
createElement,
|
||||
forwardRef,
|
||||
type DetailedHTMLProps,
|
||||
type ElementType,
|
||||
type ForwardedRef,
|
||||
type HTMLProps,
|
||||
type PropsWithoutRef,
|
||||
} from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface AnnouncementContainerProps
|
||||
extends PropsWithoutRef<
|
||||
DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* Custom component to render as.
|
||||
*/
|
||||
component?: ElementType<any>;
|
||||
/**
|
||||
* Props passed to component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props passed to the root component.
|
||||
*/
|
||||
root?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
|
||||
/**
|
||||
* Props passed to the content component.
|
||||
*/
|
||||
content?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
|
||||
};
|
||||
}
|
||||
|
||||
function AnnouncementContainer(
|
||||
{
|
||||
component = 'div',
|
||||
className,
|
||||
children,
|
||||
slotProps,
|
||||
...props
|
||||
}: AnnouncementContainerProps,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) {
|
||||
return createElement(
|
||||
component,
|
||||
{
|
||||
...props,
|
||||
...(slotProps?.root || {}),
|
||||
ref,
|
||||
className: twMerge('w-full overflow-hidden', slotProps?.root?.className),
|
||||
},
|
||||
<div
|
||||
{...(slotProps?.content || {})}
|
||||
className={twMerge(
|
||||
'mx-auto max-w-7xl px-5',
|
||||
className,
|
||||
slotProps?.content?.className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(AnnouncementContainer);
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import {
|
||||
createContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import Announcement from './Announcement';
|
||||
|
||||
interface AnnouncementType {
|
||||
id: string;
|
||||
content: ReactNode;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface AnnouncementContextProps {
|
||||
/**
|
||||
* The announcement to show.
|
||||
*/
|
||||
announcement?: AnnouncementType;
|
||||
/**
|
||||
* Whether or not to show the announcement.
|
||||
*/
|
||||
showAnnouncement?: boolean;
|
||||
/**
|
||||
* Function to close the announcement.
|
||||
*/
|
||||
handleClose?: () => void;
|
||||
/**
|
||||
* Whether or not the announcement is in view.
|
||||
*/
|
||||
inView?: boolean;
|
||||
}
|
||||
|
||||
// Note: You can define the active announcement here.
|
||||
const announcement: AnnouncementType = {
|
||||
id: 'node-18',
|
||||
href: 'https://github.com/nhost/nhost/discussions/2239',
|
||||
content:
|
||||
"Starting October 1st, we're upgrading to Node.js 18 for improved performance, security, and stability. Learn more.",
|
||||
};
|
||||
|
||||
export const AnnouncementContext = createContext<AnnouncementContextProps>({});
|
||||
|
||||
export default function AnnouncementProvider({ children }: PropsWithChildren) {
|
||||
const { ref, inView } = useInView();
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
!announcement ||
|
||||
window.localStorage.getItem(announcement.id) === '1'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShowAnnouncement(true);
|
||||
}, []);
|
||||
|
||||
function handleClose() {
|
||||
setShowAnnouncement(false);
|
||||
window.localStorage.setItem(announcement?.id, '1');
|
||||
}
|
||||
|
||||
const announcementValue = useMemo(
|
||||
() => ({ showAnnouncement, announcement, handleClose, inView }),
|
||||
[inView, showAnnouncement],
|
||||
);
|
||||
|
||||
return (
|
||||
<AnnouncementContext.Provider value={announcementValue}>
|
||||
{announcement && showAnnouncement && (
|
||||
<>
|
||||
<Announcement
|
||||
ref={ref}
|
||||
href={announcement.href}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{announcement.content}
|
||||
</Announcement>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</AnnouncementContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Announcement';
|
||||
export * from './AnnouncementProvider';
|
||||
export { default as useAnnouncement } from './useAnnouncement';
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { AnnouncementContext } from './AnnouncementProvider';
|
||||
|
||||
export default function useAnnouncement() {
|
||||
const context = useContext(AnnouncementContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useAnnouncement must be used within an AnnouncementProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,47 @@
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useGetAnnouncementsQuery } from '@/utils/__generated__/graphql';
|
||||
import formatDistance from 'date-fns/formatDistance';
|
||||
|
||||
export default function Announcements() {
|
||||
const { data, loading, error } = useGetAnnouncementsQuery();
|
||||
|
||||
const announcements = data?.announcements || [];
|
||||
|
||||
if (loading || error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Text color="secondary" className="mb-2">
|
||||
Latest announcements
|
||||
</Text>
|
||||
|
||||
<List className="relative space-y-4 border-l border-gray-200 dark:border-gray-700">
|
||||
{announcements.map((item) => (
|
||||
<ListItem.Root key={item.id} className="ml-4">
|
||||
<div className="flex flex-col">
|
||||
<time className="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500">
|
||||
{formatDistance(new Date(item.createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</time>
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer">
|
||||
<ListItem.Button
|
||||
dense
|
||||
aria-label={`View ${item.content}`}
|
||||
className="!p-1"
|
||||
>
|
||||
<p className="text-sm">{item.content}</p>
|
||||
</ListItem.Button>
|
||||
</a>
|
||||
</div>
|
||||
<div className="absolute top-[0.15rem] -ml-[1.4rem] h-3 w-3 rounded-full border border-white bg-gray-200 dark:border-gray-900 dark:bg-gray-700" />
|
||||
</ListItem.Root>
|
||||
))}
|
||||
</List>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Announcements } from './Announcements';
|
||||
@@ -8,6 +8,7 @@ import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Announcements } from '@/features/projects/common/components/Announcements';
|
||||
import { EditWorkspaceNameForm } from '@/features/projects/workspaces/components/EditWorkspaceNameForm';
|
||||
import type { Workspace } from '@/types/application';
|
||||
import Image from 'next/image';
|
||||
@@ -38,6 +39,8 @@ export default function WorkspaceSidebar({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Announcements />
|
||||
|
||||
<section className="grid grid-flow-row gap-2">
|
||||
<Text color="secondary">My Workspaces</Text>
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ export default function useProjectRoutes() {
|
||||
exact: false,
|
||||
label: 'Run',
|
||||
icon: <ServicesIcon />,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
...nhostRoutes,
|
||||
];
|
||||
|
||||
14
dashboard/src/gql/platform/getAnnouncements.gql
Normal file
14
dashboard/src/gql/platform/getAnnouncements.gql
Normal file
@@ -0,0 +1,14 @@
|
||||
query getAnnouncements($limit: Int) {
|
||||
announcements(
|
||||
order_by: { createdAt: desc }
|
||||
limit: $limit
|
||||
where: {
|
||||
_or: [{ expiresAt: { _is_null: true } }, { expiresAt: { _gt: now } }]
|
||||
}
|
||||
) {
|
||||
id
|
||||
href
|
||||
content
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import AnnouncementProvider from '@/components/common/Announcement/AnnouncementProvider';
|
||||
import { DialogProvider } from '@/components/common/DialogProvider';
|
||||
import { UIProvider } from '@/components/common/UIProvider';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
@@ -106,9 +105,7 @@ function MyApp({
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
<DialogProvider>
|
||||
<AnnouncementProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</AnnouncementProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</DialogProvider>
|
||||
</RetryableErrorBoundary>
|
||||
</ThemeProvider>
|
||||
|
||||
632
dashboard/src/utils/__generated__/graphql.ts
generated
632
dashboard/src/utils/__generated__/graphql.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user