feat(admin): centralize subscriptions under /admin?tab=subscriptions

Replaced the dedicated /admin/subscriptions page with a redirect to the central admin panel’s subscriptions tab. Removed session checks, imports, and the AdminSubscriptionDashboard since routing and access are now handled centrally. This streamlines admin UX and keeps access control unified.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-09 01:50:50 -03:00
1 parent d8e7d5d7e6
commit 62ab5b2608
19 files changed
+3091 -357

No files matched your search

+297
View File
@@ -0,0 +1,297 @@
import { NextRequest, NextResponse } from "next/server";
import { requireAdminUser } from "@/lib/auth/middleware";
import {
createAdminItem,
searchAdminItems,
updateAdminItem,
getItemById,
} from "@/lib/prisma/queries/items";
import {
insertCategoryItem,
updateCategoryItem,
listCategoryTemplates,
} from "@/lib/items/sqlite-manager";
function parseNumber(value: string | null | undefined, fallback: number): number {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function parseList(value: unknown): string[] {
if (Array.isArray(value)) {
return value
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
.filter(Boolean);
}
if (typeof value === "string") {
return value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
}
return [];
}
function normalizeDynamicFields(category: string, rawFields: Record<string, unknown> | undefined) {
if (!rawFields || typeof rawFields !== "object") {
return {} as Record<string, unknown>;
}
const templates = listCategoryTemplates();
const template = templates.find((entry) => entry.name === category.toLowerCase());
if (!template) {
return rawFields;
}
const columnMap = new Map(template.columns.map((column) => [column.name, column]));
const normalized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(rawFields)) {
if (["id", "nome", "image_url", "image_local_path"].includes(key)) {
continue;
}
const column = columnMap.get(key);
if (!column) {
continue;
}
if (value === "" || value === null || value === undefined) {
normalized[key] = null;
continue;
}
switch (column.type) {
case "INTEGER":
case "BOOLEAN": {
const parsed = Number(value);
normalized[key] = Number.isFinite(parsed) ? parsed : null;
break;
}
case "REAL": {
const parsed = Number(value);
normalized[key] = Number.isFinite(parsed) ? parsed : null;
break;
}
default:
normalized[key] = String(value);
}
}
return normalized;
}
export async function GET(request: NextRequest) {
const auth = await requireAdminUser(request);
if (auth instanceof NextResponse) {
return auth;
}
const { searchParams } = new URL(request.url);
const page = parseNumber(searchParams.get("page"), 1);
const pageSize = Math.min(parseNumber(searchParams.get("pageSize"), 50), 200);
const q = searchParams.get("q") || undefined;
const category = searchParams.get("category") || undefined;
const rarity = searchParams.get("rarity") || undefined;
const server = searchParams.get("server") || undefined;
try {
const result = await searchAdminItems({
q,
category,
rarity,
server,
page,
pageSize,
});
return NextResponse.json(result, { status: 200 });
} catch (error: any) {
console.error("[admin/items][GET] erro", error?.message);
return NextResponse.json(
{
success: false,
error: "Falha ao consultar itens",
message: error?.message || "Erro desconhecido",
},
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
const auth = await requireAdminUser(request);
if (auth instanceof NextResponse) {
return auth;
}
try {
const body = await request.json();
const name = typeof body?.name === "string" ? body.name.trim() : "";
const category = typeof body?.category === "string" ? body.category.trim().toLowerCase() : "";
if (!name || !category) {
return NextResponse.json(
{ success: false, error: "Nome e categoria são obrigatórios." },
{ status: 400 }
);
}
const dynamicFields = normalizeDynamicFields(category, body?.fields);
const imageUrl = typeof body?.imageUrl === "string" ? body.imageUrl.trim() || null : null;
const imageLocalPath =
typeof body?.imageLocalPath === "string" ? body.imageLocalPath.trim() || null : null;
const sqliteData = {
nome: name,
image_url: imageUrl,
image_local_path: imageLocalPath,
...dynamicFields,
} as Record<string, unknown>;
const sqliteId = insertCategoryItem({ category, data: sqliteData });
const metadataBase = typeof body?.metadata === "object" && body.metadata !== null ? body.metadata : {};
const metadata = { ...metadataBase, ...dynamicFields } as Record<string, unknown>;
const item = await createAdminItem({
item_name: name,
category,
rarity: typeof body?.rarity === "string" ? body.rarity.trim() || null : null,
image_url: imageUrl,
image_local_path: imageLocalPath,
level: typeof body?.level === "number" ? body.level : null,
weight: typeof body?.weight === "number" ? body.weight : null,
slots: typeof body?.slots === "number" ? body.slots : null,
skill: typeof body?.skill === "string" ? body.skill.trim() || null : null,
servers: parseList(body?.servers),
tags: parseList(body?.tags),
description:
typeof body?.description === "string" ? body.description.trim() || null : null,
metadata,
source_table_name: category,
source_table_id: sqliteId,
});
return NextResponse.json(
{
success: true,
item,
},
{ status: 201 }
);
} catch (error: any) {
console.error("[admin/items][POST] erro", error?.message);
return NextResponse.json(
{
success: false,
error: "Falha ao criar item",
message: error?.message || "Erro desconhecido",
},
{ status: 500 }
);
}
}
export async function PUT(request: NextRequest) {
const auth = await requireAdminUser(request);
if (auth instanceof NextResponse) {
return auth;
}
try {
const body = await request.json();
const id = Number(body?.id);
if (!Number.isFinite(id) || id <= 0) {
return NextResponse.json(
{ success: false, error: "ID inválido." },
{ status: 400 }
);
}
const existing = await getItemById(id);
if (!existing) {
return NextResponse.json(
{ success: false, error: "Item não encontrado." },
{ status: 404 }
);
}
const category = (typeof body?.category === "string" ? body.category.trim().toLowerCase() : existing.category) ?? existing.category;
if (category !== existing.category) {
return NextResponse.json(
{ success: false, error: "Alterar a categoria de um item não é suportado." },
{ status: 400 }
);
}
const dynamicFields = normalizeDynamicFields(category, body?.fields);
const imageUrl = typeof body?.imageUrl === "string" ? body.imageUrl.trim() : undefined;
const imageLocalPath =
typeof body?.imageLocalPath === "string" ? body.imageLocalPath.trim() : undefined;
if (existing.source_table_id && existing.source_table_name) {
const sqliteData: Record<string, unknown> = {
nome: typeof body?.name === "string" ? body.name.trim() : existing.item_name,
};
if (imageUrl !== undefined) sqliteData.image_url = imageUrl;
else sqliteData.image_url = existing.image_url;
if (imageLocalPath !== undefined) sqliteData.image_local_path = imageLocalPath;
else sqliteData.image_local_path = existing.image_local_path;
for (const [key, value] of Object.entries(dynamicFields)) {
sqliteData[key] = value;
}
updateCategoryItem(existing.source_table_name, existing.source_table_id, sqliteData);
}
const metadataBase = typeof body?.metadata === "object" && body.metadata !== null ? body.metadata : existing.metadata;
const metadata = { ...metadataBase, ...dynamicFields } as Record<string, unknown>;
const item = await updateAdminItem(id, {
item_name: typeof body?.name === "string" ? body.name.trim() : undefined,
category,
rarity: typeof body?.rarity === "string" ? body.rarity.trim() : undefined,
image_url: imageUrl,
image_local_path: imageLocalPath,
level: typeof body?.level === "number" ? body.level : undefined,
weight: typeof body?.weight === "number" ? body.weight : undefined,
slots: typeof body?.slots === "number" ? body.slots : undefined,
skill: typeof body?.skill === "string" ? body.skill.trim() : undefined,
servers: parseList(body?.servers),
tags: parseList(body?.tags),
description:
typeof body?.description === "string"
? body.description.trim()
: body?.description === null
? null
: undefined,
metadata,
});
return NextResponse.json(
{
success: true,
item,
},
{ status: 200 }
);
} catch (error: any) {
console.error("[admin/items][PUT] erro", error?.message);
return NextResponse.json(
{
success: false,
error: "Falha ao atualizar item",
message: error?.message || "Erro desconhecido",
},
{ status: 500 }
);
}
}