mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
O projeto do CRM na Vercel foi desvinculado do GitHub por decisao do dono: a plataforma fica so com a landing page, em outro repositorio. Medido com a mesma sonda nos dois lados — `gh api repos/.../commits/<sha>/status` devolve `Vercel` no commitf65d04667(antes) e vazio ema8c9e1ad3,7168961ade no head do #1081 (depois). Com isso, dezenas de afirmacoes do repositorio publico viraram falsas. As que falavam no presente sairam: - mensagem automatica ao contribuinte, molde de PR, CONTRIBUTING e os espelhos na skill de contribuir prometiam um check "Vercel" vermelho que nao aparece mais. No lugar, a mensagem passa a dizer onde olhar o que trava o merge — os checks marcados Required no proprio PR, com a ressalva de que `--required` so lista os que ja reportaram; - runbooks de operacao mandavam abrir o painel da plataforma e redeployar a main; passam a descrever o `.env` da instalacao e a recriacao dos conteineres; - `lib/env.ts` mandava, no boot, ajustar variavel "na Vercel"; - specs e PRDs descreviam topologia, crons e guarda de segredo na plataforma; - comentarios de codigo ancoravam decisoes vivas em premissa morta. Registros datados (pesquisa, pitch, epicos, handoffs) nao foram reescritos: ganharam uma linha dizendo que sao registro da fase hospedada. O `vercel.ts` fica: ele serve quem hospeda um fork, e o gate `tests/unit/cron-routes-scheduled.test.ts` continua reprovando divergencia de rotas entre ele e o crontab do scheduler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
|
import { cookieSecure } from "@/lib/supabase/cookie-secure";
|
|
import { NextResponse, type NextRequest } from "next/server";
|
|
import { env } from "@/lib/env";
|
|
import { isPublicPath } from "@/lib/auth/public-paths";
|
|
import {
|
|
verifyImpersonateCookieEdge,
|
|
IMPERSONATE_COOKIE_NAME_EDGE,
|
|
} from "@/lib/impersonate/cookie-edge";
|
|
|
|
const COOKIE_NAME = "sb-deskcomm-auth";
|
|
|
|
export async function proxy(request: NextRequest) {
|
|
const response = NextResponse.next({ request: { headers: request.headers } });
|
|
|
|
// Inject X-Request-Id for downstream correlation (audit log, error wrappers).
|
|
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
response.headers.set("x-request-id", requestId);
|
|
|
|
const { pathname, search } = request.nextUrl;
|
|
// Expose pathname to Server Components via header (used by onboarding layout).
|
|
response.headers.set("x-pathname", pathname);
|
|
request.headers.set("x-pathname", pathname);
|
|
|
|
// EPIC-11: the admin surface is reached by PATH (`/admin/*`) — the self-host kit
|
|
// points `NEXT_PUBLIC_ADMIN_URL` at the same host as the app and maps no `admin.`
|
|
// sub-domain. The host-based branch below stays a NOOP today and only exists as
|
|
// documentation of the intended deploy topology.
|
|
const host = request.headers.get("host") ?? "";
|
|
const isAdminSurface = host.startsWith("admin.") || pathname.startsWith("/admin");
|
|
|
|
if (isPublicPath(pathname)) {
|
|
return response;
|
|
}
|
|
|
|
const supabase = createServerClient(
|
|
env.NEXT_PUBLIC_SUPABASE_URL,
|
|
env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return request.cookies.getAll();
|
|
},
|
|
setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) {
|
|
cookiesToSet.forEach(({ name, value, options }) => {
|
|
request.cookies.set(name, value);
|
|
response.cookies.set(name, value, options);
|
|
});
|
|
},
|
|
},
|
|
cookieOptions: {
|
|
name: COOKIE_NAME,
|
|
sameSite: "strict",
|
|
httpOnly: true,
|
|
secure: cookieSecure(),
|
|
path: "/",
|
|
},
|
|
},
|
|
);
|
|
|
|
// Validate JWT server-side (NEVER use getSession on backend per CLAUDE.md).
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser();
|
|
|
|
if (!user) {
|
|
// API routes must respond with JSON envelope (contract: {error:{code,message}})
|
|
// — never redirect HTML to JSON consumers. UI routes redirect to /login as before.
|
|
if (pathname.startsWith("/api/")) {
|
|
return new NextResponse(
|
|
JSON.stringify({
|
|
error: {
|
|
code: "unauthenticated",
|
|
message: "Authentication required",
|
|
},
|
|
}),
|
|
{
|
|
status: 401,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-request-id": requestId,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
const loginUrl = new URL("/login", request.url);
|
|
loginUrl.searchParams.set("next", pathname + search);
|
|
return NextResponse.redirect(loginUrl);
|
|
}
|
|
|
|
// EPIC-11 S-11.07: validate impersonate cookie on /app/* paths. Middleware
|
|
// runs in Edge — no DB access, only HMAC + expiry. On any failure we delete
|
|
// the presentation cookie. The database support session remains authoritative:
|
|
// expired/revoked support still blocks the app until explicit exit.
|
|
if (pathname.startsWith("/app")) {
|
|
const impCookie = request.cookies.get(IMPERSONATE_COOKIE_NAME_EDGE)?.value;
|
|
if (impCookie) {
|
|
const result = await verifyImpersonateCookieEdge(
|
|
impCookie,
|
|
env.IMPERSONATE_COOKIE_SECRET ?? "",
|
|
);
|
|
if (!result.valid) {
|
|
console.warn(
|
|
`[middleware] impersonate cookie invalid (${result.reason ?? "unknown"}) — clearing`,
|
|
);
|
|
response.cookies.delete(IMPERSONATE_COOKIE_NAME_EDGE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// /admin/* additionally requires platform_admin (early gate — authoritative
|
|
// check is server-side in `requirePlatformAdmin`). Skip the RPC for
|
|
// `/admin/forbidden` (rendered to non-admins, would otherwise loop).
|
|
if (isAdminSurface && pathname.startsWith("/admin") && pathname !== "/admin/forbidden") {
|
|
const { data: isAdmin, error } = await supabase.rpc("fn_is_platform_admin");
|
|
if (error || !isAdmin) {
|
|
return NextResponse.redirect(new URL("/admin/forbidden", request.url));
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
// Run on all paths except static assets / Next internals.
|
|
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)$).*)",
|
|
],
|
|
};
|