import { Router } from "express"; import { db } from "@workspace/db"; import { usersTable } from "@workspace/db"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; import { logAudit } from "../lib/audit-logger.js"; const router = Router(); function serializeUser(u: typeof usersTable.$inferSelect) { return { id: u.id, name: u.name, email: u.email, team: u.team, role: u.role, avatarColor: u.avatarColor, createdAt: u.createdAt.toISOString(), }; } import { requireAuth, requireAdmin } from "../middleware/auth.js"; import { z } from "zod"; import { createClient } from "@supabase/supabase-js"; const supabaseUrl = process.env.SUPABASE_URL || "https://fgzfelifumkwjfdrswxc.supabase.co"; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || "your_supabase_service_role_key_here"; export const supabase = createClient(supabaseUrl, supabaseKey); export const supabaseAdmin = createClient(supabaseUrl, supabaseKey, { auth: { persistSession: false } }); export async function uploadAttachment( fileBuffer: Buffer, fileName: string, mimeType: string ): Promise { // Ensure the task-attachments bucket exists try { const { error: getError } = await supabaseAdmin.storage.getBucket("task-attachments"); if (getError) { console.log("Bucket 'task-attachments' not found, attempting programmatic creation..."); const { error: createError } = await supabaseAdmin.storage.createBucket("task-attachments", { public: true, }); if (createError) { console.warn("Bucket creation warning:", createError.message); } else { console.log("✅ Bucket 'task-attachments' created successfully!"); } } } catch (bucketErr: any) { console.warn("Bucket pre-check error:", bucketErr.message); } const uniqueName = `${Date.now()}-${fileName}`; const { data, error } = await supabaseAdmin.storage .from("task-attachments") .upload(uniqueName, fileBuffer, { contentType: mimeType, upsert: true, }); if (error) { throw new Error(`Failed to upload file to Supabase Storage: ${error.message}`); } const { data: publicUrlData } = supabaseAdmin.storage .from("task-attachments") .getPublicUrl(uniqueName); if (!publicUrlData || !publicUrlData.publicUrl) { throw new Error("Failed to retrieve public URL from Supabase Storage"); } return publicUrlData.publicUrl; } export async function deleteAttachmentFromStorage(fileUrl: string): Promise { const urlParts = fileUrl.split("/task-attachments/"); if (urlParts.length < 2) { throw new Error("Invalid attachment URL format"); } const filePath = decodeURIComponent(urlParts[1]); const { error } = await supabaseAdmin.storage .from("task-attachments") .remove([filePath]); if (error) { throw new Error(`Failed to delete file from Supabase Storage: ${error.message}`); } } const loginSchema = z.object({ email: z.string().email("صيغة البريد الإلكتروني غير صحيحة").min(1, "البريد الإلكتروني مطلوب"), password: z.string().min(1, "كلمة المرور مطلوبة") }).strict(); router.post("/auth/login", async (req, res) => { try { const parseResult = loginSchema.safeParse(req.body); if (!parseResult.success) { res.status(400).json({ error: parseResult.error.errors[0].message }); return; } const { email, password } = parseResult.data; const { data: authData, error: authErr } = await supabase.auth.signInWithPassword({ email: email.toLowerCase().trim(), password, }); if (authErr || !authData.user || !authData.session) { logAudit({ userId: 1, // Fallback ID for unknown user action: "login_failure", entityType: "auth", details: { attemptedEmail: email.toLowerCase().trim(), reason: authErr?.message ?? "Invalid credentials" }, req, }); res.status(401).json({ error: "البريد الإلكتروني أو كلمة المرور غير صحيحة" }); return; } const supabaseUid = authData.user.id; let [user] = await db.select().from(usersTable).where(eq(usersTable.userId, supabaseUid)); if (!user) { // Fallback check by email to auto-link if needed [user] = await db.select().from(usersTable).where(eq(usersTable.email, email.toLowerCase().trim())); if (user && !user.userId) { [user] = await db.update(usersTable).set({ userId: supabaseUid }).where(eq(usersTable.id, user.id)).returning(); } } if (!user) { logAudit({ userId: 1, // Fallback ID for unknown user action: "login_failure", entityType: "auth", details: { attemptedEmail: email, reason: "User exists in Auth but not in public.users", supabaseUid }, req, }); res.status(401).json({ error: "حساب المستخدم غير موجود في النظام الأساسي" }); return; } logAudit({ userId: user.id, // Prefer internal user.id over supabaseUid action: "login_success", entityType: "auth", details: { attemptedEmail: user.email, supabaseUid }, req, }); // Return Supabase access token instead of local JWT res.json({ token: authData.session.access_token, user: serializeUser(user) }); } catch (err) { req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Login failed"); res.status(500).json({ error: "Internal server error" }); } }); // Use requireAuth for /auth/me router.post("/auth/me", requireAuth, async (req: any, res: any) => { try { // req.user is already loaded by requireAuth if (!req.user) { res.status(404).json({ error: "User not found" }); return; } res.json({ user: serializeUser(req.user) }); } catch (err) { req.log.error({ err }, "Auth me failed"); res.status(500).json({ error: "Internal server error" }); } }); router.post("/auth/register", requireAuth, requireAdmin, async (req, res) => { try { const { name, email, password, team, role, avatarColor } = req.body as { name?: string; email?: string; password?: string; team?: string; role?: string; avatarColor?: string; }; if (!name || !email || !password || !team) { res.status(400).json({ error: "جميع الحقول مطلوبة" }); return; } const existing = await db.select().from(usersTable).where(eq(usersTable.email, email.toLowerCase().trim())); if (existing.length > 0) { res.status(409).json({ error: "البريد الإلكتروني مستخدم مسبقاً" }); return; } // Create in Supabase Auth first const { data: authData, error: authErr } = await supabase.auth.admin.createUser({ email: email.toLowerCase().trim(), password, email_confirm: true, user_metadata: { name }, }); if (authErr || !authData.user) { res.status(400).json({ error: authErr?.message ?? "Failed to create user in Supabase Auth" }); return; } const supabaseUid = authData.user.id; const passwordHash = await bcrypt.hash(password, 10); const [user] = await db.insert(usersTable).values({ userId: supabaseUid, name, email: email.toLowerCase().trim(), passwordHash, team, role: role ?? "member", avatarColor: avatarColor ?? "#6366f1", }).returning(); logAudit({ userId: (req as any).userId || user.id, action: "role_change", entityType: "user", entityId: user.id, details: { newUserId: user.id, email: user.email, role: user.role, supabaseUid, note: "User registered via admin auth endpoint" }, req, }); res.status(201).json({ user: serializeUser(user) }); } catch (err) { req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Register failed"); res.status(500).json({ error: "Internal server error" }); } }); export default router;