This commit is contained in:
2026-05-06 16:28:13 +03:30
parent 9c30295b4b
commit 1a6eb4307e
8 changed files with 377 additions and 2 deletions

View File

@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import { db } from "@/lib/db";
import { sendLoginCode } from "@/lib/ippanel";
const IRAN_MOBILE_REGEX = /^(\+98|98|0)?9\d{9}$/;
function normalizeIranMobile(phone: string) {
const digits = phone.replace(/[^\d+]/g, "");
if (!IRAN_MOBILE_REGEX.test(digits)) return null;
if (digits.startsWith("+98")) return `0${digits.slice(3)}`;
if (digits.startsWith("98")) return `0${digits.slice(2)}`;
if (digits.startsWith("9")) return `0${digits}`;
return digits;
}
function generateOtpCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function POST(req: NextRequest) {
const { phone } = await req.json().catch(() => ({}));
const normalizedPhone = typeof phone === "string" ? normalizeIranMobile(phone) : null;
if (!normalizedPhone) {
return NextResponse.json({ error: "شماره موبایل معتبر نیست" }, { status: 400 });
}
const recentOtp = await db.loginOtp.findFirst({
where: {
phone: normalizedPhone,
consumedAt: null,
createdAt: { gte: new Date(Date.now() - 60 * 1000) },
},
orderBy: { createdAt: "desc" },
});
if (recentOtp) {
return NextResponse.json(
{ error: "برای ارسال کد جدید حداقل ۶۰ ثانیه صبر کنید" },
{ status: 429 },
);
}
const code = generateOtpCode();
const codeHash = await bcrypt.hash(code, 10);
await db.loginOtp.create({
data: {
phone: normalizedPhone,
codeHash,
expiresAt: new Date(Date.now() + 2 * 60 * 1000),
},
});
try {
await sendLoginCode(normalizedPhone, code);
} catch (error) {
console.error("IPPANEL send failed", error);
return NextResponse.json({ error: "ارسال پیامک ناموفق بود" }, { status: 502 });
}
return NextResponse.json({
ok: true,
expiresIn: 120,
});
}

View File

@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import crypto from "crypto";
import { db } from "@/lib/db";
const IRAN_MOBILE_REGEX = /^(\+98|98|0)?9\d{9}$/;
const SESSION_MAX_AGE_DAYS = 30;
function normalizeIranMobile(phone: string) {
const digits = phone.replace(/[^\d+]/g, "");
if (!IRAN_MOBILE_REGEX.test(digits)) return null;
if (digits.startsWith("+98")) return `0${digits.slice(3)}`;
if (digits.startsWith("98")) return `0${digits.slice(2)}`;
if (digits.startsWith("9")) return `0${digits}`;
return digits;
}
function buildPhoneEmail(phone: string) {
return `${phone}@mobile.local`;
}
export async function POST(req: NextRequest) {
const { phone, code, name } = await req.json().catch(() => ({}));
const normalizedPhone = typeof phone === "string" ? normalizeIranMobile(phone) : null;
const normalizedCode = typeof code === "string" ? code.trim() : "";
if (!normalizedPhone || !/^\d{6}$/.test(normalizedCode)) {
return NextResponse.json({ error: "شماره موبایل یا کد ورود معتبر نیست" }, { status: 400 });
}
const otp = await db.loginOtp.findFirst({
where: {
phone: normalizedPhone,
consumedAt: null,
expiresAt: { gt: new Date() },
},
orderBy: { createdAt: "desc" },
});
if (!otp || otp.attempts >= 5) {
return NextResponse.json({ error: "کد ورود نامعتبر یا منقضی شده است" }, { status: 400 });
}
const isValidCode = await bcrypt.compare(normalizedCode, otp.codeHash);
if (!isValidCode) {
await db.loginOtp.update({
where: { id: otp.id },
data: { attempts: { increment: 1 } },
});
return NextResponse.json({ error: "کد ورود نامعتبر است" }, { status: 400 });
}
const user = await db.user.upsert({
where: { phone: normalizedPhone },
update: {
name: typeof name === "string" && name.trim() ? name.trim() : undefined,
},
create: {
phone: normalizedPhone,
email: buildPhoneEmail(normalizedPhone),
name: typeof name === "string" && name.trim() ? name.trim() : null,
password: await bcrypt.hash(crypto.randomBytes(24).toString("hex"), 10),
},
});
await db.loginOtp.update({
where: { id: otp.id },
data: { consumedAt: new Date() },
});
const token = crypto.randomBytes(32).toString("hex");
const expires = new Date(Date.now() + SESSION_MAX_AGE_DAYS * 24 * 60 * 60 * 1000);
await db.session.create({
data: {
sessionToken: token,
userId: user.id,
expires,
},
});
return NextResponse.json({
token,
tokenType: "Bearer",
expiresAt: expires.toISOString(),
user: {
id: user.id,
name: user.name,
phone: user.phone,
role: user.role,
},
});
}

View File

@@ -0,0 +1,81 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { requireApiAdmin } from "@/lib/apiAuth";
function summarize(description: string) {
if (description.length <= 100) return description;
return `${description.slice(0, 100)}...`;
}
function parseNewsTime(value: unknown) {
if (typeof value !== "string" && typeof value !== "number") return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
export async function GET(req: NextRequest) {
const mode = req.headers.get("x-news-mode")?.toLowerCase();
const summaryHeader = req.headers.get("x-news-summary")?.toLowerCase();
const latestSummary = mode === "latest" || summaryHeader === "true";
const news = await db.fantasyNews.findMany({
orderBy: [{ newsTime: "desc" }, { createdAt: "desc" }],
take: latestSummary ? 4 : undefined,
});
return NextResponse.json({
data: news.map((item) => ({
id: item.id,
icon: item.icon,
title: item.title,
description: latestSummary ? summarize(item.description) : item.description,
newsTime: item.newsTime.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
})),
});
}
export async function POST(req: NextRequest) {
const admin = await requireApiAdmin(req);
if (!admin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { icon, title, description, newsTime } = await req.json().catch(() => ({}));
const parsedNewsTime = parseNewsTime(newsTime);
if (
typeof icon !== "string" ||
!icon.trim() ||
typeof title !== "string" ||
!title.trim() ||
typeof description !== "string" ||
!description.trim() ||
!parsedNewsTime
) {
return NextResponse.json(
{ error: "icon، title، description و newsTime معتبر الزامی هستند" },
{ status: 400 },
);
}
const item = await db.fantasyNews.create({
data: {
icon: icon.trim(),
title: title.trim(),
description: description.trim(),
newsTime: parsedNewsTime,
},
});
return NextResponse.json(
{
id: item.id,
icon: item.icon,
title: item.title,
description: item.description,
newsTime: item.newsTime.toISOString(),
createdAt: item.createdAt.toISOString(),
},
{ status: 201 },
);
}