Add role-based visibility and management features for products

- Introduced a role-based visibility system for products, ensuring that only users with approved roles can view specific products.
- Added new database tables for roles, user roles, and product role visibility to manage access control.
- Implemented utility functions for role management, including fetching approved roles, checking product visibility, and assigning roles to users and products.
- Updated API endpoints to filter products based on user roles, enhancing security and user experience.
- Prepared the database schema for future role request and approval workflows in upcoming phases.
This commit is contained in:
Bastian Masanek
2025-11-02 10:17:40 +01:00
parent 6e4f858883
commit ff9960edef
10 changed files with 1865 additions and 26 deletions

145
CLAUDE.md
View File

@@ -178,6 +178,14 @@ See `docs/ARCHITECTURE.md` for full schema. Key tables:
- `products` - Products synced from NAV ERP
- `carts` / `cart_items` - Shopping cart
- `orders` / `order_items` - Orders and line items
- **`roles`** - Role definitions (private, educator, company)
- **`user_roles`** - Many-to-Many User ↔ Roles (with approval workflow prepared for Phase 2/3)
- **`product_role_visibility`** - Many-to-Many Product ↔ Roles (controls product visibility)
**Role-based Visibility (MVP):**
- Products are ONLY visible if they have `product_role_visibility` entries
- Users ONLY see products matching their approved roles in `user_roles`
- Opt-in visibility: No role assignment = invisible to everyone
Use Drizzle ORM for all database operations. All tables use UUID primary keys.
@@ -488,6 +496,143 @@ export async function submitOrderToXAPI(payload: XAPIOrderPayload) {
- Line numbers: Sequential multiples of 10000
- Salutation: 'male' → 'HERR', 'female' → 'FRAU', other → 'K_ANGABE'
## Role-based Product Visibility Patterns (MVP)
### Role-based Filtering Pattern
```typescript
// Server-side: GET /api/products - Filter products by user roles
import { getVisibleProductIdsForUser } from '~/server/utils/roles'
export default defineEventHandler(async (event) => {
const { user } = await getUserSession(event)
// MVP: Unauthenticated users see NO products (opt-in visibility)
if (!user) {
return []
}
// Get product IDs visible to this user (based on approved roles)
const visibleProductIds = await getVisibleProductIdsForUser(user.id)
if (visibleProductIds.length === 0) {
return []
}
// Fetch products with role-based filtering
const products = await db.query.products.findMany({
where: and(
eq(products.active, true),
inArray(products.id, visibleProductIds) // Role filter
)
})
return products
})
```
### ERP Category to Role Mapping Pattern
```typescript
// Auto-assign roles when importing products from NAV ERP
import { assignRolesToProductByCategory } from '~/server/utils/roles'
// server/api/erp/products.post.ts
export default defineEventHandler(async (event) => {
const { navProductId, category, ...productData } = await readBody(event)
// 1. Create/update product
const [product] = await db.insert(products)
.values({ navProductId, category, ...productData })
.onConflictDoUpdate({ target: products.navProductId, set: productData })
.returning()
// 2. Auto-assign roles based on category mapping
await assignRolesToProductByCategory(product.id, category)
return product
})
```
**Category Mapping:**
```typescript
// Defined in server/utils/roles.ts
const categoryRoleMapping = {
'makerspace-annual-pass': ['private', 'educator'],
'annual-pass': ['private'],
'educator-annual-pass': ['educator'],
'company-annual-pass': ['company']
}
```
### Get User Approved Roles Pattern
```typescript
// server/utils/roles.ts - Utility functions
import { getUserApprovedRoles, getUserApprovedRoleCodes } from '~/server/utils/roles'
// Get full role objects
const roles = await getUserApprovedRoles(userId)
// => [{ id: '...', code: 'private', displayName: 'Privatperson', ... }]
// Get just role codes (lightweight)
const roleCodes = await getUserApprovedRoleCodes(userId)
// => ['private', 'educator']
```
### Manual Role Assignment Pattern (MVP)
```typescript
// server/utils/roles.ts - For manual role assignment
import { assignRoleToUser } from '~/server/utils/roles'
// Assign role to user (MVP: always approved)
await assignRoleToUser(userId, 'private')
// With optional metadata (prepared for Phase 2/3)
await assignRoleToUser(userId, 'educator', {
organizationName: 'Hölderlin-Gymnasium Heilbronn',
adminNotes: 'Verified via school email domain'
})
```
### Check Product Visibility Pattern
```typescript
// server/utils/roles.ts - Check if specific product is visible
import { isProductVisibleForUser } from '~/server/utils/roles'
const canView = await isProductVisibleForUser(productId, userId)
// => true if user has approved role matching product's role assignments
// => false if product has no role assignments (opt-in!) or user lacks role
```
### JSONB Status History Pattern (Phase 2/3 prepared)
```typescript
// user_roles.statusHistory stores complete audit trail
const historyEntry = {
status: 'approved',
organizationName: 'Hölderlin-Gymnasium',
adminNotes: 'Verified via Lehrerausweis',
changedAt: new Date().toISOString(),
changedBy: adminUserId // null for auto-assignments
}
await db.update(userRoles)
.set({
status: 'approved',
statusHistory: sql`${userRoles.statusHistory} || ${JSON.stringify(historyEntry)}::jsonb`
})
.where(eq(userRoles.id, userRoleId))
```
**Important Visibility Rules (MVP):**
- Unauthenticated users → See NO products
- User without approved roles → See NO products
- Product without role assignments → Visible to NO ONE (opt-in)
- Product with role assignments → Visible only to users with matching approved role
## Authentication Patterns
See [`docs/CIDAAS_INTEGRATION.md`](./docs/CIDAAS_INTEGRATION.md) for complete Cidaas OAuth2 implementation guide.