File size: 3,557 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
import { Request, Response, NextFunction } from "express";
import { supabase } from "../routes/auth.js";
import { db } from "@workspace/db";
import { usersTable } from "@workspace/db";
import { eq } from "drizzle-orm";
import { logAudit } from "../lib/audit-logger.js";

export interface AuthRequest extends Request {
  userId?: number;
  userUid?: string;
  user?: typeof usersTable.$inferSelect;
}

export async function requireAuth(req: AuthRequest, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    logAudit({
      userId: req.userUid ?? req.userId ?? 1,
      action: "unauthorized_access",
      entityType: "auth",
      details: {
        attemptedEndpoint: req.originalUrl || req.url,
        httpMethod: req.method,
        reason: "Missing or invalid authorization header",
      },
      req,
    });
    res.status(401).json({ error: "Unauthorized" });
    return;
  }
  const token = authHeader.slice(7);
  const { data: { user: supabaseUser }, error } = await supabase.auth.getUser(token);
  
  if (error || !supabaseUser) {
    logAudit({
      userId: req.userUid ?? req.userId ?? 1,
      action: "unauthorized_access",
      entityType: "auth",
      details: {
        attemptedEndpoint: req.originalUrl || req.url,
        httpMethod: req.method,
        reason: error?.message ?? "Invalid or expired token",
      },
      req,
    });
    res.status(401).json({ error: "Invalid or expired token" });
    return;
  }
  
  const [user] = await db.select().from(usersTable).where(eq(usersTable.userId, supabaseUser.id));
  if (!user) {
    logAudit({
      userId: supabaseUser.id,
      action: "unauthorized_access",
      entityType: "auth",
      details: {
        attemptedEndpoint: req.originalUrl || req.url,
        httpMethod: req.method,
        reason: "User no longer exists in database",
      },
      req,
    });
    res.status(401).json({ error: "User no longer exists" });
    return;
  }

  req.userId = user.id;
  req.userUid = user.userId ?? String(user.id);
  req.user = user;
  (req as any).userUid = req.userUid; // Ensure any generic Request cast can read it
  next();
}

export function requireRoles(...roles: string[]) {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user) {
      logAudit({
        userId: 1,
        action: "unauthorized_access",
        entityType: "auth",
        details: {
          attemptedEndpoint: req.originalUrl || req.url,
          httpMethod: req.method,
          reason: "Unauthorized: No user loaded",
        },
        req,
      });
      res.status(401).json({ error: "Unauthorized" });
      return;
    }
    if (!roles.includes(req.user.role)) {
      logAudit({
        userId: req.user.id,
        action: "unauthorized_access",
        entityType: "auth",
        details: {
          attemptedEndpoint: req.originalUrl || req.url,
          httpMethod: req.method,
          callerRole: req.user.role,
          requiredRoles: roles,
          reason: "Forbidden: insufficient role",
        },
        req,
      });
      res.status(403).json({ error: "Forbidden: insufficient role" });
      return;
    }
    next();
  };
}

export const requireAdmin = requireRoles("admin");
export const requireHeadOfTeam = requireRoles("admin", "head_of_team");
export const requireTeamLeadOrAbove = requireRoles("admin", "head_of_team", "team_lead", "dept_manager");
export const requireManagerOrAdmin = requireTeamLeadOrAbove; // Compatibility alias