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.
 
 
 

261 lines
7.3 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 IDs
const userRoleRecords = await db.query.userRoles.findMany({
where: and(eq(userRoles.userId, userId), eq(userRoles.status, 'approved')),
})
const userRoleIds = userRoleRecords.map((ur) => ur.roleId)
// If user has no approved roles, they can't see any products
if (userRoleIds.length === 0) {
return []
}
// Get all products assigned to these roles
const visibleProducts = await db.query.productRoleVisibility.findMany({
where: inArray(productRoleVisibility.roleId, userRoleIds),
})
// Return unique product IDs
return [...new Set(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()
// Find role by code
const role = await db.query.roles.findFirst({
where: eq(roles.code, roleCode),
})
if (!role) {
throw new Error(`Role '${roleCode}' not found`)
}
// Check if user already has this role
const existing = await db.query.userRoles.findFirst({
where: and(eq(userRoles.userId, userId), eq(userRoles.roleId, role.id)),
})
if (existing) {
throw new Error(`User already has role '${roleCode}'`)
}
// Create user role assignment (approved in MVP)
const [userRole] = await db
.insert(userRoles)
.values({
userId,
roleId: role.id,
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 []
}
// Get role IDs
const roleRecords = await db.query.roles.findMany({
where: inArray(roles.code, roleCodes),
})
const assignments = []
// Create product-role assignments
for (const role of roleRecords) {
// Check if assignment already exists
const existing = await db.query.productRoleVisibility.findFirst({
where: and(
eq(productRoleVisibility.productId, productId),
eq(productRoleVisibility.roleId, role.id)
),
})
if (!existing) {
const [assignment] = await db
.insert(productRoleVisibility)
.values({
productId,
roleId: role.id,
})
.returning()
assignments.push(assignment)
}
}
return assignments
}