File size: 8,043 Bytes
8314cf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
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;