Spaces:
Sleeping
Sleeping
| 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<string> { | |
| // 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<void> { | |
| 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; | |