Compare commits

..

49 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
82212345c8 Merge pull request #2311 from nhost/changeset-release/main
chore: update versions
2023-10-11 13:54:25 +01:00
Hassan Ben Jobrane
32d3f167c5 Merge pull request #2313 from nhost/fix/quickstarts
fix: quickstarts: toml node version and tsconfig moduleResolution
2023-10-11 12:37:38 +01:00
github-actions[bot]
3d5f1ea922 chore: update versions 2023-10-11 10:24:24 +00:00
Hassan Ben Jobrane
97841ee5e8 Merge pull request #2312 from nhost/fix/dashboard/run-tab
fix: dashboard: disable run tab when developing locally
2023-10-11 11:21:38 +01:00
Hassan Ben Jobrane
4f3a615ebe fix: quickstarts: fix backend functions node version 2023-10-11 10:14:09 +01:00
Hassan Ben Jobrane
8e8197691c fix: quickstarts: change module resolution to node 2023-10-11 10:13:40 +01:00
Hassan Ben Jobrane
e10389ecf6 chore: add changeset 2023-10-11 10:08:11 +01:00
Hassan Ben Jobrane
cbdf6affec fix(dashboard): disable run services tab in local dev mode 2023-10-11 10:07:02 +01:00
Hassan Ben Jobrane
d19406e694 Merge pull request #2309 from nhost/fix/integration/apollo
fix: integrations: apollo: set accessToken to null after TOKEN_CHANGED event on sign-out
2023-10-10 16:24:39 +01:00
Hassan Ben Jobrane
cffc5dc65b Merge pull request #2310 from nhost/chore/ci/stop-dashboard-releases
chore: ci: stop @nhost/dashboard github releases
2023-10-10 16:24:01 +01:00
Hassan Ben Jobrane
2b5cb58553 chore: ci: stop @nhost/dashboard github releases 2023-10-10 16:03:37 +01:00
Hassan Ben Jobrane
7459a9413e chore: add changeset 2023-10-10 15:43:28 +01:00
Hassan Ben Jobrane
56871cc9f7 fix: integrations: apollo: set accessToken to null after TOKEN_CHANGED event on signout 2023-10-10 13:38:04 +01:00
Hassan Ben Jobrane
8f4d66e52d Merge pull request #2308 from nhost/changeset-release/main
chore: update versions
2023-10-10 13:31:53 +01:00
github-actions[bot]
315a820073 chore: update versions 2023-10-10 12:04:28 +00:00
Hassan Ben Jobrane
ca57ad2cbd Merge pull request #2301 from nhost/feat/quickstarts/sveltekit
feat(quickstarts): sveltekit <--> nhost
2023-10-10 13:00:14 +01:00
Hassan Ben Jobrane
40259344eb Merge pull request #2306 from nhost/chore/providers-updated-notice
chore(dashboard): show oauth providers update notice
2023-10-10 12:45:12 +01:00
Hassan Ben Jobrane
4749f60a08 chore: add changeset 2023-10-09 20:12:04 +01:00
Hassan Ben Jobrane
ac1888514d chore: update readme for sveltekit quickstart 2023-10-09 19:52:44 +01:00
Hassan Ben Jobrane
49b4af439b feat: signup/signin via webauthn 2023-10-09 19:23:50 +01:00
Hassan Ben Jobrane
61e03d6c70 chore: make sure deprication notice is under a project 2023-10-09 16:48:01 +01:00
Hassan Ben Jobrane
bec0fce497 chore: add deprication banner 2023-10-09 15:00:58 +01:00
Hassan Ben Jobrane
c01568a7dd chore: add changeset 2023-10-09 14:28:40 +01:00
Hassan Ben Jobrane
e934216a82 chore: bring back providers update alert 2023-10-09 14:26:41 +01:00
Hassan Ben Jobrane
701d6b8c84 feat: sveltekit: add delete pat 2023-10-07 19:47:05 +01:00
Hassan Ben Jobrane
e158e2440a wip: sveltekit: add echo and pat pages 2023-10-07 19:22:29 +01:00
Hassan Ben Jobrane
fbaa657001 wip: sveltekit: refresh access token + create/delete todos 2023-10-05 17:08:55 +01:00
Hassan Ben Jobrane
559db6d0ec wip: sveltekit: auth + todos(create) 2023-10-05 12:48:59 +01:00
Hassan Ben Jobrane
4c844930f1 wip: update: sveltekit 2023-10-04 16:58:59 +01:00
Hassan Ben Jobrane
3ef503ff81 Merge pull request #2298 from nhost/changeset-release/main
chore: update versions
2023-10-04 16:47:51 +02:00
github-actions[bot]
bfcfd236ea chore: update versions 2023-10-04 14:13:38 +00:00
Hassan Ben Jobrane
bfa7033506 Merge pull request #2296 from nhost/feat/query-announcements
feat: dashboard: query announcements
2023-10-04 16:09:48 +02:00
Hassan Ben Jobrane
78c29fcf0e feat: filter expired announcements 2023-10-04 14:40:22 +01:00
Hassan Ben Jobrane
f1b934ed22 chore: remove old announcement provider 2023-10-04 10:52:20 +01:00
Hassan Ben Jobrane
914369c53f feat: add announcements list component 2023-10-03 20:18:41 +01:00
Hassan Ben Jobrane
af379b967e chore: clean up commented code 2023-10-03 17:43:24 +01:00
Hassan Ben Jobrane
c3efb7ec84 chore: add changeset 2023-10-03 17:41:35 +01:00
Hassan Ben Jobrane
27cbd48c8c feat(dashboard): query latest announcement from platform 2023-10-03 17:40:50 +01:00
Hassan Ben Jobrane
236996a903 Merge pull request #2293 from nhost/changeset-release/main
chore: update versions
2023-10-02 13:01:48 +02:00
github-actions[bot]
5d0936bb93 chore: update versions 2023-10-02 10:38:35 +00:00
Hassan Ben Jobrane
733c212f2d Merge pull request #2291 from nhost/chore/announcement/node18
chore: node18 announcement
2023-10-02 12:35:53 +02:00
Hassan Ben Jobrane
8b47549189 Merge pull request #2286 from nhost/chore/ci/disable-github-releases
chore(ci): set createGithubReleases to false
2023-10-02 12:26:10 +02:00
Hassan Ben Jobrane
3c9c1025ce Merge pull request #2287 from nhost/fix/vue-sdk/nested-unref
fix(vue-sdk): correctly unref arrays
2023-10-02 12:25:14 +02:00
Hassan Ben Jobrane
3e46d3873c chore: add changeset 2023-10-02 10:50:46 +01:00
Hassan Ben Jobrane
4cf8820d72 chore: open announcement link in a new tab 2023-10-02 10:39:15 +01:00
Hassan Ben Jobrane
02a11184fb chore: change announcement link 2023-10-02 10:38:04 +01:00
Hassan Ben Jobrane
7214d47cc7 chore: add changeset 2023-09-30 17:54:42 +01:00
Hassan Ben Jobrane
238b77baad fix(vue): correctly unref arrays 2023-09-30 17:53:05 +01:00
Hassan Ben Jobrane
81b8e538b4 chore(ci): set createGithubReleases to false 2023-09-29 17:12:21 +01:00
113 changed files with 2319 additions and 1832 deletions

View File

@@ -5,6 +5,5 @@
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@nhost-examples/sveltekit"]
"updateInternalDependencies": "patch"
}

View File

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

View File

@@ -2,5 +2,6 @@
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"eslint.workingDirectories": ["./dashboard"]
"eslint.workingDirectories": ["./dashboard"],
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

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

View File

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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -1,3 +0,0 @@
export * from './Announcement';
export * from './AnnouncementProvider';
export { default as useAnnouncement } from './useAnnouncement';

View File

@@ -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;
}

View File

@@ -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>
)
);
}

View File

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

View File

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

View 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&apos;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&apos;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>
);
}

View File

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

View File

@@ -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>
);
}

View File

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

View File

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

View File

@@ -142,6 +142,7 @@ export default function useProjectRoutes() {
exact: false,
label: 'Run',
icon: <ServicesIcon />,
disabled: !isPlatform,
},
...nhostRoutes,
];

View 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
}
}

View File

@@ -0,0 +1,5 @@
mutation confirmProvidersUpdated($id: uuid!) {
updateApp(pk_columns: { id: $id }, _set: { providersUpdated: true }) {
id
}
}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

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

View File

@@ -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>
);

View File

@@ -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>
);

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.21.3'

View 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

View 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
```

View 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
}

View File

@@ -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"
}
}

View File

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

View File

@@ -1,5 +1,6 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}

View 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)
}

View File

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

View File

@@ -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}

View File

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

View File

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

View File

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

View 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)
}
}
}

View 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 = {}

View 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
}
}

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

View File

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

View File

@@ -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
}
}
}
}

View File

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

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}
}

View File

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

View File

@@ -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
}
}
}
}

View File

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

View File

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

View File

@@ -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')
}
}

View File

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

View File

@@ -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
}
}
}
}

View File

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

View File

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

View File

@@ -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
}
}

View File

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

View File

@@ -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
}
}

View File

@@ -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}

View File

@@ -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 })
}

View File

@@ -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')
}
}

View File

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

View File

@@ -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
}
}

View File

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

View File

@@ -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')
}
}

View File

@@ -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')
}
}

View File

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

View File

@@ -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)
}

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -2,5 +2,8 @@ import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()],
server: {
port: 3000
}
})

View File

@@ -1,5 +0,0 @@
---
'@nhost-examples/sveltekit': minor
---
feat: add nhost with sveltekit example project

View File

@@ -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
```

View File

@@ -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
}

View File

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

View File

@@ -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;
};

View File

@@ -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
};
};

View File

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