feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
69
apps/web/src/app/[locale]/dashboard/(user)/layout.tsx
Normal file
69
apps/web/src/app/[locale]/dashboard/(user)/layout.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
import { SidebarProvider } from "@turbostarter/ui-web/sidebar";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import { getSession } from "~/lib/auth/server";
|
||||
import { DashboardInset } from "~/modules/common/layout/dashboard/inset";
|
||||
import { DashboardSidebar } from "~/modules/common/layout/dashboard/sidebar";
|
||||
|
||||
/**
|
||||
* Dashboard sidebar menu configuration.
|
||||
*/
|
||||
const menu = [
|
||||
{
|
||||
label: "platform",
|
||||
items: [
|
||||
{
|
||||
title: "dashboard",
|
||||
href: pathsConfig.dashboard.user.index,
|
||||
icon: Icons.Home,
|
||||
},
|
||||
{
|
||||
title: "aiTools",
|
||||
href: pathsConfig.apps.chat.index,
|
||||
icon: Icons.Sparkles,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "manage",
|
||||
items: [
|
||||
{
|
||||
title: "settings",
|
||||
href: pathsConfig.dashboard.user.settings.index,
|
||||
icon: Icons.Settings,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "dev",
|
||||
items: [
|
||||
{
|
||||
title: "demos",
|
||||
href: pathsConfig.demo.index,
|
||||
icon: Icons.LayoutDashboard,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { user } = await getSession();
|
||||
|
||||
if (!user) {
|
||||
return redirect(pathsConfig.auth.login);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<DashboardSidebar user={user} menu={menu} />
|
||||
<DashboardInset>{children}</DashboardInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
67
apps/web/src/app/[locale]/dashboard/(user)/page.tsx
Normal file
67
apps/web/src/app/[locale]/dashboard/(user)/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@turbostarter/ui-web/card";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
/**
|
||||
* Dashboard Home Page
|
||||
*
|
||||
* Welcome page for authenticated users.
|
||||
*/
|
||||
export default function DashboardPage() {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
return (
|
||||
<div className="@container h-full p-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{t("welcome.title", { defaultValue: "Welcome to your Dashboard" })}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t("welcome.description", { defaultValue: "Get started by exploring the features below." })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("features.aiChat.title", { defaultValue: "AI Chat" })}</CardTitle>
|
||||
<Icons.MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("features.aiChat.description", { defaultValue: "Have a conversation with AI assistants" })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("features.imageGeneration.title", { defaultValue: "Image Generation" })}</CardTitle>
|
||||
<Icons.Image className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("features.imageGeneration.description", { defaultValue: "Create images with AI" })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("features.pdfAnalysis.title", { defaultValue: "PDF Analysis" })}</CardTitle>
|
||||
<Icons.FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("features.pdfAnalysis.description", { defaultValue: "Upload and analyze PDF documents" })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import { getSession } from "~/lib/auth/server";
|
||||
import { getMetadata } from "~/lib/metadata";
|
||||
import { ManagePlan } from "~/modules/user/settings/billing/manage-plan";
|
||||
import { PlanSummary } from "~/modules/user/settings/billing/plan-summary";
|
||||
|
||||
export const generateMetadata = getMetadata({
|
||||
title: "billing",
|
||||
description: "billing:manage.description",
|
||||
});
|
||||
|
||||
export default async function BillingPage() {
|
||||
const { user } = await getSession();
|
||||
|
||||
if (!user) {
|
||||
return redirect(pathsConfig.auth.login);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<PlanSummary />
|
||||
<ManagePlan />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import {
|
||||
DashboardHeader,
|
||||
DashboardHeaderDescription,
|
||||
DashboardHeaderTitle,
|
||||
} from "~/modules/common/layout/dashboard/header";
|
||||
import { SettingsNav } from "~/modules/user/settings/layout/nav";
|
||||
|
||||
const LINKS = [
|
||||
{
|
||||
label: "general",
|
||||
href: pathsConfig.dashboard.user.settings.index,
|
||||
},
|
||||
{
|
||||
label: "security",
|
||||
href: pathsConfig.dashboard.user.settings.security,
|
||||
},
|
||||
{
|
||||
label: "billing",
|
||||
href: pathsConfig.dashboard.user.settings.billing,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export default async function SettingsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { t } = await getTranslation({ ns: ["common", "auth"] });
|
||||
|
||||
return (
|
||||
<div className="@container h-full overflow-auto p-6">
|
||||
<DashboardHeader>
|
||||
<div>
|
||||
<DashboardHeaderTitle>
|
||||
{t("account.settings.header.title")}
|
||||
</DashboardHeaderTitle>
|
||||
<DashboardHeaderDescription>
|
||||
{t("account.settings.header.description")}
|
||||
</DashboardHeaderDescription>
|
||||
</div>
|
||||
|
||||
<div className="lg:hidden">
|
||||
<SettingsNav
|
||||
links={LINKS.map((link) => ({
|
||||
...link,
|
||||
label: t(link.label),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</DashboardHeader>
|
||||
<div className="flex w-full gap-3">
|
||||
<div className="hidden w-96 lg:block">
|
||||
<div className="sticky top-[calc(var(--banner-height)+theme(spacing.6))]">
|
||||
<SettingsNav
|
||||
links={LINKS.map((link) => ({
|
||||
...link,
|
||||
label: t(link.label),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
apps/web/src/app/[locale]/dashboard/(user)/settings/page.tsx
Normal file
33
apps/web/src/app/[locale]/dashboard/(user)/settings/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import { getSession } from "~/lib/auth/server";
|
||||
import { getMetadata } from "~/lib/metadata";
|
||||
import { DeleteAccount } from "~/modules/user/settings/general/delete-account";
|
||||
import { EditAvatar } from "~/modules/user/settings/general/edit-avatar";
|
||||
import { EditEmail } from "~/modules/user/settings/general/edit-email";
|
||||
import { EditName } from "~/modules/user/settings/general/edit-name";
|
||||
import { LanguageSwitcher } from "~/modules/user/settings/general/language-switcher";
|
||||
|
||||
export const generateMetadata = getMetadata({
|
||||
title: "auth:account.settings.title",
|
||||
description: "auth:account.settings.header.description",
|
||||
});
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const { user } = await getSession();
|
||||
|
||||
if (!user) {
|
||||
return redirect(pathsConfig.auth.login);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<EditAvatar user={user} />
|
||||
<LanguageSwitcher />
|
||||
<EditName user={user} />
|
||||
<EditEmail user={user} />
|
||||
<DeleteAccount />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { authConfig } from "~/config/auth";
|
||||
import { getMetadata } from "~/lib/metadata";
|
||||
import { Accounts } from "~/modules/user/settings/security/accounts";
|
||||
import { EditPassword } from "~/modules/user/settings/security/edit-password";
|
||||
import { Passkeys } from "~/modules/user/settings/security/passkeys";
|
||||
import { Sessions } from "~/modules/user/settings/security/sessions";
|
||||
import { TwoFactorAuthentication } from "~/modules/user/settings/security/two-factor/two-factor";
|
||||
|
||||
export const generateMetadata = getMetadata({
|
||||
title: "auth:account.settings.security.title",
|
||||
description: "auth:account.settings.security.description",
|
||||
});
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<EditPassword />
|
||||
<Accounts />
|
||||
{authConfig.providers.passkey && <Passkeys />}
|
||||
<TwoFactorAuthentication />
|
||||
<Sessions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
import { SidebarProvider } from "@turbostarter/ui-web/sidebar";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import { getOrganization, getSession } from "~/lib/auth/server";
|
||||
import { getQueryClient } from "~/lib/query/server";
|
||||
import { DashboardInset } from "~/modules/common/layout/dashboard/inset";
|
||||
import { DashboardSidebar } from "~/modules/common/layout/dashboard/sidebar";
|
||||
import { organization } from "~/modules/organization/lib/api";
|
||||
|
||||
const menu = (organization: string) => [
|
||||
{
|
||||
label: "platform",
|
||||
items: [
|
||||
{
|
||||
title: "home",
|
||||
href: pathsConfig.dashboard.organization(organization).index,
|
||||
icon: Icons.Home,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "organization",
|
||||
items: [
|
||||
{
|
||||
title: "settings",
|
||||
href: pathsConfig.dashboard.organization(organization).settings.index,
|
||||
icon: Icons.Settings,
|
||||
},
|
||||
{
|
||||
title: "members",
|
||||
href: pathsConfig.dashboard.organization(organization).members,
|
||||
icon: Icons.UsersRound,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{
|
||||
organization: string;
|
||||
}>;
|
||||
}) {
|
||||
const { user } = await getSession();
|
||||
|
||||
if (!user) {
|
||||
return redirect(pathsConfig.auth.login);
|
||||
}
|
||||
|
||||
const organizationSlug = (await params).organization;
|
||||
const activeOrganization = await getOrganization({ slug: organizationSlug });
|
||||
|
||||
if (!activeOrganization) {
|
||||
return redirect(pathsConfig.dashboard.user.index);
|
||||
}
|
||||
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.setQueryData(
|
||||
organization.queries.get({ slug: organizationSlug }).queryKey,
|
||||
activeOrganization,
|
||||
);
|
||||
queryClient.setQueryData(
|
||||
organization.queries.get({ id: activeOrganization.id }).queryKey,
|
||||
activeOrganization,
|
||||
);
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<SidebarProvider>
|
||||
<DashboardSidebar user={user} menu={menu(organizationSlug)} />
|
||||
<DashboardInset>{children}</DashboardInset>
|
||||
</SidebarProvider>
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@turbostarter/ui-web/tabs";
|
||||
|
||||
import { getOrganization } from "~/lib/auth/server";
|
||||
import { getMetadata } from "~/lib/metadata";
|
||||
import {
|
||||
DashboardHeader,
|
||||
DashboardHeaderDescription,
|
||||
DashboardHeaderTitle,
|
||||
} from "~/modules/common/layout/dashboard/header";
|
||||
import { InvitationsDataTable } from "~/modules/organization/invitations/data-table/invitations-data-table";
|
||||
import { MembersDataTable } from "~/modules/organization/members/data-table/members-data-table";
|
||||
import { InviteMember } from "~/modules/organization/members/invite-member";
|
||||
|
||||
export const generateMetadata = getMetadata({
|
||||
title: "organization:members.title",
|
||||
description: "organization:members.header.description",
|
||||
});
|
||||
|
||||
export default async function MembersPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ organization: string }>;
|
||||
}) {
|
||||
const { organization } = await params;
|
||||
|
||||
const activeOrganization = await getOrganization({ slug: organization });
|
||||
|
||||
if (!activeOrganization) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { t } = await getTranslation({ ns: "organization" });
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader>
|
||||
<div>
|
||||
<DashboardHeaderTitle>
|
||||
{t("members.header.title")}
|
||||
</DashboardHeaderTitle>
|
||||
<DashboardHeaderDescription>
|
||||
{t("members.header.description")}
|
||||
</DashboardHeaderDescription>
|
||||
</div>
|
||||
</DashboardHeader>
|
||||
|
||||
<InviteMember organizationId={activeOrganization.id} />
|
||||
|
||||
<Tabs defaultValue="members" className="mt-4 w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="members">{t("members.title")}</TabsTrigger>
|
||||
<TabsTrigger value="invitations">
|
||||
{t("invitations.title")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="members">
|
||||
<MembersDataTable organizationId={activeOrganization.id} />
|
||||
</TabsContent>
|
||||
<TabsContent value="invitations">
|
||||
<InvitationsDataTable organizationId={activeOrganization.id} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
53
apps/web/src/app/[locale]/dashboard/[organization]/page.tsx
Normal file
53
apps/web/src/app/[locale]/dashboard/[organization]/page.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
|
||||
import { getMetadata } from "~/lib/metadata";
|
||||
import {
|
||||
DashboardHeader,
|
||||
DashboardHeaderTitle,
|
||||
DashboardHeaderDescription,
|
||||
} from "~/modules/common/layout/dashboard/header";
|
||||
import { AreaChart } from "~/modules/organization/home/charts/area";
|
||||
import { BarChart } from "~/modules/organization/home/charts/bar";
|
||||
import { LineChart } from "~/modules/organization/home/charts/line";
|
||||
import { PieChart } from "~/modules/organization/home/charts/pie";
|
||||
import { RadarChart } from "~/modules/organization/home/charts/radar";
|
||||
import { RadialChart } from "~/modules/organization/home/charts/radial";
|
||||
import { ShapeChart } from "~/modules/organization/home/charts/shape";
|
||||
|
||||
export const generateMetadata = getMetadata({
|
||||
title: "common:home",
|
||||
description: "dashboard:organization.home.description",
|
||||
});
|
||||
|
||||
export default async function OrganizationPage() {
|
||||
const { t } = await getTranslation({ ns: "dashboard" });
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader>
|
||||
<div>
|
||||
<DashboardHeaderTitle>
|
||||
{t("organization.home.title")}
|
||||
</DashboardHeaderTitle>
|
||||
<DashboardHeaderDescription>
|
||||
{t("organization.home.description")}
|
||||
</DashboardHeaderDescription>
|
||||
</div>
|
||||
</DashboardHeader>
|
||||
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<BarChart />
|
||||
<PieChart />
|
||||
<ShapeChart />
|
||||
</div>
|
||||
<AreaChart />
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<RadialChart />
|
||||
<RadarChart />
|
||||
</div>
|
||||
<LineChart />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import {
|
||||
DashboardHeader,
|
||||
DashboardHeaderDescription,
|
||||
DashboardHeaderTitle,
|
||||
} from "~/modules/common/layout/dashboard/header";
|
||||
import { SettingsNav } from "~/modules/user/settings/layout/nav";
|
||||
|
||||
const LINKS = (organization: string) =>
|
||||
[
|
||||
{
|
||||
label: "general",
|
||||
href: pathsConfig.dashboard.organization(organization).settings.index,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export default async function SettingsLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{
|
||||
organization: string;
|
||||
}>;
|
||||
}) {
|
||||
const { t } = await getTranslation({ ns: ["common", "organization"] });
|
||||
|
||||
const organization = (await params).organization;
|
||||
|
||||
return (
|
||||
<div className="@container h-full overflow-auto p-6">
|
||||
<DashboardHeader>
|
||||
<div>
|
||||
<DashboardHeaderTitle>
|
||||
{t("settings.header.title")}
|
||||
</DashboardHeaderTitle>
|
||||
<DashboardHeaderDescription>
|
||||
{t("settings.header.description")}
|
||||
</DashboardHeaderDescription>
|
||||
</div>
|
||||
|
||||
<div className="lg:hidden">
|
||||
<SettingsNav
|
||||
links={LINKS(organization).map((link) => ({
|
||||
...link,
|
||||
label: t(link.label),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</DashboardHeader>
|
||||
<div className="flex w-full gap-3">
|
||||
<div className="hidden w-96 lg:block">
|
||||
<div className="sticky top-[calc(var(--banner-height)+theme(spacing.6))]">
|
||||
<SettingsNav
|
||||
links={LINKS(organization).map((link) => ({
|
||||
...link,
|
||||
label: t(link.label),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getOrganization } from "~/lib/auth/server";
|
||||
import { getMetadata } from "~/lib/metadata";
|
||||
import { DeleteOrganization } from "~/modules/organization/settings/delete-organization";
|
||||
import { EditLogo } from "~/modules/organization/settings/edit-logo";
|
||||
import { EditName } from "~/modules/organization/settings/edit-name";
|
||||
import { LeaveOrganization } from "~/modules/organization/settings/leave-organization";
|
||||
|
||||
export const generateMetadata = getMetadata({
|
||||
title: "organization:settings.title",
|
||||
description: "organization:settings.header.description",
|
||||
});
|
||||
|
||||
export default async function SettingsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ organization: string }>;
|
||||
}) {
|
||||
const { organization } = await params;
|
||||
|
||||
const activeOrganization = await getOrganization({ slug: organization });
|
||||
|
||||
if (!activeOrganization) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<EditLogo organizationId={activeOrganization.id} />
|
||||
<EditName organizationId={activeOrganization.id} />
|
||||
<LeaveOrganization organizationId={activeOrganization.id} />
|
||||
<DeleteOrganization organizationId={activeOrganization.id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
apps/web/src/app/[locale]/dashboard/layout.tsx
Normal file
34
apps/web/src/app/[locale]/dashboard/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { handle } from "@turbostarter/api/utils";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import { api } from "~/lib/api/server";
|
||||
import { getSession } from "~/lib/auth/server";
|
||||
import { getQueryClient } from "~/lib/query/server";
|
||||
import { billing } from "~/modules/billing/lib/api";
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { user } = await getSession();
|
||||
|
||||
if (!user) {
|
||||
return redirect(pathsConfig.auth.login);
|
||||
}
|
||||
|
||||
const queryClient = getQueryClient();
|
||||
await queryClient.prefetchQuery({
|
||||
...billing.queries.customer.get,
|
||||
queryFn: handle(api.billing.customer.$get),
|
||||
});
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
{children}
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user