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:
145
CLAUDE.md
145
CLAUDE.md
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user