forked from hikmet4434/winninghunter
feat: kapsamli admin dashboard ekle
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireAdmin } from "@/lib/auth/current-user";
|
||||
import { getApiAdmin } from "@/lib/admin-api";
|
||||
|
||||
export async function POST() {
|
||||
await requireAdmin();
|
||||
const admin = await getApiAdmin();
|
||||
if (!admin) return NextResponse.json({ error: "FORBIDDEN" }, { status: 403 });
|
||||
return NextResponse.json({ ok: true, message: "Demo seed komut satırından çalışır: npm run db:seed" });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { PaymentStatus } from "@prisma/client";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getApiAdmin, requestAuditContext } from "@/lib/admin-api";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const schema = z.object({ status: z.nativeEnum(PaymentStatus) });
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const actor = await getApiAdmin();
|
||||
if (!actor) return NextResponse.json({ error: "FORBIDDEN" }, { status: 403 });
|
||||
const parsed = schema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return NextResponse.json({ error: "INVALID_INPUT" }, { status: 400 });
|
||||
const { id } = await params;
|
||||
const payment = await prisma.payment.findUnique({ where: { id }, include: { user: { select: { email: true } } } });
|
||||
if (!payment) return NextResponse.json({ error: "PAYMENT_NOT_FOUND" }, { status: 404 });
|
||||
const auditContext = requestAuditContext(request);
|
||||
await prisma.$transaction([
|
||||
prisma.payment.update({ where: { id }, data: { status: parsed.data.status, paidAt: parsed.data.status === PaymentStatus.PAID ? payment.paidAt || new Date() : payment.paidAt } }),
|
||||
prisma.adminAuditLog.create({
|
||||
data: {
|
||||
actorId: actor.id, actorEmail: actor.email, action: "PAYMENT_STATUS_CHANGED", targetType: "Payment", targetId: id,
|
||||
summary: `${payment.user.email} ödeme durumu ${payment.status} → ${parsed.data.status} olarak değiştirildi.`,
|
||||
details: { before: payment.status, after: parsed.data.status }, ...auditContext
|
||||
}
|
||||
})
|
||||
]);
|
||||
return NextResponse.json({ ok: true, status: parsed.data.status });
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CreditTransactionType, PaymentStatus, Prisma } from "@prisma/client";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getApiAdmin, requestAuditContext } from "@/lib/admin-api";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const schema = z.object({
|
||||
userId: z.string().min(1), amountCents: z.coerce.number().int().positive().max(100_000_000),
|
||||
currency: z.string().trim().length(3).transform((value) => value.toUpperCase()),
|
||||
description: z.string().trim().max(240).optional().default("Manuel ödeme"),
|
||||
externalId: z.string().trim().max(120).optional().nullable(), creditGranted: z.coerce.number().int().min(0).max(1_000_000).default(0)
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const actor = await getApiAdmin();
|
||||
if (!actor) return NextResponse.json({ error: "FORBIDDEN" }, { status: 403 });
|
||||
const parsed = schema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return NextResponse.json({ error: "INVALID_INPUT" }, { status: 400 });
|
||||
const auditContext = requestAuditContext(request);
|
||||
try {
|
||||
const payment = await prisma.$transaction(async (tx) => {
|
||||
const target = await tx.user.findUnique({ where: { id: parsed.data.userId }, select: { email: true, creditBalance: true } });
|
||||
if (!target) throw new Error("USER_NOT_FOUND");
|
||||
const created = await tx.payment.create({
|
||||
data: { userId: parsed.data.userId, actorId: actor.id, amountCents: parsed.data.amountCents, currency: parsed.data.currency, status: PaymentStatus.PAID, provider: "MANUAL", externalId: parsed.data.externalId || null, description: parsed.data.description, creditGranted: parsed.data.creditGranted, paidAt: new Date() }
|
||||
});
|
||||
let balanceAfter = target.creditBalance;
|
||||
if (parsed.data.creditGranted > 0) {
|
||||
const updated = await tx.user.update({ where: { id: parsed.data.userId }, data: { creditBalance: { increment: parsed.data.creditGranted } }, select: { creditBalance: true } });
|
||||
balanceAfter = updated.creditBalance;
|
||||
await tx.creditTransaction.create({ data: { userId: parsed.data.userId, actorId: actor.id, type: CreditTransactionType.PAYMENT, amount: parsed.data.creditGranted, balanceAfter, reason: `Manuel ödeme ${created.id}`, metadata: { paymentId: created.id } } });
|
||||
}
|
||||
await tx.adminAuditLog.create({
|
||||
data: {
|
||||
actorId: actor.id, actorEmail: actor.email, action: "PAYMENT_RECORDED", targetType: "Payment", targetId: created.id,
|
||||
summary: `${target.email} için ${(parsed.data.amountCents / 100).toFixed(2)} ${parsed.data.currency} manuel ödeme kaydedildi.`,
|
||||
details: { amountCents: parsed.data.amountCents, currency: parsed.data.currency, creditGranted: parsed.data.creditGranted, balanceAfter }, ...auditContext
|
||||
}
|
||||
});
|
||||
return created;
|
||||
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
|
||||
return NextResponse.json({ ok: true, payment });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "UNKNOWN";
|
||||
return NextResponse.json({ error: message }, { status: message === "USER_NOT_FOUND" ? 404 : 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CreditTransactionType, Prisma } from "@prisma/client";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getApiAdmin, requestAuditContext } from "@/lib/admin-api";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const schema = z.object({
|
||||
operation: z.enum(["add", "remove"]),
|
||||
amount: z.coerce.number().int().positive().max(1_000_000),
|
||||
reason: z.string().trim().min(3).max(240)
|
||||
});
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const actor = await getApiAdmin();
|
||||
if (!actor) return NextResponse.json({ error: "FORBIDDEN" }, { status: 403 });
|
||||
const parsed = schema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return NextResponse.json({ error: "INVALID_INPUT" }, { status: 400 });
|
||||
|
||||
const { id } = await params;
|
||||
const { operation, amount, reason } = parsed.data;
|
||||
const signedAmount = operation === "add" ? amount : -amount;
|
||||
const auditContext = requestAuditContext(request);
|
||||
|
||||
try {
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const target = await tx.user.findUnique({ where: { id }, select: { id: true, email: true, creditBalance: true } });
|
||||
if (!target) throw new Error("USER_NOT_FOUND");
|
||||
if (target.creditBalance + signedAmount < 0) throw new Error("INSUFFICIENT_CREDIT");
|
||||
const updated = await tx.user.update({ where: { id }, data: { creditBalance: { increment: signedAmount } }, select: { creditBalance: true } });
|
||||
const transaction = await tx.creditTransaction.create({
|
||||
data: {
|
||||
userId: id,
|
||||
actorId: actor.id,
|
||||
type: operation === "add" ? CreditTransactionType.ADMIN_CREDIT : CreditTransactionType.ADMIN_DEBIT,
|
||||
amount: signedAmount,
|
||||
balanceAfter: updated.creditBalance,
|
||||
reason
|
||||
}
|
||||
});
|
||||
await tx.adminAuditLog.create({
|
||||
data: {
|
||||
actorId: actor.id,
|
||||
actorEmail: actor.email,
|
||||
action: operation === "add" ? "CREDIT_ADDED" : "CREDIT_REMOVED",
|
||||
targetType: "User",
|
||||
targetId: id,
|
||||
summary: `${target.email} için ${Math.abs(signedAmount)} kredi ${operation === "add" ? "eklendi" : "çıkarıldı"}.`,
|
||||
details: { amount: signedAmount, balanceAfter: updated.creditBalance, reason },
|
||||
...auditContext
|
||||
}
|
||||
});
|
||||
return { transaction, balance: updated.creditBalance };
|
||||
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "UNKNOWN";
|
||||
const status = message === "USER_NOT_FOUND" ? 404 : message === "INSUFFICIENT_CREDIT" ? 409 : 500;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { PlanCode, SubscriptionStatus } from "@prisma/client";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getApiAdmin, requestAuditContext } from "@/lib/admin-api";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const schema = z.object({ planCode: z.nativeEnum(PlanCode), status: z.nativeEnum(SubscriptionStatus).default(SubscriptionStatus.ACTIVE) });
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const actor = await getApiAdmin();
|
||||
if (!actor) return NextResponse.json({ error: "FORBIDDEN" }, { status: 403 });
|
||||
const parsed = schema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return NextResponse.json({ error: "INVALID_INPUT" }, { status: 400 });
|
||||
const { id } = await params;
|
||||
const [target, plan] = await Promise.all([
|
||||
prisma.user.findUnique({ where: { id }, include: { subscription: { include: { plan: true } } } }),
|
||||
prisma.plan.findUnique({ where: { code: parsed.data.planCode } })
|
||||
]);
|
||||
if (!target) return NextResponse.json({ error: "USER_NOT_FOUND" }, { status: 404 });
|
||||
if (!plan) return NextResponse.json({ error: "PLAN_NOT_FOUND" }, { status: 404 });
|
||||
const auditContext = requestAuditContext(request);
|
||||
await prisma.$transaction([
|
||||
prisma.subscription.upsert({ where: { userId: id }, update: { planId: plan.id, status: parsed.data.status, cancelAtPeriodEnd: false }, create: { userId: id, planId: plan.id, status: parsed.data.status } }),
|
||||
prisma.adminAuditLog.create({
|
||||
data: {
|
||||
actorId: actor.id, actorEmail: actor.email, action: "PLAN_CHANGED", targetType: "User", targetId: id,
|
||||
summary: `${target.email} planı ${target.subscription?.plan.code || "YOK"} → ${plan.code} olarak değiştirildi.`,
|
||||
details: { before: target.subscription?.plan.code || null, after: plan.code, status: parsed.data.status }, ...auditContext
|
||||
}
|
||||
})
|
||||
]);
|
||||
return NextResponse.json({ ok: true, plan: plan.code, status: parsed.data.status });
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { UserRole } from "@prisma/client";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getApiAdmin, requestAuditContext } from "@/lib/admin-api";
|
||||
import { isConfiguredAdminEmail } from "@/lib/admin-emails";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const schema = z.object({ role: z.nativeEnum(UserRole) });
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const actor = await getApiAdmin();
|
||||
if (!actor) return NextResponse.json({ error: "FORBIDDEN" }, { status: 403 });
|
||||
const parsed = schema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return NextResponse.json({ error: "INVALID_INPUT" }, { status: 400 });
|
||||
const { id } = await params;
|
||||
const target = await prisma.user.findUnique({ where: { id }, select: { id: true, email: true, role: true } });
|
||||
if (!target) return NextResponse.json({ error: "USER_NOT_FOUND" }, { status: 404 });
|
||||
if (parsed.data.role === UserRole.USER && (target.id === actor.id || isConfiguredAdminEmail(target.email))) {
|
||||
return NextResponse.json({ error: "PROTECTED_ADMIN" }, { status: 409 });
|
||||
}
|
||||
const auditContext = requestAuditContext(request);
|
||||
await prisma.$transaction([
|
||||
prisma.user.update({ where: { id }, data: { role: parsed.data.role } }),
|
||||
prisma.adminAuditLog.create({
|
||||
data: {
|
||||
actorId: actor.id, actorEmail: actor.email, action: "ROLE_CHANGED", targetType: "User", targetId: id,
|
||||
summary: `${target.email} yetkisi ${target.role} → ${parsed.data.role} olarak değiştirildi.`,
|
||||
details: { before: target.role, after: parsed.data.role }, ...auditContext
|
||||
}
|
||||
})
|
||||
]);
|
||||
return NextResponse.json({ ok: true, role: parsed.data.role });
|
||||
}
|
||||
@@ -67,6 +67,7 @@ export async function GET(req: Request) {
|
||||
avatarUrl: profile.picture || null,
|
||||
locale: profile.locale || "tr",
|
||||
role: isAdmin ? UserRole.ADMIN : UserRole.USER,
|
||||
lastLoginAt: new Date(),
|
||||
subscription: { create: { planId: plan.id, status: "ACTIVE" } }
|
||||
}
|
||||
});
|
||||
@@ -77,9 +78,19 @@ export async function GET(req: Request) {
|
||||
googleId: user.googleId || profile.sub,
|
||||
name: user.name || profile.name || null,
|
||||
avatarUrl: user.avatarUrl || profile.picture || null,
|
||||
role: isAdmin ? UserRole.ADMIN : user.role,
|
||||
lastLoginAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
if (isAdmin) {
|
||||
if (!plan) return redirectToLogin("PLAN_NOT_SEEDED");
|
||||
await prisma.subscription.upsert({
|
||||
where: { userId: user.id },
|
||||
update: { planId: plan.id, status: "ACTIVE", cancelAtPeriodEnd: false },
|
||||
create: { userId: user.id, planId: plan.id, status: "ACTIVE" }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await createSession(user.id);
|
||||
|
||||
@@ -10,7 +10,8 @@ export async function GET() {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
plan: user.subscription?.plan.code
|
||||
plan: user.subscription?.plan.code,
|
||||
creditBalance: user.creditBalance
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user