mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
feat(logo): integração Logo.dev para logos automáticos de estabelecimentos
- Nova tabela `establishment_logos` no schema (userId + nameKey → domain) - Utilitários: `buildLogoDevUrl`, `toNameKey`, `logoQueryKeys`, `LOGO_DEV_TOKEN` - `EstablishmentLogo`: exibe logo via Logo.dev com fallback para iniciais; hover mostra ícone de edição - `EstablishmentLogoPicker`: popover para buscar e fixar domínio Logo.dev por estabelecimento - API routes: `GET /api/logo/mapping` e `GET /api/logo/search` - Server actions/queries para persistência do mapeamento por usuário - CSP: libera `https://img.logo.dev` em `img-src` - `.env.example`: variáveis `NEXT_PUBLIC_LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
26
src/app/api/logo/mapping/route.ts
Normal file
26
src/app/api/logo/mapping/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
||||
import { fetchEstablishmentLogoDomain } from "@/shared/lib/logo/establishment-logo-queries";
|
||||
|
||||
/**
|
||||
* GET /api/logo/mapping?name={name}
|
||||
*
|
||||
* Retorna o domínio Logo.dev salvo pelo usuário para um estabelecimento.
|
||||
* Usado pelo EstablishmentLogo para hidratar o domain salvo no banco.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const session = await getOptionalUserSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ domain: null }, { status: 200 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const name = searchParams.get("name")?.trim();
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({ domain: null }, { status: 200 });
|
||||
}
|
||||
|
||||
const domain = await fetchEstablishmentLogoDomain(session.user.id, name);
|
||||
return NextResponse.json({ domain });
|
||||
}
|
||||
80
src/app/api/logo/search/route.ts
Normal file
80
src/app/api/logo/search/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
||||
|
||||
const LOGO_DEV_SEARCH_URL = "https://api.logo.dev/search";
|
||||
|
||||
interface LogoResult {
|
||||
name: string;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
async function searchByStrategy(
|
||||
q: string,
|
||||
strategy: "match" | "typeahead",
|
||||
secretKey: string,
|
||||
): Promise<LogoResult[]> {
|
||||
try {
|
||||
const url = `${LOGO_DEV_SEARCH_URL}?q=${encodeURIComponent(q)}&strategy=${strategy}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${secretKey}` },
|
||||
next: { revalidate: 3600 },
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/logo/search?q={name}
|
||||
*
|
||||
* Proxy seguro para a Logo.dev Brand Search API.
|
||||
* Faz duas buscas paralelas (match + typeahead) e retorna até 20 resultados únicos.
|
||||
* Usa LOGO_DEV_SECRET_KEY server-side — nunca exposta ao cliente.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const session = await getOptionalUserSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Não autorizado." }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const q = searchParams.get("q")?.trim();
|
||||
|
||||
if (!q) {
|
||||
return NextResponse.json(
|
||||
{ error: "Parâmetro q obrigatório." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const secretKey = process.env.LOGO_DEV_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "Logo.dev não configurado." },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
// Duas buscas paralelas para maximizar resultados (cada uma retorna até 10)
|
||||
const [matchResults, typeaheadResults] = await Promise.all([
|
||||
searchByStrategy(q, "match", secretKey),
|
||||
searchByStrategy(q, "typeahead", secretKey),
|
||||
]);
|
||||
|
||||
// Mescla e deduplica por domain, mantendo ordem (match tem prioridade)
|
||||
const seen = new Set<string>();
|
||||
const merged: LogoResult[] = [];
|
||||
|
||||
for (const result of [...matchResults, ...typeaheadResults]) {
|
||||
if (!seen.has(result.domain)) {
|
||||
seen.add(result.domain);
|
||||
merged.push(result);
|
||||
if (merged.length >= 20) break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(merged);
|
||||
}
|
||||
Reference in New Issue
Block a user