RAQIM Deploy
Deploy RAQIM 2026-05-02 19:06
0e14acb
import { Router } from "express";
import bcrypt from "bcryptjs";
import rateLimit from "express-rate-limit";
import { db } from "@workspace/db";
import { usersTable, refreshTokensTable } from "@workspace/db";
import { eq, and, gt } from "drizzle-orm";
import {
generateAccessToken,
generateRefreshToken,
requireAuth,
AuthRequest,
} from "../middlewares/auth.js";
const router = Router();
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { error: "too_many_requests", message: "محاولات كثيرة جداً. حاول مجدداً بعد 15 دقيقة." },
standardHeaders: true,
legacyHeaders: false,
skip: () => process.env.NODE_ENV === "test",
});
const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
message: { error: "too_many_requests", message: "محاولات كثيرة جداً. حاول مجدداً لاحقاً." },
standardHeaders: true,
legacyHeaders: false,
});
function validatePassword(password: string): string | null {
if (!password || password.length < 8) return "كلمة المرور يجب أن تكون 8 أحرف على الأقل";
if (password.length > 128) return "كلمة المرور طويلة جداً";
return null;
}
// POST /api/auth/register — submit join request
router.post("/register", registerLimiter, async (req, res) => {
try {
const { email, password, displayName } = req.body;
if (!email || !password) {
res.status(400).json({ error: "validation", message: "البريد الإلكتروني وكلمة المرور مطلوبان" });
return;
}
const pwError = validatePassword(password);
if (pwError) {
res.status(400).json({ error: "validation", message: pwError });
return;
}
const existing = await db.query.usersTable.findFirst({ where: eq(usersTable.email, email.toLowerCase()) });
if (existing) {
res.status(409).json({ error: "conflict", message: "البريد الإلكتروني مستخدم بالفعل" });
return;
}
const passwordHash = await bcrypt.hash(password, 12);
await db.insert(usersTable).values({
email: email.toLowerCase(),
passwordHash,
displayName: displayName || null,
role: "user",
status: "pending",
});
res.status(201).json({ message: "تم إرسال طلب الانضمام. في انتظار موافقة المسؤول." });
} catch (err) {
req.log?.error({ err }, "register error");
res.status(500).json({ error: "server_error", message: "فشل تسجيل الطلب" });
}
});
// POST /api/auth/login
router.post("/login", loginLimiter, async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
res.status(400).json({ error: "validation", message: "البريد الإلكتروني وكلمة المرور مطلوبان" });
return;
}
const user = await db.query.usersTable.findFirst({ where: eq(usersTable.email, email.toLowerCase()) });
if (!user || !user.passwordHash) {
res.status(401).json({ error: "unauthorized", message: "بيانات الدخول غير صحيحة" });
return;
}
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
res.status(401).json({ error: "unauthorized", message: "بيانات الدخول غير صحيحة" });
return;
}
if (user.status === "pending") {
res.status(401).json({ error: "pending", message: "الحساب في انتظار موافقة المسؤول" });
return;
}
if (user.status === "suspended") {
res.status(401).json({ error: "suspended", message: "تم إيقاف هذا الحساب" });
return;
}
await db.update(usersTable).set({ lastLoginAt: new Date() }).where(eq(usersTable.id, user.id));
const accessToken = generateAccessToken(user.id, user.role, user.status);
const refreshToken = generateRefreshToken(user.id);
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
await db.insert(refreshTokensTable).values({ userId: user.id, token: refreshToken, expiresAt });
res.cookie("refresh_token", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 30 * 24 * 60 * 60 * 1000,
sameSite: "lax",
});
res.json({
accessToken,
user: { id: user.id, email: user.email, displayName: user.displayName, role: user.role, status: user.status, createdAt: user.createdAt },
});
} catch (err) {
req.log?.error({ err }, "login error");
res.status(500).json({ error: "server_error", message: "فشل تسجيل الدخول" });
}
});
// POST /api/auth/logout
router.post("/logout", async (req, res) => {
const refreshToken = req.cookies?.refresh_token;
if (refreshToken) {
await db.delete(refreshTokensTable).where(eq(refreshTokensTable.token, refreshToken));
}
res.clearCookie("refresh_token");
res.json({ message: "تم تسجيل الخروج" });
});
// POST /api/auth/refresh
router.post("/refresh", async (req, res) => {
try {
const refreshToken = req.cookies?.refresh_token;
if (!refreshToken) {
res.status(401).json({ error: "unauthorized", message: "لا يوجد رمز تحديث" });
return;
}
const storedToken = await db.query.refreshTokensTable.findFirst({
where: and(eq(refreshTokensTable.token, refreshToken), gt(refreshTokensTable.expiresAt, new Date())),
});
if (!storedToken) {
res.status(401).json({ error: "unauthorized", message: "رمز التحديث غير صالح" });
return;
}
const user = await db.query.usersTable.findFirst({ where: eq(usersTable.id, storedToken.userId) });
if (!user || user.status !== "active") {
res.status(401).json({ error: "unauthorized", message: "الحساب غير نشط" });
return;
}
const accessToken = generateAccessToken(user.id, user.role, user.status);
res.json({
accessToken,
user: { id: user.id, email: user.email, displayName: user.displayName, role: user.role, status: user.status, createdAt: user.createdAt },
});
} catch (err) {
req.log?.error({ err }, "refresh error");
res.status(500).json({ error: "server_error", message: "فشل تجديد الجلسة" });
}
});
// GET /api/auth/me
router.get("/me", requireAuth, async (req: AuthRequest, res) => {
try {
const user = await db.query.usersTable.findFirst({ where: eq(usersTable.id, req.userId!) });
if (!user) { res.status(404).json({ error: "not_found", message: "المستخدم غير موجود" }); return; }
res.json({ id: user.id, email: user.email, displayName: user.displayName, role: user.role, status: user.status, createdAt: user.createdAt });
} catch (err) {
req.log?.error({ err }, "me error");
res.status(500).json({ error: "server_error", message: "فشل جلب بيانات المستخدم" });
}
});
// POST /api/auth/forgot-password
router.post("/forgot-password", async (req, res) => {
try {
const { email } = req.body;
if (!email) { res.status(400).json({ error: "validation", message: "البريد الإلكتروني مطلوب" }); return; }
const user = await db.query.usersTable.findFirst({ where: eq(usersTable.email, email.toLowerCase()) });
if (user) {
const token = crypto.randomUUID();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
await db.update(usersTable).set({ resetToken: token, resetTokenExpiresAt: expiresAt }).where(eq(usersTable.id, user.id));
// TODO: Send email. Token available in server log for dev.
req.log?.info({ token, email }, "Password reset token generated (dev mode — no email sent)");
}
res.json({ message: "إذا كان البريد الإلكتروني مسجلاً، سيتم إرسال رابط إعادة التعيين." });
} catch (err) {
req.log?.error({ err }, "forgot-password error");
res.status(500).json({ error: "server_error", message: "فشلت العملية" });
}
});
// POST /api/auth/reset-password
router.post("/reset-password", async (req, res) => {
try {
const { token, password } = req.body;
if (!token || !password) {
res.status(400).json({ error: "validation", message: "الرمز وكلمة المرور مطلوبان" });
return;
}
const pwError = validatePassword(password);
if (pwError) {
res.status(400).json({ error: "validation", message: pwError });
return;
}
const user = await db.query.usersTable.findFirst({
where: and(eq(usersTable.resetToken, token), gt(usersTable.resetTokenExpiresAt!, new Date())),
});
if (!user) {
res.status(400).json({ error: "invalid_token", message: "الرمز غير صالح أو منتهي الصلاحية" });
return;
}
const passwordHash = await bcrypt.hash(password, 12);
await db.update(usersTable).set({ passwordHash, resetToken: null, resetTokenExpiresAt: null }).where(eq(usersTable.id, user.id));
await db.delete(refreshTokensTable).where(eq(refreshTokensTable.userId, user.id));
res.json({ message: "تم إعادة تعيين كلمة المرور بنجاح" });
} catch (err) {
req.log?.error({ err }, "reset-password error");
res.status(500).json({ error: "server_error", message: "فشلت العملية" });
}
});
// PATCH /api/auth/profile — update own display name
router.patch("/profile", requireAuth, async (req: AuthRequest, res) => {
try {
const { displayName } = req.body;
const [updated] = await db.update(usersTable)
.set({ displayName: displayName || null, updatedAt: new Date() })
.where(eq(usersTable.id, req.userId!))
.returning();
res.json({ id: updated.id, email: updated.email, displayName: updated.displayName, role: updated.role, status: updated.status });
} catch (err) {
req.log?.error({ err }, "profile update error");
res.status(500).json({ error: "server_error", message: "فشل تحديث الملف الشخصي" });
}
});
// POST /api/auth/change-password — change own password
router.post("/change-password", requireAuth, async (req: AuthRequest, res) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
res.status(400).json({ error: "validation", message: "كلمة المرور الحالية والجديدة مطلوبتان" });
return;
}
const pwError = validatePassword(newPassword);
if (pwError) { res.status(400).json({ error: "validation", message: pwError }); return; }
const user = await db.query.usersTable.findFirst({ where: eq(usersTable.id, req.userId!) });
if (!user || !user.passwordHash) { res.status(404).json({ error: "not_found" }); return; }
const valid = await bcrypt.compare(currentPassword, user.passwordHash);
if (!valid) { res.status(401).json({ error: "unauthorized", message: "كلمة المرور الحالية غير صحيحة" }); return; }
const passwordHash = await bcrypt.hash(newPassword, 12);
await db.update(usersTable).set({ passwordHash, updatedAt: new Date() }).where(eq(usersTable.id, user.id));
res.json({ message: "تم تغيير كلمة المرور بنجاح" });
} catch (err) {
req.log?.error({ err }, "change-password error");
res.status(500).json({ error: "server_error", message: "فشل تغيير كلمة المرور" });
}
});
export default router;