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:
1 parent
d8e7d5d7e6
commit
62ab5b2608
19 files changed
+3091
-357
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import { addColumnToCategory, type SQLiteColumnDefinition } from "@/lib/items/sqlite-manager";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
function sanitizeColumn(column: any): SQLiteColumnDefinition {
|
||||
const name = typeof column?.name === "string" ? column.name.trim().toLowerCase() : "";
|
||||
const type = typeof column?.type === "string" ? column.type : "TEXT";
|
||||
const notNull = column?.notNull === true;
|
||||
const defaultValue = column?.defaultValue ?? undefined;
|
||||
|
||||
if (!name) {
|
||||
throw new Error("Nome da coluna é obrigatório");
|
||||
}
|
||||
|
||||
if (["id", "nome", "image_url", "image_local_path"].includes(name)) {
|
||||
throw new Error("Coluna reservada não pode ser criada");
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
type: type.toUpperCase(),
|
||||
notNull,
|
||||
defaultValue,
|
||||
editable: column?.editable !== false,
|
||||
} satisfies SQLiteColumnDefinition;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ name: string }> }) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
const { name } = await params;
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const column = sanitizeColumn(body?.column ?? body);
|
||||
const created = addColumnToCategory({ category: name, column });
|
||||
return NextResponse.json({ success: true, column: created }, { status: 201 });
|
||||
} catch (error) {
|
||||
secureLogger.error("Falha ao adicionar coluna à categoria", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "Erro ao adicionar coluna" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import {
|
||||
listCategoryTemplates,
|
||||
createCategoryTemplate,
|
||||
type SQLiteColumnDefinition,
|
||||
type CreateCategoryInput,
|
||||
} from "@/lib/items/sqlite-manager";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
try {
|
||||
const templates = listCategoryTemplates();
|
||||
return NextResponse.json({ success: true, categories: templates }, { status: 200 });
|
||||
} catch (error) {
|
||||
secureLogger.error("Falha ao listar categorias dinâmicas", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Não foi possível listar categorias" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeColumn(column: any): SQLiteColumnDefinition {
|
||||
const name = typeof column?.name === "string" ? column.name.trim().toLowerCase() : "";
|
||||
const type = typeof column?.type === "string" ? column.type : "TEXT";
|
||||
const notNull = column?.notNull === true;
|
||||
const defaultValue = column?.defaultValue ?? undefined;
|
||||
|
||||
if (!name) {
|
||||
throw new Error("Nome da coluna é obrigatório");
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
type: type.toUpperCase(),
|
||||
notNull,
|
||||
defaultValue,
|
||||
editable: column?.editable !== false,
|
||||
} satisfies SQLiteColumnDefinition;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const rawName = typeof body?.name === "string" ? body.name.trim().toLowerCase() : "";
|
||||
if (!rawName) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Nome da categoria é obrigatório" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const columnsArray: unknown[] = Array.isArray(body?.columns) ? body.columns : [];
|
||||
const columns = columnsArray.map((column) => sanitizeColumn(column)).filter((column) => {
|
||||
return !(column && ["id", "nome", "image_url", "image_local_path"].includes(column.name));
|
||||
});
|
||||
|
||||
const input: CreateCategoryInput = {
|
||||
name: rawName,
|
||||
columns,
|
||||
};
|
||||
|
||||
const category = createCategoryTemplate(input);
|
||||
return NextResponse.json({ success: true, category }, { status: 201 });
|
||||
} catch (error) {
|
||||
secureLogger.error("Falha ao criar categoria dinâmica", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "Erro ao criar categoria" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import { generateItemStructure } from "@/lib/ai/generate-item-structure";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
||||
const description =
|
||||
typeof body?.description === "string" ? body.description.trim() : "";
|
||||
const scope = body?.scope === "server" ? "server" : "global";
|
||||
const server =
|
||||
scope === "server" && typeof body?.server === "string"
|
||||
? body.server.trim()
|
||||
: null;
|
||||
|
||||
if (!name || !description) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Informe nome e descrição para gerar o JSON estrutural.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await generateItemStructure({
|
||||
name,
|
||||
description,
|
||||
scope,
|
||||
server,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
structure: result.structure,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user