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,27 @@
"use server";
import env from "env.config";
import { EmailTemplate } from "@turbostarter/email";
import { sendEmail } from "@turbostarter/email/server";
import { getTranslation } from "@turbostarter/i18n/server";
import type { ContactFormPayload } from "../utils/schema";
export const sendContactForm = async (data: ContactFormPayload) => {
try {
await sendEmail({
to: env.CONTACT_EMAIL,
template: EmailTemplate.CONTACT_FORM,
variables: data,
});
return { error: null };
} catch (e) {
if (e instanceof Error) {
return { error: e.message };
}
const { t } = await getTranslation({ ns: "common" });
return { error: t("error.general") };
}
};

View File

@@ -0,0 +1,136 @@
"use client";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { useTranslation } from "@turbostarter/i18n";
import { Button } from "@turbostarter/ui-web/button";
import { Card, CardContent } from "@turbostarter/ui-web/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@turbostarter/ui-web/form";
import { Icons } from "@turbostarter/ui-web/icons";
import { Input } from "@turbostarter/ui-web/input";
import { Textarea } from "@turbostarter/ui-web/textarea";
import { sendContactForm } from "./actions";
import { contactFormSchema, MAX_MESSAGE_LENGTH } from "./utils/schema";
import type { ContactFormPayload } from "./utils/schema";
export function ContactForm() {
const { t } = useTranslation(["common", "marketing"]);
const form = useForm({
resolver: standardSchemaResolver(contactFormSchema),
defaultValues: {
name: "",
email: "",
message: "",
},
});
const message = form.watch("message");
const onSubmit = async (data: ContactFormPayload) => {
const { error } = await sendContactForm(data);
if (error) {
return toast.error(error);
}
toast.success(t("contact.form.success.title"), {
description: t("contact.form.success.description"),
});
form.reset();
};
return (
<Card className="w-full max-w-lg border-none bg-transparent shadow-none">
<CardContent className="w-full p-0">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input
placeholder={t("contact.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("email")}</FormLabel>
<FormControl>
<Input
placeholder={t("contact.form.email.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel className="mt-2 flex justify-between">
<span>{t("message")}</span>
<span className="text-muted-foreground text-xs">
{message.length}/{MAX_MESSAGE_LENGTH}
</span>
</FormLabel>
<FormControl>
<Textarea
placeholder={t("contact.form.message.placeholder")}
className="min-h-[120px]"
maxLength={MAX_MESSAGE_LENGTH}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="mt-2"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? (
<Icons.Loader2 className="size-5 animate-spin" />
) : (
t("contact.form.submit")
)}
</Button>
</form>
</Form>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,11 @@
import * as z from "zod";
export const MAX_MESSAGE_LENGTH = 4000;
export const contactFormSchema = z.object({
name: z.string().min(2),
email: z.email().max(254),
message: z.string().min(10).max(MAX_MESSAGE_LENGTH),
});
export type ContactFormPayload = z.infer<typeof contactFormSchema>;