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,194 @@
"use client";
import dayjs from "dayjs";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { useEffect, useState } from "react";
import {
I18nextProvider,
initReactI18next,
useTranslation as useTranslationBase,
} from "react-i18next";
import * as z from "zod";
import { logger } from "@turbostarter/shared/logger";
import { getInitOptions, config } from "../config";
import { loadTranslation, makeZodI18nMap } from "../utils";
import type {
i18n,
FlatNamespace,
Namespace,
KeyPrefix,
TFunction,
} from "i18next";
import type { FallbackNs, UseTranslationOptions } from "react-i18next";
let client: i18n | null = null;
let iteration = 0;
const MAX_ITERATIONS = 20;
export const initializeI18nClient = async ({
locale,
defaultLocale,
ns,
}: {
locale?: string;
defaultLocale?: string;
ns?: Namespace;
} = {}) => {
if (client) {
return client;
}
const loadedLanguages = new Set<string>();
const loadedNamespaces = new Set<string>();
const i18n = i18next.createInstance();
await i18n
.use(initReactI18next)
.use(initReactI18next)
.use(
resourcesToBackend(
async (
language: (typeof config.locales)[number],
namespace: (typeof config.namespaces)[number],
callback,
) => {
const data = await loadTranslation(language, namespace);
if (!loadedLanguages.has(language)) {
loadedLanguages.add(language);
}
if (!loadedNamespaces.has(namespace)) {
loadedNamespaces.add(namespace);
}
return callback(null, data);
},
),
)
.use(LanguageDetector)
.init(
{
...getInitOptions({
locale,
defaultLocale,
ns,
}),
detection: {
order: ["htmlTag", "cookie", "navigator"],
caches: ["cookie"],
lookupCookie: config.cookie,
},
},
(err) => {
if (err) {
logger.error("Error initializing i18n client", err);
}
},
);
if (iteration >= MAX_ITERATIONS) {
logger.debug(`Max iterations reached: ${MAX_ITERATIONS}`);
client = i18n;
return client;
}
if (loadedLanguages.size === 0 || loadedNamespaces.size === 0) {
iteration++;
logger.debug(
`Keeping component from rendering if no languages or namespaces are loaded. Iteration: ${iteration}. Will stop after ${MAX_ITERATIONS} iterations.`,
);
throw new Error("No languages or namespaces loaded");
}
client = i18n;
return client;
};
export const useTranslation = <
Ns extends FlatNamespace | FlatNamespace[] | undefined = undefined,
KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
>(
ns?: Ns,
options?: UseTranslationOptions<KPrefix>,
) => useTranslationBase<Ns, KPrefix>(ns, options);
export const getI18n = async ({
locale,
defaultLocale,
ns,
}: {
locale?: string;
defaultLocale?: string;
ns?: Namespace;
}) => {
if (!client) {
return initializeI18nClient({ locale, defaultLocale, ns });
}
const t = client.getFixedT<Namespace>(client.language, ns);
await client.changeLanguage(locale);
dayjs.locale(locale);
z.config({
localeError: makeZodI18nMap({ t: t as TFunction }),
});
if (ns) {
await client.loadNamespaces(ns);
}
return client;
};
export const I18nProvider = ({
children,
locale,
defaultLocale,
ns,
}: {
children: React.ReactNode;
locale?: string;
defaultLocale?: string;
ns?: Namespace;
}) => {
const [i18nClient, setI18nClient] = useState<i18n | null>(client);
useEffect(() => {
void (async () => {
if (!client) {
setI18nClient(await getI18n({ locale, defaultLocale, ns }));
}
})();
}, [client]);
useEffect(() => {
if (i18nClient) {
void i18nClient.changeLanguage(locale);
}
}, [locale]);
if (!i18nClient) {
return null;
}
const t = i18nClient.getFixedT<Namespace>(i18nClient.language, ns);
z.config({
localeError: makeZodI18nMap({ t: t as TFunction }),
});
dayjs.locale(locale);
return <I18nextProvider i18n={i18nClient}>{children}</I18nextProvider>;
};
export type { ParseKeys as TranslationKey } from "i18next";