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:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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