Implement Role Management Features and UI Enhancements
- Introduced a new composable `useActiveRole` for managing user roles, including fetching role status and switching roles with server validation. - Updated `RoleSwitcher.vue` to utilize the new composable, enhancing role selection with improved error handling and UI feedback. - Added new API endpoints for role management, including fetching user role status and switching active roles. - Enhanced product visibility logic to filter based on the user's active role, ensuring a tailored experience. - Updated database schema to support last active role tracking for users, improving session management. - Refined UI components across the application to reflect role-based changes and improve user experience.
This commit is contained in:
111
server/utils/role-session.ts
Normal file
111
server/utils/role-session.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { users } from '../database/schema'
|
||||
import { getUserApprovedRoleCodes } from './roles'
|
||||
|
||||
/**
|
||||
* Role validation cache TTL (Time To Live)
|
||||
* Roles are re-validated against DB after this period
|
||||
*/
|
||||
const ROLE_VALIDATION_TTL = 5 * 60 * 1000 // 5 minutes in milliseconds
|
||||
|
||||
/**
|
||||
* Get user's active role with automatic validation
|
||||
*
|
||||
* Features:
|
||||
* - Returns cached role from session if TTL hasn't expired
|
||||
* - Re-validates against DB after TTL expires (every 5 minutes)
|
||||
* - Auto-fallback if active role was revoked by admin
|
||||
* - Updates session automatically with validated data
|
||||
*
|
||||
* @param event - H3 Event from API handler
|
||||
* @returns Active role code (e.g., 'private', 'educator', 'company')
|
||||
* @throws 401 if user is not authenticated
|
||||
*/
|
||||
export async function getUserActiveRole(event: H3Event): Promise<string> {
|
||||
const session = await getUserSession(event)
|
||||
|
||||
if (!session?.user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Not authenticated',
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const lastValidated = session.rolesLastValidated
|
||||
? new Date(session.rolesLastValidated).getTime()
|
||||
: 0
|
||||
const needsRevalidation = now - lastValidated > ROLE_VALIDATION_TTL
|
||||
|
||||
if (needsRevalidation) {
|
||||
// TTL expired → Re-validate against database
|
||||
const currentRoles = await getUserApprovedRoleCodes(session.user.id)
|
||||
const activeRole = session.activeRoleCode || 'private'
|
||||
|
||||
// Check: Was active role revoked by admin?
|
||||
if (!currentRoles.includes(activeRole)) {
|
||||
// Auto-fallback to first available role
|
||||
const newActiveRole = currentRoles[0] || 'private'
|
||||
|
||||
await replaceUserSession(event, {
|
||||
activeRoleCode: newActiveRole,
|
||||
userApprovedRoles: currentRoles,
|
||||
rolesLastValidated: new Date().toISOString(),
|
||||
roleChangedByAdmin: true, // Flag for client notification
|
||||
})
|
||||
|
||||
return newActiveRole
|
||||
}
|
||||
|
||||
// Role still valid, just update cache + timestamp
|
||||
await replaceUserSession(event, {
|
||||
userApprovedRoles: currentRoles,
|
||||
rolesLastValidated: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
return session.activeRoleCode || 'private'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user's active role with validation
|
||||
*
|
||||
* Validates that user has the requested role before switching.
|
||||
* Updates both session (immediate) and database (preference for next login).
|
||||
*
|
||||
* @param event - H3 Event from API handler
|
||||
* @param roleCode - Role to switch to (must be in user's approved roles)
|
||||
* @throws 401 if not authenticated
|
||||
* @throws 403 if user doesn't have the requested role
|
||||
*/
|
||||
export async function setUserActiveRole(
|
||||
event: H3Event,
|
||||
roleCode: string
|
||||
): Promise<void> {
|
||||
const session = await requireUserSession(event)
|
||||
|
||||
// Validate that user has this role
|
||||
const approvedRoles = await getUserApprovedRoleCodes(session.user.id)
|
||||
|
||||
if (!approvedRoles.includes(roleCode)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Du hast diese Rolle nicht',
|
||||
})
|
||||
}
|
||||
|
||||
// Update session with new active role
|
||||
await replaceUserSession(event, {
|
||||
activeRoleCode: roleCode,
|
||||
roleChangedByAdmin: false, // User switched manually
|
||||
rolesLastValidated: new Date().toISOString(), // Reset TTL
|
||||
})
|
||||
|
||||
// Update database: Save as user preference for next login
|
||||
const db = useDatabase()
|
||||
await db
|
||||
.update(users)
|
||||
.set({ lastActiveRoleCode: roleCode })
|
||||
.where(eq(users.id, session.user.id))
|
||||
}
|
||||
@@ -120,6 +120,48 @@ export async function getVisibleProductIdsForUser(userId: string): Promise<strin
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user