96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
}
|