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,63 @@
import { notFound, redirect } from "next/navigation";
import { z } from "zod";
import { messageSchema, partSchema } from "@turbostarter/ai/chat/schema";
import { toChatMessage } from "@turbostarter/ai/chat/utils";
import { handle } from "@turbostarter/api/utils";
import { pathsConfig } from "~/config/paths";
import { api } from "~/lib/api/server";
import { getSession } from "~/lib/auth/server";
import { getMetadata } from "~/lib/metadata";
import { ViewChat } from "~/modules/chat/layout/view";
export const generateMetadata = async ({
params,
}: {
params: Promise<{ id: string; locale: string }>;
}) => {
const id = (await params).id;
const data = await handle(api.ai.chat.chats[":id"].$get, { throwOnError: false })({
param: { id },
});
return getMetadata({
...(data?.name && { title: data.name }),
})({ params });
};
export default async function Chat({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { user } = await getSession();
if (!user) {
return redirect(pathsConfig.auth.login);
}
const id = (await params).id;
const data = await handle(api.ai.chat.chats[":id"].$get, { throwOnError: false })({
param: { id },
});
if (!data) {
return notFound();
}
const messages = await handle(api.ai.chat.chats[":id"].messages.$get, {
throwOnError: false,
schema: z.array(
messageSchema.extend({
parts: z.array(partSchema),
}),
),
})({
param: { id },
});
const initialMessages = (messages ?? []).map(toChatMessage);
return <ViewChat id={id} initialMessages={initialMessages} />;
}

View File

@@ -0,0 +1,30 @@
import { getMetadata } from "~/lib/metadata";
import { ChatHistory } from "~/modules/chat/history";
import { Header } from "~/modules/common/layout/header";
import { ThemeSwitcher } from "~/modules/common/theme";
export const generateMetadata = getMetadata({
title: "ai:chat.title",
description: "ai:chat.description",
});
export default function ChatLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Header>
<div className="flex items-center gap-1">
<ChatHistory />
<ThemeSwitcher />
</div>
</Header>
<div className="@container relative flex h-full flex-col items-center contain-layout">
{children}
</div>
</>
);
}

View File

@@ -0,0 +1,23 @@
"use client";
import { useMemo } from "react";
import { generateId } from "@turbostarter/shared/utils";
import { useComposer } from "~/modules/chat/composer/hooks/use-composer";
import { NewChat } from "~/modules/chat/layout/new";
import { ViewChat } from "~/modules/chat/layout/view";
export default function Chat() {
const id = useMemo(() => generateId(), []);
const { messages } = useComposer({
id,
});
if (messages.length) {
return <ViewChat id={id} />;
}
return <NewChat id={id} />;
}