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.
249 lines
7.2 KiB
249 lines
7.2 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))]
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|