You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

291 lines
8.4 KiB

/**
* 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<string[]> {
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<boolean> {
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<string[]> {
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<string[]> {
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<string, Array<'private' | 'educator' | 'company'>> = {
'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
}