AeThex-OS/server/org-middleware.ts
MrPiglr 4b84eedbd3 feat: Add revenue_events table and fix migration FK constraints
- Add revenue_events table to track org/project revenue with source tracking
- Add Drizzle schema for revenue_events with proper org/project references
- Create migration 0006_revenue_events.sql with indexes
- Fix migration 0004: Remove FK constraints to profiles.id (auth schema incompatibility)
- Document auth.users/profiles.id type mismatch (UUID vs VARCHAR)
- Harden profile update authorization (self-update or org admin/owner only)
- Complete org-scoping security audit implementation (42 gaps closed)
2026-01-05 04:54:12 +00:00

194 lines
5.3 KiB
TypeScript

import { Request, Response, NextFunction } from "express";
import { supabase } from "./supabase.js";
// Extend Express Request to include org context
declare global {
namespace Express {
interface Request {
orgId?: string;
orgRole?: string;
orgMemberId?: string;
orgMembership?: {
id: string;
organization_id: string;
user_id: string;
role: string;
};
}
}
}
/**
* Middleware: Attach organization context to request
* Looks for org ID in header 'x-org-id' or session
* Non-blocking - sets orgId if found, continues if not
*/
export async function attachOrgContext(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.session.userId;
if (!userId) {
return next(); // No user, no org context
}
// Try to get org ID from header first
let orgId = req.headers['x-org-id'] as string;
// If no header, try session (if we add session-based org selection later)
if (!orgId && (req.session as any).currentOrgId) {
orgId = (req.session as any).currentOrgId;
}
// If still no org, try to get user's default/first org
if (!orgId) {
const { data: membership } = await supabase
.from('organization_members')
.select('organization_id')
.eq('user_id', userId)
.limit(1)
.single();
if (membership) {
orgId = membership.organization_id;
}
}
// If we have an org, verify membership and attach context
if (orgId) {
const { data: membership, error } = await supabase
.from('organization_members')
.select('*')
.eq('organization_id', orgId)
.eq('user_id', userId)
.single();
if (!error && membership) {
req.orgId = orgId;
req.orgRole = membership.role;
req.orgMemberId = membership.id;
req.orgMembership = membership;
}
}
next();
} catch (error) {
console.error('Error attaching org context:', error);
next(); // Continue even on error
}
}
/**
* Middleware: Require organization membership
* Must be used after attachOrgContext
*/
export function requireOrgMember(req: Request, res: Response, next: NextFunction) {
if (!req.orgId || !req.orgRole) {
return res.status(400).json({
error: "Organization context required",
message: "Please select an organization (x-org-id header) to access this resource"
});
}
next();
}
/**
* Middleware: Require specific org role
*/
export function requireOrgRole(minRole: 'owner' | 'admin' | 'member' | 'viewer') {
const roleHierarchy = ['viewer', 'member', 'admin', 'owner'];
const minLevel = roleHierarchy.indexOf(minRole);
return (req: Request, res: Response, next: NextFunction) => {
if (!req.orgRole) {
return res.status(403).json({ error: "Organization role required" });
}
const userLevel = roleHierarchy.indexOf(req.orgRole);
if (userLevel < minLevel) {
return res.status(403).json({
error: "Insufficient permissions",
required: minRole,
current: req.orgRole
});
}
next();
};
}
/**
* Helper: Check if user has access to a project
* Returns true if user is:
* - Project owner
* - Project collaborator with sufficient role
* - Org member (if project is in an org)
*/
export async function assertProjectAccess(
projectId: string,
userId: string,
minRole: 'owner' | 'admin' | 'contributor' | 'viewer' = 'viewer'
): Promise<{ hasAccess: boolean; reason?: string; project?: any }> {
try {
// Get project
const { data: project, error: projectError } = await supabase
.from('projects')
.select('*')
.eq('id', projectId)
.single();
if (projectError || !project) {
return { hasAccess: false, reason: 'Project not found' };
}
// Check if user is owner
const ownerId = project.owner_user_id || project.user_id || project.owner_id;
if (ownerId === userId) {
return { hasAccess: true, project };
}
// Check collaborator status
const { data: collab } = await supabase
.from('project_collaborators')
.select('role')
.eq('project_id', projectId)
.eq('user_id', userId)
.single();
if (collab) {
const roleHierarchy = ['viewer', 'contributor', 'admin', 'owner'];
const userLevel = roleHierarchy.indexOf(collab.role);
const minLevel = roleHierarchy.indexOf(minRole);
if (userLevel >= minLevel) {
return { hasAccess: true, project };
}
}
// Check org membership (if project is in an org)
if (project.organization_id) {
const { data: orgMember } = await supabase
.from('organization_members')
.select('role')
.eq('organization_id', project.organization_id)
.eq('user_id', userId)
.single();
if (orgMember) {
// Org members can at least view org projects
if (minRole === 'viewer') {
return { hasAccess: true, project };
}
// Admin+ can manage
if (['admin', 'owner'].includes(orgMember.role)) {
return { hasAccess: true, project };
}
}
}
return { hasAccess: false, reason: 'Insufficient permissions' };
} catch (error) {
console.error('Error checking project access:', error);
return { hasAccess: false, reason: 'Access check failed' };
}
}