@@ -27,7 +27,8 @@ import { randomUUID } from "node:crypto";
2727import type { NextRequest } from "next/server" ;
2828
2929import { fail , ok } from "@/lib/api/wrappers" ;
30- import { loadAuthUser , resolveActiveOrg } from "@/lib/auth/server" ;
30+ import { requireRole } from "@/lib/auth/require-role" ;
31+ import { extensaoDe , farejarTipo , pareceSvg } from "@/lib/branding/logo-arquivo" ;
3132import { traduzir } from "@/lib/i18n/dicionario" ;
3233import { logger } from "@/lib/logger" ;
3334import { createAdminClient } from "@/lib/supabase/admin" ;
@@ -37,13 +38,7 @@ export const dynamic = "force-dynamic";
3738/** A revisão leva até 24h; a margem tem de caber num feriado. */
3839const VALIDADE_SEGUNDOS = 7 * 24 * 60 * 60 ;
3940
40- /**
41- * Só imagem, e só os formatos que a plataforma aceita no cabeçalho.
42- *
43- * Recusar aqui é melhor que deixar subir: o arquivo iria para o storage, a
44- * definição seria criada, e a recusa chegaria horas depois falando de um
45- * formato que o operador escolheu porque a tela deixou.
46- */
41+ /** Só imagem, e só os formatos que a plataforma aceita no cabeçalho. */
4742const TIPOS = new Set ( [ "image/jpeg" , "image/png" ] ) ;
4843const TAMANHO_MAX = 5 * 1024 * 1024 ;
4944
@@ -53,38 +48,52 @@ export async function POST(req: NextRequest): Promise<Response> {
5348
5449 const requestId = randomUUID ( ) ;
5550
56- const user = await loadAuthUser ( ) ;
57- if ( ! user ) return fail ( "unauthenticated" , "Faça login." , 401 , { requestId } ) ;
51+ const authz = await requireRole ( "agent" , { requestId, resource : "channel_templates" } ) ;
52+ if ( ! authz . ok ) return authz . response ;
53+ const { user, org } = authz ;
5854 const t = ( texto : string ) => traduzir ( texto , user . idioma ) ;
59- const org = await resolveActiveOrg ( user ) ;
60- if ( ! org ) return fail ( "forbidden" , t ( "Sem organização ativa." ) , 403 , { requestId } ) ;
6155
6256 const form = await req . formData ( ) . catch ( ( ) => null ) ;
6357 const file = form ?. get ( "file" ) ;
6458 if ( ! ( file instanceof File ) ) {
6559 return fail ( "validation_failed" , t ( "Campo 'file' (multipart) obrigatório." ) , 422 , { requestId } ) ;
6660 }
6761
62+ if ( file . size > TAMANHO_MAX ) {
63+ return fail ( "payload_too_large" , t ( "A imagem precisa ter até 5 MB." ) , 413 , { requestId } ) ;
64+ }
65+
6866 const mime = file . type || "application/octet-stream" ;
6967 if ( ! TIPOS . has ( mime ) ) {
68+ return fail ( "unsupported_media_type" , t ( "O cabeçalho aceita imagem JPG ou PNG." ) , 415 , { requestId } ) ;
69+ }
70+
71+ const bytes = new Uint8Array ( await file . arrayBuffer ( ) ) ;
72+ const tipoReal = farejarTipo ( bytes ) ;
73+ if ( ! tipoReal || ! TIPOS . has ( tipoReal ) ) {
74+ if ( pareceSvg ( bytes ) ) {
75+ return fail (
76+ "unsupported_media_type" ,
77+ t ( "Arquivos SVG não são aceitos. O cabeçalho aceita imagem JPG ou PNG." ) ,
78+ 415 ,
79+ { requestId } ,
80+ ) ;
81+ }
7082 return fail (
7183 "unsupported_media_type" ,
72- t ( "O cabeçalho aceita imagem JPG ou PNG." ) ,
84+ t ( "O cabeçalho aceita imagem JPG ou PNG válida ." ) ,
7385 415 ,
7486 { requestId } ,
7587 ) ;
7688 }
77- if ( file . size > TAMANHO_MAX ) {
78- return fail ( "payload_too_large" , t ( "A imagem precisa ter até 5 MB." ) , 413 , { requestId } ) ;
79- }
8089
81- const ext = mime === "image/png" ? "png" : "jpg" ;
90+ const ext = extensaoDe ( tipoReal ) ;
8291 const caminho = `${ org . orgId } /templates/${ randomUUID ( ) } .${ ext } ` ;
8392 const admin = createAdminClient ( ) ;
8493
8594 const { error : erroUp } = await admin . storage
8695 . from ( "whatsapp-media" )
87- . upload ( caminho , Buffer . from ( await file . arrayBuffer ( ) ) , { contentType : mime , upsert : false } ) ;
96+ . upload ( caminho , Buffer . from ( bytes ) , { contentType : tipoReal , upsert : false } ) ;
8897 if ( erroUp ) {
8998 logger . error ( "[partner/templates/media] upload falhou" , { detail : erroUp . message , requestId } ) ;
9099 return fail ( "internal_error" , "Erro ao subir a imagem." , 500 , { requestId } ) ;
0 commit comments