/** * Role Management Utilities * * Helper functions for managing user roles and product visibility. * MVP: These functions work with manually assigned roles (status='approved') * Phase 2/3: Will be extended to handle role request/approval workflow */ import { and, eq, inArray } from 'drizzle-orm' import type { SQL } from 'drizzle-orm' import { roles, userRoles, productRoleVisibility } from '../database/schema' /** * Get all approved roles for a user * MVP: Returns roles with status='approved' (manually assigned via DB) * Phase 2/3: Used to check which products a user can see * * @param userId - The user's UUID * @returns Array of role objects with full details */ export async function getUserApprovedRoles(userId: string) { const db = useDatabase() const approvedRoles = await db.query.userRoles.findMany({ where: and(eq(userRoles.userId, userId), eq(userRoles.status, 'approved')), with: { role: true, }, }) return approvedRoles.map((ur) => ur.role) } /** * Get approved role codes for a user (lightweight version) * Returns just the role codes ['private', 'educator'] instead of full objects * * @param userId - The user's UUID * @returns Array of role codes */ export async function getUserApprovedRoleCodes(userId: string): Promise { const approvedRoles = await getUserApprovedRoles(userId) return approvedRoles.map((role) => role.code) } /** * Check if a specific product is visible to a user based on their roles * * Logic: * - If product has NO role assignments → NOT visible (opt-in visibility) * - If product HAS role assignments → visible only if user has at least one matching role * * @param productId - The product's UUID * @param userId - The user's UUID * @returns true if user can see the product, false otherwise */ export async function isProductVisibleForUser( productId: string, userId: string ): Promise { const db = useDatabase() // Get user's approved role codes const userRoleCodes = await getUserApprovedRoleCodes(userId) // Get product's role visibility assignments const productRoles = await db.query.productRoleVisibility.findMany({ where: eq(productRoleVisibility.productId, productId), with: { role: true, }, }) // No role assignments → product is NOT visible (opt-in) if (productRoles.length === 0) { return false } // Check if user has at least one matching role const productRoleCodes = productRoles.map((pr) => pr.role.code) return productRoleCodes.some((code) => userRoleCodes.includes(code)) } /** * Get all products visible to a user based on their roles * Returns a WHERE clause that can be used with Drizzle queries * * Usage: * ```typescript * const visibleProductIds = await getVisibleProductIdsForUser(userId) * const products = await db.query.products.findMany({ * where: inArray(products.id, visibleProductIds) * }) * ``` * * @param userId - The user's UUID * @returns Array of product UUIDs that the user can see */ export async function getVisibleProductIdsForUser(userId: string): Promise { const db = useDatabase() // Get user's approved role codes const userRoleRecords = await db.query.userRoles.findMany({ where: and(eq(userRoles.userId, userId), eq(userRoles.status, 'approved')), }) const userRoleCodes = userRoleRecords.map((ur) => ur.roleCode) // If user has no approved roles, they can't see any products if (userRoleCodes.length === 0) { return [] } // Get all products assigned to these roles const visibleProducts = await db.query.productRoleVisibility.findMany({ where: inArray(productRoleVisibility.roleCode, userRoleCodes), }) // Return unique product IDs return [...new Set(visibleProducts.map((pv) => pv.productId))] } /** * Get products visible for a SPECIFIC role (used with RoleSwitcher) * Unlike getVisibleProductIdsForUser(), this only returns products for ONE role * * Usage: * ```typescript * const activeRole = await getUserActiveRole(event) * const visibleProductIds = await getVisibleProductIdsForRole(userId, activeRole) * ``` * * @param userId - The user's UUID * @param roleCode - Specific role to filter by ('private', 'educator', 'company') * @returns Array of product UUIDs visible for this role */ export async function getVisibleProductIdsForRole( userId: string, roleCode: string ): Promise { const db = useDatabase() // Verify user has this role (approved) const hasRole = await db.query.userRoles.findFirst({ where: and( eq(userRoles.userId, userId), eq(userRoles.roleCode, roleCode), eq(userRoles.status, 'approved') ), }) // User doesn't have this role → return empty array if (!hasRole) { return [] } // Get all products assigned to this specific role const visibleProducts = await db.query.productRoleVisibility.findMany({ where: eq(productRoleVisibility.roleCode, roleCode), }) return visibleProducts.map((pv) => pv.productId) } /** * Assign a role to a user (MVP: manual assignment, always approved) * Phase 2/3: This will be replaced by role request workflow * * @param userId - The user's UUID * @param roleCode - Role code ('private', 'educator', 'company') * @param options - Optional metadata (organization name, admin notes) */ export async function assignRoleToUser( userId: string, roleCode: 'private' | 'educator' | 'company', options?: { organizationName?: string adminNotes?: string } ) { const db = useDatabase() // Check if user already has this role const existing = await db.query.userRoles.findFirst({ where: and(eq(userRoles.userId, userId), eq(userRoles.roleCode, roleCode)), }) if (existing) { throw new Error(`User already has role '${roleCode}'`) } // Create user role assignment (approved in MVP) // Note: No need to lookup role - we can use roleCode directly as FK const [userRole] = await db .insert(userRoles) .values({ userId, roleCode, // Direct reference to roles.code (PK) status: 'approved', // MVP: Always approved organizationName: options?.organizationName, adminNotes: options?.adminNotes, statusHistory: [ { status: 'approved', organizationName: options?.organizationName || null, adminNotes: options?.adminNotes || null, changedAt: new Date().toISOString(), changedBy: null, // MVP: Manual assignment (no admin tracking) }, ], }) .returning() return userRole } /** * Get role by code (helper function) * * @param roleCode - Role code ('private', 'educator', 'company') * @returns Role object or null if not found */ export async function getRoleByCode(roleCode: 'private' | 'educator' | 'company') { const db = useDatabase() return await db.query.roles.findFirst({ where: eq(roles.code, roleCode), }) } /** * Assign roles to a product based on category (for ERP import) * * Category Mapping: * - 'makerspace-annual-pass' → ['private', 'educator'] * - 'annual-pass' → ['private'] * - 'educator-annual-pass' → ['educator'] * - 'company-annual-pass' → ['company'] * * @param productId - The product's UUID * @param category - Product category from NAV ERP */ export async function assignRolesToProductByCategory( productId: string, category: string ) { const db = useDatabase() // Category to role mapping const categoryRoleMapping: Record> = { 'makerspace-annual-pass': ['private', 'educator'], 'annual-pass': ['private'], 'educator-annual-pass': ['educator'], 'company-annual-pass': ['company'], } const roleCodes = categoryRoleMapping[category] || [] if (roleCodes.length === 0) { console.warn(`No role mapping found for category '${category}'`) return [] } const assignments = [] // Create product-role assignments // Note: No need to lookup roles - we can use roleCode directly as FK for (const roleCode of roleCodes) { // Check if assignment already exists const existing = await db.query.productRoleVisibility.findFirst({ where: and( eq(productRoleVisibility.productId, productId), eq(productRoleVisibility.roleCode, roleCode) ), }) if (!existing) { const [assignment] = await db .insert(productRoleVisibility) .values({ productId, roleCode, // Direct reference to roles.code (PK) }) .returning() assignments.push(assignment) } } return assignments }