o134's picture
Upload full TeamTasker system with all fixes
8314cf4 verified
Raw
History Blame Contribute Delete
11.5 kB
import { Router } from "express";
import { db } from "@workspace/db";
import { usersTable } from "@workspace/db";
import { eq } from "drizzle-orm";
import { CreateUserBody, UpdateUserBody, UpdateUserParams } from "@workspace/api-zod";
import { logAudit } from "../lib/audit-logger.js";
import { requireAdmin, requireManagerOrAdmin, requireAuth, AuthRequest } from "../middleware/auth.js";
import { canAccessUser, User, isAdmin, isHeadOfTeam, isTeamLead, canAccessAllTeams, ROLES } from "../lib/auth-utils.js";
import { z } from "zod";
import bcrypt from "bcryptjs";
const router = Router();
router.get("/users", requireManagerOrAdmin, async (req: AuthRequest, res) => {
try {
const requestingUser = req.user as User;
let { team } = req.query as { team?: string };
let rows;
if (canAccessAllTeams(requestingUser)) {
// Admins and Head of Team see all users or filtered by team
rows = team
? await db.select().from(usersTable).where(eq(usersTable.team, team)).orderBy(usersTable.name)
: await db.select().from(usersTable).orderBy(usersTable.name);
} else if (isTeamLead(requestingUser)) {
// Team Leads only see their own team
rows = await db.select().from(usersTable).where(eq(usersTable.team, requestingUser.team)).orderBy(usersTable.name);
} else {
// Members can only see themselves
rows = await db.select().from(usersTable).where(eq(usersTable.id, requestingUser.id));
}
res.json(rows.map(u => ({
id: u.id,
name: u.name,
email: u.email,
team: u.team,
role: u.role,
avatarColor: u.avatarColor,
createdAt: u.createdAt.toISOString(),
})));
} catch (err) {
req.log.error({ err }, "Failed to list users");
res.status(500).json({ error: "Internal server error" });
}
});
import { supabase } from "./auth.js";
const createUserSchema = z.object({
email: z.string().email("صيغة البريد الإلكتروني غير صحيحة"),
name: z.string().trim().min(1, "الاسم مطلوب"),
role: z.enum(["admin", "head_of_team", "team_lead", "member"], { errorMap: () => ({ message: "دور غير صالح" }) }),
password: z.string()
.min(8, "كلمة المرور يجب أن تكون 8 أحرف على الأقل")
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, "كلمة المرور يجب أن تحتوي على حرف كبير، حرف صغير، ورقم"),
avatarColor: z.string().optional(),
team: z.string().optional()
}).strict();
router.post("/users", requireManagerOrAdmin, async (req: AuthRequest, res) => {
try {
const requestingUser = req.user as User;
const parseResult = createUserSchema.safeParse(req.body);
if (!parseResult.success) {
res.status(400).json({ error: parseResult.error.errors[0].message });
return;
}
const { email, name, role, password, avatarColor, team } = parseResult.data;
if (!isAdmin(requestingUser)) {
if (role === "admin") {
res.status(403).json({ error: "لا يمكنك إضافة مدير نظام" });
return;
}
if (isTeamLead(requestingUser) && team !== requestingUser.team) {
res.status(403).json({ error: "يمكنك إضافة أعضاء لفريقك فقط" });
return;
}
}
// Check if user exists
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 },
});
let supabaseUid = authData?.user?.id;
if (authErr || !authData?.user) {
if (process.env.SUPABASE_SERVICE_ROLE_KEY === "your_supabase_service_role_key_here" || process.env.NODE_ENV !== "production") {
req.log.warn("Supabase Service Role Key is placeholder or in dev mode. Generating local UUID for Supabase Auth simulation.");
supabaseUid = crypto.randomUUID();
} else {
res.status(400).json({ error: authErr?.message ?? "Failed to create user in Supabase Auth" });
return;
}
}
const passwordHash = await bcrypt.hash(password, 10);
const [user] = await db.insert(usersTable).values({
userId: supabaseUid,
name,
email: email.toLowerCase().trim(),
role,
passwordHash,
team: team ?? "Unassigned",
avatarColor: avatarColor ?? "#6366f1",
}).returning();
logAudit({
userId: req.userUid ?? supabaseUid,
action: "role_change",
entityType: "user",
entityId: user.id,
details: {
newUserId: user.id,
name: user.name,
email: user.email,
role: user.role,
team: user.team,
avatarColor: user.avatarColor,
supabaseUid,
note: "Initial role assignment on user creation",
},
req,
});
res.status(201).json({
id: user.id,
name: user.name,
email: user.email,
team: user.team,
role: user.role,
avatarColor: user.avatarColor,
createdAt: user.createdAt.toISOString()
});
} catch (err) {
req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to create user");
res.status(500).json({ error: "Internal server error" });
}
});
const getUserParamsSchema = z.object({
id: z.coerce.number().int().positive("معرف المستخدم غير صالح")
});
router.get("/users/:id", requireAuth, async (req: AuthRequest, res) => {
try {
const requestingUser = req.user as User;
const parseResult = getUserParamsSchema.safeParse(req.params);
if (!parseResult.success) {
res.status(400).json({ error: parseResult.error.errors[0].message });
return;
}
const { id } = parseResult.data;
const [targetUser] = await db.select().from(usersTable).where(eq(usersTable.id, id));
if (!targetUser) {
res.status(404).json({ error: "User not found" });
return;
}
let allowed = false;
if (isAdmin(requestingUser)) {
allowed = true;
} else if ((isHeadOfTeam(requestingUser) || isTeamLead(requestingUser)) && requestingUser.team === targetUser.team) {
allowed = true;
} else if (requestingUser.id === targetUser.id) {
allowed = true;
}
if (!allowed) {
res.status(403).json({ error: "Forbidden: you do not have permission to view this profile" });
return;
}
res.json({
id: targetUser.id,
name: targetUser.name,
email: targetUser.email,
team: targetUser.team,
role: targetUser.role,
avatarColor: targetUser.avatarColor,
createdAt: targetUser.createdAt.toISOString(),
});
} catch (err) {
req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to get user");
res.status(500).json({ error: "Internal server error" });
}
});
const updateUserSchema = z.object({
email: z.string().email("صيغة البريد الإلكتروني غير صحيحة").optional(),
name: z.string().trim().min(1, "الاسم مطلوب").optional(),
role: z.enum(["admin", "head_of_team", "team_lead", "member"], { errorMap: () => ({ message: "دور غير صالح" }) }).optional(),
password: z.string()
.min(8, "كلمة المرور يجب أن تكون 8 أحرف على الأقل")
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, "كلمة المرور يجب أن تحتوي على حرف كبير، حرف صغير، ورقم")
.optional(),
avatarColor: z.string().optional(),
team: z.string().optional()
}).strict();
router.patch("/users/:id", requireAuth, async (req: AuthRequest, res) => {
try {
const requestingUser = req.user as User;
const id = Number(req.params.id);
if (isNaN(id)) { res.status(400).json({ error: "Invalid ID" }); return; }
const parseResult = updateUserSchema.safeParse(req.body);
if (!parseResult.success) {
res.status(400).json({ error: parseResult.error.errors[0].message });
return;
}
const { email, name, role, password, avatarColor, team } = parseResult.data;
const [existingUser] = await db.select().from(usersTable).where(eq(usersTable.id, id));
if (!existingUser) { res.status(404).json({ error: "User not found" }); return; }
// RBAC
let allowed = false;
if (isAdmin(requestingUser)) {
allowed = true;
} else if ((isHeadOfTeam(requestingUser) || isTeamLead(requestingUser)) && existingUser.team === requestingUser.team) {
allowed = true;
}
if (!allowed) {
res.status(403).json({ error: "Forbidden: insufficient permissions to update this user" });
return;
}
// Admin role assignment restriction
if (role === "admin" && !isAdmin(requestingUser)) {
res.status(403).json({ error: "Forbidden: only admins can assign admin role" });
return;
}
// Team update restriction
if (team !== undefined && team !== existingUser.team && !isAdmin(requestingUser)) {
res.status(403).json({ error: "Forbidden: only admins can change user team" });
return;
}
let passwordHash = undefined;
if (password) {
passwordHash = await bcrypt.hash(password, 10);
}
const updatePayload: any = {};
if (email) updatePayload.email = email.toLowerCase().trim();
if (name) updatePayload.name = name;
if (role) updatePayload.role = role;
if (avatarColor) updatePayload.avatarColor = avatarColor;
if (team) updatePayload.team = team;
if (passwordHash) updatePayload.passwordHash = passwordHash;
if (Object.keys(updatePayload).length === 0) {
res.json({
id: existingUser.id,
name: existingUser.name,
email: existingUser.email,
team: existingUser.team,
role: existingUser.role,
avatarColor: existingUser.avatarColor,
createdAt: existingUser.createdAt.toISOString(),
});
return;
}
const [updatedUser] = await db.update(usersTable).set(updatePayload).where(eq(usersTable.id, id)).returning();
const permChanged = (role && role !== existingUser.role) || (team && team !== existingUser.team);
if (permChanged) {
const sensitiveChanges: any = {};
if (role && role !== existingUser.role) {
sensitiveChanges.role = { old: existingUser.role, new: updatedUser.role };
}
if (team && team !== existingUser.team) {
sensitiveChanges.team = { old: existingUser.team, new: updatedUser.team };
}
logAudit({
userId: requestingUser.id,
action: "role_change",
entityType: "user",
entityId: updatedUser.id,
details: {
email: updatedUser.email,
changes: sensitiveChanges,
},
req,
});
}
res.json({
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
team: updatedUser.team,
role: updatedUser.role,
avatarColor: updatedUser.avatarColor,
createdAt: updatedUser.createdAt.toISOString(),
});
} catch (err) {
req.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to update user");
res.status(500).json({ error: "Internal server error" });
}
});
export default router;