Compare commits
23 Commits
@nhost/das
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60b685ab02 | ||
|
|
2e65bc6dc0 | ||
|
|
14e6100722 | ||
|
|
479dba102e | ||
|
|
c9b84c7658 | ||
|
|
c78a765941 | ||
|
|
72899a600f | ||
|
|
fe6e8e2d15 | ||
|
|
737945bd0b | ||
|
|
8f77914eb3 | ||
|
|
839ca68f74 | ||
|
|
10b0f7490e | ||
|
|
9cb18747e8 | ||
|
|
d872d45a60 | ||
|
|
7324d8c089 | ||
|
|
7a50849ab3 | ||
|
|
b0558fcb19 | ||
|
|
5f94486faf | ||
|
|
2e58b9fd26 | ||
|
|
eb9539277b | ||
|
|
65c01c1e81 | ||
|
|
8e4282b094 | ||
|
|
81e1d78315 |
@@ -15,6 +15,8 @@
|
||||
<a href="https://twitter.com/nhost">Twitter</a>
|
||||
<span> • </span>
|
||||
<a href="https://nhost.io/discord">Discord</a>
|
||||
<span> • </span>
|
||||
<a href="https://gurubase.io/g/nhost">Ask Nhost Guru (third party, unofficial)</a>
|
||||
<br />
|
||||
|
||||
<hr />
|
||||
@@ -148,4 +150,4 @@ Here are some ways of contributing to making Nhost better:
|
||||
<p align="center">
|
||||
<img width="720" src="https://contrib.rocks/image?repo=nhost/nhost" alt="A table of avatars from the project's contributors" />
|
||||
</p>
|
||||
</a>
|
||||
</a>
|
||||
17
changelog_summary.sh
Executable file
17
changelog_summary.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#/usr/bin/env bash
|
||||
PREV_MONTH=$(date -d "1 month ago" +%Y-%m)
|
||||
|
||||
echo "prev: $PREV_MONTH"
|
||||
|
||||
files=$(git log --since="$PREV_MONTH-01" --until="$PREV_MONTH-31" --name-only -- '**/CHANGELOG.md' | grep CHANGE | sort -u)
|
||||
|
||||
echo "files: $files"
|
||||
|
||||
echo "Below you can find the latest release for each individual package released during this month:"
|
||||
echo
|
||||
|
||||
for file in $files; do
|
||||
name=$(grep '^# ' $file | awk '{ print substr($0, 4) }')
|
||||
last_release=$(grep '^## ' $file | awk '{ print substr($0, 4) }' | head -n 1)
|
||||
echo "@$name: $last_release [CHANGELOG.md](https://github.com/nhost/nhost/blob/main/$file)"
|
||||
done
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "2.4.0",
|
||||
"version": "2.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -5,8 +5,8 @@ import { LocalAccountMenu } from '@/components/layout/LocalAccountMenu';
|
||||
import { MobileNav } from '@/components/layout/MobileNav';
|
||||
import { Logo } from '@/components/presentational/Logo';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { DevAssistant as WorkspaceProjectDevAssistant } from '@/features/ai/DevAssistant';
|
||||
import { AnnouncementsTray } from '@/features/orgs/components/members/components/AnnouncementsTray';
|
||||
import { NotificationsTray } from '@/features/orgs/components/members/components/NotificationsTray';
|
||||
@@ -74,8 +74,12 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
<BreadcrumbNav />
|
||||
|
||||
<div className="hidden grid-flow-col items-center gap-1 sm:grid">
|
||||
<Button className="rounded-full" onClick={openDevAssistant}>
|
||||
<GraphiteIcon className="h-4 w-4" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8"
|
||||
onClick={openDevAssistant}
|
||||
>
|
||||
<GraphiteIcon className="h-4" />
|
||||
</Button>
|
||||
|
||||
<NotificationsTray />
|
||||
|
||||
@@ -38,6 +38,11 @@ const projectSettingsPages = [
|
||||
slug: 'authentication',
|
||||
route: 'authentication',
|
||||
},
|
||||
{
|
||||
name: 'JWT',
|
||||
slug: 'jwt',
|
||||
route: 'jwt',
|
||||
},
|
||||
{
|
||||
name: 'Sign-In methods',
|
||||
slug: 'sign-in-methods',
|
||||
@@ -126,7 +131,7 @@ export default function ProjectSettingsPagesComboBox() {
|
||||
) : (
|
||||
<>Select a page</>
|
||||
)}
|
||||
<ChevronsUpDown className="w-5 h-5 text-muted-foreground" />
|
||||
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="start">
|
||||
@@ -156,7 +161,7 @@ export default function ProjectSettingsPagesComboBox() {
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="truncate max-w-52">{option.label}</span>
|
||||
<span className="max-w-52 truncate">{option.label}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
|
||||
@@ -8,13 +8,20 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/v3/command';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/v3/hover-card';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/v3/popover';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Box, Check, ChevronsUpDown } from 'lucide-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -24,17 +31,76 @@ type Option = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
function ProjectStatusIndicator({ status }: { status: ApplicationStatus }) {
|
||||
const indicatorStyles: Record<
|
||||
number,
|
||||
{ className: string; description: string }
|
||||
> = {
|
||||
[ApplicationStatus.Errored]: {
|
||||
className: 'bg-destructive',
|
||||
description: 'Project errored',
|
||||
},
|
||||
[ApplicationStatus.Pausing]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is pausing',
|
||||
},
|
||||
[ApplicationStatus.Restoring]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is restoring',
|
||||
},
|
||||
[ApplicationStatus.Paused]: {
|
||||
className: 'bg-slate-400',
|
||||
description: 'Project is paused',
|
||||
},
|
||||
[ApplicationStatus.Unpausing]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is unpausing',
|
||||
},
|
||||
[ApplicationStatus.Live]: {
|
||||
className: 'bg-primary-main',
|
||||
description: 'Project is live',
|
||||
},
|
||||
};
|
||||
const style = indicatorStyles[status];
|
||||
|
||||
if (style) {
|
||||
return (
|
||||
<HoverCard openDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span
|
||||
className={cn('mt-[1px] h-2 w-2 rounded-full', style.className)}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="top" className="h-fit w-fit py-2">
|
||||
{style.description}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function ProjectsComboBox() {
|
||||
const {
|
||||
query: { appSubdomain },
|
||||
push,
|
||||
} = useRouter();
|
||||
|
||||
const { state: appState } = useAppState();
|
||||
const { currentOrg: { slug: orgSlug, apps = [] } = {} } = useOrgs();
|
||||
const selectedProjectFromUrl = apps.find(
|
||||
(item) => item.subdomain === appSubdomain,
|
||||
);
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState<Option | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const options: Option[] = apps.map((app) => ({
|
||||
label: app.name,
|
||||
value: app.subdomain,
|
||||
}));
|
||||
|
||||
const selectedProjectFromUrl = apps.find(
|
||||
(app) => app.subdomain === appSubdomain,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProjectFromUrl) {
|
||||
@@ -45,68 +111,64 @@ export default function ProjectsComboBox() {
|
||||
}
|
||||
}, [selectedProjectFromUrl]);
|
||||
|
||||
const options: Option[] = apps.map((app) => ({
|
||||
label: app.name,
|
||||
value: app.subdomain,
|
||||
}));
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleProjectSelect = (option: Option) => {
|
||||
setSelectedProject(option);
|
||||
setOpen(false);
|
||||
push(`/orgs/${orgSlug}/projects/${option.value}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-2 bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
||||
>
|
||||
{selectedProject ? (
|
||||
<div className="flex flex-row items-center justify-center gap-1">
|
||||
<Box className="h-4 w-4" />
|
||||
{selectedProject.label}
|
||||
<ProjectStatus />
|
||||
</div>
|
||||
) : (
|
||||
<>Select a project</>
|
||||
)}
|
||||
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select a project..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
keywords={[option.label]}
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={() => {
|
||||
setSelectedProject(option);
|
||||
setOpen(false);
|
||||
push(`/orgs/${orgSlug}/projects/${option.value}`);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedProject?.value === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<Box className="h-4 w-4" />
|
||||
<span className="max-w-52 truncate">{option.label}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="flex items-center gap-1">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-2 bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
||||
>
|
||||
{selectedProject ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ProjectStatusIndicator status={appState} />
|
||||
{selectedProject.label}
|
||||
<ProjectStatus />
|
||||
</div>
|
||||
) : (
|
||||
<>Select a project</>
|
||||
)}
|
||||
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select a project..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={() => handleProjectSelect(option)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedProject?.value === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-1">
|
||||
<Box className="h-4 w-4" />
|
||||
<span className="max-w-52 truncate">{option.label}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,73 +30,73 @@ import { useTreeNavState } from './TreeNavStateContext';
|
||||
const projectPages = [
|
||||
{
|
||||
name: 'Overview',
|
||||
icon: <HomeIcon className="w-4 h-4" />,
|
||||
icon: <HomeIcon className="h-4 w-4" />,
|
||||
route: '',
|
||||
slug: 'overview',
|
||||
},
|
||||
{
|
||||
name: 'Database',
|
||||
icon: <DatabaseIcon className="w-4 h-4" />,
|
||||
icon: <DatabaseIcon className="h-4 w-4" />,
|
||||
route: 'database/browser/default',
|
||||
slug: 'database',
|
||||
},
|
||||
{
|
||||
name: 'GraphQL',
|
||||
icon: <GraphQLIcon className="w-4 h-4" />,
|
||||
icon: <GraphQLIcon className="h-4 w-4" />,
|
||||
route: 'graphql',
|
||||
slug: 'graphql',
|
||||
},
|
||||
{
|
||||
name: 'Hasura',
|
||||
icon: <HasuraIcon className="w-4 h-4" />,
|
||||
icon: <HasuraIcon className="h-4 w-4" />,
|
||||
route: 'hasura',
|
||||
slug: 'hasura',
|
||||
},
|
||||
{
|
||||
name: 'Auth',
|
||||
icon: <UserIcon className="w-4 h-4" />,
|
||||
icon: <UserIcon className="h-4 w-4" />,
|
||||
route: 'users',
|
||||
slug: 'users',
|
||||
},
|
||||
{
|
||||
name: 'Storage',
|
||||
icon: <StorageIcon className="w-4 h-4" />,
|
||||
icon: <StorageIcon className="h-4 w-4" />,
|
||||
route: 'storage',
|
||||
slug: 'storage',
|
||||
},
|
||||
{
|
||||
name: 'Run',
|
||||
icon: <ServicesIcon className="w-4 h-4" />,
|
||||
icon: <ServicesIcon className="h-4 w-4" />,
|
||||
route: 'run',
|
||||
slug: 'run',
|
||||
},
|
||||
{
|
||||
name: 'AI',
|
||||
icon: <AIIcon className="w-4 h-4" />,
|
||||
icon: <AIIcon className="h-4 w-4" />,
|
||||
route: 'ai/auto-embeddings',
|
||||
slug: 'ai',
|
||||
},
|
||||
{
|
||||
name: 'Deployments',
|
||||
icon: <RocketIcon className="w-4 h-4" />,
|
||||
icon: <RocketIcon className="h-4 w-4" />,
|
||||
route: 'deployments',
|
||||
slug: 'deployments',
|
||||
},
|
||||
{
|
||||
name: 'Backups',
|
||||
icon: <CloudIcon className="w-4 h-4" />,
|
||||
icon: <CloudIcon className="h-4 w-4" />,
|
||||
route: 'backups',
|
||||
slug: 'backups',
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
icon: <FileTextIcon className="w-4 h-4" />,
|
||||
icon: <FileTextIcon className="h-4 w-4" />,
|
||||
route: 'logs',
|
||||
slug: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'Metrics',
|
||||
icon: <GaugeIcon className="w-4 h-4" />,
|
||||
icon: <GaugeIcon className="h-4 w-4" />,
|
||||
route: 'metrics',
|
||||
slug: 'metrics',
|
||||
},
|
||||
@@ -121,6 +121,11 @@ const projectSettingsPages = [
|
||||
slug: 'authentication',
|
||||
route: 'authentication',
|
||||
},
|
||||
{
|
||||
name: 'JWT',
|
||||
slug: 'jwt',
|
||||
route: 'jwt',
|
||||
},
|
||||
{
|
||||
name: 'Sign-In methods',
|
||||
slug: 'sign-in-methods',
|
||||
@@ -204,7 +209,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
data: {
|
||||
name: 'New project',
|
||||
slug: 'new',
|
||||
icon: <Plus className="w-4 h-4 mr-1 font-bold" strokeWidth={3} />,
|
||||
icon: <Plus className="mr-1 h-4 w-4 font-bold" strokeWidth={3} />,
|
||||
targetUrl: `/orgs/${org.slug}/projects/new`,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
@@ -219,7 +224,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
data: {
|
||||
name: app.name,
|
||||
slug: app.subdomain,
|
||||
icon: <Box className="w-4 h-4" />,
|
||||
icon: <Box className="h-4 w-4" />,
|
||||
targetUrl: `/orgs/${org.slug}/projects/${app.subdomain}`,
|
||||
},
|
||||
children: projectPages.map(
|
||||
@@ -398,9 +403,9 @@ export default function NavTree() {
|
||||
className="h-8 px-1"
|
||||
>
|
||||
{context.isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 font-bold" strokeWidth={3} />
|
||||
<ChevronDown className="h-4 w-4 font-bold" strokeWidth={3} />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" strokeWidth={3} />
|
||||
<ChevronRight className="h-4 w-4" strokeWidth={3} />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
@@ -492,9 +497,9 @@ export default function NavTree() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex justify-center px-[12px] pb-3">
|
||||
<div className="w-0 h-full border-r border-dashed" />
|
||||
<div className="h-full w-0 border-r border-dashed" />
|
||||
</div>
|
||||
<ul {...containerProps} className="w-full">
|
||||
{children}
|
||||
|
||||
@@ -13,7 +13,7 @@ const buttonVariants = cva(
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
'border bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Badge } from '@/components/ui/v3/badge';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/v3/dropdown-menu';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -7,15 +14,64 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/v3/sheet';
|
||||
import { Announcements } from '@/features/projects/common/components/Announcements';
|
||||
import { Megaphone } from 'lucide-react';
|
||||
import {
|
||||
useDeleteAnnouncementReadMutation,
|
||||
useGetAnnouncementsQuery,
|
||||
useInsertAnnouncementReadMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useAuthenticationStatus } from '@nhost/nextjs';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { EllipsisVertical, Megaphone } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function AnnouncementsTray() {
|
||||
const { isAuthenticated } = useAuthenticationStatus();
|
||||
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
refetch: refetchAnnouncements,
|
||||
} = useGetAnnouncementsQuery({
|
||||
skip: !isAuthenticated,
|
||||
});
|
||||
|
||||
const [insertAnnouncementRead] = useInsertAnnouncementReadMutation();
|
||||
const [deleteAnnouncementRead] = useDeleteAnnouncementReadMutation();
|
||||
|
||||
const announcements = data?.announcements ?? [];
|
||||
const unreadAnnouncementsCount = announcements.filter(
|
||||
(ann) => ann.read.length === 0,
|
||||
).length;
|
||||
|
||||
const handleSetUnread = async (announcementReadId: string) => {
|
||||
await deleteAnnouncementRead({
|
||||
variables: {
|
||||
id: announcementReadId,
|
||||
},
|
||||
});
|
||||
|
||||
await refetchAnnouncements();
|
||||
};
|
||||
|
||||
const handleSetRead = async (announcementID: string) => {
|
||||
await insertAnnouncementRead({
|
||||
variables: { announcementID },
|
||||
});
|
||||
|
||||
await refetchAnnouncements();
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-fit px-3 py-1">
|
||||
<Megaphone className="h-5 w-5" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="relative flex h-8 items-center gap-2 px-2"
|
||||
>
|
||||
<Megaphone className="h-4.5 w-4.5" />
|
||||
{unreadAnnouncementsCount > 0 && (
|
||||
<Badge variant="destructive">{unreadAnnouncementsCount}</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="h-full w-full bg-background p-0 text-foreground sm:max-w-[310px]">
|
||||
@@ -25,8 +81,102 @@ export default function AnnouncementsTray() {
|
||||
Latest news and announcements.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col px-8 pt-3">
|
||||
<Announcements />
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-12 items-center border-b px-2">
|
||||
<h3 className="font-medium">
|
||||
Latest Announcements{' '}
|
||||
{unreadAnnouncementsCount > 0 && `(${unreadAnnouncementsCount})`}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-col gap-2 overflow-auto p-2">
|
||||
{!loading && announcements.length === 0 && (
|
||||
<span className="text-muted-foreground">
|
||||
No new announcements
|
||||
</span>
|
||||
)}
|
||||
{announcements.map((announcement) => (
|
||||
<Button
|
||||
key={announcement.id}
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="h-fit w-full items-start gap-2 rounded-md border p-2"
|
||||
>
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={announcement.href}
|
||||
shallow
|
||||
onClick={() => {
|
||||
if (announcement.read.length === 0) {
|
||||
handleSetRead(announcement.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{announcement.read.length === 0 ? (
|
||||
<span className="mt-[5px] h-2 w-2 flex-shrink-0 rounded-full bg-primary" />
|
||||
) : (
|
||||
<span className="mt-[5px] h-2 w-2 flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDistance(
|
||||
new Date(announcement.createdAt),
|
||||
new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
<p className="whitespace-normal">
|
||||
{announcement.content}
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 px-3"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="absolute">
|
||||
<EllipsisVertical className="h-4 w-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={-5}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
disabled={announcement.read.length > 0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSetRead(announcement.id);
|
||||
}}
|
||||
>
|
||||
Mark as read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={announcement.read.length === 0}
|
||||
onClick={() =>
|
||||
handleSetUnread(announcement.read.at(0).id)
|
||||
}
|
||||
>
|
||||
Mark as unread
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
useOrganizationMemberInvitesLazyQuery,
|
||||
useOrganizationNewRequestsLazyQuery,
|
||||
usePostOrganizationRequestMutation,
|
||||
type OrganizationMemberInvitesQuery,
|
||||
type PostOrganizationRequestResponse,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
@@ -33,10 +34,13 @@ import { Bell } from 'lucide-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type Invite = OrganizationMemberInvitesQuery['organizationMemberInvites'][0];
|
||||
|
||||
export default function NotificationsTray() {
|
||||
const userData = useUserData();
|
||||
const { asPath, route } = useRouter();
|
||||
const { asPath, route, push } = useRouter();
|
||||
const { refetch: refetchOrgs } = useOrgs();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [stripeFormDialogOpen, setStripeFormDialogOpen] = useState(false);
|
||||
|
||||
@@ -113,17 +117,19 @@ export default function NotificationsTray() {
|
||||
const [acceptInvite] = useOrganizationMemberInviteAcceptMutation();
|
||||
const [deleteInvite] = useDeleteOrganizationMemberInviteMutation();
|
||||
|
||||
const handleAccept = async (inviteId: string) => {
|
||||
const handleAccept = async (invite: Invite) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await acceptInvite({
|
||||
variables: {
|
||||
inviteId,
|
||||
inviteId: invite.id,
|
||||
},
|
||||
});
|
||||
|
||||
refetchInvites();
|
||||
refetchOrgs();
|
||||
await refetchInvites();
|
||||
await refetchOrgs();
|
||||
await push(`/orgs/${invite?.organization?.slug}/projects`);
|
||||
setOpen(false);
|
||||
},
|
||||
{
|
||||
loadingMessage: `Accepting invite...`,
|
||||
@@ -154,12 +160,18 @@ export default function NotificationsTray() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-fit px-3 py-1">
|
||||
<Bell className="mt-[2px] h-[1.15rem] w-[1.15rem]" />
|
||||
{(pendingOrgRequest || Boolean(invites.length)) && (
|
||||
<div className="absolute right-3 top-2 h-2 w-2 rounded-full bg-red-500" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="relative flex h-8 items-center gap-2 border px-2"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<Bell className="h-4.5 w-4.5" />
|
||||
{(pendingOrgRequest || invites.length > 0) && (
|
||||
<Badge variant="destructive">
|
||||
{invites.length + (pendingOrgRequest ? 1 : 0)}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
@@ -248,7 +260,7 @@ export default function NotificationsTray() {
|
||||
</Button>
|
||||
<Button
|
||||
className="h-fit"
|
||||
onClick={() => handleAccept(invite.id)}
|
||||
onClick={() => handleAccept(invite)}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Alert } from '@/components/ui/v2/Alert';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { ApplicationPaused } from '@/features/orgs/projects/common/components/ApplicationPaused';
|
||||
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||
import { ApplicationProvisioning } from '@/features/orgs/projects/common/components/ApplicationProvisioning';
|
||||
import { ApplicationRestoring } from '@/features/orgs/projects/common/components/ApplicationRestoring';
|
||||
import { ApplicationUnknown } from '@/features/orgs/projects/common/components/ApplicationUnknown';
|
||||
@@ -15,7 +16,7 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo, type ReactNode } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface ProjectLayoutProps extends AuthenticatedLayoutProps {
|
||||
@@ -34,11 +35,53 @@ function ProjectLayoutContent({
|
||||
query: { appSubdomain },
|
||||
} = useRouter();
|
||||
|
||||
const isPlatform = useIsPlatform();
|
||||
const { state } = useAppState();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { project, loading, error } = useProject({ poll: true });
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
|
||||
const renderPausedProjectContent = useCallback(
|
||||
(_children: ReactNode) => {
|
||||
const baseProjectPageRoute = '/orgs/[orgSlug]/projects/[appSubdomain]/';
|
||||
const blockedPausedProjectPages = [
|
||||
'database',
|
||||
'database/browser/[dataSourceSlug]',
|
||||
'graphql',
|
||||
'hasura',
|
||||
'users',
|
||||
'storage',
|
||||
'ai/auto-embeddings',
|
||||
'ai/assistants',
|
||||
'metrics',
|
||||
].map((page) => baseProjectPageRoute.concat(page));
|
||||
|
||||
// show an alert box on top of the overview page with a wake up button
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-5 flex max-w-7xl p-4 pb-0">
|
||||
<ApplicationPausedBanner
|
||||
alertClassName="flex-row"
|
||||
textContainerClassName="flex flex-col items-center justify-center text-left"
|
||||
wakeUpButtonClassName="w-fit self-center"
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// block these pages when the project is paused
|
||||
if (blockedPausedProjectPages.includes(route)) {
|
||||
return <ApplicationPaused />;
|
||||
}
|
||||
|
||||
return _children;
|
||||
},
|
||||
[route, isOnOverviewPage, children],
|
||||
);
|
||||
|
||||
// Render application state based on the current state
|
||||
const projectPageContent = useMemo(() => {
|
||||
if (!appSubdomain || state === undefined) {
|
||||
@@ -67,7 +110,7 @@ function ProjectLayoutContent({
|
||||
return children;
|
||||
case ApplicationStatus.Pausing:
|
||||
case ApplicationStatus.Paused:
|
||||
return <ApplicationPaused />;
|
||||
return renderPausedProjectContent(children);
|
||||
case ApplicationStatus.Unpausing:
|
||||
return <ApplicationUnpausing />;
|
||||
case ApplicationStatus.Restoring:
|
||||
@@ -79,7 +122,13 @@ function ProjectLayoutContent({
|
||||
default:
|
||||
return <ApplicationUnknown />;
|
||||
}
|
||||
}, [state, children, appSubdomain, isOnOverviewPage]);
|
||||
}, [
|
||||
state,
|
||||
children,
|
||||
appSubdomain,
|
||||
isOnOverviewPage,
|
||||
renderPausedProjectContent,
|
||||
]);
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type OTPEmailSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function OTPEmailSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { enabled } = data?.config?.auth?.method?.otp?.email || {};
|
||||
|
||||
const form = useForm<OTPEmailSettingsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({ enabled });
|
||||
}
|
||||
}, [loading, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading one-time passwords over email settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const handleOTPEmailSettingsChange = async (
|
||||
values: OTPEmailSettingsFormValues,
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
otp: {
|
||||
email: {
|
||||
enabled: values.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset(values);
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage:
|
||||
'One-time passwords over email settings are being updated...',
|
||||
successMessage:
|
||||
'One-time passwords over email settings have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update one-time passwords over email settings.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleOTPEmailSettingsChange}>
|
||||
<SettingsContainer
|
||||
title="One-Time Passwords over email"
|
||||
description="Allow users to sign in with a one-time password sent to their email address."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as OTPEmailSettings } from './OTPEmailSettings';
|
||||
@@ -50,6 +50,7 @@ const validationSchema = Yup.object({
|
||||
is: true,
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
audience: Yup.string().label('Audience'),
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
@@ -72,7 +73,7 @@ export default function AppleProviderSettings() {
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { clientId, enabled, keyId, privateKey, teamId } =
|
||||
const { clientId, enabled, keyId, privateKey, teamId, audience } =
|
||||
data?.config?.auth?.method?.oauth?.apple || {};
|
||||
|
||||
const form = useForm<AppleProviderFormValues>({
|
||||
@@ -82,6 +83,7 @@ export default function AppleProviderSettings() {
|
||||
keyId: keyId || '',
|
||||
clientId: clientId || '',
|
||||
privateKey: privateKey || '',
|
||||
audience: audience || '',
|
||||
enabled: enabled || false,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
@@ -94,10 +96,11 @@ export default function AppleProviderSettings() {
|
||||
keyId: keyId || '',
|
||||
clientId: clientId || '',
|
||||
privateKey: privateKey || '',
|
||||
audience: audience || '',
|
||||
enabled: enabled || false,
|
||||
});
|
||||
}
|
||||
}, [loading, teamId, keyId, clientId, privateKey, enabled, form]);
|
||||
}, [loading, teamId, keyId, clientId, privateKey, audience, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -237,6 +240,18 @@ export default function AppleProviderSettings() {
|
||||
error={!!formState.errors?.privateKey}
|
||||
helperText={formState.errors?.privateKey?.message}
|
||||
/>
|
||||
<Input
|
||||
{...register('audience')}
|
||||
name="audience"
|
||||
id="audience"
|
||||
label="Audience (optional)"
|
||||
placeholder="Apple Audience"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!formState.errors?.audience}
|
||||
helperText={formState.errors?.audience?.message}
|
||||
/>
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="apple-redirectUrl"
|
||||
|
||||
@@ -9,10 +9,6 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import type { BaseProviderSettingsFormValues } from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
BaseProviderSettings,
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
@@ -28,6 +24,28 @@ import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/gen
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const googleProviderValidationSchema = Yup.object({
|
||||
clientId: Yup.string()
|
||||
.label('Client ID')
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
clientSecret: Yup.string()
|
||||
.label('Client Secret')
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
audience: Yup.string().label('Audience'),
|
||||
enabled: Yup.bool(),
|
||||
});
|
||||
|
||||
export type GoogleProviderFormValues = Yup.InferType<
|
||||
typeof googleProviderValidationSchema
|
||||
>;
|
||||
|
||||
export default function GoogleProviderSettings() {
|
||||
const { project } = useProject();
|
||||
@@ -45,17 +63,18 @@ export default function GoogleProviderSettings() {
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { clientId, clientSecret, enabled } =
|
||||
const { clientId, clientSecret, enabled, audience } =
|
||||
data?.config?.auth?.method?.oauth?.google || {};
|
||||
|
||||
const form = useForm<BaseProviderSettingsFormValues>({
|
||||
const form = useForm<GoogleProviderFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
clientId: clientId || '',
|
||||
clientSecret: clientSecret || '',
|
||||
audience: audience || '',
|
||||
enabled: enabled || false,
|
||||
},
|
||||
resolver: yupResolver(baseProviderValidationSchema),
|
||||
resolver: yupResolver(googleProviderValidationSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,10 +82,11 @@ export default function GoogleProviderSettings() {
|
||||
form.reset({
|
||||
clientId: clientId || '',
|
||||
clientSecret: clientSecret || '',
|
||||
audience: audience || '',
|
||||
enabled: enabled || false,
|
||||
});
|
||||
}
|
||||
}, [loading, clientId, clientSecret, enabled, form]);
|
||||
}, [loading, clientId, clientSecret, audience, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -82,7 +102,7 @@ export default function GoogleProviderSettings() {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState, watch } = form;
|
||||
const { formState, watch, register } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
@@ -148,11 +168,44 @@ export default function GoogleProviderSettings() {
|
||||
switchId="enabled"
|
||||
showSwitch
|
||||
className={twMerge(
|
||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
|
||||
'grid-flow-rows grid grid-cols-2 grid-rows-3 gap-x-3 gap-y-4 px-4 py-2',
|
||||
!authEnabled && 'hidden',
|
||||
)}
|
||||
>
|
||||
<BaseProviderSettings providerName="google" />
|
||||
<Input
|
||||
{...register('clientId')}
|
||||
id="google-clientId"
|
||||
label="Client ID"
|
||||
placeholder="Enter your Client ID"
|
||||
className="col-span-1"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!formState.errors?.clientId}
|
||||
helperText={formState.errors?.clientId?.message}
|
||||
/>
|
||||
<Input
|
||||
{...register('clientSecret')}
|
||||
id="google-clientSecret"
|
||||
label="Client Secret"
|
||||
placeholder="Enter your Client Secret"
|
||||
className="col-span-1"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!formState.errors?.clientSecret}
|
||||
helperText={formState.errors?.clientSecret?.message}
|
||||
/>
|
||||
<Input
|
||||
{...register('audience')}
|
||||
name="audience"
|
||||
id="audience"
|
||||
label="Audience (optional)"
|
||||
placeholder="Enter Audience"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!formState.errors?.audience}
|
||||
helperText={formState.errors?.audience?.message}
|
||||
/>
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="google-redirectUrl"
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Form } from '@/components/form/Form';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
useGetSignInMethodsQuery,
|
||||
@@ -38,10 +38,10 @@ export default function EditUserPasswordForm({
|
||||
client: remoteProjectGQLClient,
|
||||
});
|
||||
const { closeDialog } = useDialog();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const { project } = useProject();
|
||||
const { data } = useGetSignInMethodsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
skip: !currentProject?.id,
|
||||
variables: { appId: project?.id },
|
||||
skip: !project?.id,
|
||||
});
|
||||
|
||||
const passwordMinLength =
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { Modal } from '@/components/ui/v1/Modal';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog';
|
||||
import { ApplicationInfo } from '@/features/orgs/projects/common/components/ApplicationInfo';
|
||||
import { ApplicationLockedReason } from '@/features/orgs/projects/common/components/ApplicationLockedReason';
|
||||
import { ApplicationPausedReason } from '@/features/orgs/projects/common/components/ApplicationPausedReason';
|
||||
import { ApplicationPausedSymbol } from '@/features/orgs/projects/common/components/ApplicationPausedSymbol';
|
||||
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||
import { RemoveApplicationModal } from '@/features/orgs/projects/common/components/RemoveApplicationModal';
|
||||
import { StagingMetadata } from '@/features/orgs/projects/common/components/StagingMetadata';
|
||||
import { useAppPausedReason } from '@/features/orgs/projects/common/hooks/useAppPausedReason';
|
||||
import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
@@ -19,46 +13,19 @@ import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useUnpauseApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ApplicationPaused() {
|
||||
const { org } = useCurrentOrg();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
const { project } = useProject();
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
const [transferProjectDialogOpen, setTransferProjectDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
const [showDeletingModal, setShowDeletingModal] = useState(false);
|
||||
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
||||
useUnpauseApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
|
||||
const { isLocked, lockedReason, freeAndLiveProjectsNumberExceeded, loading } =
|
||||
useAppPausedReason();
|
||||
|
||||
async function handleTriggerUnpausing() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await unpauseApplication({ variables: { appId: project.id } });
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
await refetchProject();
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Starting the project...',
|
||||
successMessage: 'The project has been started successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while waking up the project. Please try again.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator label="Loading user data..." delay={1000} />;
|
||||
}
|
||||
useUnpauseApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -77,65 +44,38 @@ export default function ApplicationPaused() {
|
||||
</Modal>
|
||||
|
||||
<Container className="mx-auto grid max-w-lg grid-flow-row gap-6 text-center">
|
||||
<div className="mx-auto flex w-centImage flex-col text-center">
|
||||
<ApplicationPausedSymbol isLocked={isLocked} />
|
||||
</div>
|
||||
|
||||
<Box className="grid grid-flow-row gap-6">
|
||||
<Text variant="h3" component="h1">
|
||||
{project.name} is {isLocked ? 'locked' : 'paused'}
|
||||
</Text>
|
||||
{isLocked ? (
|
||||
<ApplicationLockedReason reason={lockedReason} />
|
||||
) : (
|
||||
<div className="mx-auto flex w-full max-w-xs flex-col gap-4">
|
||||
<ApplicationPausedBanner
|
||||
alertClassName="items-center"
|
||||
textContainerClassName="items-center text-center"
|
||||
/>
|
||||
{org && (
|
||||
<>
|
||||
<ApplicationPausedReason
|
||||
freeAndLiveProjectsNumberExceeded={
|
||||
freeAndLiveProjectsNumberExceeded
|
||||
}
|
||||
/>
|
||||
<div className="grid grid-flow-row gap-4">
|
||||
{org && (
|
||||
<>
|
||||
<Button
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => setTransferProjectDialogOpen(true)}
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
<TransferProjectDialog
|
||||
open={transferProjectDialogOpen}
|
||||
setOpen={setTransferProjectDialogOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={
|
||||
changingApplicationStateLoading ||
|
||||
freeAndLiveProjectsNumberExceeded
|
||||
}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
Wake Up
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => setTransferProjectDialogOpen(true)}
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="outlined"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<TransferProjectDialog
|
||||
open={transferProjectDialogOpen}
|
||||
setOpen={setTransferProjectDialogOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="outlined"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<StagingMetadata>
|
||||
<ApplicationInfo />
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { useAppPausedReason } from '@/features/orgs/projects/common/hooks/useAppPausedReason';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useUnpauseApplicationMutation } from '@/utils/__generated__/graphql';
|
||||
import Image from 'next/image';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export default function ApplicationPausedBanner({
|
||||
alertClassName,
|
||||
textContainerClassName,
|
||||
wakeUpButtonClassName,
|
||||
}: {
|
||||
alertClassName?: string;
|
||||
textContainerClassName?: string;
|
||||
wakeUpButtonClassName?: string;
|
||||
}) {
|
||||
const { org } = useCurrentOrg();
|
||||
const { state } = useAppState();
|
||||
const { freeAndLiveProjectsNumberExceeded } = useAppPausedReason();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
||||
useUnpauseApplicationMutation({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const handleTriggerUnpausing = useCallback(async () => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await unpauseApplication({ variables: { appId: project.id } });
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
await refetchProject();
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Starting the project...',
|
||||
successMessage: 'The project has been started successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while waking up the project. Please try again.',
|
||||
},
|
||||
);
|
||||
}, [unpauseApplication, project?.id, refetchProject]);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className={cn(
|
||||
'flex w-full flex-col items-start justify-between gap-4 p-4',
|
||||
alertClassName,
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src="/assets/PausedApp.svg"
|
||||
className="mt-1"
|
||||
alt="Closed Eye"
|
||||
width={52}
|
||||
height={40}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col gap-2',
|
||||
textContainerClassName,
|
||||
)}
|
||||
>
|
||||
<p className="w-full">
|
||||
Project <b>{project?.name}</b> is paused.
|
||||
</p>
|
||||
<p className="w-full">
|
||||
Wake up your project to make it accessible again. Once reactivated,
|
||||
all features will be fully functional. Go to settings to manage your
|
||||
project.
|
||||
</p>
|
||||
{org?.plan?.isFree && (
|
||||
<p>
|
||||
Projects under your Personal Organization will stop responding to
|
||||
API calls after 7 days of inactivity, so consider transferring the
|
||||
project to a <b>Pro Organization</b> to avoid auto-sleep.
|
||||
</p>
|
||||
)}
|
||||
{freeAndLiveProjectsNumberExceeded && (
|
||||
<p>
|
||||
Additionally, only 1 free project can be active at any given time,
|
||||
so please pause your current active free project before unpausing
|
||||
another.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{state === ApplicationStatus.Paused && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn('w-full', wakeUpButtonClassName)}
|
||||
disabled={changingApplicationStateLoading}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
{changingApplicationStateLoading ? <ActivityIndicator /> : 'Wake up'}
|
||||
</Button>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ApplicationPausedBanner } from './ApplicationPausedBanner';
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
|
||||
interface ApplicationPausedReasonProps {
|
||||
freeAndLiveProjectsNumberExceeded?: boolean;
|
||||
}
|
||||
|
||||
export default function ApplicationPausedReason({
|
||||
freeAndLiveProjectsNumberExceeded,
|
||||
}: ApplicationPausedReasonProps) {
|
||||
const { org } = useCurrentOrg();
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="flex flex-col w-full max-w-xs gap-4 p-6 mx-auto text-left"
|
||||
>
|
||||
{org?.plan?.isFree ? (
|
||||
<p>
|
||||
Projects under your Personal Organization will stop responding to API
|
||||
calls after 7 days of inactivity, so consider transferring the project
|
||||
to a <b>Pro Organization</b> to avoid auto-sleep.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center">Your project is Paused.</p>
|
||||
)}
|
||||
{freeAndLiveProjectsNumberExceeded && (
|
||||
<p className="text-center">
|
||||
Additionally, only 1 free project can be active at any given time, so
|
||||
please pause your current active free project before unpausing
|
||||
another.
|
||||
</p>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as ApplicationPausedReason } from './ApplicationPausedReason';
|
||||
@@ -48,7 +48,7 @@ export default function StagingMetadata({
|
||||
}: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
isDevOrStaging() && (
|
||||
<div className="mx-auto mt-10 max-w-sm">
|
||||
<div className="mx-auto max-w-sm">
|
||||
<Box className="mx-auto grid grid-flow-row justify-items-center rounded-md border p-5 text-center">
|
||||
<Status status={StatusEnum.Deploying}>Internal info</Status>
|
||||
{children}
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { StateBadge } from '@/components/presentational/StateBadge';
|
||||
import type { DeploymentStatus } from '@/components/presentational/StatusCircle';
|
||||
import { StatusCircle } from '@/components/presentational/StatusCircle';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import type { ButtonProps } from '@/components/ui/v2/Button';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
|
||||
import { SearchIcon } from '@/components/ui/v2/icons/SearchIcon';
|
||||
import type { InputProps } from '@/components/ui/v2/Input';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { DeploymentStatusMessage } from '@/features/projects/deployments/components/DeploymentStatusMessage';
|
||||
import type { ApplicationState, Workspace } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import NavLink from 'next/link';
|
||||
import type { ChangeEvent, PropsWithoutRef } from 'react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface WorkspaceAndProjectListProps extends BoxProps {
|
||||
/**
|
||||
* List of workspaces to be displayed.
|
||||
*/
|
||||
workspaces: Workspace[];
|
||||
/**
|
||||
* Props to be passed to individual slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
root?: BoxProps;
|
||||
header?: BoxProps;
|
||||
search?: PropsWithoutRef<InputProps>;
|
||||
button?: PropsWithoutRef<ButtonProps>;
|
||||
};
|
||||
}
|
||||
|
||||
function checkStatusOfTheApplication(stateHistory: ApplicationState[] | []) {
|
||||
if (stateHistory.length === 0) {
|
||||
return ApplicationStatus.Empty;
|
||||
}
|
||||
|
||||
if (stateHistory[0].stateId === undefined) {
|
||||
return ApplicationStatus.Empty;
|
||||
}
|
||||
|
||||
return stateHistory[0].stateId;
|
||||
}
|
||||
|
||||
export default function WorkspaceAndProjectList({
|
||||
workspaces,
|
||||
className,
|
||||
slotProps = {},
|
||||
...props
|
||||
}: WorkspaceAndProjectListProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
const { maintenanceActive } = useUI();
|
||||
|
||||
const handleQueryChange = debounce((event: ChangeEvent<HTMLInputElement>) => {
|
||||
slotProps?.search?.onChange?.(event);
|
||||
setQuery(event.target.value);
|
||||
}, 500);
|
||||
|
||||
const filteredWorkspaces = workspaces
|
||||
.map((workspace) => ({
|
||||
...workspace,
|
||||
projects: workspace.projects.filter((project) =>
|
||||
project.name.toLowerCase().includes(query.toLowerCase()),
|
||||
),
|
||||
}))
|
||||
.filter((workspace) => workspace.projects.length > 0);
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...props}
|
||||
{...slotProps.root}
|
||||
className={twMerge(
|
||||
'grid grid-flow-row content-start gap-4',
|
||||
className,
|
||||
slotProps.root?.className,
|
||||
)}
|
||||
>
|
||||
<Box
|
||||
{...slotProps.header}
|
||||
className={twMerge(
|
||||
'grid grid-flow-col place-content-between items-center',
|
||||
slotProps.header?.className,
|
||||
)}
|
||||
>
|
||||
<Text variant="h2" component="h1" className="hidden md:block">
|
||||
My Projects
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
placeholder="Find Project"
|
||||
startAdornment={
|
||||
<SearchIcon
|
||||
className="w-4 h-4 ml-2 -mr-1 shrink-0"
|
||||
sx={{ color: 'text.disabled' }}
|
||||
/>
|
||||
}
|
||||
{...slotProps.search}
|
||||
onChange={handleQueryChange}
|
||||
/>
|
||||
|
||||
<NavLink href="/new" passHref legacyBehavior>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
startIcon={<PlusCircleIcon />}
|
||||
disabled={maintenanceActive}
|
||||
{...slotProps.button}
|
||||
>
|
||||
New Project
|
||||
</Button>
|
||||
</NavLink>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-row gap-8 my-8">
|
||||
{filteredWorkspaces.map((workspace) => (
|
||||
<div key={workspace.slug}>
|
||||
<NavLink href={`/${workspace.slug}`} passHref legacyBehavior>
|
||||
<Link
|
||||
href={`${workspace.slug}`}
|
||||
className="mb-1.5 block font-medium"
|
||||
underline="none"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
{workspace.name}
|
||||
</Link>
|
||||
</NavLink>
|
||||
|
||||
<List className="grid grid-flow-row border-y">
|
||||
{workspace.projects.map((project, index) => {
|
||||
const [latestDeployment] = project.deployments;
|
||||
|
||||
return (
|
||||
<Fragment key={project.slug}>
|
||||
<ListItem.Root
|
||||
secondaryAction={
|
||||
<div className="grid grid-flow-col gap-px">
|
||||
{latestDeployment && (
|
||||
<div className="flex self-center mr-2 align-middle">
|
||||
<StatusCircle
|
||||
status={
|
||||
latestDeployment.deploymentStatus as DeploymentStatus
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StateBadge
|
||||
state={checkStatusOfTheApplication(
|
||||
project.appStates,
|
||||
)}
|
||||
desiredState={project.desiredState}
|
||||
title={getApplicationStatusString(
|
||||
checkStatusOfTheApplication(project.appStates),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<NavLink
|
||||
href={`${workspace?.slug}/${project.slug}`}
|
||||
passHref
|
||||
className='w-full'
|
||||
legacyBehavior>
|
||||
<ListItem.Button className="rounded-none">
|
||||
<ListItem.Avatar>
|
||||
<div className="w-10 h-10 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
</ListItem.Avatar>
|
||||
|
||||
<ListItem.Text
|
||||
primary={project.name}
|
||||
secondary={
|
||||
<DeploymentStatusMessage
|
||||
appCreatedAt={project.createdAt}
|
||||
deployment={latestDeployment}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem.Button>
|
||||
</NavLink>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < workspace.projects.length - 1 && (
|
||||
<Divider component="li" role="listitem" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './WorkspaceAndProjectList';
|
||||
export { default as WorkspaceAndProjectList } from './WorkspaceAndProjectList';
|
||||
@@ -1,43 +0,0 @@
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useTheme } from '@mui/material';
|
||||
import Image from 'next/image';
|
||||
|
||||
export interface ResourceProps {
|
||||
text: string;
|
||||
logo: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export default function Resource({ text, logo, link }: ResourceProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex justify-between py-1 align-middle"
|
||||
>
|
||||
<div className="flex items-center align-middle">
|
||||
<Image
|
||||
src={
|
||||
theme.palette.mode === 'dark'
|
||||
? `/logos/light/${logo}.svg`
|
||||
: `/logos/${logo}.svg`
|
||||
}
|
||||
alt={text}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
|
||||
<Text className="ml-2 inline-flex self-center align-middle font-medium">
|
||||
{text}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex self-center">
|
||||
<ArrowSquareOutIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
|
||||
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';
|
||||
import NavLink from 'next/link';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import Resource from './Resource';
|
||||
|
||||
export interface WorkspaceSidebarProps extends BoxProps {
|
||||
/**
|
||||
* List of workspaces to be displayed.
|
||||
*/
|
||||
workspaces: Workspace[];
|
||||
}
|
||||
|
||||
export default function WorkspaceSidebar({
|
||||
className,
|
||||
workspaces,
|
||||
...props
|
||||
}: WorkspaceSidebarProps) {
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="aside"
|
||||
className={twMerge(
|
||||
'grid w-full grid-flow-row content-start gap-8 md:grid',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Announcements />
|
||||
|
||||
<section className="grid grid-flow-row gap-2">
|
||||
<Text color="secondary">My Workspaces</Text>
|
||||
|
||||
{workspaces.length > 0 ? (
|
||||
<List className="grid grid-flow-row gap-2">
|
||||
{workspaces.map(({ id, name, slug }) => (
|
||||
<ListItem.Root key={id}>
|
||||
<NavLink href={`/${slug}`} passHref className='w-full' legacyBehavior>
|
||||
<ListItem.Button
|
||||
dense
|
||||
aria-label={`View ${name}`}
|
||||
className="!p-1"
|
||||
>
|
||||
<ListItem.Avatar className="w-8 h-8">
|
||||
<div className="inline-block w-8 h-8 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
</ListItem.Avatar>
|
||||
<ListItem.Text primary={name} />
|
||||
</ListItem.Button>
|
||||
</NavLink>
|
||||
</ListItem.Root>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<ActivityIndicator
|
||||
label="Creating your first workspace..."
|
||||
className="py-1"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
startIcon={<PlusCircleIcon />}
|
||||
className="justify-self-start"
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>New Workspace</span>
|
||||
|
||||
<Text variant="subtitle1" component="span">
|
||||
Invite team members to workspaces to work collaboratively.
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
component: <EditWorkspaceNameForm />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
New Workspace
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-flow-row gap-2">
|
||||
<Text color="secondary">Resources</Text>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Resource
|
||||
text="Documentation"
|
||||
logo="Note"
|
||||
link="https://docs.nhost.io"
|
||||
/>
|
||||
<Resource
|
||||
text="JavaScript Client"
|
||||
logo="js"
|
||||
link="https://docs.nhost.io/reference/javascript/"
|
||||
/>
|
||||
<Resource
|
||||
text="Nhost CLI"
|
||||
logo="CLI"
|
||||
link="https://docs.nhost.io/platform/cli"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-flow-row gap-2">
|
||||
<NavLink
|
||||
href="https://github.com/nhost/nhost"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
legacyBehavior>
|
||||
<Button
|
||||
className="grid w-full grid-flow-col gap-1"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
startIcon={<GitHubIcon />}
|
||||
>
|
||||
Star us on GitHub
|
||||
</Button>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
href="https://discord.com/invite/9V7Qb2U"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
legacyBehavior>
|
||||
<Button
|
||||
className="grid w-full grid-flow-col gap-1"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
aria-labelledby="discord-button-label"
|
||||
>
|
||||
<Image
|
||||
src="/assets/brands/discord.svg"
|
||||
alt="Discord Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
|
||||
<span id="discord-button-label">Join Discord</span>
|
||||
</Button>
|
||||
</NavLink>
|
||||
</section>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as WorkspaceSidebar } from './WorkspaceSidebar';
|
||||
@@ -25,7 +25,7 @@ export default function useAppPausedReason(): {
|
||||
});
|
||||
|
||||
const { data: isLockedData } = useGetProjectIsLockedQuery({
|
||||
variables: { appId: project.id },
|
||||
variables: { appId: project?.id },
|
||||
skip: !project,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
|
||||
export default function DatabaseMigrateWarning() {
|
||||
const { state } = useAppState();
|
||||
|
||||
if (
|
||||
state === ApplicationStatus.Paused ||
|
||||
state === ApplicationStatus.Pausing
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert severity="error" className="flex flex-col gap-3 text-left">
|
||||
<Text
|
||||
|
||||
@@ -316,7 +316,7 @@ export default function DatabaseServiceVersionSettings() {
|
||||
size="medium"
|
||||
className="self-center"
|
||||
onClick={openLatestUpgradeLogsModal}
|
||||
startIcon={<RepeatIcon className="w-4 h-4" />}
|
||||
startIcon={<RepeatIcon className="h-4 w-4" />}
|
||||
>
|
||||
View latest upgrade logs
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
@@ -34,7 +33,6 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const [showAdminSecret, setShowAdminSecret] = useState(false);
|
||||
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
||||
@@ -74,8 +72,8 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
<span>Auth JWT Secret</span>
|
||||
|
||||
<Text variant="subtitle1" component="span">
|
||||
This is the key used for generating JWTs. It's HMAC-SHA-based
|
||||
and the same as configured in Hasura.
|
||||
This is the key used for generating JWTs. It's the same as
|
||||
configured in Hasura.
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
@@ -85,22 +83,6 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
});
|
||||
}
|
||||
|
||||
function showEditJwtSecretModal() {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>Edit JWT Secret</span>
|
||||
|
||||
<Text variant="subtitle1" component="span">
|
||||
You can add your custom JWT secret here. Hasura will use it to
|
||||
validate the identity of your users.
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
component: <EditJwtSecretForm jwtSecret={stringifiedJwtSecrets} />,
|
||||
});
|
||||
}
|
||||
|
||||
const systemEnvironmentVariables = [
|
||||
{ key: 'NHOST_SUBDOMAIN', value: project.subdomain },
|
||||
{ key: 'NHOST_REGION', value: project.region.name },
|
||||
@@ -131,7 +113,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
className="mb-2.5 mt-2 px-0"
|
||||
slotProps={{ submitButton: { className: 'hidden' } }}
|
||||
>
|
||||
<Box className="grid grid-cols-3 gap-2 px-4 py-3 border-b-1">
|
||||
<Box className="grid grid-cols-3 gap-2 border-b-1 px-4 py-3">
|
||||
<Text className="font-medium">Variable Name</Text>
|
||||
<Text className="font-medium lg:col-span-2">Value</Text>
|
||||
</Box>
|
||||
@@ -140,7 +122,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
<ListItem.Root className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3">
|
||||
<ListItem.Text>NHOST_ADMIN_SECRET</ListItem.Text>
|
||||
|
||||
<div className="grid items-center justify-start grid-flow-col gap-2 lg:col-span-2">
|
||||
<div className="grid grid-flow-col items-center justify-start gap-2 lg:col-span-2">
|
||||
<Text className="truncate" color="secondary">
|
||||
{showAdminSecret ? (
|
||||
<InlineCode className="!text-sm font-medium">
|
||||
@@ -160,9 +142,9 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
onClick={() => setShowAdminSecret((show) => !show)}
|
||||
>
|
||||
{showAdminSecret ? (
|
||||
<EyeOffIcon className="w-5 h-5" />
|
||||
<EyeOffIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
<EyeIcon className="h-5 w-5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -173,7 +155,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
<ListItem.Root className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3">
|
||||
<ListItem.Text>NHOST_WEBHOOK_SECRET</ListItem.Text>
|
||||
|
||||
<div className="grid items-center justify-start grid-flow-col gap-2 lg:col-span-2">
|
||||
<div className="grid grid-flow-col items-center justify-start gap-2 lg:col-span-2">
|
||||
<Text className="truncate" color="secondary">
|
||||
{showWebhookSecret ? (
|
||||
<InlineCode className="!text-sm font-medium">
|
||||
@@ -195,9 +177,9 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
onClick={() => setShowWebhookSecret((show) => !show)}
|
||||
>
|
||||
{showWebhookSecret ? (
|
||||
<EyeOffIcon className="w-5 h-5" />
|
||||
<EyeOffIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
<EyeIcon className="h-5 w-5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -223,7 +205,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
|
||||
<Divider component="li" className="!mb-2.5 !mt-4" />
|
||||
|
||||
<ListItem.Root className="grid justify-start grid-cols-2 px-4 lg:grid-cols-3">
|
||||
<ListItem.Root className="grid grid-cols-2 justify-start px-4 lg:grid-cols-3">
|
||||
<ListItem.Text>NHOST_JWT_SECRET</ListItem.Text>
|
||||
|
||||
<div className="grid grid-flow-row items-center justify-center gap-1.5 text-center md:grid-flow-col lg:col-span-2 lg:justify-start lg:text-left">
|
||||
@@ -234,17 +216,6 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
>
|
||||
Show JWT Secret
|
||||
</Button>
|
||||
|
||||
<Text component="span">or</Text>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={showEditJwtSecretModal}
|
||||
size="small"
|
||||
disabled={maintenanceActive}
|
||||
>
|
||||
Edit JWT Secret
|
||||
</Button>
|
||||
</div>
|
||||
</ListItem.Root>
|
||||
</List>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
query GetJWTSecrets($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
id: __typename
|
||||
__typename
|
||||
hasura {
|
||||
jwtSecrets {
|
||||
type
|
||||
key
|
||||
signingKey
|
||||
kid
|
||||
jwk_url
|
||||
allowed_skew
|
||||
audience
|
||||
claims_format
|
||||
claims_map {
|
||||
claim
|
||||
default
|
||||
path
|
||||
value
|
||||
}
|
||||
claims_namespace
|
||||
claims_namespace_path
|
||||
header
|
||||
issuer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { type JWTSettingsFormValues } from '@/features/orgs/projects/jwt/settings/types';
|
||||
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { ASYMMETRIC_ALGORITHMS } from '@/features/orgs/projects/jwt/settings/utils/constants';
|
||||
|
||||
export default function AsymmetricKeyFormSection() {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue,
|
||||
} = useFormContext<JWTSettingsFormValues>();
|
||||
|
||||
const type = watch('type');
|
||||
|
||||
return (
|
||||
<Box className="grid grid-cols-5 gap-4">
|
||||
<Select
|
||||
id="type"
|
||||
className="col-span-5 lg:col-span-1"
|
||||
placeholder="RS256"
|
||||
hideEmptyHelperText
|
||||
variant="normal"
|
||||
defaultValue={ASYMMETRIC_ALGORITHMS[0]}
|
||||
error={!!errors.type}
|
||||
helperText={errors?.type?.message}
|
||||
label="Hashing algorithm"
|
||||
value={type}
|
||||
onChange={(_event, value) =>
|
||||
setValue('type', value as string, { shouldDirty: true })
|
||||
}
|
||||
>
|
||||
{ASYMMETRIC_ALGORITHMS.map((algorithm) => (
|
||||
<Option key={algorithm} value={algorithm}>
|
||||
{algorithm}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
{...register('kid')}
|
||||
name="kid"
|
||||
id="kid"
|
||||
label="Key ID"
|
||||
placeholder="Enter unique key ID"
|
||||
className="col-span-5 lg:col-span-3"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.kid}
|
||||
helperText={errors?.kid?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('key')}
|
||||
name="key"
|
||||
id="key"
|
||||
label="Public Key"
|
||||
placeholder="-----BEGIN PUBLIC KEY-----"
|
||||
className="col-span-5 lg:col-span-4"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.key}
|
||||
helperText={errors?.key?.message}
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[130px]',
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
{...register('signingKey')}
|
||||
name="signingKey"
|
||||
id="signingKey"
|
||||
label="Signing key"
|
||||
placeholder="-----BEGIN PRIVATE KEY-----"
|
||||
className="col-span-5 lg:col-span-4"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.signingKey}
|
||||
helperText={errors?.signingKey?.message}
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[130px]',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AsymmetricKeyFormSection } from './AsymmetricKeyFormSection';
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ASYMMETRIC_ALGORITHMS } from '@/features/orgs/projects/jwt/settings/utils/constants';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import type {
|
||||
ExternalSigningType,
|
||||
JWTSettingsFormValues,
|
||||
} from '@/features/orgs/projects/jwt/settings/types';
|
||||
|
||||
interface ExternalSigningFieldProps {
|
||||
externalSigningType: ExternalSigningType;
|
||||
}
|
||||
|
||||
export default function ExternalSigningField({
|
||||
externalSigningType,
|
||||
}: ExternalSigningFieldProps) {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
} = useFormContext<JWTSettingsFormValues>();
|
||||
|
||||
const type = watch('type');
|
||||
|
||||
if (externalSigningType === 'jwk-endpoint') {
|
||||
return (
|
||||
<Input
|
||||
{...register('jwkUrl')}
|
||||
name="jwkUrl"
|
||||
id="jwkUrl"
|
||||
placeholder="https://acme.com/jwks.json"
|
||||
className="col-span-5 lg:col-span-4"
|
||||
label="JWK URL"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.jwkUrl}
|
||||
helperText={errors?.jwkUrl?.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (externalSigningType === 'public-key') {
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
id="type"
|
||||
className="col-span-5 lg:col-span-1"
|
||||
placeholder="RS256"
|
||||
hideEmptyHelperText
|
||||
variant="normal"
|
||||
defaultValue={ASYMMETRIC_ALGORITHMS[0]}
|
||||
error={!!errors.type}
|
||||
helperText={errors?.type?.message}
|
||||
label="Hashing algorithm"
|
||||
value={type}
|
||||
onChange={(_event, value) =>
|
||||
setValue('type', value as string, { shouldDirty: true })
|
||||
}
|
||||
>
|
||||
{ASYMMETRIC_ALGORITHMS.map((algorithm) => (
|
||||
<Option key={algorithm} value={algorithm}>
|
||||
{algorithm}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="lg:col-span-4" />
|
||||
|
||||
<Input
|
||||
{...register('key')}
|
||||
name="key"
|
||||
id="key"
|
||||
placeholder="-----BEGIN PUBLIC KEY-----"
|
||||
className="col-span-5 lg:col-span-4"
|
||||
label="Public Key"
|
||||
fullWidth
|
||||
multiline
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.key}
|
||||
helperText={errors?.key?.message}
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[130px]',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ExternalSigningField } from './ExternalSigningField';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Label } from '@/components/ui/v3/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
|
||||
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
|
||||
import { ExternalSigningField } from '@/features/orgs/projects/jwt/settings/components/ExternalSigningField';
|
||||
import type { ExternalSigningType } from '@/features/orgs/projects/jwt/settings/types';
|
||||
|
||||
interface ExternalSigningFormSectionProps {
|
||||
externalSigningType: ExternalSigningType;
|
||||
handleExternalSigningTypeChange: (value: ExternalSigningType) => void;
|
||||
}
|
||||
|
||||
export default function ExternalSigningFormSection({
|
||||
externalSigningType,
|
||||
handleExternalSigningTypeChange,
|
||||
}: ExternalSigningFormSectionProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Alert severity="warning">
|
||||
<Text>
|
||||
When using external signing the Auth service will be automatically
|
||||
disabled.
|
||||
</Text>
|
||||
</Alert>
|
||||
<Box className="grid grid-cols-5 gap-4">
|
||||
<div className="col-span-5">
|
||||
<RadioGroup
|
||||
defaultValue="jwk-endpoint"
|
||||
value={externalSigningType}
|
||||
onValueChange={handleExternalSigningTypeChange}
|
||||
className="flex flex-col gap-4 lg:flex-row"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="jwk-endpoint" id="jwk-endpoint" />
|
||||
<Label htmlFor="jwk-endpoint">JWK Endpoint</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="public-key" id="public-key" />
|
||||
<Label htmlFor="public-key">Public Key</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<ExternalSigningField externalSigningType={externalSigningType} />
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ExternalSigningFormSection } from './ExternalSigningFormSection';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AsymmetricKeyFormSection } from '@/features/orgs/projects/jwt/settings/components/AsymmetricKeyFormSection';
|
||||
import { ExternalSigningFormSection } from '@/features/orgs/projects/jwt/settings/components/ExternalSigningFormSection';
|
||||
import { SymmetricKeyFormSection } from '@/features/orgs/projects/jwt/settings/components/SymmetricKeyFormSection';
|
||||
import type {
|
||||
ExternalSigningType,
|
||||
JWTSecretType,
|
||||
} from '@/features/orgs/projects/jwt/settings/types';
|
||||
|
||||
interface JWTSecretFieldProps {
|
||||
secretType: JWTSecretType;
|
||||
externalSigningType: ExternalSigningType;
|
||||
handleExternalSigningTypeChange: (value: ExternalSigningType) => void;
|
||||
}
|
||||
|
||||
export default function JWTSecretField({
|
||||
secretType,
|
||||
externalSigningType,
|
||||
handleExternalSigningTypeChange,
|
||||
}: JWTSecretFieldProps) {
|
||||
if (secretType === 'symmetric') {
|
||||
return <SymmetricKeyFormSection />;
|
||||
}
|
||||
if (secretType === 'asymmetric') {
|
||||
return <AsymmetricKeyFormSection />;
|
||||
}
|
||||
if (secretType === 'external') {
|
||||
return (
|
||||
<ExternalSigningFormSection
|
||||
externalSigningType={externalSigningType}
|
||||
handleExternalSigningTypeChange={handleExternalSigningTypeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as JWTSecretField } from './JWTSecretField';
|
||||
@@ -0,0 +1,412 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
useGetJwtSecretsQuery,
|
||||
useUpdateConfigMutation,
|
||||
type ConfigConfigUpdateInput,
|
||||
} from '@/generated/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { Label } from '@/components/ui/v3/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { JWTSecretField } from '@/features/orgs/projects/jwt/settings/components/JWTSecretField';
|
||||
import type {
|
||||
ExternalSigningType,
|
||||
JWTSecretType,
|
||||
JWTSettingsFormValues,
|
||||
} from '@/features/orgs/projects/jwt/settings/types';
|
||||
import { validationSchema } from '@/features/orgs/projects/jwt/settings/types';
|
||||
import {
|
||||
ASYMMETRIC_ALGORITHMS,
|
||||
SYMMETRIC_ALGORITHMS,
|
||||
} from '@/features/orgs/projects/jwt/settings/utils/constants';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
|
||||
export default function JWTSettings() {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const {
|
||||
data: jwtSecretsData,
|
||||
loading: jwtSecretsLoading,
|
||||
error: jwtSecretsError,
|
||||
refetch: refetchJwtSecrets,
|
||||
} = useGetJwtSecretsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const {
|
||||
type: jwtType,
|
||||
key: jwtKey,
|
||||
signingKey,
|
||||
kid,
|
||||
jwk_url,
|
||||
...rest
|
||||
} = jwtSecretsData?.config?.hasura?.jwtSecrets?.[0] || {};
|
||||
|
||||
let initialSignatureType: JWTSecretType = 'symmetric';
|
||||
if (
|
||||
typeof jwtType === 'string' &&
|
||||
SYMMETRIC_ALGORITHMS.includes(
|
||||
jwtType as (typeof SYMMETRIC_ALGORITHMS)[number],
|
||||
)
|
||||
) {
|
||||
initialSignatureType = 'symmetric';
|
||||
} else if (
|
||||
typeof jwtType === 'string' &&
|
||||
ASYMMETRIC_ALGORITHMS.includes(
|
||||
jwtType as (typeof ASYMMETRIC_ALGORITHMS)[number],
|
||||
) &&
|
||||
kid
|
||||
) {
|
||||
initialSignatureType = 'asymmetric';
|
||||
} else {
|
||||
initialSignatureType = 'external';
|
||||
}
|
||||
|
||||
const initialExternalSigningType: ExternalSigningType = jwk_url
|
||||
? 'jwk-endpoint'
|
||||
: 'public-key';
|
||||
|
||||
const [signatureType, setSignatureType] =
|
||||
useState<JWTSecretType>(initialSignatureType);
|
||||
|
||||
const [externalSigningType, setExternalSigningType] =
|
||||
useState<ExternalSigningType>(initialExternalSigningType);
|
||||
|
||||
const form = useForm<JWTSettingsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
type: jwtType || '',
|
||||
key: jwtKey || '',
|
||||
signingKey: signingKey || '',
|
||||
kid: kid || '',
|
||||
jwkUrl: jwk_url || '',
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
context: {
|
||||
signatureType,
|
||||
externalSigningType,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!jwtSecretsLoading && !jwtSecretsError) {
|
||||
form.reset({
|
||||
type: jwtType || '',
|
||||
key: jwtKey || '',
|
||||
signingKey: signingKey || '',
|
||||
kid: kid || '',
|
||||
jwkUrl: jwk_url || '',
|
||||
});
|
||||
}
|
||||
}, [
|
||||
jwtSecretsLoading,
|
||||
jwtSecretsData,
|
||||
jwtType,
|
||||
jwtKey,
|
||||
signingKey,
|
||||
kid,
|
||||
jwk_url,
|
||||
jwtSecretsError,
|
||||
form,
|
||||
]);
|
||||
|
||||
const { formState, reset, setValue } = form;
|
||||
|
||||
const formValues = form.getValues();
|
||||
|
||||
const handleSignatureTypeChange = (value: JWTSecretType) => {
|
||||
if (value === initialSignatureType) {
|
||||
reset({
|
||||
...formValues,
|
||||
type: jwtType || '',
|
||||
key: jwtKey || '',
|
||||
signingKey: signingKey || '',
|
||||
kid: kid || '',
|
||||
jwkUrl: jwk_url || '',
|
||||
});
|
||||
} else {
|
||||
const newType =
|
||||
value === 'symmetric'
|
||||
? SYMMETRIC_ALGORITHMS[0]
|
||||
: ASYMMETRIC_ALGORITHMS[0];
|
||||
reset({
|
||||
...formValues,
|
||||
type: '',
|
||||
key: '',
|
||||
signingKey: '',
|
||||
kid: '',
|
||||
jwkUrl: '',
|
||||
});
|
||||
setValue('type', newType, { shouldDirty: true });
|
||||
}
|
||||
|
||||
setSignatureType(value);
|
||||
};
|
||||
|
||||
const handleExternalSigningTypeChange = (value: ExternalSigningType) => {
|
||||
if (value === initialExternalSigningType) {
|
||||
reset({
|
||||
...formValues,
|
||||
type: jwtType || '',
|
||||
key: jwtKey || '',
|
||||
signingKey: signingKey || '',
|
||||
kid: kid || '',
|
||||
jwkUrl: jwk_url || '',
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
...formValues,
|
||||
type: '',
|
||||
key: '',
|
||||
signingKey: '',
|
||||
kid: '',
|
||||
jwkUrl: '',
|
||||
});
|
||||
setValue('type', ASYMMETRIC_ALGORITHMS[0], { shouldDirty: true });
|
||||
}
|
||||
|
||||
setExternalSigningType(value);
|
||||
};
|
||||
|
||||
const getFormattedConfig = (
|
||||
values: JWTSettingsFormValues,
|
||||
): ConfigConfigUpdateInput => {
|
||||
// Remove any __typename property from the values
|
||||
const sanitizedValues = removeTypename(values) as JWTSettingsFormValues;
|
||||
const sanitizedRest = removeTypename(rest);
|
||||
|
||||
let jwtSecret = {};
|
||||
if (signatureType === 'symmetric') {
|
||||
jwtSecret = {
|
||||
type: sanitizedValues.type,
|
||||
key: sanitizedValues.key,
|
||||
};
|
||||
} else if (signatureType === 'asymmetric') {
|
||||
jwtSecret = {
|
||||
type: sanitizedValues.type,
|
||||
key: sanitizedValues.key,
|
||||
signingKey: sanitizedValues.signingKey,
|
||||
kid: sanitizedValues.kid,
|
||||
};
|
||||
} else if (externalSigningType === 'jwk-endpoint') {
|
||||
jwtSecret = {
|
||||
jwk_url: sanitizedValues.jwkUrl,
|
||||
};
|
||||
} else if (externalSigningType === 'public-key') {
|
||||
jwtSecret = {
|
||||
type: sanitizedValues.type,
|
||||
key: sanitizedValues.key,
|
||||
};
|
||||
}
|
||||
|
||||
jwtSecret = {
|
||||
...sanitizedRest,
|
||||
...jwtSecret,
|
||||
};
|
||||
|
||||
const config: ConfigConfigUpdateInput = {
|
||||
hasura: {
|
||||
jwtSecrets: [jwtSecret],
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const handleJWTSettingsChange = async (values: JWTSettingsFormValues) => {
|
||||
const formattedConfig = getFormattedConfig(values);
|
||||
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
config: formattedConfig,
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset(values);
|
||||
refetchJwtSecrets();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'JWT settings are being updated...',
|
||||
successMessage: 'JWT settings have been updated successfully.',
|
||||
errorMessage: 'An error occurred while trying to update JWT settings.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if (jwtSecretsLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading JWT settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (jwtSecretsError) {
|
||||
throw jwtSecretsError;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleJWTSettingsChange}>
|
||||
<SettingsContainer
|
||||
title="JSON Web Token Settings"
|
||||
description="Select how JSON Web Tokens (JWTs) are signed and verified."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/guides/auth/jwt"
|
||||
docsTitle="JSON Web Token (JWT) Settings"
|
||||
className="grid grid-flow-row gap-x-4 gap-y-2 px-4"
|
||||
>
|
||||
<Box className="flex flex-col gap-6">
|
||||
<RadioGroup
|
||||
className="flex flex-col gap-4 lg:flex-row"
|
||||
defaultValue="public"
|
||||
value={signatureType}
|
||||
onValueChange={handleSignatureTypeChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="symmetric" id="symmetric" />
|
||||
<Label htmlFor="symmetric" className="flex items-center gap-1">
|
||||
Symmetric key
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
With symmetric keys your project uses a single for both
|
||||
signing and verifying JWTs. Refer to{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/guides/auth/jwt#symmetric-keys"
|
||||
className="underline"
|
||||
>
|
||||
symmetric keys
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="asymmetric" id="asymmetric" />
|
||||
<Label htmlFor="asymmetric" className="flex items-center gap-1">
|
||||
Asymmetric key
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
With asymmetric keys your project uses a public and
|
||||
private key pair for signing and verifying JWTs. Refer
|
||||
to{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/guides/auth/jwt#asymmetric-keys"
|
||||
className="underline"
|
||||
>
|
||||
asymmetric keys
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="external" id="external" />
|
||||
<Label htmlFor="external" className="flex items-center gap-1">
|
||||
External signing
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
This will use a third party service's JWK endpoint
|
||||
to verify JWT's. Alternatively you can configure
|
||||
the public key directly. Refer to{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/guides/auth/jwt#external-signing"
|
||||
className="underline"
|
||||
>
|
||||
external signing
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<JWTSecretField
|
||||
secretType={signatureType}
|
||||
externalSigningType={externalSigningType}
|
||||
handleExternalSigningTypeChange={handleExternalSigningTypeChange}
|
||||
/>
|
||||
</Box>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as JWTSettings } from './JWTSettings';
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { type JWTSettingsFormValues } from '@/features/orgs/projects/jwt/settings/types';
|
||||
import { SYMMETRIC_ALGORITHMS } from '@/features/orgs/projects/jwt/settings/utils/constants';
|
||||
|
||||
export default function SymmetricKeyFormSection() {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue,
|
||||
} = useFormContext<JWTSettingsFormValues>();
|
||||
|
||||
const type = watch('type');
|
||||
|
||||
return (
|
||||
<Box className="grid grid-cols-5 gap-4">
|
||||
<Select
|
||||
id="type"
|
||||
className="col-span-5 lg:col-span-1"
|
||||
placeholder="HS256"
|
||||
hideEmptyHelperText
|
||||
variant="normal"
|
||||
defaultValue={SYMMETRIC_ALGORITHMS[0]}
|
||||
error={!!errors.type}
|
||||
helperText={errors?.type?.message}
|
||||
label="Hashing algorithm"
|
||||
value={type}
|
||||
onChange={(_event, value) =>
|
||||
setValue('type', value as string, { shouldDirty: true })
|
||||
}
|
||||
>
|
||||
{SYMMETRIC_ALGORITHMS.map((algorithm) => (
|
||||
<Option key={algorithm} value={algorithm}>
|
||||
{algorithm}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
{...register('key')}
|
||||
name="key"
|
||||
id="key"
|
||||
label="Key"
|
||||
placeholder="Enter symmetric key"
|
||||
className="col-span-5 lg:col-span-3"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.key}
|
||||
helperText={errors?.key?.message}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SymmetricKeyFormSection } from './SymmetricKeyFormSection';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './jwtSecrets';
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export type JWTSecretType = 'symmetric' | 'asymmetric' | 'external';
|
||||
|
||||
export type ExternalSigningType = 'jwk-endpoint' | 'public-key';
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
type: Yup.string()
|
||||
.label('Type')
|
||||
.when(['$signatureType', '$externalSigningType'], {
|
||||
is: (
|
||||
signatureType: JWTSecretType,
|
||||
externalSigningType: ExternalSigningType,
|
||||
) => {
|
||||
if (signatureType === 'external') {
|
||||
return externalSigningType === 'public-key';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
key: Yup.string()
|
||||
.label('Key')
|
||||
.when(['$signatureType', '$externalSigningType'], {
|
||||
is: (
|
||||
signatureType: JWTSecretType,
|
||||
externalSigningType: ExternalSigningType,
|
||||
) => {
|
||||
if (signatureType === 'external') {
|
||||
return externalSigningType === 'public-key';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
signingKey: Yup.string()
|
||||
.label('Signing key')
|
||||
.when('$signatureType', {
|
||||
is: (signatureType: JWTSecretType) => signatureType === 'asymmetric',
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
kid: Yup.string()
|
||||
.label('Key ID')
|
||||
.when('$signatureType', {
|
||||
is: (signatureType: JWTSecretType) => signatureType === 'asymmetric',
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
jwkUrl: Yup.string()
|
||||
.label('JWK endpoint URL')
|
||||
.url()
|
||||
.when(['$signatureType', '$externalSigningType'], {
|
||||
is: (
|
||||
signatureType: JWTSecretType,
|
||||
externalSigningType: ExternalSigningType,
|
||||
) =>
|
||||
signatureType === 'external' && externalSigningType === 'jwk-endpoint',
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type JWTSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
@@ -0,0 +1,4 @@
|
||||
// UI will take first value as default SYMMETRIC_ALGORITHMS[0]
|
||||
export const SYMMETRIC_ALGORITHMS = ['HS256', 'HS384', 'HS512'] as const;
|
||||
|
||||
export const ASYMMETRIC_ALGORITHMS = ['RS256', 'RS384', 'RS512'] as const;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './constants';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { CogIcon } from '@/components/ui/v2/icons/CogIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
@@ -94,12 +94,13 @@ export default function OverviewTopBar() {
|
||||
legacyBehavior
|
||||
>
|
||||
<Button
|
||||
endIcon={<CogIcon className="h-4 w-4" />}
|
||||
variant="outlined"
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
color="secondary"
|
||||
disabled={maintenanceActive}
|
||||
>
|
||||
Settings
|
||||
<CogIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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({
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Announcements } from './Announcements';
|
||||
@@ -8,7 +8,6 @@ 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';
|
||||
@@ -39,8 +38,6 @@ export default function WorkspaceSidebar({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Announcements />
|
||||
|
||||
<section className="grid grid-flow-row gap-2">
|
||||
<Text color="secondary">My Workspaces</Text>
|
||||
|
||||
@@ -48,14 +45,19 @@ export default function WorkspaceSidebar({
|
||||
<List className="grid grid-flow-row gap-2">
|
||||
{workspaces.map(({ id, name, slug }) => (
|
||||
<ListItem.Root key={id}>
|
||||
<NavLink href={`/${slug}`} passHref className='w-full' legacyBehavior>
|
||||
<NavLink
|
||||
href={`/${slug}`}
|
||||
passHref
|
||||
className="w-full"
|
||||
legacyBehavior
|
||||
>
|
||||
<ListItem.Button
|
||||
dense
|
||||
aria-label={`View ${name}`}
|
||||
className="!p-1"
|
||||
>
|
||||
<ListItem.Avatar className="w-8 h-8">
|
||||
<div className="inline-block w-8 h-8 overflow-hidden rounded-lg">
|
||||
<ListItem.Avatar className="h-8 w-8">
|
||||
<div className="inline-block h-8 w-8 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
@@ -129,7 +131,8 @@ export default function WorkspaceSidebar({
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
legacyBehavior>
|
||||
legacyBehavior
|
||||
>
|
||||
<Button
|
||||
className="grid w-full grid-flow-col gap-1"
|
||||
variant="outlined"
|
||||
@@ -145,7 +148,8 @@ export default function WorkspaceSidebar({
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
legacyBehavior>
|
||||
legacyBehavior
|
||||
>
|
||||
<Button
|
||||
className="grid w-full grid-flow-col gap-1"
|
||||
variant="outlined"
|
||||
|
||||
@@ -8,9 +8,17 @@ fragment JWTSecret on ConfigJWTSecret {
|
||||
issuer
|
||||
key
|
||||
type
|
||||
signingKey
|
||||
kid
|
||||
jwk_url
|
||||
header
|
||||
claims_namespace_path
|
||||
claims_map {
|
||||
claim
|
||||
default
|
||||
path
|
||||
value
|
||||
}
|
||||
claims_namespace
|
||||
claims_format
|
||||
audience
|
||||
|
||||
@@ -16,6 +16,11 @@ query GetSignInMethods($appId: uuid!) {
|
||||
id: __typename
|
||||
__typename
|
||||
method {
|
||||
otp {
|
||||
email {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
emailPassword {
|
||||
emailVerificationRequired
|
||||
hibpEnabled
|
||||
@@ -40,6 +45,7 @@ query GetSignInMethods($appId: uuid!) {
|
||||
keyId
|
||||
teamId
|
||||
privateKey
|
||||
audience
|
||||
}
|
||||
bitbucket {
|
||||
enabled
|
||||
@@ -81,6 +87,7 @@ query GetSignInMethods($appId: uuid!) {
|
||||
clientId
|
||||
clientSecret
|
||||
scope
|
||||
audience
|
||||
}
|
||||
linkedin {
|
||||
enabled
|
||||
|
||||
5
dashboard/src/gql/platform/deleteAnnouncementRead.gql
Normal file
5
dashboard/src/gql/platform/deleteAnnouncementRead.gql
Normal file
@@ -0,0 +1,5 @@
|
||||
mutation deleteAnnouncementRead($id: uuid!) {
|
||||
deleteAnnouncementRead(id: $id) {
|
||||
announcementID
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
query getAnnouncements($limit: Int) {
|
||||
query getAnnouncements {
|
||||
announcements(
|
||||
order_by: { createdAt: desc }
|
||||
limit: $limit
|
||||
where: {
|
||||
_or: [{ expiresAt: { _is_null: true } }, { expiresAt: { _gt: now } }]
|
||||
}
|
||||
@@ -10,5 +9,8 @@ query getAnnouncements($limit: Int) {
|
||||
href
|
||||
content
|
||||
createdAt
|
||||
read {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
dashboard/src/gql/platform/insertAnnouncementRead.gql
Normal file
5
dashboard/src/gql/platform/insertAnnouncementRead.gql
Normal file
@@ -0,0 +1,5 @@
|
||||
mutation insertAnnouncementRead($announcementID: uuid!) {
|
||||
insertAnnouncementRead(object: { announcementID: $announcementID }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { TransferProject } from '@/features/orgs/components/TransferProject';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { SettingsLayout } from '@/features/orgs/layout/SettingsLayout';
|
||||
import { RemoveApplicationModal } from '@/features/orgs/projects/common/components/RemoveApplicationModal';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
@@ -17,8 +18,10 @@ import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWith
|
||||
import {
|
||||
useBillingDeleteAppMutation,
|
||||
usePauseApplicationMutation,
|
||||
useUnpauseApplicationMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -46,12 +49,21 @@ export default function SettingsGeneralPage() {
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
const { project, loading, refetch: refetchProject } = useProject();
|
||||
const { state } = useAppState();
|
||||
|
||||
const [updateApp] = useUpdateApplicationMutation();
|
||||
const [deleteApplication] = useBillingDeleteAppMutation();
|
||||
const [pauseApplication] = usePauseApplicationMutation({
|
||||
variables: { appId: project?.id },
|
||||
});
|
||||
const [pauseApplication, { loading: pauseApplicationLoading }] =
|
||||
usePauseApplicationMutation({
|
||||
variables: { appId: project?.id },
|
||||
});
|
||||
|
||||
const [unpauseApplication, { loading: unpauseApplicationLoading }] =
|
||||
useUnpauseApplicationMutation({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ProjectNameValidationSchema>({
|
||||
mode: 'onSubmit',
|
||||
@@ -137,6 +149,24 @@ export default function SettingsGeneralPage() {
|
||||
);
|
||||
}
|
||||
|
||||
async function handleTriggerUnpausing() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await unpauseApplication();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
await refetchProject();
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Starting the project...',
|
||||
successMessage: 'The project has been started successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while waking up the project. Please try again.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator label="Loading project..." />;
|
||||
}
|
||||
@@ -176,29 +206,53 @@ export default function SettingsGeneralPage() {
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<SettingsContainer
|
||||
title="Pause Project"
|
||||
description="While your project is paused, it will not be accessible. You can wake it up anytime after."
|
||||
submitButtonText="Pause"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
type: 'button',
|
||||
color: 'primary',
|
||||
variant: 'contained',
|
||||
disabled: maintenanceActive || !isPlatform,
|
||||
onClick: () => {
|
||||
openAlertDialog({
|
||||
title: 'Pause Project?',
|
||||
payload:
|
||||
'Are you sure you want to pause this project? It will not be accessible until you unpause it.',
|
||||
props: {
|
||||
onPrimaryAction: handlePauseApplication,
|
||||
},
|
||||
});
|
||||
{state === ApplicationStatus.Paused && (
|
||||
<SettingsContainer
|
||||
title="Wake up Project"
|
||||
description="Wake up your project to make it accessible again. Once reactivated, all features will be fully functional."
|
||||
submitButtonText="Wake up"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
type: 'button',
|
||||
color: 'primary',
|
||||
variant: 'contained',
|
||||
loading: unpauseApplicationLoading,
|
||||
disabled:
|
||||
maintenanceActive || !isPlatform || unpauseApplicationLoading,
|
||||
onClick: handleTriggerUnpausing,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state !== ApplicationStatus.Paused &&
|
||||
state !== ApplicationStatus.Pausing && (
|
||||
<SettingsContainer
|
||||
title="Pause Project"
|
||||
description="While your project is paused, it will not be accessible. You can wake it up anytime after."
|
||||
submitButtonText="Pause"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
type: 'button',
|
||||
color: 'primary',
|
||||
variant: 'contained',
|
||||
loading: pauseApplicationLoading,
|
||||
disabled:
|
||||
maintenanceActive || !isPlatform || pauseApplicationLoading,
|
||||
onClick: () => {
|
||||
openAlertDialog({
|
||||
title: 'Pause Project?',
|
||||
payload:
|
||||
'Are you sure you want to pause this project? It will not be accessible until you unpause it.',
|
||||
props: {
|
||||
onPrimaryAction: handlePauseApplication,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TransferProject />
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useGetJwtSecretsQuery } from '@/utils/__generated__/graphql';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { SettingsLayout } from '@/features/orgs/layout/SettingsLayout';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { JWTSettings } from '@/features/orgs/projects/jwt/settings/components/JWTSettings';
|
||||
|
||||
export default function SettingsJWTPage() {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const { data, loading, error } = useGetJwtSecretsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
skip: !project,
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading JWT settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
|
||||
rootClassName="bg-transparent"
|
||||
>
|
||||
<JWTSettings />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsJWTPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<SettingsLayout>
|
||||
<Container
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="max-w-5xl"
|
||||
>
|
||||
{page}
|
||||
</Container>
|
||||
</SettingsLayout>
|
||||
</ProjectLayout>
|
||||
);
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import type { ReactElement } from 'react';
|
||||
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { SettingsLayout } from '@/features/orgs/layout/SettingsLayout';
|
||||
import { OTPEmailSettings } from '@/features/orgs/projects/authentication/settings/OTPEmailSettings';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
@@ -61,6 +62,7 @@ export default function SettingsSignInMethodsPage() {
|
||||
<WebAuthnSettings />
|
||||
<AnonymousSignInSettings />
|
||||
<SMSSettings />
|
||||
<OTPEmailSettings />
|
||||
<AppleProviderSettings />
|
||||
<AzureADProviderSettings />
|
||||
<DiscordProviderSettings />
|
||||
|
||||
596
dashboard/src/utils/__generated__/graphql.ts
generated
596
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -282,6 +282,7 @@ export type ConfigAuthMethod = {
|
||||
emailPassword?: Maybe<ConfigAuthMethodEmailPassword>;
|
||||
emailPasswordless?: Maybe<ConfigAuthMethodEmailPasswordless>;
|
||||
oauth?: Maybe<ConfigAuthMethodOauth>;
|
||||
otp?: Maybe<ConfigAuthMethodOtp>;
|
||||
smsPasswordless?: Maybe<ConfigAuthMethodSmsPasswordless>;
|
||||
webauthn?: Maybe<ConfigAuthMethodWebauthn>;
|
||||
};
|
||||
@@ -314,6 +315,7 @@ export type ConfigAuthMethodComparisonExp = {
|
||||
emailPassword?: InputMaybe<ConfigAuthMethodEmailPasswordComparisonExp>;
|
||||
emailPasswordless?: InputMaybe<ConfigAuthMethodEmailPasswordlessComparisonExp>;
|
||||
oauth?: InputMaybe<ConfigAuthMethodOauthComparisonExp>;
|
||||
otp?: InputMaybe<ConfigAuthMethodOtpComparisonExp>;
|
||||
smsPasswordless?: InputMaybe<ConfigAuthMethodSmsPasswordlessComparisonExp>;
|
||||
webauthn?: InputMaybe<ConfigAuthMethodWebauthnComparisonExp>;
|
||||
};
|
||||
@@ -375,6 +377,7 @@ export type ConfigAuthMethodInsertInput = {
|
||||
emailPassword?: InputMaybe<ConfigAuthMethodEmailPasswordInsertInput>;
|
||||
emailPasswordless?: InputMaybe<ConfigAuthMethodEmailPasswordlessInsertInput>;
|
||||
oauth?: InputMaybe<ConfigAuthMethodOauthInsertInput>;
|
||||
otp?: InputMaybe<ConfigAuthMethodOtpInsertInput>;
|
||||
smsPasswordless?: InputMaybe<ConfigAuthMethodSmsPasswordlessInsertInput>;
|
||||
webauthn?: InputMaybe<ConfigAuthMethodWebauthnInsertInput>;
|
||||
};
|
||||
@@ -400,6 +403,7 @@ export type ConfigAuthMethodOauth = {
|
||||
|
||||
export type ConfigAuthMethodOauthApple = {
|
||||
__typename?: 'ConfigAuthMethodOauthApple';
|
||||
audience?: Maybe<Scalars['String']>;
|
||||
clientId?: Maybe<Scalars['String']>;
|
||||
enabled?: Maybe<Scalars['Boolean']>;
|
||||
keyId?: Maybe<Scalars['String']>;
|
||||
@@ -412,6 +416,7 @@ export type ConfigAuthMethodOauthAppleComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigAuthMethodOauthAppleComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigAuthMethodOauthAppleComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigAuthMethodOauthAppleComparisonExp>>;
|
||||
audience?: InputMaybe<ConfigStringComparisonExp>;
|
||||
clientId?: InputMaybe<ConfigStringComparisonExp>;
|
||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
keyId?: InputMaybe<ConfigStringComparisonExp>;
|
||||
@@ -421,6 +426,7 @@ export type ConfigAuthMethodOauthAppleComparisonExp = {
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOauthAppleInsertInput = {
|
||||
audience?: InputMaybe<Scalars['String']>;
|
||||
clientId?: InputMaybe<Scalars['String']>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
keyId?: InputMaybe<Scalars['String']>;
|
||||
@@ -430,6 +436,7 @@ export type ConfigAuthMethodOauthAppleInsertInput = {
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOauthAppleUpdateInput = {
|
||||
audience?: InputMaybe<Scalars['String']>;
|
||||
clientId?: InputMaybe<Scalars['String']>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
keyId?: InputMaybe<Scalars['String']>;
|
||||
@@ -591,6 +598,46 @@ export type ConfigAuthMethodOauthWorkosUpdateInput = {
|
||||
organization?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtp = {
|
||||
__typename?: 'ConfigAuthMethodOtp';
|
||||
email?: Maybe<ConfigAuthMethodOtpEmail>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtpComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigAuthMethodOtpComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigAuthMethodOtpComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigAuthMethodOtpComparisonExp>>;
|
||||
email?: InputMaybe<ConfigAuthMethodOtpEmailComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtpEmail = {
|
||||
__typename?: 'ConfigAuthMethodOtpEmail';
|
||||
enabled?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtpEmailComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigAuthMethodOtpEmailComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigAuthMethodOtpEmailComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigAuthMethodOtpEmailComparisonExp>>;
|
||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtpEmailInsertInput = {
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtpEmailUpdateInput = {
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtpInsertInput = {
|
||||
email?: InputMaybe<ConfigAuthMethodOtpEmailInsertInput>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodOtpUpdateInput = {
|
||||
email?: InputMaybe<ConfigAuthMethodOtpEmailUpdateInput>;
|
||||
};
|
||||
|
||||
export type ConfigAuthMethodSmsPasswordless = {
|
||||
__typename?: 'ConfigAuthMethodSmsPasswordless';
|
||||
enabled?: Maybe<Scalars['Boolean']>;
|
||||
@@ -616,6 +663,7 @@ export type ConfigAuthMethodUpdateInput = {
|
||||
emailPassword?: InputMaybe<ConfigAuthMethodEmailPasswordUpdateInput>;
|
||||
emailPasswordless?: InputMaybe<ConfigAuthMethodEmailPasswordlessUpdateInput>;
|
||||
oauth?: InputMaybe<ConfigAuthMethodOauthUpdateInput>;
|
||||
otp?: InputMaybe<ConfigAuthMethodOtpUpdateInput>;
|
||||
smsPasswordless?: InputMaybe<ConfigAuthMethodSmsPasswordlessUpdateInput>;
|
||||
webauthn?: InputMaybe<ConfigAuthMethodWebauthnUpdateInput>;
|
||||
};
|
||||
@@ -2085,6 +2133,8 @@ export type ConfigJwtSecret = {
|
||||
issuer?: Maybe<Scalars['String']>;
|
||||
jwk_url?: Maybe<Scalars['ConfigUrl']>;
|
||||
key?: Maybe<Scalars['String']>;
|
||||
kid?: Maybe<Scalars['String']>;
|
||||
signingKey?: Maybe<Scalars['String']>;
|
||||
type?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@@ -2102,6 +2152,8 @@ export type ConfigJwtSecretComparisonExp = {
|
||||
issuer?: InputMaybe<ConfigStringComparisonExp>;
|
||||
jwk_url?: InputMaybe<ConfigUrlComparisonExp>;
|
||||
key?: InputMaybe<ConfigStringComparisonExp>;
|
||||
kid?: InputMaybe<ConfigStringComparisonExp>;
|
||||
signingKey?: InputMaybe<ConfigStringComparisonExp>;
|
||||
type?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
@@ -2116,6 +2168,8 @@ export type ConfigJwtSecretInsertInput = {
|
||||
issuer?: InputMaybe<Scalars['String']>;
|
||||
jwk_url?: InputMaybe<Scalars['ConfigUrl']>;
|
||||
key?: InputMaybe<Scalars['String']>;
|
||||
kid?: InputMaybe<Scalars['String']>;
|
||||
signingKey?: InputMaybe<Scalars['String']>;
|
||||
type?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@@ -2130,6 +2184,8 @@ export type ConfigJwtSecretUpdateInput = {
|
||||
issuer?: InputMaybe<Scalars['String']>;
|
||||
jwk_url?: InputMaybe<Scalars['ConfigUrl']>;
|
||||
key?: InputMaybe<Scalars['String']>;
|
||||
kid?: InputMaybe<Scalars['String']>;
|
||||
signingKey?: InputMaybe<Scalars['String']>;
|
||||
type?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@@ -2780,6 +2836,7 @@ export type ConfigStandardOauthProviderUpdateInput = {
|
||||
|
||||
export type ConfigStandardOauthProviderWithScope = {
|
||||
__typename?: 'ConfigStandardOauthProviderWithScope';
|
||||
audience?: Maybe<Scalars['String']>;
|
||||
clientId?: Maybe<Scalars['String']>;
|
||||
clientSecret?: Maybe<Scalars['String']>;
|
||||
enabled?: Maybe<Scalars['Boolean']>;
|
||||
@@ -2790,6 +2847,7 @@ export type ConfigStandardOauthProviderWithScopeComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigStandardOauthProviderWithScopeComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigStandardOauthProviderWithScopeComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigStandardOauthProviderWithScopeComparisonExp>>;
|
||||
audience?: InputMaybe<ConfigStringComparisonExp>;
|
||||
clientId?: InputMaybe<ConfigStringComparisonExp>;
|
||||
clientSecret?: InputMaybe<ConfigStringComparisonExp>;
|
||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
@@ -2797,6 +2855,7 @@ export type ConfigStandardOauthProviderWithScopeComparisonExp = {
|
||||
};
|
||||
|
||||
export type ConfigStandardOauthProviderWithScopeInsertInput = {
|
||||
audience?: InputMaybe<Scalars['String']>;
|
||||
clientId?: InputMaybe<Scalars['String']>;
|
||||
clientSecret?: InputMaybe<Scalars['String']>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -2804,6 +2863,7 @@ export type ConfigStandardOauthProviderWithScopeInsertInput = {
|
||||
};
|
||||
|
||||
export type ConfigStandardOauthProviderWithScopeUpdateInput = {
|
||||
audience?: InputMaybe<Scalars['String']>;
|
||||
clientId?: InputMaybe<Scalars['String']>;
|
||||
clientSecret?: InputMaybe<Scalars['String']>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -3270,9 +3330,33 @@ export type Announcements = {
|
||||
expiresAt?: Maybe<Scalars['timestamptz']>;
|
||||
href: Scalars['String'];
|
||||
id: Scalars['uuid'];
|
||||
/** An array relationship */
|
||||
read: Array<Announcements_Read>;
|
||||
/** An aggregate relationship */
|
||||
read_aggregate: Announcements_Read_Aggregate;
|
||||
updatedAt: Scalars['timestamptz'];
|
||||
};
|
||||
|
||||
|
||||
/** columns and relationships of "announcements" */
|
||||
export type AnnouncementsReadArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Announcements_Read_Order_By>>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
/** columns and relationships of "announcements" */
|
||||
export type AnnouncementsRead_AggregateArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Announcements_Read_Order_By>>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
/** aggregated selection of "announcements" */
|
||||
export type Announcements_Aggregate = {
|
||||
__typename?: 'announcements_aggregate';
|
||||
@@ -3305,6 +3389,8 @@ export type Announcements_Bool_Exp = {
|
||||
expiresAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||
href?: InputMaybe<String_Comparison_Exp>;
|
||||
id?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
read?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
read_aggregate?: InputMaybe<Announcements_Read_Aggregate_Bool_Exp>;
|
||||
updatedAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||
};
|
||||
|
||||
@@ -3321,6 +3407,7 @@ export type Announcements_Insert_Input = {
|
||||
expiresAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
href?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
read?: InputMaybe<Announcements_Read_Arr_Rel_Insert_Input>;
|
||||
updatedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
};
|
||||
|
||||
@@ -3369,6 +3456,7 @@ export type Announcements_Order_By = {
|
||||
expiresAt?: InputMaybe<Order_By>;
|
||||
href?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
read_aggregate?: InputMaybe<Announcements_Read_Aggregate_Order_By>;
|
||||
updatedAt?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
@@ -3377,6 +3465,207 @@ export type Announcements_Pk_Columns_Input = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
/** columns and relationships of "announcements_read" */
|
||||
export type Announcements_Read = {
|
||||
__typename?: 'announcements_read';
|
||||
announcementID: Scalars['uuid'];
|
||||
createdAt: Scalars['timestamptz'];
|
||||
id: Scalars['uuid'];
|
||||
userID: Scalars['uuid'];
|
||||
};
|
||||
|
||||
/** aggregated selection of "announcements_read" */
|
||||
export type Announcements_Read_Aggregate = {
|
||||
__typename?: 'announcements_read_aggregate';
|
||||
aggregate?: Maybe<Announcements_Read_Aggregate_Fields>;
|
||||
nodes: Array<Announcements_Read>;
|
||||
};
|
||||
|
||||
export type Announcements_Read_Aggregate_Bool_Exp = {
|
||||
count?: InputMaybe<Announcements_Read_Aggregate_Bool_Exp_Count>;
|
||||
};
|
||||
|
||||
export type Announcements_Read_Aggregate_Bool_Exp_Count = {
|
||||
arguments?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
distinct?: InputMaybe<Scalars['Boolean']>;
|
||||
filter?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
predicate: Int_Comparison_Exp;
|
||||
};
|
||||
|
||||
/** aggregate fields of "announcements_read" */
|
||||
export type Announcements_Read_Aggregate_Fields = {
|
||||
__typename?: 'announcements_read_aggregate_fields';
|
||||
count: Scalars['Int'];
|
||||
max?: Maybe<Announcements_Read_Max_Fields>;
|
||||
min?: Maybe<Announcements_Read_Min_Fields>;
|
||||
};
|
||||
|
||||
|
||||
/** aggregate fields of "announcements_read" */
|
||||
export type Announcements_Read_Aggregate_FieldsCountArgs = {
|
||||
columns?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
distinct?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
/** order by aggregate values of table "announcements_read" */
|
||||
export type Announcements_Read_Aggregate_Order_By = {
|
||||
count?: InputMaybe<Order_By>;
|
||||
max?: InputMaybe<Announcements_Read_Max_Order_By>;
|
||||
min?: InputMaybe<Announcements_Read_Min_Order_By>;
|
||||
};
|
||||
|
||||
/** input type for inserting array relation for remote table "announcements_read" */
|
||||
export type Announcements_Read_Arr_Rel_Insert_Input = {
|
||||
data: Array<Announcements_Read_Insert_Input>;
|
||||
/** upsert condition */
|
||||
on_conflict?: InputMaybe<Announcements_Read_On_Conflict>;
|
||||
};
|
||||
|
||||
/** Boolean expression to filter rows from the table "announcements_read". All fields are combined with a logical 'AND'. */
|
||||
export type Announcements_Read_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Announcements_Read_Bool_Exp>>;
|
||||
_not?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
_or?: InputMaybe<Array<Announcements_Read_Bool_Exp>>;
|
||||
announcementID?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
createdAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||
id?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
userID?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
};
|
||||
|
||||
/** unique or primary key constraints on table "announcements_read" */
|
||||
export enum Announcements_Read_Constraint {
|
||||
/** unique or primary key constraint on columns "user_id", "announcement_id" */
|
||||
AnnouncementsReadAnnouncementIdUserIdKey = 'announcements_read_announcement_id_user_id_key',
|
||||
/** unique or primary key constraint on columns "id" */
|
||||
AnnouncementsReadPkey = 'announcements_read_pkey'
|
||||
}
|
||||
|
||||
/** input type for inserting data into table "announcements_read" */
|
||||
export type Announcements_Read_Insert_Input = {
|
||||
announcementID?: InputMaybe<Scalars['uuid']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
userID?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** aggregate max on columns */
|
||||
export type Announcements_Read_Max_Fields = {
|
||||
__typename?: 'announcements_read_max_fields';
|
||||
announcementID?: Maybe<Scalars['uuid']>;
|
||||
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
userID?: Maybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** order by max() on columns of table "announcements_read" */
|
||||
export type Announcements_Read_Max_Order_By = {
|
||||
announcementID?: InputMaybe<Order_By>;
|
||||
createdAt?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
userID?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
/** aggregate min on columns */
|
||||
export type Announcements_Read_Min_Fields = {
|
||||
__typename?: 'announcements_read_min_fields';
|
||||
announcementID?: Maybe<Scalars['uuid']>;
|
||||
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
userID?: Maybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** order by min() on columns of table "announcements_read" */
|
||||
export type Announcements_Read_Min_Order_By = {
|
||||
announcementID?: InputMaybe<Order_By>;
|
||||
createdAt?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
userID?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
/** response of any mutation on the table "announcements_read" */
|
||||
export type Announcements_Read_Mutation_Response = {
|
||||
__typename?: 'announcements_read_mutation_response';
|
||||
/** number of rows affected by the mutation */
|
||||
affected_rows: Scalars['Int'];
|
||||
/** data from the rows affected by the mutation */
|
||||
returning: Array<Announcements_Read>;
|
||||
};
|
||||
|
||||
/** on_conflict condition type for table "announcements_read" */
|
||||
export type Announcements_Read_On_Conflict = {
|
||||
constraint: Announcements_Read_Constraint;
|
||||
update_columns?: Array<Announcements_Read_Update_Column>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
/** Ordering options when selecting data from "announcements_read". */
|
||||
export type Announcements_Read_Order_By = {
|
||||
announcementID?: InputMaybe<Order_By>;
|
||||
createdAt?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
userID?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
/** primary key columns input for table: announcements_read */
|
||||
export type Announcements_Read_Pk_Columns_Input = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
/** select columns of table "announcements_read" */
|
||||
export enum Announcements_Read_Select_Column {
|
||||
/** column name */
|
||||
AnnouncementId = 'announcementID',
|
||||
/** column name */
|
||||
CreatedAt = 'createdAt',
|
||||
/** column name */
|
||||
Id = 'id',
|
||||
/** column name */
|
||||
UserId = 'userID'
|
||||
}
|
||||
|
||||
/** input type for updating data in table "announcements_read" */
|
||||
export type Announcements_Read_Set_Input = {
|
||||
announcementID?: InputMaybe<Scalars['uuid']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
userID?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** Streaming cursor of the table "announcements_read" */
|
||||
export type Announcements_Read_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Announcements_Read_Stream_Cursor_Value_Input;
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>;
|
||||
};
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Announcements_Read_Stream_Cursor_Value_Input = {
|
||||
announcementID?: InputMaybe<Scalars['uuid']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
userID?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** update columns of table "announcements_read" */
|
||||
export enum Announcements_Read_Update_Column {
|
||||
/** column name */
|
||||
AnnouncementId = 'announcementID',
|
||||
/** column name */
|
||||
CreatedAt = 'createdAt',
|
||||
/** column name */
|
||||
Id = 'id',
|
||||
/** column name */
|
||||
UserId = 'userID'
|
||||
}
|
||||
|
||||
export type Announcements_Read_Updates = {
|
||||
/** sets the columns of the filtered rows to the given values */
|
||||
_set?: InputMaybe<Announcements_Read_Set_Input>;
|
||||
/** filter the rows which have to be updated */
|
||||
where: Announcements_Read_Bool_Exp;
|
||||
};
|
||||
|
||||
/** select columns of table "announcements" */
|
||||
export enum Announcements_Select_Column {
|
||||
/** column name */
|
||||
@@ -12830,6 +13119,10 @@ export type Mutation_Root = {
|
||||
/** execute VOLATILE function "billing.reports_delete_older_than_days" which returns "billing.reports" */
|
||||
billing_reports_delete_older_than_days: Array<Billing_Reports>;
|
||||
changeDatabaseVersion: Scalars['Boolean'];
|
||||
/** delete single row from the table: "announcements_read" */
|
||||
deleteAnnouncementRead?: Maybe<Announcements_Read>;
|
||||
/** delete data from the table: "announcements_read" */
|
||||
deleteAnnouncementsRead?: Maybe<Announcements_Read_Mutation_Response>;
|
||||
/** delete single row from the table: "apps" */
|
||||
deleteApp?: Maybe<Apps>;
|
||||
/** delete single row from the table: "app_states" */
|
||||
@@ -13041,6 +13334,10 @@ export type Mutation_Root = {
|
||||
delete_regions?: Maybe<Regions_Mutation_Response>;
|
||||
/** delete single row from the table: "regions" */
|
||||
delete_regions_by_pk?: Maybe<Regions>;
|
||||
/** insert a single row into the table: "announcements_read" */
|
||||
insertAnnouncementRead?: Maybe<Announcements_Read>;
|
||||
/** insert data into the table: "announcements_read" */
|
||||
insertAnnouncementsRead?: Maybe<Announcements_Read_Mutation_Response>;
|
||||
/** insert a single row into the table: "apps" */
|
||||
insertApp?: Maybe<Apps>;
|
||||
/** insert a single row into the table: "app_states" */
|
||||
@@ -13265,6 +13562,12 @@ export type Mutation_Root = {
|
||||
sendEmailOrganizationStatusChange: Scalars['Boolean'];
|
||||
sendEmailOrganizationThreshold: Scalars['Boolean'];
|
||||
sendEmailTemplate: Scalars['Boolean'];
|
||||
/** update single row of the table: "announcements_read" */
|
||||
updateAnnouncementRead?: Maybe<Announcements_Read>;
|
||||
/** update data of the table: "announcements_read" */
|
||||
updateAnnouncementsRead?: Maybe<Announcements_Read_Mutation_Response>;
|
||||
/** update multiples rows of table: "announcements_read" */
|
||||
updateAnnouncementsReadMany?: Maybe<Array<Maybe<Announcements_Read_Mutation_Response>>>;
|
||||
/** update single row of the table: "apps" */
|
||||
updateApp?: Maybe<Apps>;
|
||||
/** update single row of the table: "app_states" */
|
||||
@@ -13700,6 +14003,18 @@ export type Mutation_RootChangeDatabaseVersionArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteAnnouncementReadArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteAnnouncementsReadArgs = {
|
||||
where: Announcements_Read_Bool_Exp;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteAppArgs = {
|
||||
id: Scalars['uuid'];
|
||||
@@ -14344,6 +14659,20 @@ export type Mutation_RootDelete_Regions_By_PkArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootInsertAnnouncementReadArgs = {
|
||||
object: Announcements_Read_Insert_Input;
|
||||
on_conflict?: InputMaybe<Announcements_Read_On_Conflict>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootInsertAnnouncementsReadArgs = {
|
||||
objects: Array<Announcements_Read_Insert_Input>;
|
||||
on_conflict?: InputMaybe<Announcements_Read_On_Conflict>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootInsertAppArgs = {
|
||||
object: Apps_Insert_Input;
|
||||
@@ -15173,6 +15502,26 @@ export type Mutation_RootSendEmailTemplateArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdateAnnouncementReadArgs = {
|
||||
_set?: InputMaybe<Announcements_Read_Set_Input>;
|
||||
pk_columns: Announcements_Read_Pk_Columns_Input;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdateAnnouncementsReadArgs = {
|
||||
_set?: InputMaybe<Announcements_Read_Set_Input>;
|
||||
where: Announcements_Read_Bool_Exp;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdateAnnouncementsReadManyArgs = {
|
||||
updates: Array<Announcements_Read_Updates>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdateAppArgs = {
|
||||
_append?: InputMaybe<Apps_Append_Input>;
|
||||
@@ -19125,8 +19474,14 @@ export type Plans_Variance_Fields = {
|
||||
|
||||
export type Query_Root = {
|
||||
__typename?: 'query_root';
|
||||
/** fetch data from the table: "announcements_read" using primary key columns */
|
||||
announcementRead?: Maybe<Announcements_Read>;
|
||||
/** fetch data from the table: "announcements" */
|
||||
announcements: Array<Announcements>;
|
||||
/** fetch data from the table: "announcements_read" */
|
||||
announcementsRead: Array<Announcements_Read>;
|
||||
/** fetch aggregated fields from the table: "announcements_read" */
|
||||
announcementsReadAggregate: Announcements_Read_Aggregate;
|
||||
/** fetch aggregated fields from the table: "announcements" */
|
||||
announcements_aggregate: Announcements_Aggregate;
|
||||
/** fetch data from the table: "announcements" using primary key columns */
|
||||
@@ -19491,6 +19846,11 @@ export type Query_Root = {
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootAnnouncementReadArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootAnnouncementsArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
@@ -19500,6 +19860,24 @@ export type Query_RootAnnouncementsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootAnnouncementsReadArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Announcements_Read_Order_By>>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootAnnouncementsReadAggregateArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Announcements_Read_Order_By>>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootAnnouncements_AggregateArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
@@ -22628,8 +23006,16 @@ export type Software_Versions_Updates = {
|
||||
|
||||
export type Subscription_Root = {
|
||||
__typename?: 'subscription_root';
|
||||
/** fetch data from the table: "announcements_read" using primary key columns */
|
||||
announcementRead?: Maybe<Announcements_Read>;
|
||||
/** fetch data from the table: "announcements" */
|
||||
announcements: Array<Announcements>;
|
||||
/** fetch data from the table: "announcements_read" */
|
||||
announcementsRead: Array<Announcements_Read>;
|
||||
/** fetch aggregated fields from the table: "announcements_read" */
|
||||
announcementsReadAggregate: Announcements_Read_Aggregate;
|
||||
/** fetch data from the table in a streaming manner: "announcements_read" */
|
||||
announcementsReadStream: Array<Announcements_Read>;
|
||||
/** fetch aggregated fields from the table: "announcements" */
|
||||
announcements_aggregate: Announcements_Aggregate;
|
||||
/** fetch data from the table: "announcements" using primary key columns */
|
||||
@@ -23052,6 +23438,11 @@ export type Subscription_Root = {
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootAnnouncementReadArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootAnnouncementsArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
@@ -23061,6 +23452,31 @@ export type Subscription_RootAnnouncementsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootAnnouncementsReadArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Announcements_Read_Order_By>>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootAnnouncementsReadAggregateArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Read_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Announcements_Read_Order_By>>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootAnnouncementsReadStreamArgs = {
|
||||
batch_size: Scalars['Int'];
|
||||
cursor: Array<InputMaybe<Announcements_Read_Stream_Cursor_Input>>;
|
||||
where?: InputMaybe<Announcements_Read_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootAnnouncements_AggregateArgs = {
|
||||
distinct_on?: InputMaybe<Array<Announcements_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
@@ -26877,6 +27293,13 @@ export type GetBackupPresignedUrlQueryVariables = Exact<{
|
||||
|
||||
export type GetBackupPresignedUrlQuery = { __typename?: 'query_root', getBackupPresignedUrl: { __typename?: 'BackupPresignedURL', url: string, expiresAt: any } };
|
||||
|
||||
export type GetJwtSecretsQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetJwtSecretsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', jwtSecrets?: Array<{ __typename?: 'ConfigJWTSecret', type?: string | null, key?: string | null, signingKey?: string | null, kid?: string | null, jwk_url?: any | null, allowed_skew?: any | null, audience?: string | null, claims_format?: string | null, claims_namespace?: string | null, claims_namespace_path?: string | null, header?: string | null, issuer?: string | null, claims_map?: Array<{ __typename?: 'ConfigClaimMap', claim: string, default?: string | null, path?: string | null, value?: string | null }> | null }> | null } } | null };
|
||||
|
||||
export type GetObservabilitySettingsQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
@@ -27039,14 +27462,14 @@ export type DnsLookupCnameQuery = { __typename?: 'query_root', dnsLookupCNAME: s
|
||||
|
||||
export type EnvironmentVariableFragment = { __typename?: 'ConfigGlobalEnvironmentVariable', name: string, value: string, id: string };
|
||||
|
||||
export type JwtSecretFragment = { __typename?: 'ConfigJWTSecret', issuer?: string | null, key?: string | null, type?: string | null, jwk_url?: any | null, header?: string | null, claims_namespace_path?: string | null, claims_namespace?: string | null, claims_format?: string | null, audience?: string | null, allowed_skew?: any | null };
|
||||
export type JwtSecretFragment = { __typename?: 'ConfigJWTSecret', issuer?: string | null, key?: string | null, type?: string | null, signingKey?: string | null, kid?: string | null, jwk_url?: any | null, header?: string | null, claims_namespace_path?: string | null, claims_namespace?: string | null, claims_format?: string | null, audience?: string | null, allowed_skew?: any | null, claims_map?: Array<{ __typename?: 'ConfigClaimMap', claim: string, default?: string | null, path?: string | null, value?: string | null }> | null };
|
||||
|
||||
export type GetEnvironmentVariablesQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetEnvironmentVariablesQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', global?: { __typename?: 'ConfigGlobal', environment?: Array<{ __typename?: 'ConfigGlobalEnvironmentVariable', name: string, value: string, id: string }> | null } | null, hasura: { __typename?: 'ConfigHasura', adminSecret: string, webhookSecret: string, jwtSecrets?: Array<{ __typename?: 'ConfigJWTSecret', issuer?: string | null, key?: string | null, type?: string | null, jwk_url?: any | null, header?: string | null, claims_namespace_path?: string | null, claims_namespace?: string | null, claims_format?: string | null, audience?: string | null, allowed_skew?: any | null }> | null } } | null };
|
||||
export type GetEnvironmentVariablesQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', global?: { __typename?: 'ConfigGlobal', environment?: Array<{ __typename?: 'ConfigGlobalEnvironmentVariable', name: string, value: string, id: string }> | null } | null, hasura: { __typename?: 'ConfigHasura', adminSecret: string, webhookSecret: string, jwtSecrets?: Array<{ __typename?: 'ConfigJWTSecret', issuer?: string | null, key?: string | null, type?: string | null, signingKey?: string | null, kid?: string | null, jwk_url?: any | null, header?: string | null, claims_namespace_path?: string | null, claims_namespace?: string | null, claims_format?: string | null, audience?: string | null, allowed_skew?: any | null, claims_map?: Array<{ __typename?: 'ConfigClaimMap', claim: string, default?: string | null, path?: string | null, value?: string | null }> | null }> | null } } | null };
|
||||
|
||||
export type GetConfigRawJsonQueryVariables = Exact<{
|
||||
appID: Scalars['uuid'];
|
||||
@@ -27126,7 +27549,7 @@ export type GetSignInMethodsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetSignInMethodsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', provider?: { __typename: 'ConfigProvider', id: 'ConfigProvider', sms?: { __typename?: 'ConfigSms', accountSid: string, authToken: string, messagingServiceId: string, provider?: string | null } | null } | null, auth?: { __typename: 'ConfigAuth', id: 'ConfigAuth', method?: { __typename?: 'ConfigAuthMethod', emailPassword?: { __typename?: 'ConfigAuthMethodEmailPassword', emailVerificationRequired?: boolean | null, hibpEnabled?: boolean | null, passwordMinLength?: any | null } | null, emailPasswordless?: { __typename?: 'ConfigAuthMethodEmailPasswordless', enabled?: boolean | null } | null, smsPasswordless?: { __typename?: 'ConfigAuthMethodSmsPasswordless', enabled?: boolean | null } | null, anonymous?: { __typename?: 'ConfigAuthMethodAnonymous', enabled?: boolean | null } | null, webauthn?: { __typename?: 'ConfigAuthMethodWebauthn', enabled?: boolean | null } | null, oauth?: { __typename?: 'ConfigAuthMethodOauth', apple?: { __typename?: 'ConfigAuthMethodOauthApple', enabled?: boolean | null, clientId?: string | null, keyId?: string | null, teamId?: string | null, privateKey?: string | null } | null, bitbucket?: { __typename?: 'ConfigStandardOauthProvider', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null } | null, gitlab?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, strava?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, discord?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, facebook?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, github?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, google?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, linkedin?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, spotify?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitch?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitter?: { __typename?: 'ConfigAuthMethodOauthTwitter', enabled?: boolean | null, consumerKey?: string | null, consumerSecret?: string | null } | null, windowslive?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, workos?: { __typename?: 'ConfigAuthMethodOauthWorkos', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, connection?: string | null, organization?: string | null } | null, azuread?: { __typename?: 'ConfigAuthMethodOauthAzuread', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, tenant?: string | null } | null } | null } | null } | null } | null };
|
||||
export type GetSignInMethodsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', provider?: { __typename: 'ConfigProvider', id: 'ConfigProvider', sms?: { __typename?: 'ConfigSms', accountSid: string, authToken: string, messagingServiceId: string, provider?: string | null } | null } | null, auth?: { __typename: 'ConfigAuth', id: 'ConfigAuth', method?: { __typename?: 'ConfigAuthMethod', otp?: { __typename?: 'ConfigAuthMethodOtp', email?: { __typename?: 'ConfigAuthMethodOtpEmail', enabled?: boolean | null } | null } | null, emailPassword?: { __typename?: 'ConfigAuthMethodEmailPassword', emailVerificationRequired?: boolean | null, hibpEnabled?: boolean | null, passwordMinLength?: any | null } | null, emailPasswordless?: { __typename?: 'ConfigAuthMethodEmailPasswordless', enabled?: boolean | null } | null, smsPasswordless?: { __typename?: 'ConfigAuthMethodSmsPasswordless', enabled?: boolean | null } | null, anonymous?: { __typename?: 'ConfigAuthMethodAnonymous', enabled?: boolean | null } | null, webauthn?: { __typename?: 'ConfigAuthMethodWebauthn', enabled?: boolean | null } | null, oauth?: { __typename?: 'ConfigAuthMethodOauth', apple?: { __typename?: 'ConfigAuthMethodOauthApple', enabled?: boolean | null, clientId?: string | null, keyId?: string | null, teamId?: string | null, privateKey?: string | null, audience?: string | null } | null, bitbucket?: { __typename?: 'ConfigStandardOauthProvider', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null } | null, gitlab?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, strava?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, discord?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, facebook?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, github?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, google?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null, audience?: string | null } | null, linkedin?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, spotify?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitch?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitter?: { __typename?: 'ConfigAuthMethodOauthTwitter', enabled?: boolean | null, consumerKey?: string | null, consumerSecret?: string | null } | null, windowslive?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, workos?: { __typename?: 'ConfigAuthMethodOauthWorkos', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, connection?: string | null, organization?: string | null } | null, azuread?: { __typename?: 'ConfigAuthMethodOauthAzuread', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, tenant?: string | null } | null } | null } | null } | null } | null };
|
||||
|
||||
export type GetSmtpSettingsQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
@@ -27525,12 +27948,17 @@ export type SetNewDefaultPaymentMethodMutationVariables = Exact<{
|
||||
|
||||
export type SetNewDefaultPaymentMethodMutation = { __typename?: 'mutation_root', setAllPaymentMethodToDefaultFalse?: { __typename?: 'paymentMethods_mutation_response', affected_rows: number } | null, updatePaymentMethods?: { __typename?: 'paymentMethods_mutation_response', affected_rows: number } | null };
|
||||
|
||||
export type GetAnnouncementsQueryVariables = Exact<{
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
export type DeleteAnnouncementReadMutationVariables = Exact<{
|
||||
id: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAnnouncementsQuery = { __typename?: 'query_root', announcements: Array<{ __typename?: 'announcements', id: any, href: string, content: string, createdAt: any }> };
|
||||
export type DeleteAnnouncementReadMutation = { __typename?: 'mutation_root', deleteAnnouncementRead?: { __typename?: 'announcements_read', announcementID: any } | null };
|
||||
|
||||
export type GetAnnouncementsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetAnnouncementsQuery = { __typename?: 'query_root', announcements: Array<{ __typename?: 'announcements', id: any, href: string, content: string, createdAt: any, read: Array<{ __typename?: 'announcements_read', id: any }> }> };
|
||||
|
||||
export type GetPlansQueryVariables = Exact<{
|
||||
where?: InputMaybe<Plans_Bool_Exp>;
|
||||
@@ -27551,6 +27979,13 @@ export type GetSoftwareVersionsQueryVariables = Exact<{
|
||||
|
||||
export type GetSoftwareVersionsQuery = { __typename?: 'query_root', softwareVersions: Array<{ __typename?: 'software_versions', version: string, software: Software_Type_Enum }> };
|
||||
|
||||
export type InsertAnnouncementReadMutationVariables = Exact<{
|
||||
announcementID: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type InsertAnnouncementReadMutation = { __typename?: 'mutation_root', insertAnnouncementRead?: { __typename?: 'announcements_read', id: any } | null };
|
||||
|
||||
export type RestoreApplicationDatabaseMutationVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
backupId: Scalars['String'];
|
||||
@@ -27950,9 +28385,17 @@ export const JwtSecretFragmentDoc = gql`
|
||||
issuer
|
||||
key
|
||||
type
|
||||
signingKey
|
||||
kid
|
||||
jwk_url
|
||||
header
|
||||
claims_namespace_path
|
||||
claims_map {
|
||||
claim
|
||||
default
|
||||
path
|
||||
value
|
||||
}
|
||||
claims_namespace
|
||||
claims_format
|
||||
audience
|
||||
@@ -28794,6 +29237,67 @@ export type GetBackupPresignedUrlQueryResult = Apollo.QueryResult<GetBackupPresi
|
||||
export function refetchGetBackupPresignedUrlQuery(variables: GetBackupPresignedUrlQueryVariables) {
|
||||
return { query: GetBackupPresignedUrlDocument, variables: variables }
|
||||
}
|
||||
export const GetJwtSecretsDocument = gql`
|
||||
query GetJWTSecrets($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
id: __typename
|
||||
__typename
|
||||
hasura {
|
||||
jwtSecrets {
|
||||
type
|
||||
key
|
||||
signingKey
|
||||
kid
|
||||
jwk_url
|
||||
allowed_skew
|
||||
audience
|
||||
claims_format
|
||||
claims_map {
|
||||
claim
|
||||
default
|
||||
path
|
||||
value
|
||||
}
|
||||
claims_namespace
|
||||
claims_namespace_path
|
||||
header
|
||||
issuer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetJwtSecretsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetJwtSecretsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetJwtSecretsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetJwtSecretsQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetJwtSecretsQuery(baseOptions: Apollo.QueryHookOptions<GetJwtSecretsQuery, GetJwtSecretsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetJwtSecretsQuery, GetJwtSecretsQueryVariables>(GetJwtSecretsDocument, options);
|
||||
}
|
||||
export function useGetJwtSecretsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetJwtSecretsQuery, GetJwtSecretsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetJwtSecretsQuery, GetJwtSecretsQueryVariables>(GetJwtSecretsDocument, options);
|
||||
}
|
||||
export type GetJwtSecretsQueryHookResult = ReturnType<typeof useGetJwtSecretsQuery>;
|
||||
export type GetJwtSecretsLazyQueryHookResult = ReturnType<typeof useGetJwtSecretsLazyQuery>;
|
||||
export type GetJwtSecretsQueryResult = Apollo.QueryResult<GetJwtSecretsQuery, GetJwtSecretsQueryVariables>;
|
||||
export function refetchGetJwtSecretsQuery(variables: GetJwtSecretsQueryVariables) {
|
||||
return { query: GetJwtSecretsDocument, variables: variables }
|
||||
}
|
||||
export const GetObservabilitySettingsDocument = gql`
|
||||
query GetObservabilitySettings($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
@@ -30238,6 +30742,11 @@ export const GetSignInMethodsDocument = gql`
|
||||
id: __typename
|
||||
__typename
|
||||
method {
|
||||
otp {
|
||||
email {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
emailPassword {
|
||||
emailVerificationRequired
|
||||
hibpEnabled
|
||||
@@ -30262,6 +30771,7 @@ export const GetSignInMethodsDocument = gql`
|
||||
keyId
|
||||
teamId
|
||||
privateKey
|
||||
audience
|
||||
}
|
||||
bitbucket {
|
||||
enabled
|
||||
@@ -30303,6 +30813,7 @@ export const GetSignInMethodsDocument = gql`
|
||||
clientId
|
||||
clientSecret
|
||||
scope
|
||||
audience
|
||||
}
|
||||
linkedin {
|
||||
enabled
|
||||
@@ -32520,17 +33031,52 @@ export function useSetNewDefaultPaymentMethodMutation(baseOptions?: Apollo.Mutat
|
||||
export type SetNewDefaultPaymentMethodMutationHookResult = ReturnType<typeof useSetNewDefaultPaymentMethodMutation>;
|
||||
export type SetNewDefaultPaymentMethodMutationResult = Apollo.MutationResult<SetNewDefaultPaymentMethodMutation>;
|
||||
export type SetNewDefaultPaymentMethodMutationOptions = Apollo.BaseMutationOptions<SetNewDefaultPaymentMethodMutation, SetNewDefaultPaymentMethodMutationVariables>;
|
||||
export const DeleteAnnouncementReadDocument = gql`
|
||||
mutation deleteAnnouncementRead($id: uuid!) {
|
||||
deleteAnnouncementRead(id: $id) {
|
||||
announcementID
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteAnnouncementReadMutationFn = Apollo.MutationFunction<DeleteAnnouncementReadMutation, DeleteAnnouncementReadMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteAnnouncementReadMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteAnnouncementReadMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteAnnouncementReadMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [deleteAnnouncementReadMutation, { data, loading, error }] = useDeleteAnnouncementReadMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteAnnouncementReadMutation(baseOptions?: Apollo.MutationHookOptions<DeleteAnnouncementReadMutation, DeleteAnnouncementReadMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteAnnouncementReadMutation, DeleteAnnouncementReadMutationVariables>(DeleteAnnouncementReadDocument, options);
|
||||
}
|
||||
export type DeleteAnnouncementReadMutationHookResult = ReturnType<typeof useDeleteAnnouncementReadMutation>;
|
||||
export type DeleteAnnouncementReadMutationResult = Apollo.MutationResult<DeleteAnnouncementReadMutation>;
|
||||
export type DeleteAnnouncementReadMutationOptions = Apollo.BaseMutationOptions<DeleteAnnouncementReadMutation, DeleteAnnouncementReadMutationVariables>;
|
||||
export const GetAnnouncementsDocument = gql`
|
||||
query getAnnouncements($limit: Int) {
|
||||
query getAnnouncements {
|
||||
announcements(
|
||||
order_by: {createdAt: desc}
|
||||
limit: $limit
|
||||
where: {_or: [{expiresAt: {_is_null: true}}, {expiresAt: {_gt: now}}]}
|
||||
) {
|
||||
id
|
||||
href
|
||||
content
|
||||
createdAt
|
||||
read {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -32547,7 +33093,6 @@ export const GetAnnouncementsDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useGetAnnouncementsQuery({
|
||||
* variables: {
|
||||
* limit: // value for 'limit'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@@ -32683,6 +33228,39 @@ export type GetSoftwareVersionsQueryResult = Apollo.QueryResult<GetSoftwareVersi
|
||||
export function refetchGetSoftwareVersionsQuery(variables: GetSoftwareVersionsQueryVariables) {
|
||||
return { query: GetSoftwareVersionsDocument, variables: variables }
|
||||
}
|
||||
export const InsertAnnouncementReadDocument = gql`
|
||||
mutation insertAnnouncementRead($announcementID: uuid!) {
|
||||
insertAnnouncementRead(object: {announcementID: $announcementID}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type InsertAnnouncementReadMutationFn = Apollo.MutationFunction<InsertAnnouncementReadMutation, InsertAnnouncementReadMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useInsertAnnouncementReadMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useInsertAnnouncementReadMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useInsertAnnouncementReadMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [insertAnnouncementReadMutation, { data, loading, error }] = useInsertAnnouncementReadMutation({
|
||||
* variables: {
|
||||
* announcementID: // value for 'announcementID'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useInsertAnnouncementReadMutation(baseOptions?: Apollo.MutationHookOptions<InsertAnnouncementReadMutation, InsertAnnouncementReadMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<InsertAnnouncementReadMutation, InsertAnnouncementReadMutationVariables>(InsertAnnouncementReadDocument, options);
|
||||
}
|
||||
export type InsertAnnouncementReadMutationHookResult = ReturnType<typeof useInsertAnnouncementReadMutation>;
|
||||
export type InsertAnnouncementReadMutationResult = Apollo.MutationResult<InsertAnnouncementReadMutation>;
|
||||
export type InsertAnnouncementReadMutationOptions = Apollo.BaseMutationOptions<InsertAnnouncementReadMutation, InsertAnnouncementReadMutationVariables>;
|
||||
export const RestoreApplicationDatabaseDocument = gql`
|
||||
mutation RestoreApplicationDatabase($appId: String!, $backupId: String!) {
|
||||
restoreApplicationDatabase(appID: $appId, backupID: $backupId)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 2.23.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 14e6100: feat: add documentation for sign-in with ID token
|
||||
|
||||
## 2.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 10b0f74: feat: add jwt docs + openapi improvements
|
||||
- fe6e8e2: feat: add signin with otp reference docs
|
||||
- 8f77914: fix: added pg_repack and an extension overview to database guide
|
||||
|
||||
## 2.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
315
docs/guides/auth/jwt.mdx
Normal file
315
docs/guides/auth/jwt.mdx
Normal file
@@ -0,0 +1,315 @@
|
||||
---
|
||||
title: JSON Web Tokens (JWTs)
|
||||
description: Configure JSON Web Tokens to your needs
|
||||
icon: key
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
JSON Web Tokens (JWT) are encoded strings designed to securely transmit information between parties in the form of a JSON object. Each JWT consists of three parts:
|
||||
|
||||
- header
|
||||
- payload
|
||||
- signature
|
||||
|
||||
JWTs are commonly used for authentication post-login. The server generates a token containing user claims (like identity and permissions) that subsequent requests can include to prove authorization.
|
||||
|
||||
Here's how JWTs typically work in an authentication flow:
|
||||
|
||||
1. User logs in with credentials (username/password)
|
||||
2. Server validates credentials and generates a signed JWT containing user information and permissions
|
||||
3. Server sends the JWT to the client, which stores it (usually in browser storage)
|
||||
4. For subsequent requests, the client includes the JWT in the Authorization header
|
||||
5. Server verifies the token's signature and grants access based on the encoded permissions
|
||||
|
||||
The main advantage is that the server doesn't need to store session information - all necessary data is contained within the token itself, making it ideal for stateless authentication.
|
||||
|
||||
<Info>For more information about JSON Web Tokens, visit [jwt.io](https://jwt.io).</Info>
|
||||
|
||||
## JWT Configuration
|
||||
|
||||
You can configure your project to use three different kinds of JWTs:
|
||||
|
||||
- JWTs signed with symmetric keys
|
||||
- JWTs signed with asymmetric keys
|
||||
- JWTs signed externally via a third-party service
|
||||
|
||||
<Note>
|
||||
Currently we default to using symmetric keys for signing JWTs. However, we plan to change this to use asymmetric keys in the near future.
|
||||
</Note>
|
||||
|
||||
### Symmetric Keys
|
||||
|
||||
With symmetric keys, your project uses a single key for both signing and verifying JWTs. This key is stored in the project's configuration and is responsible for signing JWTs. When a client sends a JWT to the server, the server uses the same key to verify the JWT’s signature. If you need to verify JWTs in a different service, the same key can be used for verification. Since the same key is used for both signing and verification, it is crucial to keep it secret, as sharing it with others can compromise the security of your JWTs.
|
||||
|
||||
|
||||
Below you can see an example of a symmetric key configuration:
|
||||
|
||||
|
||||
<Tabs>
|
||||
<Tab title="nhost.toml">
|
||||
```toml
|
||||
[[hasura.jwtSecrets]]
|
||||
type = 'HS256'
|
||||
key = 'f03d5f5a0ed055e3fcbc0a3639405aca0511e6abe6d60e40d1fff610c6248f2a'
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="dashboard">
|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
We recommend using a [secret](/platform/secrets) to configure the key.
|
||||
</Note>
|
||||
|
||||
In addition to `HS256`, you can also use `HS384` and `HS512` for extra security. To quickly generate a key, you can use the following command:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="HS256">
|
||||
```shell
|
||||
openssl rand -base64 32
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="HS384">
|
||||
```shell
|
||||
openssl rand -base64 48
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="HS512">
|
||||
```shell
|
||||
openssl rand -base64 64
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Asymmetric Keys
|
||||
|
||||
With asymmetric keys, your project uses a pair of public and private keys for signing and verifying JWTs. The private key, stored securely in the project's configuration, is used to sign the JWTs. The public key, on the other hand, is made available to clients and is used to verify the JWTs. When a client sends a JWT to the server, the server uses the public key to validate the JWT’s signature. If verification is needed in a different service, the public key can be used without compromising security. Since the public key is only used for verification and the private key for signing, sharing the public key is safe and does not jeopardize the security of your JWTs.
|
||||
|
||||
|
||||
Below you can see an example of an asymmetric key configuration:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="nhost.toml">
|
||||
```toml
|
||||
[[hasura.jwtSecrets]]
|
||||
type = "RS256"
|
||||
kid = "bskhwtelkajsd"
|
||||
key = ""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqSFS8Kx9LuiYpIms+NoZ
|
||||
(ommited for brevity)
|
||||
jwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
""
|
||||
signingKey = ""
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCpIVLwrH0u6Jik
|
||||
(ommited for brevity)
|
||||
s6fJmz3ZeArPI8KFSI3Q2xqm
|
||||
-----END PRIVATE KEY-----
|
||||
""
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="dashboard">
|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
In addition to `RS256`, you can also use `RS384` and `RS512` for extra security. To quickly generate a key pair, you can use the following commands:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="RS256">
|
||||
```shell
|
||||
# Generate a private key
|
||||
openssl genpkey -algorithm RSA -out jwt_private.pem -pkeyopt rsa_keygen_bits:2048
|
||||
|
||||
# Generate a public key from the private key
|
||||
openssl rsa -pubout -in jwt_private.pem -out jwt_public.pem
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="RS384">
|
||||
```shell
|
||||
# Generate a private key
|
||||
openssl genpkey -algorithm RSA -out jwt_private.pem -pkeyopt rsa_keygen_bits:3072
|
||||
|
||||
# Generate a public key from the private key
|
||||
openssl rsa -pubout -in jwt_private.pem -out jwt_public.pem
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="RS512">
|
||||
```shell
|
||||
# Generate a private key
|
||||
openssl genpkey -algorithm RSA -out jwt_private.pem -pkeyopt rsa_keygen_bits:4096
|
||||
|
||||
# Generate a public key from the private key
|
||||
openssl rsa -pubout -in jwt_private.pem -out jwt_public.pem
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
You can then copy the contents of `jwt_private.pem` into the `signingKey` field and the contents of `jwt_public.pem` into the `key` field.
|
||||
|
||||
The `kid` value in your configuration can be any unique string of your choice and must be distinct for each key. It is used to identify the correct key when verifying JWTs through the JWKS endpoint.
|
||||
|
||||
### External Signing
|
||||
|
||||
If you are using a third party service like Auth0 or Clerk you can configure your project to use their JWK endpoint to verify JWTs. Below you can see an example of an external signing configuration:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="nhost.toml">
|
||||
|
||||
```toml
|
||||
[[hasura.jwtSecrets]]
|
||||
jwk_url = "https://mythirdpartyservice.com/jwks.json"
|
||||
```
|
||||
|
||||
Alternatively, you can configure the public key directly:
|
||||
|
||||
```toml
|
||||
[[hasura.jwtSecrets]]
|
||||
type = "RS256"
|
||||
key = ""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqSFS8Kx9LuiYpIms+NoZ
|
||||
(ommited for brevity)
|
||||
jwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
""
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="dashboard">
|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
When using external signing the Auth service will be automatically disabled.
|
||||
</Note>
|
||||
|
||||
## Verify a JWT
|
||||
|
||||
|
||||
### Symmetric Keys
|
||||
|
||||
To verify a JWT signed with a symmetric key in a serverless function or third party service you can use code similar to the following:
|
||||
|
||||
```javascript
|
||||
import { Request, Response } from 'express'
|
||||
import process from 'process'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
const JWT_SECRET = process.env.HASURA_GRAPHQL_JWT_SECRET;
|
||||
|
||||
export default (req: Request, res: Response) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Unauthorized: missing header' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
const verifyToken = new Promise((resolve, reject) => {
|
||||
const verifyOptions = {
|
||||
algorithms: ['HS256', 'HS384', 'HS512'],
|
||||
};
|
||||
|
||||
jwt.verify(token, JWT_SECRET, verifyOptions, (err, decoded) => {
|
||||
if (err) reject(err);
|
||||
else resolve(decoded);
|
||||
});
|
||||
});
|
||||
|
||||
verifyToken
|
||||
.then((decoded) => {
|
||||
res.status(200).json({
|
||||
token: decoded,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(401).json({ error: `Unauthorized: ${err}` });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that you need access to the same key that was used to sign the JWT in order to verify it so this mechanism may not be suitable for all use cases.
|
||||
|
||||
### Asymmetric Keys
|
||||
|
||||
To verify a JWT signed with an asymmetric key you can leverage the JWKS endpoint that is automatically enabled in your project when you configure it to use asymmetric keys. The JWKS endpoint can be found at `https://<subdomain>.auth.<region>.nhost.run/v1/.well-known/jwks.json`. For instance:
|
||||
|
||||
```shell
|
||||
$ curl -s https://local.auth.local.nhost.run/v1/.well-known/jwks.json | jq
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"alg": "RS256",
|
||||
"e": "AQAB",
|
||||
"kid": "bskhwtelkajsd",
|
||||
"kty": "RSA",
|
||||
"n": "qSFS8Kx9LuiYpIms-NoZdSIcIgVp3z531bCSq1shx6ZqsKxHyNAjQ9vcYDBcW1gS1q0NFCDWyDLoNyd_lYUDlsc6zjXZAGyjiT1l_Qe9USHjXhT6Yv8SQlVbj8YCYPhYV9g6Bj922gXOmwXpWToHVYK5bjZmq897doksTErKiny6-FlPJvLVp3cpTFuNy6DKkZkIliuZnmf8EMFOVoFuQtNVlDZZZjk9TK9SP-qN1bvFPTdlCxdkA8ws8IkvhFivgfOflLRlzEE4fECEkaC3tZzGzjhPOmV5T8UC8eNz0Ir87nez8_fVyq61ffPkFftfGOjZ4hUfQqn-YW4sH_VTjw",
|
||||
"use": "sig"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Using the public key from the JWKS endpoint you can verify the JWT in a serverless function using code similar to the following:
|
||||
|
||||
```javascript
|
||||
import { Request, Response } from 'express'
|
||||
import process from 'process'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import jwksClient from 'jwks-rsa'
|
||||
|
||||
const subdomain = process.env.NHOST_SUBDOMAIN;
|
||||
const region = process.env.NHOST_REGION;
|
||||
|
||||
// Initialize the JWKS client
|
||||
const client = jwksClient({
|
||||
jwksUri: `https://${subdomain}.auth.${region}.nhost.run/v1/.well-known/jwks.json`,
|
||||
cache: true,
|
||||
cacheMaxAge: 86400000, // 24 hours cache
|
||||
});
|
||||
|
||||
export default (req: Request, res: Response) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Unauthorized: missing header' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
const verifyToken = new Promise((resolve, reject) => {
|
||||
const verifyOptions = {
|
||||
algorithms: ['RS256', 'RS384', 'RS512'],
|
||||
};
|
||||
|
||||
jwt.verify(token, (header, callback) => {
|
||||
client.getSigningKey(header.kid, (err, key) => {
|
||||
if (err) return callback(err);
|
||||
callback(null, key.getPublicKey());
|
||||
});
|
||||
}, verifyOptions, (err, decoded) => {
|
||||
if (err) reject(err);
|
||||
else resolve(decoded);
|
||||
});
|
||||
});
|
||||
|
||||
verifyToken
|
||||
.then((decoded) => {
|
||||
res.status(200).json({
|
||||
token: decoded,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(401).json({ error: `Unauthorized: ${err}` });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Claims
|
||||
|
||||
You can attach extra information to your JWTs in the form of custom claims. These claims can be used for authorization purposes in your application. For more details on how to add custom claims to your JWTs and how to use them, see the [Permissions Variables](/guides/api/permissions#permission-variables) documentation.
|
||||
93
docs/guides/auth/sign-in-idtokens.mdx
Normal file
93
docs/guides/auth/sign-in-idtokens.mdx
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: Sign In with ID tokens
|
||||
sidebarTitle: IDTokens
|
||||
description: Learn about ID tokens
|
||||
icon: binary
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
ID tokens are tokens provided by identity providers that contain authenticated user information and are specifically designed for authentication purposes, unlike access tokens which are used for authorization. ID tokens include claims about the user's identity, such as user ID, name, and email, along with metadata like token expiration time and intended audience.
|
||||
|
||||
ID tokens serve as a secure proof that a user has already been authenticated by a trusted identity provider. When someone logs in through their device's built-in authentication (like Sign in with Apple on iOS/macOS or Google Sign-in on Android), the system generates an ID token. This token can then be passed to your authentication service, confirming the user's identity without requiring them to log in again. This streamlined approach works with any OpenID Connect (OIDC) provider, including popular services like Google One Tap sign-in, making the authentication process both secure and user-friendly.
|
||||
|
||||
## Usage
|
||||
|
||||
To use ID tokens, you need to configure supported identity providers (currently [apple](/guides/auth/social/sign-in-apple) and [google](/guides/auth/social/sign-in-google)) and make sure the `audience` is set correctly.
|
||||
|
||||
### Sign in
|
||||
|
||||
Once everything is configured you can use an ID token to authenticate users with just a single call:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="javascript">
|
||||
```js
|
||||
nhost.auth.signInIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="react">See [react docs](/reference/react/use-sign-in-id-token) for details</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="vue">See [vue docs](/reference/react/use-sign-in-id-token) for details</Tab>
|
||||
|
||||
<Tab title="dart">
|
||||
```dart
|
||||
nhost.auth.signInIdToken(provider: 'google', idToken: '...', nonce: '...');
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Link Provider to existing user
|
||||
|
||||
Similarly to the [Social Connect](/guides/auth/social-connect) feature, you can link an identity provider to an existing user:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="javascript">
|
||||
```js
|
||||
nhost.auth.linkIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
{' '}
|
||||
<Note>Keep in mind this is an authenticated method so the user must be logged in already.</Note>
|
||||
|
||||
</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="react">See [react docs](/reference/react/use-link-id-token) for details</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="vue">See [vue docs](/reference/vue/use-link-id-token) for details</Tab>
|
||||
|
||||
<Tab title="dart">
|
||||
```dart
|
||||
nhost.auth.linkIdToken(provider: 'google', idToken: '...', nonce: '...');
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Examples
|
||||
|
||||
Below you can find some examples on how to extract an ID Token from various identity providers to be used with the Auth service. Keep in mind these are just some examples, use cases and sources are not limited to the examples below.
|
||||
|
||||
### React Native
|
||||
|
||||
#### Apple
|
||||
|
||||
For an example on how to authenticate using "Sign in with Apple" on iOS using React Native you can refer to our [sample component](https://github.com/nhost/nhost/blob/main/examples/react_native/src/components/SignInWithAppleButton.tsx).
|
||||
|
||||
#### Google
|
||||
|
||||
For an example on how to authenticate using "Sign in with Google" on Android using React Native you can refer to our [sample component](https://github.com/nhost/nhost/blob/main/examples/react_native/src/components/SignInWithGoogleButton.tsx).
|
||||
73
docs/guides/auth/sign-in-otp.mdx
Normal file
73
docs/guides/auth/sign-in-otp.mdx
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Sign In with One-Time Passwords
|
||||
sidebarTitle: One-Time Passwords
|
||||
description: Learn about One-Time Passwords
|
||||
icon: lock-hashtag
|
||||
---
|
||||
|
||||
One-Time Passwords (OTPs) are temporary codes for single use that can be delivered to users via email. OTPs expire after 5 minutes and can only be used once. OTPs can provide a more secure and convenient alternative to regular passwords.
|
||||
|
||||
To use One-Time Passwords, they need to be enabled in the configuration:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="nhost.toml">
|
||||
```toml
|
||||
[auth.method.otp.email]
|
||||
enabled = true
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Dashboard">
|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
After the functionality has been enabled the flow is as follows:
|
||||
|
||||
1. User requests an OTP:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="javascript">
|
||||
```js
|
||||
nhost.auth.signInEmailOTP('user@example.com')
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="react">
|
||||
See [react docs](/reference/react/use-sign-in-email-otp) for details
|
||||
</Tab>
|
||||
|
||||
<Tab title="vue">
|
||||
See [vue docs](/reference/vue/use-sign-in-email-otp) for details
|
||||
</Tab>
|
||||
|
||||
<Tab title="dart">
|
||||
```dart
|
||||
nhost.auth.signInEmailOTP(email: "user@example.com");
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
2. User receives an email with the OTP
|
||||
3. User enters the OTP
|
||||
|
||||
<Tabs>
|
||||
<Tab title="javascript">
|
||||
```js
|
||||
nhost.auth.verifyEmailOTP('user@example.com', '123456')
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="react">
|
||||
See [react docs](/reference/react/use-sign-in-email-otp) for details
|
||||
</Tab>
|
||||
|
||||
<Tab title="vue">
|
||||
See [vue docs](/reference/vue/use-sign-in-email-otp) for details
|
||||
</Tab>
|
||||
|
||||
<Tab title="dart">
|
||||
```dart
|
||||
nhost.auth.verifyEmailOTP(email: "user@example.com", otp: "123456");
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -4,6 +4,78 @@ description: List of available extensions with Nhost Postgres.
|
||||
icon: grid
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
In the table below you can find a list of available extensions with Nhost Postgres:
|
||||
|
||||
| name | version | comment |
|
||||
| -----------------------------|---------|--------------------------------------------------------------------------------------------------------------------- |
|
||||
| address_standardizer | 3.5.0 | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. |
|
||||
| address_standardizer_data_us | 3.5.0 | Address Standardizer US dataset example |
|
||||
| adminpack | 2.1 | administrative functions for PostgreSQL |
|
||||
| amcheck | 1.3 | functions for verifying relation integrity |
|
||||
| autoinc | 1.0 | functions for autoincrementing fields |
|
||||
| bloom | 1.0 | bloom access method - signature file based index |
|
||||
| btree_gin | 1.3 | support for indexing common datatypes in GIN |
|
||||
| btree_gist | 1.6 | support for indexing common datatypes in GiST |
|
||||
| citext | 1.6 | data type for case-insensitive character strings |
|
||||
| cube | 1.5 | data type for multidimensional cubes |
|
||||
| dblink | 1.2 | connect to other PostgreSQL databases from within a database |
|
||||
| dict_int | 1.0 | text search dictionary template for integers |
|
||||
| dict_xsyn | 1.0 | text search dictionary template for extended synonym processing |
|
||||
| earthdistance | 1.1 | calculate great-circle distances on the surface of the Earth |
|
||||
| file_fdw | 1.0 | foreign-data wrapper for flat file access |
|
||||
| fuzzystrmatch | 1.1 | determine similarities and distance between strings |
|
||||
| hstore | 1.8 | data type for storing sets of (key, value) pairs |
|
||||
| http | 1.6 | HTTP client for PostgreSQL, allows web page retrieval inside the database. |
|
||||
| hypopg | 1.4.1 | Hypothetical indexes for PostgreSQL |
|
||||
| insert_username | 1.0 | functions for tracking who changed a table |
|
||||
| intagg | 1.1 | integer aggregator and enumerator (obsolete) |
|
||||
| intarray | 1.5 | functions, operators, and index support for 1-D arrays of integers |
|
||||
| ip4r | 2.4 | |
|
||||
| isn | 1.2 | data types for international product numbering standards |
|
||||
| lo | 1.1 | Large Object maintenance |
|
||||
| ltree | 1.2 | data type for hierarchical tree-like structures |
|
||||
| moddatetime | 1.0 | functions for tracking last modification time |
|
||||
| old_snapshot | 1.0 | utilities in support of old_snapshot_threshold |
|
||||
| pageinspect | 1.9 | inspect the contents of database pages at a low level |
|
||||
| pg_buffercache | 1.3 | examine the shared buffer cache |
|
||||
| pg_cron | 1.6 | Job scheduler for PostgreSQL |
|
||||
| pg_freespacemap | 1.2 | examine the free space map (FSM) |
|
||||
| pg_hashids | 1.3 | pg_hashids |
|
||||
| pg_ivm | 1.9 | incremental view maintenance on PostgreSQL |
|
||||
| pg_prewarm | 1.2 | prewarm relation data |
|
||||
| pg_repack | 1.5.1 | Reorganize tables in PostgreSQL databases with minimal locks |
|
||||
| pg_squeeze | 1.7 | A tool to remove unused space from a relation. |
|
||||
| pg_stat_statements | 1.9 | track planning and execution statistics of all SQL statements executed |
|
||||
| pg_surgery | 1.0 | extension to perform surgery on a damaged relation |
|
||||
| pg_trgm | 1.6 | text similarity measurement and index searching based on trigrams |
|
||||
| pg_visibility | 1.2 | examine the visibility map (VM) and page-level visibility info |
|
||||
| pgcrypto | 1.3 | cryptographic functions |
|
||||
| pgrowlocks | 1.2 | show row-level locking information |
|
||||
| pgstattuple | 1.5 | show tuple-level statistics |
|
||||
| plpgsql | 1.0 | PL/pgSQL procedural language |
|
||||
| postgis | 3.5.0 | PostGIS geometry and geography spatial types and functions |
|
||||
| postgis_raster | 3.5.0 | PostGIS raster types and functions |
|
||||
| postgis_tiger_geocoder | 3.5.0 | PostGIS tiger geocoder and reverse geocoder |
|
||||
| postgis_topology | 3.5.0 | PostGIS topology spatial types and functions |
|
||||
| postgres_fdw | 1.1 | foreign-data wrapper for remote PostgreSQL servers |
|
||||
| refint | 1.0 | functions for implementing referential integrity (obsolete) |
|
||||
| seg | 1.4 | data type for representing line segments or floating-point intervals |
|
||||
| sslinfo | 1.2 | information about SSL certificates |
|
||||
| tablefunc | 1.0 | functions that manipulate whole tables, including crosstab |
|
||||
| tcn | 1.0 | Triggered change notifications |
|
||||
| timescaledb | 2.17.2 | Enables scalable inserts and complex queries for time-series data (Community Edition) |
|
||||
| tsm_system_rows | 1.0 | TABLESAMPLE method which accepts number of rows as a limit |
|
||||
| tsm_system_time | 1.0 | TABLESAMPLE method which accepts time in milliseconds as a limit |
|
||||
| unaccent | 1.1 | text search dictionary that removes accents |
|
||||
| uuid-ossp | 1.1 | generate universally unique identifiers (UUIDs) |
|
||||
| vector | 0.8.0 | vector data type and ivfflat and hnsw access methods |
|
||||
| xml2 | 1.1 | XPath querying and XSLT |
|
||||
|
||||
In addition, you can find more information about some of the extensions below
|
||||
|
||||
|
||||
## hypopg
|
||||
|
||||
HypoPG is a PostgreSQL extension adding support for hypothetical indexes.
|
||||
@@ -205,6 +277,31 @@ DROP EXTENSION pg_ivm;
|
||||
|
||||
- [GitHub](https://github.com/sraoss/pg_ivm)
|
||||
|
||||
## pg_repack
|
||||
|
||||
pg_repack is a PostgreSQL extension which lets you remove bloat from tables and indexes, and optionally restore the physical order of clustered indexes. Unlike CLUSTER and VACUUM FULL it works online, without holding an exclusive lock on the processed tables during processing. pg_repack is efficient to boot, with performance comparable to using CLUSTER directly.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION pg_repack;
|
||||
```
|
||||
In addition, you may need to configure the WAL level and replication slots. Check the official documentation for details.
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```sql SQL
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION pg_repack;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [GitHub](https://github.com/cybertec-postgresql/pg_squeeze)
|
||||
|
||||
## pg_squeeze
|
||||
|
||||
PostgreSQL extension that removes unused space from a table and optionally sorts tuples according to particular index (as if CLUSTER command was executed concurrently with regular reads / writes). In fact we try to replace pg_repack extension.
|
||||
|
||||
@@ -8,7 +8,7 @@ Ensuring a healthy and performant PostgreSQL service is crucial as it directly i
|
||||
|
||||
In case your Postgres service is not meeting your performance expectations, you can explore the following options:
|
||||
|
||||
1. Consider upgrading your [dedicated compute](/platform/compute) resources to provide more processing power and memory to the Postgres server.
|
||||
1. Consider upgrading your [dedicated compute](/platform/compute-resources#dedicated-compute) resources to provide more processing power and memory to the Postgres server.
|
||||
|
||||
2. Fine-tune the configuration parameters of Postgres to optimize its performance. Adjust settings such as `shared_buffers`, `work_mem`, and `effective_cache_size` to better align with your workload and server resources.
|
||||
|
||||
@@ -26,11 +26,11 @@ Before trying anything else, always upgrade to our latest postgres image first.
|
||||
|
||||
## Upgrade to dedicated compute
|
||||
|
||||
Increasing CPU and memory is the simplest way to address performance issues. You can read more about compute resources [here](/platform/compute).
|
||||
Increasing CPU and memory is the simplest way to address performance issues. You can read more about compute resources [here](/platform/compute-resources#dedicated-compute).
|
||||
|
||||
## Fine-tune configuration parameters
|
||||
|
||||
When optimizing your Postgres setup, you can consider adjusting various Postgres settings. You can find a list of these parameters [here](/database/settings). Keep in mind that the optimal values for these parameters will depend on factors such as available resources, workload, and data distribution.
|
||||
When optimizing your Postgres setup, you can consider adjusting various Postgres settings. You can find a list of these parameters [here](/guides/database/configuring-postgres). Keep in mind that the optimal values for these parameters will depend on factors such as available resources, workload, and data distribution.
|
||||
|
||||
To help you get started, you can use [pgtune](https://pgtune.leopard.in.ua) as a reference tool. Pgtune can generate recommended configuration settings based on your system specifications. By providing information about your system, it can suggest parameter values that may be a good starting point for optimization.
|
||||
|
||||
@@ -42,9 +42,9 @@ Monitoring slow queries is a highly effective method for tackling performance is
|
||||
|
||||
### pghero
|
||||
|
||||
[PgHero](https://github.com/ankane/pghero) is one of such tools you can use to idenfity and address slow queries. You can easily run pghero alongside your postgres with [Nhost Run](/run):
|
||||
[PgHero](https://github.com/ankane/pghero) is one of such tools you can use to idenfity and address slow queries. You can easily run pghero alongside your postgres with [Nhost Run](/product/run):
|
||||
|
||||
1. First, make sure the extension [pg_stat_statements](/database/extensions#pg_stat_statements) is enabled.
|
||||
1. First, make sure the extension [pg_stat_statements](/guides/database/extensions#pg-stat-statements) is enabled.
|
||||
|
||||
2. Click on this [one-click install link](https://app.nhost.io:/run-one-click-install?config=eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbeyJuYW1lIjoiREFUQUJBU0VfVVJMIiwidmFsdWUiOiJwb3N0Z3JlczovL3Bvc3RncmVzOltQQVNTV09SRF1AcG9zdGdyZXMtc2VydmljZTo1NDMyL1tTVUJET01BSU5dP3NzbG1vZGU9ZGlzYWJsZSJ9LHsibmFtZSI6IlBHSEVST19VU0VSTkFNRSIsInZhbHVlIjoiW1VTRVJdIn0seyJuYW1lIjoiUEdIRVJPX1BBU1NXT1JEIiwidmFsdWUiOiJbUEFTU1dPUkRdIn1dLCJwb3J0cyI6W3sicG9ydCI6ODA4MCwidHlwZSI6Imh0dHAiLCJwdWJsaXNoIjp0cnVlfV19)
|
||||
|
||||
@@ -78,7 +78,7 @@ There are tools you can use to help analyze your workload and detect missing ind
|
||||
|
||||
### pghero
|
||||
|
||||
[PgHero](https://github.com/ankane/pghero), in addition to help with slow queries, can also help finding missing and duplicate indexes. See previous section on how to deploy pghero with [Nhost Run](/run).
|
||||
[PgHero](https://github.com/ankane/pghero), in addition to help with slow queries, can also help finding missing and duplicate indexes. See previous section on how to deploy pghero with [Nhost Run](/product/run).
|
||||
|
||||
### dexter
|
||||
|
||||
|
||||
BIN
docs/images/guides/auth/jwt/asymmetric.png
Normal file
BIN
docs/images/guides/auth/jwt/asymmetric.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/guides/auth/jwt/external.png
Normal file
BIN
docs/images/guides/auth/jwt/external.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 994 KiB |
BIN
docs/images/guides/auth/jwt/symmetric.png
Normal file
BIN
docs/images/guides/auth/jwt/symmetric.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 984 KiB |
BIN
docs/images/guides/auth/sign-in-otp.png
Normal file
BIN
docs/images/guides/auth/sign-in-otp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1014 KiB |
126
docs/main.go
Normal file
126
docs/main.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const tpl = `---
|
||||
title: "%s"
|
||||
openapi: %s %s
|
||||
---`
|
||||
|
||||
const authReferencePath = "reference/auth"
|
||||
|
||||
type OpenAPIMinimal struct {
|
||||
Paths map[string]map[string]any
|
||||
}
|
||||
|
||||
type Endpoint struct {
|
||||
Method string
|
||||
Path string
|
||||
}
|
||||
|
||||
func (e Endpoint) Filepath() string {
|
||||
return authReferencePath + "/" + e.Method + strings.ReplaceAll(e.Path, "/", "-") + ".mdx"
|
||||
}
|
||||
|
||||
func (e Endpoint) Content() string {
|
||||
return fmt.Sprintf(tpl, e.Path, e.Method, e.Path)
|
||||
}
|
||||
|
||||
func (e Endpoint) Mintlify() string {
|
||||
return ` "` + strings.Replace(e.Filepath(), ".mdx", "", 1) + `",`
|
||||
}
|
||||
|
||||
type Endpoints []Endpoint
|
||||
|
||||
func (e Endpoints) Sort() {
|
||||
slices.SortFunc(e, func(a, b Endpoint) int {
|
||||
if a.Path == b.Path {
|
||||
return strings.Compare(a.Method, b.Method)
|
||||
}
|
||||
return strings.Compare(a.Path, b.Path)
|
||||
})
|
||||
}
|
||||
|
||||
func funcReadOpenAPIFile(filepath string) (*OpenAPIMinimal, error) {
|
||||
b, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
var oam *OpenAPIMinimal
|
||||
if err := yaml.Unmarshal(b, &oam); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
|
||||
}
|
||||
|
||||
return oam, nil
|
||||
}
|
||||
|
||||
func processOAMFiles(oam *OpenAPIMinimal) (Endpoints, error) {
|
||||
endpoints := make(Endpoints, 0, len(oam.Paths)*2)
|
||||
|
||||
for path, methods := range oam.Paths {
|
||||
for method := range methods {
|
||||
e := Endpoint{Method: method, Path: path}
|
||||
endpoints = append(endpoints, e)
|
||||
|
||||
if err := os.WriteFile(e.Filepath(), []byte(e.Content()), 0o644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func process() error {
|
||||
if err := os.RemoveAll(authReferencePath); err != nil {
|
||||
return fmt.Errorf("failed to remove directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(authReferencePath, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
oam, err := funcReadOpenAPIFile("reference/openapi-auth.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read openapi-auth.yaml file: %w", err)
|
||||
}
|
||||
|
||||
endpoints, err := processOAMFiles(oam)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process OAM files: %w", err)
|
||||
}
|
||||
|
||||
oamOld, err := funcReadOpenAPIFile("reference/openapi-auth-old.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read openapi-auth-old.yaml file: %w", err)
|
||||
}
|
||||
|
||||
endpointsOld, err := processOAMFiles(oamOld)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process OAM files: %w", err)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpointsOld...)
|
||||
endpoints.Sort()
|
||||
|
||||
for _, e := range endpoints {
|
||||
fmt.Println(e.Mintlify())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := process(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
145
docs/mint.json
145
docs/mint.json
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Documentation",
|
||||
"openapi": ["reference/openapi-auth.yaml", "reference/openapi-storage.yaml"],
|
||||
"openapi": [
|
||||
"reference/openapi-auth.yaml",
|
||||
"reference/openapi-auth-old.yaml",
|
||||
"reference/openapi-storage.yaml"
|
||||
],
|
||||
"logo": {
|
||||
"dark": "/logo/dark.svg",
|
||||
"light": "/logo/light.svg"
|
||||
@@ -135,6 +139,7 @@
|
||||
"pages": [
|
||||
"guides/auth/overview",
|
||||
"guides/auth/users",
|
||||
"guides/auth/jwt",
|
||||
{
|
||||
"group": "Social Sign In",
|
||||
"icon": "at",
|
||||
@@ -153,9 +158,11 @@
|
||||
},
|
||||
"guides/auth/social-connect",
|
||||
"guides/auth/sign-in-email-password",
|
||||
"guides/auth/sign-in-otp",
|
||||
"guides/auth/sign-in-magic-link",
|
||||
"guides/auth/sign-in-phone-number",
|
||||
"guides/auth/sign-in-webauthn",
|
||||
"guides/auth/sign-in-idtokens",
|
||||
"guides/auth/elevated-permissions",
|
||||
"guides/auth/bot-protection",
|
||||
"guides/auth/email-templates",
|
||||
@@ -176,10 +183,7 @@
|
||||
},
|
||||
{
|
||||
"group": "Functions",
|
||||
"pages": [
|
||||
"guides/functions/overview",
|
||||
"guides/functions/runtimes"
|
||||
]
|
||||
"pages": ["guides/functions/overview", "guides/functions/runtimes"]
|
||||
},
|
||||
{
|
||||
"group": "Run",
|
||||
@@ -213,76 +217,46 @@
|
||||
"group": "Authentication",
|
||||
"icon": "users",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Email and Password",
|
||||
"icon": "envelope",
|
||||
"pages": [
|
||||
"reference/auth/sign-up-email-and-password",
|
||||
"reference/auth/sign-in-email-and-password"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Passwordless",
|
||||
"icon": "message-sms",
|
||||
"pages": [
|
||||
"reference/auth/sign-in-email-passwordless",
|
||||
"reference/auth/sign-in-sms-passwordless",
|
||||
"reference/auth/sign-in-sms-passwordless-otp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "OAuth",
|
||||
"icon": "at",
|
||||
"pages": [
|
||||
"reference/auth/sign-in-oauth-provider",
|
||||
"reference/auth/oauth-callback-url-that-will-be-used-by-the-oauth-provider-to-redirect-to-the-client-application-attention:-all-providers-are-using-a-get-operation-except-apple-and-azure-ad-that-use-post"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "WebAuthn",
|
||||
"icon": "atom",
|
||||
"pages": [
|
||||
"reference/auth/sign-up-using-email-via-fido2-webauthn-authentication",
|
||||
"reference/auth/verfiy-fido2-webauthn-authentication-and-complete-signup",
|
||||
"reference/auth/sign-in-using-email-via-fido2-webauthn-authentication",
|
||||
"reference/auth/verfiy-fido2-webauthn-authentication-using-public-key-cryptography",
|
||||
"reference/auth/elevate-webauthn",
|
||||
"reference/auth/elevate-webauthn-verify"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Anonymous",
|
||||
"icon": "luchador-mask",
|
||||
"pages": [
|
||||
"reference/auth/sign-in-anonymous",
|
||||
"reference/auth/deanonymize-an-anonymous-user-in-adding-missing-email-or-email+password-depending-on-the-chosen-authentication-method-will-send-a-confirmation-email-if-the-server-is-configured-to-do-so"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "MFA",
|
||||
"icon": "message-sms",
|
||||
"pages": [
|
||||
"reference/auth/generate-a-secret-to-request-the-activation-of-time-based-one-time-password-totp-multi-factor-authentication",
|
||||
"reference/auth/sign-in-totp",
|
||||
"reference/auth/activatedeactivate-multi-factor-authentication"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "User",
|
||||
"icon": "user",
|
||||
"pages": [
|
||||
"reference/auth/change-the-current-users-email",
|
||||
"reference/auth/send-an-email-asking-the-user-to-reset-their-password",
|
||||
"reference/auth/send-an-email-to-verify-the-account",
|
||||
"reference/auth/set-a-new-password",
|
||||
"reference/auth/get-user-information",
|
||||
"reference/auth/refresh-the-oauth-access-tokens-of-a-given-user-you-must-be-an-admin-to-perform-this-operation",
|
||||
"reference/auth/initialize-adding-of-a-new-webauthn-security-key-device-browser",
|
||||
"reference/auth/verfiy-adding-of-a-new-webauth-security-key-device-browser",
|
||||
"reference/auth/create-personal-access-token-pat"
|
||||
]
|
||||
},
|
||||
"reference/auth/sign-out"
|
||||
"reference/auth/get-.well-known-jwks.json",
|
||||
"reference/auth/post-elevate-webauthn",
|
||||
"reference/auth/post-elevate-webauthn-verify",
|
||||
"reference/auth/get-healthz",
|
||||
"reference/auth/head-healthz",
|
||||
"reference/auth/post-link-idtoken",
|
||||
"reference/auth/get-mfa-totp-generate",
|
||||
"reference/auth/post-pat",
|
||||
"reference/auth/post-signin-anonymous",
|
||||
"reference/auth/post-signin-email-password",
|
||||
"reference/auth/post-signin-idtoken",
|
||||
"reference/auth/post-signin-mfa-totp",
|
||||
"reference/auth/post-signin-otp-email",
|
||||
"reference/auth/post-signin-otp-email-verify",
|
||||
"reference/auth/post-signin-passwordless-email",
|
||||
"reference/auth/post-signin-passwordless-sms",
|
||||
"reference/auth/post-signin-passwordless-sms-otp",
|
||||
"reference/auth/post-signin-pat",
|
||||
"reference/auth/get-signin-provider-{provider}",
|
||||
"reference/auth/get-signin-provider-{provider}-callback",
|
||||
"reference/auth/post-signin-webauthn",
|
||||
"reference/auth/post-signin-webauthn-verify",
|
||||
"reference/auth/post-signout",
|
||||
"reference/auth/post-signup-email-password",
|
||||
"reference/auth/post-signup-webauthn",
|
||||
"reference/auth/post-signup-webauthn-verify",
|
||||
"reference/auth/post-token",
|
||||
"reference/auth/post-token-verify",
|
||||
"reference/auth/get-user",
|
||||
"reference/auth/post-user-deanonymize",
|
||||
"reference/auth/post-user-email-change",
|
||||
"reference/auth/post-user-email-send-verification-email",
|
||||
"reference/auth/post-user-mfa",
|
||||
"reference/auth/post-user-password",
|
||||
"reference/auth/post-user-password-reset",
|
||||
"reference/auth/post-user-provider-tokens",
|
||||
"reference/auth/post-user-webauthn-add",
|
||||
"reference/auth/post-user-webauthn-verify",
|
||||
"reference/auth/get-verify",
|
||||
"reference/auth/get-version"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -376,7 +350,11 @@
|
||||
"reference/javascript/auth/sign-up",
|
||||
"reference/javascript/auth/add-security-key",
|
||||
"reference/javascript/auth/elevate-email-security-key",
|
||||
"reference/javascript/auth/connect-provider"
|
||||
"reference/javascript/auth/connect-provider",
|
||||
"reference/javascript/auth/sign-in-email-otp",
|
||||
"reference/javascript/auth/verify-email-otp",
|
||||
"reference/javascript/auth/sign-in-id-token",
|
||||
"reference/javascript/auth/link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -459,7 +437,10 @@
|
||||
"reference/react/use-user-id",
|
||||
"reference/react/use-user-is-anonymous",
|
||||
"reference/react/use-user-locale",
|
||||
"reference/react/use-user-roles"
|
||||
"reference/react/use-user-roles",
|
||||
"reference/react/use-sign-in-email-otp",
|
||||
"reference/react/use-sign-in-id-token",
|
||||
"reference/react/use-link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -506,7 +487,10 @@
|
||||
"reference/nextjs/use-user-id",
|
||||
"reference/nextjs/use-user-is-anonymous",
|
||||
"reference/nextjs/use-user-locale",
|
||||
"reference/nextjs/use-user-roles"
|
||||
"reference/nextjs/use-user-roles",
|
||||
"reference/nextjs/use-sign-in-email-otp",
|
||||
"reference/nextjs/use-sign-in-id-token",
|
||||
"reference/nextjs/use-link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -548,7 +532,10 @@
|
||||
"reference/vue/use-add-security-key",
|
||||
"reference/vue/use-elevate-security-key-email",
|
||||
"reference/vue/use-sign-in-email-security-key",
|
||||
"reference/vue/use-sign-up-email-security-key"
|
||||
"reference/vue/use-sign-up-email-security-key",
|
||||
"reference/vue/use-sign-in-email-otp",
|
||||
"reference/vue/use-sign-in-id-token",
|
||||
"reference/vue/use-link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "2.21.0",
|
||||
"version": "2.23.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "mintlify dev"
|
||||
|
||||
@@ -13,13 +13,17 @@ Combined with a powerful **Permission Rules** system, Nhost Auth offers everythi
|
||||
<CardGroup cols={4}>
|
||||
<Card title="Email and Password" icon="square-1" href="../guides/auth/sign-in-email-password">
|
||||
</Card>
|
||||
<Card title="Magic Link" icon="square-2" href="../guides/auth/sign-in-magic-link">
|
||||
<Card title="One-Time Passwords (OTP)" icon="square-2" href="../guides/auth/sign-in-otp">
|
||||
</Card>
|
||||
<Card title="Phone Number (SMS)" icon="square-3" href="../guides/auth/sign-in-phone-number">
|
||||
<Card title="Magic Link" icon="square-3" href="../guides/auth/sign-in-magic-link">
|
||||
</Card>
|
||||
<Card title="Security Keys (WebAuthn)" icon="square-4" href="../guides/auth/sign-in-webauthn">
|
||||
<Card title="Phone Number (SMS)" icon="square-4" href="../guides/auth/sign-in-phone-number">
|
||||
</Card>
|
||||
<Card title="Elevated Permissions" icon="square-5" href="../guides/auth/elevated-permissions">
|
||||
<Card title="Security Keys (WebAuthn)" icon="square-5" href="../guides/auth/sign-in-webauthn">
|
||||
</Card>
|
||||
<Card title="ID Tokens" icon="square-6" href="../guides/auth/sign-in-idtokens">
|
||||
</Card>
|
||||
<Card title="Elevated Permissions" icon="square-7" href="../guides/auth/elevated-permissions">
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
4
docs/reference/auth/get-.well-known-jwks.json.mdx
Normal file
4
docs/reference/auth/get-.well-known-jwks.json.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "/.well-known/jwks.json"
|
||||
openapi: get /.well-known/jwks.json
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/healthz"
|
||||
openapi: get /healthz
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/mfa/totp/generate"
|
||||
openapi: get /mfa/totp/generate
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/provider/{provider}/callback"
|
||||
openapi: get /signin/provider/{provider}/callback
|
||||
sidebarTitle: Sign In Callback
|
||||
---
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/provider/{provider}"
|
||||
openapi: get /signin/provider/{provider}
|
||||
sidebarTitle: Sign In
|
||||
---
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/user"
|
||||
openapi: get /user
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/verify"
|
||||
openapi: get /verify
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/version"
|
||||
openapi: get /version
|
||||
---
|
||||
4
docs/reference/auth/head-healthz.mdx
Normal file
4
docs/reference/auth/head-healthz.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "/healthz"
|
||||
openapi: head /healthz
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/elevate/webauthn/verify"
|
||||
openapi: post /elevate/webauthn/verify
|
||||
sidebarTitle: Elevate Verify
|
||||
---
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/elevate/webauthn"
|
||||
openapi: post /elevate/webauthn
|
||||
sidebarTitle: Elevate
|
||||
---
|
||||
---
|
||||
4
docs/reference/auth/post-link-idtoken.mdx
Normal file
4
docs/reference/auth/post-link-idtoken.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "/link/idtoken"
|
||||
openapi: post /link/idtoken
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/pat"
|
||||
openapi: post /pat
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/signin/anonymous"
|
||||
openapi: post /signin/anonymous
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/email-password"
|
||||
openapi: post /signin/email-password
|
||||
sidebarTitle: 'Sign In'
|
||||
---
|
||||
---
|
||||
4
docs/reference/auth/post-signin-idtoken.mdx
Normal file
4
docs/reference/auth/post-signin-idtoken.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "/signin/idtoken"
|
||||
openapi: post /signin/idtoken
|
||||
---
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: "/signin/mfa/totp"
|
||||
openapi: post /signin/mfa/totp
|
||||
---
|
||||
4
docs/reference/auth/post-signin-otp-email-verify.mdx
Normal file
4
docs/reference/auth/post-signin-otp-email-verify.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "/signin/otp/email/verify"
|
||||
openapi: post /signin/otp/email/verify
|
||||
---
|
||||
4
docs/reference/auth/post-signin-otp-email.mdx
Normal file
4
docs/reference/auth/post-signin-otp-email.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "/signin/otp/email"
|
||||
openapi: post /signin/otp/email
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/passwordless/email"
|
||||
openapi: post /signin/passwordless/email
|
||||
sidebarTitle: Sign In Email
|
||||
---
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/passwordless/sms/otp"
|
||||
openapi: post /signin/passwordless/sms/otp
|
||||
sidebarTitle: Sign In SMS Verify OTP
|
||||
---
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/passwordless/sms"
|
||||
openapi: post /signin/passwordless/sms
|
||||
sidebarTitle: Sign In SMS
|
||||
---
|
||||
---
|
||||
4
docs/reference/auth/post-signin-pat.mdx
Normal file
4
docs/reference/auth/post-signin-pat.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "/signin/pat"
|
||||
openapi: post /signin/pat
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/webauthn/verify"
|
||||
openapi: post /signin/webauthn/verify
|
||||
sidebarTitle: Sign In Verify
|
||||
---
|
||||
---
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "/signin/webauthn"
|
||||
openapi: post /signin/webauthn
|
||||
sidebarTitle: 'Sign In'
|
||||
---
|
||||
---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user