feat: V2 microservices stack — backend services, gateway, JWT auth

Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-29 23:29:31 +03:30
parent 53ea78a00d
commit 90ac0b81d1
7636 changed files with 3707504 additions and 240 deletions
+3 -11
View File
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { DashboardShell } from "@/components/dashboard/DashboardShell";
import { createClient } from "@/lib/supabase/server";
import { getCurrentUser } from "@/lib/auth/session";
export const dynamic = "force-dynamic";
@@ -10,24 +10,16 @@ export default async function DashboardLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const user = await getCurrentUser();
if (!user) {
redirect("/auth");
}
const userName =
typeof user.user_metadata?.full_name === "string"
? user.user_metadata.full_name
: null;
return (
<DashboardShell
userEmail={user.email ?? ""}
userName={userName}
userName={user.full_name ?? null}
userId={user.id}
>
{children}
+14 -15
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Inter, Plus_Jakarta_Sans, Vazirmatn } from "next/font/google";
import localFont from "next/font/local";
import { notFound } from "next/navigation";
import { getMessages, getTranslations } from "next-intl/server";
import { NextIntlClientProvider } from "next-intl";
@@ -10,23 +10,28 @@ import type { Locale } from "@/i18n/routing";
import "../globals.css";
/* ── Fonts ─────────────────────────────────────────────────────── */
/* ── Fonts (self-hosted via @fontsource — no Google CDN required) ── */
const vazirmatn = Vazirmatn({
subsets: ["arabic"],
const vazirmatn = localFont({
src: [
{ path: "../../../node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-400-normal.woff2", weight: "400" },
{ path: "../../../node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-500-normal.woff2", weight: "500" },
{ path: "../../../node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-600-normal.woff2", weight: "600" },
{ path: "../../../node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-700-normal.woff2", weight: "700" },
{ path: "../../../node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-800-normal.woff2", weight: "800" },
],
variable: "--font-vazirmatn",
display: "swap",
weight: ["400", "500", "600", "700", "800"],
});
const plusJakartaSans = Plus_Jakarta_Sans({
subsets: ["latin"],
const plusJakartaSans = localFont({
src: "../../../node_modules/@fontsource-variable/plus-jakarta-sans/files/plus-jakarta-sans-latin-wght-normal.woff2",
variable: "--font-heading",
display: "swap",
});
const inter = Inter({
subsets: ["latin"],
const inter = localFont({
src: "../../../node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2",
variable: "--font-body",
display: "swap",
});
@@ -97,12 +102,6 @@ export default async function LocaleLayout({
>
<head>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link rel="preconnect" href="https://picsum.photos" />
</head>
<body
+3 -6
View File
@@ -1,6 +1,6 @@
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import { getSession } from "@/lib/auth/session";
export const dynamic = "force-dynamic";
@@ -9,12 +9,9 @@ export default async function VideoProjectNewLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const session = await getSession();
if (!user) {
if (!session) {
redirect("/auth");
}