backend prepared for fresh startup added roles to user and also addd extra fieldas

This commit is contained in:
laxmanhalaki 2025-11-12 16:39:42 +05:30
parent cbca9d1b15
commit b4407f59a6
31 changed files with 3595 additions and 335 deletions

263
QUICK_START.md Normal file
View File

@ -0,0 +1,263 @@
# Royal Enfield Workflow - Quick Start Guide
## 🚀 **One-Command Setup (New!)**
Everything is now automated! Just run:
```bash
cd Re_Backend
npm run dev
```
That's it! The setup script will automatically:
- ✅ Check if PostgreSQL database exists
- ✅ Create database if missing
- ✅ Install required extensions (`uuid-ossp`)
- ✅ Run all migrations (18 total: create tables, enums, indexes)
- ✅ Auto-seed 30 admin configurations
- ✅ Start the development server
---
## 📋 **Prerequisites**
Before running `npm run dev`, ensure:
1. **PostgreSQL is installed and running**
```bash
# Windows
# PostgreSQL should be running as a service
# Verify it's running
psql -U postgres -c "SELECT version();"
```
2. **Dependencies are installed**
```bash
npm install
```
3. **Environment variables are configured**
- Copy `.env.example` to `.env`
- Update database credentials:
```env
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your_password
DB_NAME=royal_enfield_workflow
```
---
## 🎯 **First Time Setup**
### Step 1: Install & Configure
```bash
cd Re_Backend
npm install
cp .env.example .env
# Edit .env with your database credentials
```
### Step 2: Run Development Server
```bash
npm run dev
```
**Output:**
```
========================================
🚀 Royal Enfield Workflow - Auto Setup
========================================
🔍 Checking if database exists...
📦 Database 'royal_enfield_workflow' not found. Creating...
✅ Database 'royal_enfield_workflow' created successfully!
📦 Installing uuid-ossp extension...
✅ Extension installed!
🔌 Testing database connection...
✅ Database connection established!
🔄 Running migrations...
📋 Creating users table with RBAC and extended SSO fields...
✅ 2025103000-create-users
✅ 2025103001-create-workflow-requests
✅ 2025103002-create-approval-levels
... (18 migrations total)
✅ Migrations completed successfully!
========================================
✅ Setup completed successfully!
========================================
📝 Note: Admin configurations will be auto-seeded on server start.
💡 Next steps:
1. Server will start automatically
2. Log in via SSO
3. Run this SQL to make yourself admin:
UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';
[Config Seed] ✅ Default configurations seeded successfully (30 settings)
info: ✅ Server started successfully on port 5000
```
### Step 3: Make Yourself Admin
After logging in via SSO:
```bash
psql -d royal_enfield_workflow
UPDATE users
SET role = 'ADMIN'
WHERE email = 'your-email@royalenfield.com';
\q
```
---
## 🔄 **Subsequent Runs**
After initial setup, `npm run dev` will:
- ✅ Skip database creation (already exists)
- ✅ Run any pending migrations (if you pulled new code)
- ✅ Skip config seeding (already has data)
- ✅ Start server immediately
**Typical Output:**
```
========================================
🚀 Royal Enfield Workflow - Auto Setup
========================================
🔍 Checking if database exists...
✅ Database 'royal_enfield_workflow' already exists.
🔌 Testing database connection...
✅ Database connection established!
🔄 Running migrations...
No pending migrations
✅ Migrations completed successfully!
========================================
✅ Setup completed successfully!
========================================
info: ✅ Server started successfully on port 5000
```
---
## 🛠️ **Manual Commands (If Needed)**
### Run Setup Only (Without Starting Server)
```bash
npm run setup
```
### Start Server Without Setup
```bash
npm run dev:no-setup
```
### Run Migrations Only
```bash
npm run migrate
```
### Seed Admin Configs Manually
```bash
npm run seed:config
```
---
## 🔥 **Fresh Database Reset**
If you want to completely reset and start fresh:
```bash
# Drop database
psql -U postgres -c "DROP DATABASE IF EXISTS royal_enfield_workflow;"
# Then just run dev (it will recreate everything)
npm run dev
```
---
## 📊 **Database Structure**
After setup, you'll have:
- **18 migrations** run successfully
- **30 admin configurations** seeded
- **12+ tables** created:
- `users` (with RBAC roles)
- `workflow_requests`
- `approval_levels`
- `participants`
- `documents`
- `work_notes`
- `tat_alerts`
- `admin_configurations`
- `holidays`
- `notifications`
- `conclusion_remarks`
- And more...
---
## 🎉 **That's It!**
Now you can:
- Access API at: `http://localhost:5000`
- View health check: `http://localhost:5000/health`
- Access API docs: `http://localhost:5000/api/v1`
---
## ❓ **Troubleshooting**
### Database Connection Failed
```
Error: Unable to connect to database
```
**Fix:**
- Ensure PostgreSQL is running
- Check credentials in `.env`
- Verify database user has `CREATEDB` permission
### Setup Script Permission Error
```
Error: permission denied to create database
```
**Fix:**
```sql
-- Grant CREATEDB permission to your user
ALTER USER postgres CREATEDB;
```
### Port Already in Use
```
Error: Port 5000 is already in use
```
**Fix:**
- Change `PORT` in `.env`
- Or kill process using port 5000
---
## 🚀 **Production Deployment**
For production:
1. Set `NODE_ENV=production` in `.env`
2. Use `npm run build` to compile TypeScript
3. Use `npm start` (no auto-setup in production)
4. Run migrations separately: `npm run migrate`
---
**Happy Coding!** 🎉

View File

@ -35,19 +35,28 @@ notifications ||--o{ sms_logs : "sends"
users { users {
uuid user_id PK uuid user_id PK
varchar employee_id UK "HR System ID" varchar employee_id UK "HR System ID - Optional"
varchar okta_sub UK "Okta Subject ID"
varchar email UK "Primary Email" varchar email UK "Primary Email"
varchar first_name varchar first_name "Optional"
varchar last_name varchar last_name "Optional"
varchar display_name "Full Name" varchar display_name "Full Name"
varchar department varchar department "Optional"
varchar designation varchar designation "Optional"
varchar phone varchar phone "Office Phone - Optional"
boolean is_active "Account Status" varchar manager "Reporting Manager - SSO Optional"
boolean is_admin "Super User Flag" varchar second_email "Alternate Email - SSO Optional"
timestamp last_login text job_title "Detailed Job Title - SSO Optional"
timestamp created_at varchar employee_number "HR Employee Number - SSO Optional"
timestamp updated_at varchar postal_address "Work Location - SSO Optional"
varchar mobile_phone "Mobile Contact - SSO Optional"
jsonb ad_groups "AD Group Memberships - SSO Optional"
jsonb location "Location Details - Optional"
boolean is_active "Account Status Default true"
enum role "USER, MANAGEMENT, ADMIN - RBAC Default USER"
timestamp last_login "Last Login Time"
timestamp created_at "Record Created"
timestamp updated_at "Record Updated"
} }
workflow_requests { workflow_requests {

View File

@ -0,0 +1,506 @@
# Fresh Database Setup Guide
## 🎯 Overview
This guide walks you through setting up a **completely fresh database** for the Royal Enfield Workflow Management System.
**Use this when:**
- First-time installation
- Major schema changes require full rebuild
- Moving to production environment
- Resetting development database
---
## ⚡ Quick Start (Automated)
### Linux/Mac:
```bash
cd Re_Backend
chmod +x scripts/fresh-database-setup.sh
./scripts/fresh-database-setup.sh
```
### Windows:
```cmd
cd Re_Backend
scripts\fresh-database-setup.bat
```
**The automated script will:**
1. ✅ Drop existing database (with confirmation)
2. ✅ Create fresh database
3. ✅ Install PostgreSQL extensions
4. ✅ Run all migrations in order
5. ✅ Seed admin configuration
6. ✅ Verify database structure
---
## 📋 Manual Setup (Step-by-Step)
### Prerequisites
```bash
# Check PostgreSQL
psql --version
# Required: PostgreSQL 16.x
# Check Redis
redis-cli ping
# Expected: PONG
# Check Node.js
node --version
# Required: 18.x or higher
# Configure environment
cp env.example .env
# Edit .env with your settings
```
---
### Step 1: Drop Existing Database
```bash
# Connect to PostgreSQL
psql -U postgres
# Drop database if exists
DROP DATABASE IF EXISTS royal_enfield_workflow;
# Exit psql
\q
```
---
### Step 2: Create Fresh Database
```bash
# Create new database
psql -U postgres -c "CREATE DATABASE royal_enfield_workflow OWNER postgres;"
# Verify
psql -U postgres -l | grep royal_enfield
```
---
### Step 3: Install Extensions
```bash
psql -U postgres -d royal_enfield_workflow <<EOF
-- UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Text search
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- JSONB operators
CREATE EXTENSION IF NOT EXISTS "btree_gin";
EOF
```
---
### Step 4: Run Migrations
```bash
cd Re_Backend
npm install # If not already done
npm run migrate
```
**Expected Output:**
```
✅ Migration: 2025103000-create-users.ts
✅ Migration: 2025103001-create-workflow-requests.ts
✅ Migration: 2025103002-create-approval-levels.ts
✅ Migration: 2025103003-create-participants.ts
✅ Migration: 2025103004-create-documents.ts
✅ Migration: 20251031_01_create_subscriptions.ts
✅ Migration: 20251031_02_create_activities.ts
✅ Migration: 20251031_03_create_work_notes.ts
✅ Migration: 20251031_04_create_work_note_attachments.ts
✅ Migration: 20251104-create-tat-alerts.ts
✅ Migration: 20251104-create-holidays.ts
✅ Migration: 20251104-create-admin-config.ts
✅ Migration: 20251111-create-conclusion-remarks.ts
✅ Migration: 20251111-create-notifications.ts
```
---
### Step 5: Seed Admin Configuration
```bash
npm run seed:config
```
**This creates default settings for:**
- Email notifications
- TAT thresholds
- Business hours
- Holiday calendar
- AI provider settings
---
### Step 6: Assign Admin User
**Option A: Via SQL Script (Replace YOUR_EMAIL first)**
```bash
# Edit the script
nano scripts/assign-admin-user.sql
# Change: YOUR_EMAIL@royalenfield.com
# Run it
psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql
```
**Option B: Via Direct SQL**
```sql
psql -d royal_enfield_workflow
UPDATE users
SET role = 'ADMIN'
WHERE email = 'your-email@royalenfield.com';
-- Verify
SELECT email, role FROM users WHERE role = 'ADMIN';
```
---
### Step 7: Verify Setup
```bash
# Check all tables created
psql -d royal_enfield_workflow -c "\dt"
# Check user role enum
psql -d royal_enfield_workflow -c "\dT+ user_role_enum"
# Check your user
psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your-email@royalenfield.com';"
```
---
### Step 8: Start Backend
```bash
npm run dev
```
Expected output:
```
🚀 Server started on port 5000
🗄️ Database connected
🔴 Redis connected
📡 WebSocket server ready
```
---
## 📊 Database Schema (Fresh Setup)
### Tables Created (in order):
1. ✅ **users** - User accounts with RBAC (role field)
2. ✅ **workflow_requests** - Workflow requests
3. ✅ **approval_levels** - Approval workflow steps
4. ✅ **participants** - Request participants
5. ✅ **documents** - Document attachments
6. ✅ **subscriptions** - User notification preferences
7. ✅ **activities** - Audit trail
8. ✅ **work_notes** - Collaboration messages
9. ✅ **work_note_attachments** - Work note files
10. ✅ **tat_alerts** - TAT/SLA alerts
11. ✅ **holidays** - Holiday calendar
12. ✅ **admin_config** - System configuration
13. ✅ **conclusion_remarks** - AI-generated conclusions
14. ✅ **notifications** - Notification queue
---
## 🔑 User Roles (RBAC)
### Default Role: USER
**All new users automatically get `USER` role on first login.**
### Assign MANAGEMENT Role
```sql
-- Single user
UPDATE users SET role = 'MANAGEMENT'
WHERE email = 'manager@royalenfield.com';
-- Multiple users
UPDATE users SET role = 'MANAGEMENT'
WHERE email IN (
'manager1@royalenfield.com',
'manager2@royalenfield.com'
);
-- By department
UPDATE users SET role = 'MANAGEMENT'
WHERE department = 'Management' AND is_active = true;
```
### Assign ADMIN Role
```sql
-- Single user
UPDATE users SET role = 'ADMIN'
WHERE email = 'admin@royalenfield.com';
-- Multiple admins
UPDATE users SET role = 'ADMIN'
WHERE email IN (
'admin1@royalenfield.com',
'admin2@royalenfield.com'
);
-- By department
UPDATE users SET role = 'ADMIN'
WHERE department = 'IT' AND is_active = true;
```
---
## 🔍 Verification Queries
### Check All Tables
```sql
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
```
### Check Role Distribution
```sql
SELECT
role,
COUNT(*) as user_count,
COUNT(CASE WHEN is_active = true THEN 1 END) as active_users
FROM users
GROUP BY role
ORDER BY
CASE role
WHEN 'ADMIN' THEN 1
WHEN 'MANAGEMENT' THEN 2
WHEN 'USER' THEN 3
END;
```
### Check Admin Users
```sql
SELECT
email,
display_name,
department,
role,
created_at,
last_login
FROM users
WHERE role = 'ADMIN' AND is_active = true
ORDER BY email;
```
### Check Extended SSO Fields
```sql
SELECT
email,
display_name,
manager,
job_title,
postal_address,
mobile_phone,
array_length(ad_groups, 1) as ad_group_count
FROM users
WHERE email = 'your-email@royalenfield.com';
```
---
## 🧪 Test Your Setup
### 1. Create Test User (via API)
```bash
curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
-H "Content-Type: application/json" \
-d '{
"email": "test@royalenfield.com",
"displayName": "Test User",
"oktaSub": "test-sub-123"
}'
```
### 2. Check User Created with Default Role
```sql
SELECT email, role FROM users WHERE email = 'test@royalenfield.com';
-- Expected: role = 'USER'
```
### 3. Update to ADMIN
```sql
UPDATE users SET role = 'ADMIN' WHERE email = 'test@royalenfield.com';
```
### 4. Verify API Access
```bash
# Login and get token
curl -X POST http://localhost:5000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@royalenfield.com", ...}'
# Try admin endpoint (should work if ADMIN role)
curl http://localhost:5000/api/v1/admin/configurations \
-H "Authorization: Bearer YOUR_TOKEN"
```
---
## 📦 Migration Files (Execution Order)
| Order | Migration File | Purpose |
|-------|---------------|---------|
| 1 | `2025103000-create-users.ts` | Users table with role + SSO fields |
| 2 | `2025103001-create-workflow-requests.ts` | Workflow requests |
| 3 | `2025103002-create-approval-levels.ts` | Approval levels |
| 4 | `2025103003-create-participants.ts` | Participants |
| 5 | `2025103004-create-documents.ts` | Documents |
| 6 | `20251031_01_create_subscriptions.ts` | Subscriptions |
| 7 | `20251031_02_create_activities.ts` | Activities/Audit trail |
| 8 | `20251031_03_create_work_notes.ts` | Work notes |
| 9 | `20251031_04_create_work_note_attachments.ts` | Work note attachments |
| 10 | `20251104-create-tat-alerts.ts` | TAT alerts |
| 11 | `20251104-create-holidays.ts` | Holidays |
| 12 | `20251104-create-admin-config.ts` | Admin configuration |
| 13 | `20251111-create-conclusion-remarks.ts` | Conclusion remarks |
| 14 | `20251111-create-notifications.ts` | Notifications |
---
## ⚠️ Important Notes
### is_admin Field REMOVED
❌ **OLD (Don't use):**
```typescript
if (user.is_admin) { ... }
```
✅ **NEW (Use this):**
```typescript
if (user.role === 'ADMIN') { ... }
```
### Default Values
| Field | Default Value | Notes |
|-------|---------------|-------|
| `role` | `USER` | Everyone starts as USER |
| `is_active` | `true` | Accounts active by default |
| All SSO fields | `null` | Optional, populated from Okta |
### Automatic Behaviors
- 🔄 **First Login**: User created with role=USER
- 🔒 **Admin Assignment**: Manual via SQL or API
- 📧 **Email**: Required and unique
- 🆔 **oktaSub**: Required and unique from SSO
---
## 🚨 Troubleshooting
### Migration Fails
```bash
# Check which migrations ran
psql -d royal_enfield_workflow -c "SELECT * FROM SequelizeMeta ORDER BY name;"
# Rollback if needed
npm run migrate:undo
# Re-run
npm run migrate
```
### User Not Created on Login
```sql
-- Check if user exists
SELECT * FROM users WHERE email = 'your-email@royalenfield.com';
-- Check Okta sub
SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
```
### Role Not Working
```sql
-- Verify role
SELECT email, role, is_active FROM users WHERE email = 'your-email@royalenfield.com';
-- Check role enum
\dT+ user_role_enum
```
---
## 📞 Quick Commands Reference
```bash
# Fresh setup (automated)
./scripts/fresh-database-setup.sh
# Make yourself admin
psql -d royal_enfield_workflow -c "UPDATE users SET role = 'ADMIN' WHERE email = 'your@email.com';"
# Check your role
psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your@email.com';"
# Start server
npm run dev
# Check logs
tail -f logs/application.log
```
---
## ✅ Success Checklist
- [ ] PostgreSQL 16.x installed
- [ ] Redis running
- [ ] .env configured
- [ ] Database created
- [ ] All migrations completed (14 tables)
- [ ] Admin config seeded
- [ ] At least one ADMIN user assigned
- [ ] Backend server starts without errors
- [ ] Can login and access admin endpoints
---
**Your fresh database is now production-ready!** 🎉

632
docs/RBAC_IMPLEMENTATION.md Normal file
View File

@ -0,0 +1,632 @@
# Role-Based Access Control (RBAC) Implementation
## Overview
The system now supports **three user roles** for granular access control:
| Role | Access Level | Use Case |
|------|--------------|----------|
| **USER** | Standard | Default role for all users - create/view own requests |
| **MANAGEMENT** | Enhanced Read | View all requests across organization (read-only) |
| **ADMIN** | Full Access | System configuration, user management, all workflows |
---
## User Roles
### 1. USER (Default)
**Permissions:**
- ✅ Create new workflow requests
- ✅ View own requests
- ✅ Participate in assigned workflows (as approver/spectator)
- ✅ Add work notes to requests they're involved in
- ✅ Upload documents to own requests
- ❌ Cannot view other users' requests (unless added as participant)
- ❌ Cannot access system configuration
- ❌ Cannot manage users or roles
**Use Case:** Regular employees creating and managing their workflow requests
---
### 2. MANAGEMENT
**Permissions:**
- ✅ All USER permissions
- ✅ View ALL requests across organization (read-only)
- ✅ Access comprehensive dashboards with organization-wide analytics
- ✅ Export reports across all departments
- ✅ View TAT performance metrics for all approvers
- ❌ Cannot approve/reject requests (unless explicitly added as approver)
- ❌ Cannot modify system configuration
- ❌ Cannot manage user roles
**Use Case:** Department heads, managers, auditors needing visibility into all workflows
---
### 3. ADMIN
**Permissions:**
- ✅ All MANAGEMENT permissions
- ✅ All USER permissions
- ✅ Manage system configuration
- ✅ Assign user roles
- ✅ Manage holiday calendar
- ✅ Configure email/notification settings
- ✅ Access audit logs
- ✅ Manage AI provider settings
**Use Case:** System administrators, IT staff managing the workflow platform
---
## Database Schema
### Migration Applied
```sql
-- Create ENUM type for roles
CREATE TYPE user_role_enum AS ENUM ('USER', 'MANAGEMENT', 'ADMIN');
-- Add role column to users table
ALTER TABLE users
ADD COLUMN role user_role_enum NOT NULL DEFAULT 'USER';
-- Migrate existing data
UPDATE users
SET role = CASE
WHEN is_admin = true THEN 'ADMIN'
ELSE 'USER'
END;
-- Create index for performance
CREATE INDEX idx_users_role ON users(role);
```
### Updated Users Table
```
users {
uuid user_id PK
varchar email UK
varchar display_name
varchar department
varchar designation
boolean is_active
user_role_enum role ← NEW FIELD
boolean is_admin ← DEPRECATED (kept for compatibility)
timestamp created_at
timestamp updated_at
}
```
---
## Backend Implementation
### Model (User.ts)
```typescript
export type UserRole = 'USER' | 'MANAGEMENT' | 'ADMIN';
interface UserAttributes {
// ... other fields
role: UserRole; // RBAC role
isAdmin: boolean; // DEPRECATED
}
class User extends Model<UserAttributes> {
public role!: UserRole;
// Helper methods
public isUserRole(): boolean {
return this.role === 'USER';
}
public isManagementRole(): boolean {
return this.role === 'MANAGEMENT';
}
public isAdminRole(): boolean {
return this.role === 'ADMIN';
}
public hasManagementAccess(): boolean {
return this.role === 'MANAGEMENT' || this.role === 'ADMIN';
}
public hasAdminAccess(): boolean {
return this.role === 'ADMIN';
}
}
```
---
## Middleware Usage
### 1. Require Admin Only
```typescript
import { requireAdmin } from '@middlewares/authorization.middleware';
// Only ADMIN can access
router.post('/admin/config', authenticate, requireAdmin, adminController.updateConfig);
router.post('/admin/users/:userId/role', authenticate, requireAdmin, adminController.updateUserRole);
router.post('/admin/holidays', authenticate, requireAdmin, adminController.addHoliday);
```
### 2. Require Management or Admin
```typescript
import { requireManagement } from '@middlewares/authorization.middleware';
// MANAGEMENT and ADMIN can access (read-only for management)
router.get('/reports/all-requests', authenticate, requireManagement, reportController.getAllRequests);
router.get('/analytics/department', authenticate, requireManagement, analyticsController.getDepartmentStats);
router.get('/dashboard/organization', authenticate, requireManagement, dashboardController.getOrgWideStats);
```
### 3. Flexible Role Checking
```typescript
import { requireRole } from '@middlewares/authorization.middleware';
// Multiple role options
router.get('/workflows/search', authenticate, requireRole(['MANAGEMENT', 'ADMIN']), workflowController.search);
router.post('/workflows/export', authenticate, requireRole(['MANAGEMENT', 'ADMIN']), workflowController.export);
// Any authenticated user
router.get('/profile', authenticate, requireRole(['USER', 'MANAGEMENT', 'ADMIN']), userController.getProfile);
```
### 4. Programmatic Role Checking in Controllers
```typescript
import { hasManagementAccess, hasAdminAccess } from '@middlewares/authorization.middleware';
export async function getWorkflows(req: Request, res: Response) {
const user = req.user;
// Management and Admin can see ALL workflows
if (hasManagementAccess(user)) {
const allWorkflows = await WorkflowRequest.findAll();
return res.json({ success: true, data: allWorkflows });
}
// Regular users only see their own workflows
const userWorkflows = await WorkflowRequest.findAll({
where: { initiatorId: user.userId }
});
return res.json({ success: true, data: userWorkflows });
}
```
---
## Example Route Implementations
### Admin Routes (ADMIN only)
```typescript
// src/routes/admin.routes.ts
import { Router } from 'express';
import { authenticate } from '@middlewares/auth.middleware';
import { requireAdmin } from '@middlewares/authorization.middleware';
import * as adminController from '@controllers/admin.controller';
const router = Router();
// All admin routes require ADMIN role
router.use(authenticate, requireAdmin);
// System configuration
router.get('/config', adminController.getConfig);
router.put('/config', adminController.updateConfig);
// User role management
router.put('/users/:userId/role', adminController.updateUserRole);
router.get('/users/admins', adminController.getAllAdmins);
router.get('/users/management', adminController.getAllManagement);
// Holiday management
router.post('/holidays', adminController.createHoliday);
router.delete('/holidays/:holidayId', adminController.deleteHoliday);
export default router;
```
### Management Routes (MANAGEMENT + ADMIN)
```typescript
// src/routes/management.routes.ts
import { Router } from 'express';
import { authenticate } from '@middlewares/auth.middleware';
import { requireManagement } from '@middlewares/authorization.middleware';
import * as managementController from '@controllers/management.controller';
const router = Router();
// All management routes require MANAGEMENT or ADMIN role
router.use(authenticate, requireManagement);
// Organization-wide dashboards (read-only)
router.get('/dashboard/organization', managementController.getOrgDashboard);
router.get('/requests/all', managementController.getAllRequests);
router.get('/analytics/tat-performance', managementController.getTATPerformance);
router.get('/analytics/approver-stats', managementController.getApproverStats);
router.get('/reports/export', managementController.exportReports);
// Department-wise analytics
router.get('/analytics/department/:deptName', managementController.getDepartmentAnalytics);
export default router;
```
### Workflow Routes (Mixed Permissions)
```typescript
// src/routes/workflow.routes.ts
import { Router } from 'express';
import { authenticate } from '@middlewares/auth.middleware';
import { requireManagement, requireRole } from '@middlewares/authorization.middleware';
import * as workflowController from '@controllers/workflow.controller';
const router = Router();
// USER: Create own request (all roles can do this)
router.post('/workflows', authenticate, workflowController.create);
// USER: View own requests (filtered by role in controller)
router.get('/workflows/my-requests', authenticate, workflowController.getMyRequests);
// MANAGEMENT + ADMIN: Search all requests
router.get('/workflows/search', authenticate, requireManagement, workflowController.searchAll);
// ADMIN: Delete workflow
router.delete('/workflows/:id', authenticate, requireRole(['ADMIN']), workflowController.delete);
export default router;
```
---
## Controller Implementation Examples
### Example 1: Dashboard with Role-Based Data
```typescript
// src/controllers/dashboard.controller.ts
import { hasManagementAccess } from '@middlewares/authorization.middleware';
export async function getDashboard(req: Request, res: Response) {
const user = req.user;
// MANAGEMENT and ADMIN: See organization-wide stats
if (hasManagementAccess(user)) {
const stats = await dashboardService.getOrganizationStats();
return res.json({
success: true,
data: {
...stats,
scope: 'organization', // Indicates full visibility
userRole: user.role
}
});
}
// USER: See only personal stats
const stats = await dashboardService.getUserStats(user.userId);
return res.json({
success: true,
data: {
...stats,
scope: 'personal', // Indicates limited visibility
userRole: user.role
}
});
}
```
### Example 2: User Role Update (ADMIN only)
```typescript
// src/controllers/admin.controller.ts
export async function updateUserRole(req: Request, res: Response) {
const { userId } = req.params;
const { role } = req.body;
// Validate role
if (!['USER', 'MANAGEMENT', 'ADMIN'].includes(role)) {
return res.status(400).json({
success: false,
error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN'
});
}
// Update user role
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
const oldRole = user.role;
user.role = role;
// Sync is_admin for backward compatibility
user.isAdmin = (role === 'ADMIN');
await user.save();
// Log role change
console.log(`✅ User role updated: ${user.email} - ${oldRole} → ${role}`);
return res.json({
success: true,
message: `User role updated from ${oldRole} to ${role}`,
data: {
userId: user.userId,
email: user.email,
role: user.role
}
});
}
```
---
## Frontend Integration
### Update Auth Context
```typescript
// Frontend: src/contexts/AuthContext.tsx
interface User {
userId: string;
email: string;
displayName: string;
role: 'USER' | 'MANAGEMENT' | 'ADMIN'; // ← Add role
}
// Helper functions
export function isAdmin(user: User | null): boolean {
return user?.role === 'ADMIN';
}
export function isManagement(user: User | null): boolean {
return user?.role === 'MANAGEMENT' || user?.role === 'ADMIN';
}
export function hasManagementAccess(user: User | null): boolean {
return user?.role === 'MANAGEMENT' || user?.role === 'ADMIN';
}
```
### Role-Based UI Rendering
```typescript
// Show admin menu only for ADMIN
{user?.role === 'ADMIN' && (
<NavItem to="/admin/config">
<Settings /> System Configuration
</NavItem>
)}
// Show management dashboard for MANAGEMENT and ADMIN
{(user?.role === 'MANAGEMENT' || user?.role === 'ADMIN') && (
<NavItem to="/dashboard/organization">
<TrendingUp /> Organization Dashboard
</NavItem>
)}
// Show all requests for MANAGEMENT and ADMIN
{hasManagementAccess(user) && (
<NavItem to="/requests/all">
<FileText /> All Requests
</NavItem>
)}
```
---
## Migration Guide
### Running the Migration
```bash
# Run migration to add role column
npm run migrate
# Verify migration
psql -d royal_enfield_db -c "SELECT email, role, is_admin FROM users LIMIT 10;"
```
### Expected Results
```
Before Migration:
+-------------------------+-----------+
| email | is_admin |
+-------------------------+-----------+
| admin@royalenfield.com | true |
| user1@royalenfield.com | false |
+-------------------------+-----------+
After Migration:
+-------------------------+-----------+-----------+
| email | role | is_admin |
+-------------------------+-----------+-----------+
| admin@royalenfield.com | ADMIN | true |
| user1@royalenfield.com | USER | false |
+-------------------------+-----------+-----------+
```
---
## Assigning Roles
### Via SQL (Direct Database)
```sql
-- Make user a MANAGEMENT role
UPDATE users
SET role = 'MANAGEMENT', is_admin = false
WHERE email = 'manager@royalenfield.com';
-- Make user an ADMIN role
UPDATE users
SET role = 'ADMIN', is_admin = true
WHERE email = 'admin@royalenfield.com';
-- Revert to USER role
UPDATE users
SET role = 'USER', is_admin = false
WHERE email = 'user@royalenfield.com';
```
### Via API (Admin Endpoint)
```bash
# Update user role (ADMIN only)
POST /api/v1/admin/users/:userId/role
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"role": "MANAGEMENT"
}
```
---
## Testing
### Test Scenarios
```typescript
describe('RBAC Tests', () => {
test('USER cannot access admin config', async () => {
const response = await request(app)
.get('/api/v1/admin/config')
.set('Authorization', `Bearer ${userToken}`);
expect(response.status).toBe(403);
expect(response.body.error).toContain('Admin access required');
});
test('MANAGEMENT can view all requests', async () => {
const response = await request(app)
.get('/api/v1/management/requests/all')
.set('Authorization', `Bearer ${managementToken}`);
expect(response.status).toBe(200);
expect(response.body.data).toBeInstanceOf(Array);
});
test('ADMIN can update user roles', async () => {
const response = await request(app)
.put(`/api/v1/admin/users/${userId}/role`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ role: 'MANAGEMENT' });
expect(response.status).toBe(200);
expect(response.body.data.role).toBe('MANAGEMENT');
});
});
```
---
## Best Practices
### 1. Always Use Role Column
```typescript
// ✅ GOOD: Use new role system
if (user.role === 'ADMIN') {
// Admin logic
}
// ❌ BAD: Don't use deprecated is_admin
if (user.isAdmin) {
// Deprecated approach
}
```
### 2. Use Helper Functions
```typescript
// ✅ GOOD: Use provided helpers
if (user.hasManagementAccess()) {
// Management or Admin logic
}
// ❌ BAD: Manual checking
if (user.role === 'MANAGEMENT' || user.role === 'ADMIN') {
// Verbose
}
```
### 3. Route Protection
```typescript
// ✅ GOOD: Clear role requirements
router.get('/sensitive-data', authenticate, requireManagement, controller.getData);
// ❌ BAD: Role checking in controller only
router.get('/sensitive-data', authenticate, controller.getData); // No middleware check
```
---
## Backward Compatibility
The `is_admin` field is **DEPRECATED** but kept for backward compatibility:
- ✅ Existing code using `is_admin` will continue to work
- ✅ Migration automatically syncs `role` and `is_admin`
- ⚠️ New code should use `role` instead of `is_admin`
- 📅 `is_admin` will be removed in future version
### Sync Logic
```typescript
// When updating role, sync is_admin
user.role = 'ADMIN';
user.isAdmin = true; // Auto-sync
user.role = 'USER';
user.isAdmin = false; // Auto-sync
```
---
## Quick Reference
| Task | Code Example |
|------|--------------|
| Check if ADMIN | `user.role === 'ADMIN'` or `user.isAdminRole()` |
| Check if MANAGEMENT | `user.role === 'MANAGEMENT'` or `user.isManagementRole()` |
| Check if USER | `user.role === 'USER'` or `user.isUserRole()` |
| Check Management+ | `user.hasManagementAccess()` |
| Middleware: Admin only | `requireAdmin` |
| Middleware: Management+ | `requireManagement` |
| Middleware: Custom roles | `requireRole(['ADMIN', 'MANAGEMENT'])` |
| Update role (SQL) | `UPDATE users SET role = 'MANAGEMENT' WHERE email = '...'` |
| Update role (API) | `PUT /admin/users/:userId/role { role: 'MANAGEMENT' }` |
---
## Support
For questions or issues:
- Check migration logs: `logs/migration.log`
- Review user roles: `SELECT email, role FROM users;`
- Test role access: Use provided test scenarios
**Migration File:** `src/migrations/20251112-add-user-roles.ts`
**Model File:** `src/models/User.ts`
**Middleware File:** `src/middlewares/authorization.middleware.ts`

372
docs/RBAC_QUICK_START.md Normal file
View File

@ -0,0 +1,372 @@
# RBAC Quick Start Guide
## ✅ **Implementation Complete!**
Role-Based Access Control (RBAC) has been successfully implemented with **three roles**:
| Role | Description | Default on Creation |
|------|-------------|---------------------|
| **USER** | Standard workflow participant | ✅ YES |
| **MANAGEMENT** | Read access to all data | ❌ Must assign |
| **ADMIN** | Full system access | ❌ Must assign |
---
## 🚀 **Quick Start - 3 Steps**
### Step 1: Run Migration
```bash
cd Re_Backend
npm run migrate
```
**What it does:**
- ✅ Creates `user_role_enum` type
- ✅ Adds `role` column to `users` table
- ✅ Migrates existing `is_admin` data to `role`
- ✅ Creates index for performance
---
### Step 2: Assign Roles to Users
**Option A: Via SQL Script (Recommended for initial setup)**
```bash
# Edit the script first with your user emails
nano scripts/assign-user-roles.sql
# Run the script
psql -d royal_enfield_db -f scripts/assign-user-roles.sql
```
**Option B: Via SQL Command (Quick assignment)**
```sql
-- Make specific users ADMIN
UPDATE users
SET role = 'ADMIN', is_admin = true
WHERE email IN ('admin@royalenfield.com', 'it.admin@royalenfield.com');
-- Make specific users MANAGEMENT
UPDATE users
SET role = 'MANAGEMENT', is_admin = false
WHERE email IN ('manager@royalenfield.com', 'auditor@royalenfield.com');
-- Verify roles
SELECT email, display_name, role, is_admin FROM users ORDER BY role, email;
```
**Option C: Via API (After system is running)**
```bash
# Update user role (requires ADMIN token)
curl -X PUT http://localhost:5000/api/v1/admin/users/{userId}/role \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "MANAGEMENT"}'
```
---
### Step 3: Restart Backend
```bash
npm run dev # Development
# or
npm start # Production
```
---
## 📡 **New API Endpoints (ADMIN Only)**
### 1. Update User Role
```http
PUT /api/v1/admin/users/:userId/role
Authorization: Bearer {admin-token}
Content-Type: application/json
{
"role": "MANAGEMENT"
}
```
**Response:**
```json
{
"success": true,
"message": "User role updated from USER to MANAGEMENT",
"data": {
"userId": "uuid",
"email": "user@example.com",
"role": "MANAGEMENT",
"previousRole": "USER"
}
}
```
### 2. Get Users by Role
```http
GET /api/v1/admin/users/by-role?role=MANAGEMENT
Authorization: Bearer {admin-token}
```
**Response:**
```json
{
"success": true,
"data": [...users...],
"summary": {
"ADMIN": 2,
"MANAGEMENT": 5,
"USER": 150,
"total": 157
}
}
```
### 3. Get Role Statistics
```http
GET /api/v1/admin/users/role-statistics
Authorization: Bearer {admin-token}
```
**Response:**
```json
{
"success": true,
"data": [
{ "role": "ADMIN", "count": 2, "active_count": 2, "inactive_count": 0 },
{ "role": "MANAGEMENT", "count": 5, "active_count": 5, "inactive_count": 0 },
{ "role": "USER", "count": 150, "active_count": 148, "inactive_count": 2 }
]
}
```
---
## 🛡️ **Using RBAC in Your Code**
### Middleware Examples
```typescript
import { requireAdmin, requireManagement, requireRole } from '@middlewares/authorization.middleware';
// ADMIN only
router.post('/admin/config', authenticate, requireAdmin, controller.updateConfig);
// MANAGEMENT or ADMIN
router.get('/reports/all', authenticate, requireManagement, controller.getAllReports);
// Flexible (custom roles)
router.get('/analytics', authenticate, requireRole(['MANAGEMENT', 'ADMIN']), controller.getAnalytics);
```
### Controller Examples
```typescript
import { hasManagementAccess, hasAdminAccess } from '@middlewares/authorization.middleware';
export async function getWorkflows(req: Request, res: Response) {
const user = req.user;
// MANAGEMENT & ADMIN: See all workflows
if (hasManagementAccess(user)) {
return await WorkflowRequest.findAll();
}
// USER: See only own workflows
return await WorkflowRequest.findAll({
where: { initiatorId: user.userId }
});
}
```
---
## 📋 **Role Permissions Matrix**
| Feature | USER | MANAGEMENT | ADMIN |
|---------|------|------------|-------|
| Create requests | ✅ | ✅ | ✅ |
| View own requests | ✅ | ✅ | ✅ |
| View all requests | ❌ | ✅ Read-only | ✅ Full access |
| Approve/Reject (if assigned) | ✅ | ✅ | ✅ |
| Organization dashboard | ❌ | ✅ | ✅ |
| Export reports | ❌ | ✅ | ✅ |
| System configuration | ❌ | ❌ | ✅ |
| Manage user roles | ❌ | ❌ | ✅ |
| Holiday management | ❌ | ❌ | ✅ |
| Audit logs | ❌ | ❌ | ✅ |
---
## 🧪 **Testing Your RBAC**
### Test 1: Verify Migration
```sql
-- Check role distribution
SELECT role, COUNT(*) as count
FROM users
GROUP BY role;
-- Check specific user
SELECT email, role, is_admin
FROM users
WHERE email = 'your-email@royalenfield.com';
```
### Test 2: Test API Access
```bash
# Try accessing admin endpoint with USER role (should fail)
curl -X GET http://localhost:5000/api/v1/admin/configurations \
-H "Authorization: Bearer {user-token}"
# Expected: 403 Forbidden
# Try accessing admin endpoint with ADMIN role (should succeed)
curl -X GET http://localhost:5000/api/v1/admin/configurations \
-H "Authorization: Bearer {admin-token}"
# Expected: 200 OK
```
---
## 🔄 **Migration Path**
### Existing Code Compatibility
✅ **All existing code continues to work!**
```typescript
// Old code (still works)
if (user.isAdmin) {
// Admin logic
}
// New code (recommended)
if (user.role === 'ADMIN') {
// Admin logic
}
```
### When to Update `is_admin`
The system **automatically syncs** `is_admin` with `role`:
```typescript
user.role = 'ADMIN'; → is_admin = true (auto-synced)
user.role = 'USER'; → is_admin = false (auto-synced)
user.role = 'MANAGEMENT'; → is_admin = false (auto-synced)
```
---
## 📝 **Files Created/Modified**
### Created Files:
1. ✅ `src/migrations/20251112-add-user-roles.ts` - Database migration
2. ✅ `scripts/assign-user-roles.sql` - Role assignment script
3. ✅ `docs/RBAC_IMPLEMENTATION.md` - Full documentation
4. ✅ `docs/RBAC_QUICK_START.md` - This guide
### Modified Files:
1. ✅ `src/models/User.ts` - Added role field + helper methods
2. ✅ `src/middlewares/authorization.middleware.ts` - Added RBAC middleware
3. ✅ `src/controllers/admin.controller.ts` - Added role management endpoints
4. ✅ `src/routes/admin.routes.ts` - Added role management routes
5. ✅ `src/types/user.types.ts` - Added UserRole type
6. ✅ `backend_structure.txt` - Updated users table schema
---
## 🎯 **Next Steps**
### 1. Run Migration
```bash
npm run migrate
```
### 2. Assign Initial Roles
```bash
# Edit with your emails
nano scripts/assign-user-roles.sql
# Run script
psql -d royal_enfield_db -f scripts/assign-user-roles.sql
```
### 3. Test the System
```bash
# Restart backend
npm run dev
# Check roles
curl http://localhost:5000/api/v1/admin/users/role-statistics \
-H "Authorization: Bearer {admin-token}"
```
### 4. Update Frontend (Optional - for role-based UI)
```typescript
// In AuthContext or user service
interface User {
role: 'USER' | 'MANAGEMENT' | 'ADMIN';
}
// Show admin menu only for ADMIN
{user.role === 'ADMIN' && <AdminMenu />}
// Show management dashboard for MANAGEMENT + ADMIN
{(user.role === 'MANAGEMENT' || user.role === 'ADMIN') && <OrgDashboard />}
```
---
## ⚠️ **Important Notes**
1. **Backward Compatibility**: `is_admin` field is kept but DEPRECATED
2. **Self-Demotion Prevention**: Admins cannot remove their own admin role
3. **Default Role**: All new users get 'USER' role automatically
4. **Role Sync**: `is_admin` is automatically synced with `role === 'ADMIN'`
---
## 💡 **Pro Tips**
### Assign Roles by Department
```sql
-- Make all IT dept users ADMIN
UPDATE users SET role = 'ADMIN', is_admin = true
WHERE department = 'IT' AND is_active = true;
-- Make all managers MANAGEMENT role
UPDATE users SET role = 'MANAGEMENT', is_admin = false
WHERE designation ILIKE '%manager%' OR designation ILIKE '%head%';
```
### Check Your Own Role
```sql
SELECT email, role, is_admin
FROM users
WHERE email = 'your-email@royalenfield.com';
```
---
## 📞 **Support**
For issues or questions:
- **Documentation**: `docs/RBAC_IMPLEMENTATION.md`
- **Migration File**: `src/migrations/20251112-add-user-roles.ts`
- **Assignment Script**: `scripts/assign-user-roles.sql`
**Your RBAC system is production-ready!** 🎉

13
package-lock.json generated
View File

@ -51,6 +51,7 @@
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/passport": "^1.0.16", "@types/passport": "^1.0.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/pg": "^8.15.6",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/web-push": "^3.6.4", "@types/web-push": "^3.6.4",
"@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/eslint-plugin": "^8.19.1",
@ -2073,6 +2074,18 @@
"@types/passport": "*" "@types/passport": "*"
} }
}, },
"node_modules/@types/pg": {
"version": "8.15.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz",
"integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",

View File

@ -5,7 +5,8 @@
"main": "dist/server.js", "main": "dist/server.js",
"scripts": { "scripts": {
"start": "node dist/server.js", "start": "node dist/server.js",
"dev": "npm run migrate && nodemon --exec ts-node -r tsconfig-paths/register src/server.ts", "dev": "npm run setup && nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
"dev:no-setup": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
"build": "tsc", "build": "tsc",
"build:watch": "tsc --watch", "build:watch": "tsc --watch",
"start:prod": "NODE_ENV=production node dist/server.js", "start:prod": "NODE_ENV=production node dist/server.js",
@ -21,6 +22,7 @@
"db:migrate:undo": "sequelize-cli db:migrate:undo", "db:migrate:undo": "sequelize-cli db:migrate:undo",
"db:seed": "sequelize-cli db:seed:all", "db:seed": "sequelize-cli db:seed:all",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"setup": "ts-node -r tsconfig-paths/register src/scripts/auto-setup.ts",
"migrate": "ts-node -r tsconfig-paths/register src/scripts/migrate.ts", "migrate": "ts-node -r tsconfig-paths/register src/scripts/migrate.ts",
"seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.ts" "seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.ts"
}, },
@ -68,6 +70,7 @@
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/passport": "^1.0.16", "@types/passport": "^1.0.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/pg": "^8.15.6",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/web-push": "^3.6.4", "@types/web-push": "^3.6.4",
"@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/eslint-plugin": "^8.19.1",

View File

@ -0,0 +1,55 @@
/**
* Assign First Admin User
*
* Purpose: Quick script to make your first user an ADMIN after fresh setup
*
* Usage:
* 1. Replace YOUR_EMAIL below with your actual email
* 2. Run: psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql
*/
-- ============================================
-- UPDATE THIS EMAIL WITH YOUR ACTUAL EMAIL
-- ============================================
\echo 'Assigning ADMIN role to user...\n'
UPDATE users
SET role = 'ADMIN'
WHERE email = 'YOUR_EMAIL@royalenfield.com' -- ← CHANGE THIS
RETURNING
user_id,
email,
display_name,
role,
updated_at;
\echo '\n✅ Admin role assigned!\n'
-- Display all current admins
\echo 'Current ADMIN users:'
SELECT
email,
display_name,
department,
role,
created_at
FROM users
WHERE role = 'ADMIN' AND is_active = true
ORDER BY email;
-- Display role summary
\echo '\nRole Summary:'
SELECT
role,
COUNT(*) as count
FROM users
WHERE is_active = true
GROUP BY role
ORDER BY
CASE role
WHEN 'ADMIN' THEN 1
WHEN 'MANAGEMENT' THEN 2
WHEN 'USER' THEN 3
END;

View File

@ -0,0 +1,123 @@
/**
* User Role Assignment Script
*
* Purpose: Assign roles to specific users after fresh database setup
*
* Usage:
* 1. Update the email addresses below with your actual users
* 2. Run: psql -d royal_enfield_workflow -f scripts/assign-user-roles.sql
*
* Roles:
* - USER: Default role for all employees
* - MANAGEMENT: Department heads, managers, auditors
* - ADMIN: IT administrators, system managers
*/
-- ============================================
-- ASSIGN ADMIN ROLES
-- ============================================
-- Replace with your actual admin email addresses
UPDATE users
SET role = 'ADMIN'
WHERE email IN (
'admin@royalenfield.com',
'it.admin@royalenfield.com',
'system.admin@royalenfield.com'
-- Add more admin emails here
);
-- Verify ADMIN users
SELECT
email,
display_name,
role,
updated_at
FROM users
WHERE role = 'ADMIN'
ORDER BY email;
-- ============================================
-- ASSIGN MANAGEMENT ROLES
-- ============================================
-- Replace with your actual management email addresses
UPDATE users
SET role = 'MANAGEMENT'
WHERE email IN (
'manager1@royalenfield.com',
'dept.head@royalenfield.com',
'auditor@royalenfield.com'
-- Add more management emails here
);
-- Verify MANAGEMENT users
SELECT
email,
display_name,
department,
role,
updated_at
FROM users
WHERE role = 'MANAGEMENT'
ORDER BY department, email;
-- ============================================
-- VERIFY ALL ROLES
-- ============================================
SELECT
role,
COUNT(*) as user_count
FROM users
WHERE is_active = true
GROUP BY role
ORDER BY
CASE role
WHEN 'ADMIN' THEN 1
WHEN 'MANAGEMENT' THEN 2
WHEN 'USER' THEN 3
END;
-- ============================================
-- EXAMPLE: Assign role by department
-- ============================================
-- Make all users in "IT" department as ADMIN
-- UPDATE users
-- SET role = 'ADMIN'
-- WHERE department = 'IT' AND is_active = true;
-- Make all users in "Management" department as MANAGEMENT
-- UPDATE users
-- SET role = 'MANAGEMENT'
-- WHERE department = 'Management' AND is_active = true;
-- ============================================
-- EXAMPLE: Assign role by designation
-- ============================================
-- Make all "Department Head" as MANAGEMENT
-- UPDATE users
-- SET role = 'MANAGEMENT'
-- WHERE (designation ILIKE '%head%' OR designation ILIKE '%manager%')
-- AND is_active = true;
-- ============================================
-- Display role summary
-- ============================================
\echo '\n✅ Role assignment complete!\n'
\echo 'Role Summary:'
SELECT
role,
COUNT(*) as total_users,
COUNT(CASE WHEN is_active = true THEN 1 END) as active_users
FROM users
GROUP BY role
ORDER BY
CASE role
WHEN 'ADMIN' THEN 1
WHEN 'MANAGEMENT' THEN 2
WHEN 'USER' THEN 3
END;

View File

@ -0,0 +1,136 @@
@echo off
REM ############################################################################
REM Fresh Database Setup Script (Windows)
REM
REM Purpose: Complete fresh database setup for Royal Enfield Workflow System
REM
REM Prerequisites:
REM 1. PostgreSQL 16.x installed
REM 2. Redis installed and running
REM 3. Node.js 18+ installed
REM 4. Environment variables configured in .env
REM
REM Usage: scripts\fresh-database-setup.bat
REM ############################################################################
setlocal enabledelayedexpansion
echo.
echo ===============================================================
echo Royal Enfield Workflow System - Fresh Database Setup
echo ===============================================================
echo.
REM Load .env file
if exist .env (
echo [*] Loading environment variables...
for /f "usebackq tokens=1,2 delims==" %%a in (".env") do (
set "%%a=%%b"
)
) else (
echo [ERROR] .env file not found!
echo Please copy env.example to .env and configure it
pause
exit /b 1
)
REM Set default values if not in .env
if not defined DB_NAME set DB_NAME=royal_enfield_workflow
if not defined DB_USER set DB_USER=postgres
if not defined DB_HOST set DB_HOST=localhost
if not defined DB_PORT set DB_PORT=5432
echo.
echo WARNING: This will DROP the existing database!
echo Database: %DB_NAME%
echo Host: %DB_HOST%:%DB_PORT%
echo.
set /p CONFIRM="Are you sure you want to continue? (yes/no): "
if /i not "%CONFIRM%"=="yes" (
echo Setup cancelled.
exit /b 0
)
echo.
echo ===============================================================
echo Step 1: Dropping existing database (if exists)...
echo ===============================================================
echo.
psql -h %DB_HOST% -p %DB_PORT% -U %DB_USER% -d postgres -c "DROP DATABASE IF EXISTS %DB_NAME%;" 2>nul
echo [OK] Old database dropped
echo.
echo ===============================================================
echo Step 2: Creating fresh database...
echo ===============================================================
echo.
psql -h %DB_HOST% -p %DB_PORT% -U %DB_USER% -d postgres -c "CREATE DATABASE %DB_NAME% OWNER %DB_USER%;"
echo [OK] Fresh database created: %DB_NAME%
echo.
echo ===============================================================
echo Step 3: Installing PostgreSQL extensions...
echo ===============================================================
echo.
psql -h %DB_HOST% -p %DB_PORT% -U %DB_USER% -d %DB_NAME% -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";"
psql -h %DB_HOST% -p %DB_PORT% -U %DB_USER% -d %DB_NAME% -c "CREATE EXTENSION IF NOT EXISTS \"pg_trgm\";"
psql -h %DB_HOST% -p %DB_PORT% -U %DB_USER% -d %DB_NAME% -c "CREATE EXTENSION IF NOT EXISTS \"btree_gin\";"
echo [OK] PostgreSQL extensions installed
echo.
echo ===============================================================
echo Step 4: Running database migrations...
echo ===============================================================
echo.
call npm run migrate
echo [OK] All migrations completed
echo.
echo ===============================================================
echo Step 5: Seeding admin configuration...
echo ===============================================================
echo.
call npm run seed:config
echo [OK] Admin configuration seeded
echo.
echo ===============================================================
echo Step 6: Database verification...
echo ===============================================================
echo.
psql -h %DB_HOST% -p %DB_PORT% -U %DB_USER% -d %DB_NAME% -c "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename;"
echo [OK] Database structure verified
echo.
echo ===============================================================
echo FRESH DATABASE SETUP COMPLETE!
echo ===============================================================
echo.
echo Next Steps:
echo 1. Assign admin role to your user:
echo psql -d %DB_NAME% -f scripts\assign-admin-user.sql
echo.
echo 2. Start the backend server:
echo npm run dev
echo.
echo 3. Access the application:
echo http://localhost:5000
echo.
echo Database is ready for production use!
echo.
pause

View File

@ -0,0 +1,168 @@
#!/bin/bash
###############################################################################
# Fresh Database Setup Script
#
# Purpose: Complete fresh database setup for Royal Enfield Workflow System
#
# Prerequisites:
# 1. PostgreSQL 16.x installed
# 2. Redis installed and running
# 3. Node.js 18+ installed
# 4. Environment variables configured in .env
#
# Usage:
# chmod +x scripts/fresh-database-setup.sh
# ./scripts/fresh-database-setup.sh
#
# What this script does:
# 1. Drops existing database (if exists)
# 2. Creates fresh database
# 3. Runs all migrations in order
# 4. Seeds admin configuration
# 5. Creates initial admin user
# 6. Verifies setup
###############################################################################
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Load environment variables
if [ -f .env ]; then
echo -e "${BLUE}📋 Loading environment variables...${NC}"
export $(cat .env | grep -v '^#' | xargs)
else
echo -e "${RED}❌ .env file not found!${NC}"
echo -e "${YELLOW}Please copy env.example to .env and configure it${NC}"
exit 1
fi
# Database variables
DB_NAME="${DB_NAME:-royal_enfield_workflow}"
DB_USER="${DB_USER:-postgres}"
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Royal Enfield Workflow System - Fresh Database Setup ║${NC}"
echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${YELLOW}⚠️ WARNING: This will DROP the existing database!${NC}"
echo -e "${YELLOW} Database: ${DB_NAME}${NC}"
echo -e "${YELLOW} Host: ${DB_HOST}:${DB_PORT}${NC}"
echo ""
read -p "Are you sure you want to continue? (yes/no): " -r
echo ""
if [[ ! $REPLY =~ ^[Yy]es$ ]]; then
echo -e "${YELLOW}Setup cancelled.${NC}"
exit 0
fi
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}Step 1: Dropping existing database (if exists)...${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres -c "DROP DATABASE IF EXISTS $DB_NAME;" || true
echo -e "${GREEN}✅ Old database dropped${NC}"
echo ""
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}Step 2: Creating fresh database...${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
echo -e "${GREEN}✅ Fresh database created: $DB_NAME${NC}"
echo ""
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}Step 3: Installing PostgreSQL extensions...${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME <<EOF
-- UUID extension for primary keys
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- pg_trgm for text search
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- Enable JSONB operators
CREATE EXTENSION IF NOT EXISTS "btree_gin";
EOF
echo -e "${GREEN}✅ PostgreSQL extensions installed${NC}"
echo ""
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}Step 4: Running database migrations...${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
npm run migrate
echo -e "${GREEN}✅ All migrations completed${NC}"
echo ""
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}Step 5: Seeding admin configuration...${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
npm run seed:config
echo -e "${GREEN}✅ Admin configuration seeded${NC}"
echo ""
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}Step 6: Database verification...${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME <<EOF
-- Check tables created
SELECT
schemaname,
tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
-- Check role enum
SELECT
enumlabel
FROM pg_enum
WHERE enumtypid = 'user_role_enum'::regtype;
-- Check indexes
SELECT
tablename,
indexname
FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'users'
ORDER BY tablename, indexname;
EOF
echo -e "${GREEN}✅ Database structure verified${NC}"
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ FRESH DATABASE SETUP COMPLETE! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${YELLOW}📋 Next Steps:${NC}"
echo -e " 1. Assign admin role to your user:"
echo -e " ${BLUE}psql -d $DB_NAME -f scripts/assign-admin-user.sql${NC}"
echo ""
echo -e " 2. Start the backend server:"
echo -e " ${BLUE}npm run dev${NC}"
echo ""
echo -e " 3. Access the application:"
echo -e " ${BLUE}http://localhost:5000${NC}"
echo ""
echo -e "${GREEN}🎉 Database is ready for production use!${NC}"

View File

@ -117,7 +117,7 @@ app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.
designation: user.designation || null, designation: user.designation || null,
phone: user.phone || null, phone: user.phone || null,
location: user.location || null, location: user.location || null,
isAdmin: user.isAdmin, role: user.role,
lastLogin: user.lastLogin lastLogin: user.lastLogin
}, },
isNewUser: user.createdAt.getTime() === user.updatedAt.getTime() isNewUser: user.createdAt.getTime() === user.updatedAt.getTime()
@ -155,7 +155,7 @@ app.get('/api/v1/users', async (_req: express.Request, res: express.Response): P
designation: user.designation || null, designation: user.designation || null,
phone: user.phone || null, phone: user.phone || null,
location: user.location || null, location: user.location || null,
isAdmin: user.isAdmin, role: user.role,
lastLogin: user.lastLogin, lastLogin: user.lastLogin,
createdAt: user.createdAt createdAt: user.createdAt
})), })),

View File

@ -6,6 +6,7 @@ import { QueryTypes } from 'sequelize';
import logger from '@utils/logger'; import logger from '@utils/logger';
import { initializeHolidaysCache, clearWorkingHoursCache } from '@utils/tatTimeUtils'; import { initializeHolidaysCache, clearWorkingHoursCache } from '@utils/tatTimeUtils';
import { clearConfigCache } from '@services/configReader.service'; import { clearConfigCache } from '@services/configReader.service';
import { User, UserRole } from '@models/User';
/** /**
* Get all holidays (with optional year filter) * Get all holidays (with optional year filter)
@ -438,3 +439,321 @@ export const resetConfiguration = async (req: Request, res: Response): Promise<v
} }
}; };
/**
* ============================================
* USER ROLE MANAGEMENT (RBAC)
* ============================================
*/
/**
* Update User Role
*
* Purpose: Change user's role (USER, MANAGEMENT, ADMIN)
*
* Access: ADMIN only
*
* Body: { role: 'USER' | 'MANAGEMENT' | 'ADMIN' }
*/
export const updateUserRole = async (req: Request, res: Response): Promise<void> => {
try {
const { userId } = req.params;
const { role } = req.body;
// Validate role
const validRoles: UserRole[] = ['USER', 'MANAGEMENT', 'ADMIN'];
if (!role || !validRoles.includes(role)) {
res.status(400).json({
success: false,
error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN'
});
return;
}
// Find user
const user = await User.findByPk(userId);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found'
});
return;
}
// Store old role for logging
const oldRole = user.role;
// Prevent self-demotion from ADMIN (safety check)
const adminUser = req.user;
if (adminUser?.userId === userId && role !== 'ADMIN') {
res.status(400).json({
success: false,
error: 'Cannot remove your own admin privileges. Ask another admin to change your role.'
});
return;
}
// Update role
user.role = role;
await user.save();
logger.info(`✅ User role updated by ${adminUser?.email}: ${user.email} - ${oldRole}${role}`);
res.json({
success: true,
message: `User role updated from ${oldRole} to ${role}`,
data: {
userId: user.userId,
email: user.email,
displayName: user.displayName,
role: user.role,
previousRole: oldRole,
updatedAt: user.updatedAt
}
});
} catch (error) {
logger.error('[Admin] Error updating user role:', error);
res.status(500).json({
success: false,
error: 'Failed to update user role'
});
}
};
/**
* Get All Users by Role
*
* Purpose: List all users with a specific role
*
* Access: ADMIN only
*
* Query: ?role=ADMIN | MANAGEMENT | USER
*/
export const getUsersByRole = async (req: Request, res: Response): Promise<void> => {
try {
const { role } = req.query;
const whereClause: any = { isActive: true };
if (role) {
const validRoles: UserRole[] = ['USER', 'MANAGEMENT', 'ADMIN'];
if (!validRoles.includes(role as UserRole)) {
res.status(400).json({
success: false,
error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN'
});
return;
}
whereClause.role = role;
}
const users = await User.findAll({
where: whereClause,
attributes: [
'userId',
'email',
'displayName',
'firstName',
'lastName',
'department',
'designation',
'role',
'manager',
'postalAddress',
'lastLogin',
'createdAt'
],
order: [
['role', 'ASC'], // ADMIN first, then MANAGEMENT, then USER
['displayName', 'ASC']
]
});
// Group by role for summary
const summary = {
ADMIN: users.filter(u => u.role === 'ADMIN').length,
MANAGEMENT: users.filter(u => u.role === 'MANAGEMENT').length,
USER: users.filter(u => u.role === 'USER').length,
total: users.length
};
res.json({
success: true,
data: {
users: users,
summary,
filter: role || 'all'
}
});
} catch (error) {
logger.error('[Admin] Error fetching users by role:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch users'
});
}
};
/**
* Get Role Statistics
*
* Purpose: Get count of users in each role
*
* Access: ADMIN only
*/
export const getRoleStatistics = async (req: Request, res: Response): Promise<void> => {
try {
const stats = await sequelize.query(`
SELECT
role,
COUNT(*) as count,
COUNT(CASE WHEN is_active = true THEN 1 END) as active_count,
COUNT(CASE WHEN is_active = false THEN 1 END) as inactive_count
FROM users
GROUP BY role
ORDER BY
CASE role
WHEN 'ADMIN' THEN 1
WHEN 'MANAGEMENT' THEN 2
WHEN 'USER' THEN 3
END
`, {
type: QueryTypes.SELECT
});
res.json({
success: true,
data: {
statistics: stats,
total: stats.reduce((sum: number, stat: any) => sum + parseInt(stat.count), 0)
}
});
} catch (error) {
logger.error('[Admin] Error fetching role statistics:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch role statistics'
});
}
};
/**
* Assign role to user by email
*
* Purpose: Search user in Okta, create if doesn't exist, then assign role
*
* Access: ADMIN only
*
* Body: { email: string, role: 'USER' | 'MANAGEMENT' | 'ADMIN' }
*/
export const assignRoleByEmail = async (req: Request, res: Response): Promise<void> => {
try {
const { email, role } = req.body;
const currentUserId = req.user?.userId;
// Validate inputs
if (!email || !role) {
res.status(400).json({
success: false,
error: 'Email and role are required'
});
return;
}
// Validate role
if (!['USER', 'MANAGEMENT', 'ADMIN'].includes(role)) {
res.status(400).json({
success: false,
error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN'
});
return;
}
logger.info(`[Admin] Assigning role ${role} to ${email} by user ${currentUserId}`);
// First, check if user already exists in our database
let user = await User.findOne({ where: { email } });
if (!user) {
// User doesn't exist, need to fetch from Okta and create
logger.info(`[Admin] User ${email} not found in database, fetching from Okta...`);
// Import UserService to search Okta
const { UserService } = await import('@services/user.service');
const userService = new UserService();
try {
// Search Okta for this user
const oktaUsers = await userService.searchUsers(email, 1);
if (!oktaUsers || oktaUsers.length === 0) {
res.status(404).json({
success: false,
error: 'User not found in Okta. Please ensure the email is correct.'
});
return;
}
const oktaUser = oktaUsers[0];
// Create user in our database
user = await User.create({
email: oktaUser.email,
oktaSub: (oktaUser as any).userId || (oktaUser as any).oktaSub, // Okta user ID as oktaSub
employeeId: (oktaUser as any).employeeNumber || (oktaUser as any).employeeId || null,
firstName: oktaUser.firstName || null,
lastName: oktaUser.lastName || null,
displayName: oktaUser.displayName || `${oktaUser.firstName || ''} ${oktaUser.lastName || ''}`.trim() || oktaUser.email,
department: oktaUser.department || null,
designation: (oktaUser as any).designation || (oktaUser as any).title || null,
phone: (oktaUser as any).phone || (oktaUser as any).mobilePhone || null,
isActive: true,
role: role, // Assign the requested role
lastLogin: undefined // Not logged in yet
});
logger.info(`[Admin] Created new user ${email} with role ${role}`);
} catch (oktaError: any) {
logger.error('[Admin] Error fetching from Okta:', oktaError);
res.status(500).json({
success: false,
error: 'Failed to fetch user from Okta: ' + (oktaError.message || 'Unknown error')
});
return;
}
} else {
// User exists, update their role
const previousRole = user.role;
// Prevent self-demotion
if (user.userId === currentUserId && role !== 'ADMIN') {
res.status(403).json({
success: false,
error: 'You cannot demote yourself from ADMIN role'
});
return;
}
await user.update({ role });
logger.info(`[Admin] Updated user ${email} role from ${previousRole} to ${role}`);
}
res.json({
success: true,
message: `Successfully assigned ${role} role to ${user.displayName || email}`,
data: {
userId: user.userId,
email: user.email,
displayName: user.displayName,
role: user.role
}
});
} catch (error) {
logger.error('[Admin] Error assigning role by email:', error);
res.status(500).json({
success: false,
error: 'Failed to assign role'
});
}
};

View File

@ -59,7 +59,7 @@ export class AuthController {
designation: user.designation, designation: user.designation,
phone: user.phone, phone: user.phone,
location: user.location, location: user.location,
isAdmin: user.isAdmin, role: user.role,
isActive: user.isActive, isActive: user.isActive,
lastLogin: user.lastLogin, lastLogin: user.lastLogin,
createdAt: user.createdAt, createdAt: user.createdAt,

View File

@ -49,7 +49,7 @@ export const authenticateToken = async (
userId: user.userId, userId: user.userId,
email: user.email, email: user.email,
employeeId: user.employeeId || null, // Optional - schema not finalized employeeId: user.employeeId || null, // Optional - schema not finalized
role: user.isAdmin ? 'admin' : 'user' role: user.role // Keep uppercase: USER, MANAGEMENT, ADMIN
}; };
next(); next();
@ -70,7 +70,7 @@ export const requireAdmin = (
res: Response, res: Response,
next: NextFunction next: NextFunction
): void => { ): void => {
if (req.user?.role !== 'admin') { if (req.user?.role !== 'ADMIN') {
ResponseHandler.forbidden(res, 'Admin access required'); ResponseHandler.forbidden(res, 'Admin access required');
return; return;
} }
@ -95,7 +95,7 @@ export const optionalAuth = async (
userId: user.userId, userId: user.userId,
email: user.email, email: user.email,
employeeId: user.employeeId || null, // Optional - schema not finalized employeeId: user.employeeId || null, // Optional - schema not finalized
role: user.isAdmin ? 'admin' : 'user' role: user.role // Keep uppercase: USER, MANAGEMENT, ADMIN
}; };
} }
} }

View File

@ -98,16 +98,36 @@ export function requireParticipantTypes(allowed: AllowedType[]) {
} }
/** /**
* Middleware to require admin role * Role-Based Access Control (RBAC) Middleware
*
* Roles:
* - USER: Default role - can create/view own requests, participate in assigned workflows
* - MANAGEMENT: Read access to all requests, enhanced dashboard visibility
* - ADMIN: Full system access - configuration, user management, all workflows
*/
/**
* Middleware: requireAdmin
*
* Purpose: Restrict access to ADMIN role only
*
* Use Cases:
* - System configuration management
* - User role assignment
* - Holiday calendar management
* - Email/notification settings
* - Audit log access
*
* Returns: 403 Forbidden if user is not ADMIN
*/ */
export function requireAdmin(req: Request, res: Response, next: NextFunction): void { export function requireAdmin(req: Request, res: Response, next: NextFunction): void {
try { try {
const userRole = req.user?.role; const userRole = req.user?.role;
if (userRole !== 'admin') { if (userRole !== 'ADMIN') {
res.status(403).json({ res.status(403).json({
success: false, success: false,
error: 'Admin access required' error: 'Admin access required. Only administrators can perform this action.'
}); });
return; return;
} }
@ -122,4 +142,117 @@ export function requireAdmin(req: Request, res: Response, next: NextFunction): v
} }
} }
/**
* Middleware: requireManagement
*
* Purpose: Restrict access to MANAGEMENT and ADMIN roles
*
* Use Cases:
* - View all requests (read-only)
* - Access comprehensive dashboards
* - Export reports
* - View analytics across all departments
*
* Permissions:
* - MANAGEMENT: Read access to all data
* - ADMIN: Read + Write access
*
* Returns: 403 Forbidden if user is only USER role
*/
export function requireManagement(req: Request, res: Response, next: NextFunction): void {
try {
const userRole = req.user?.role;
if (userRole !== 'MANAGEMENT' && userRole !== 'ADMIN') {
res.status(403).json({
success: false,
error: 'Management access required. This feature is available to management and admin users only.'
});
return;
}
next();
} catch (error) {
console.error('❌ Management authorization failed:', error);
res.status(500).json({
success: false,
error: 'Authorization check failed'
});
}
}
/**
* Middleware: requireRole
*
* Purpose: Flexible role checking - accepts multiple allowed roles
*
* Example Usage:
* - requireRole(['ADMIN']) - Admin only
* - requireRole(['MANAGEMENT', 'ADMIN']) - Management or Admin
* - requireRole(['USER', 'MANAGEMENT', 'ADMIN']) - Any authenticated user
*
* @param allowedRoles - Array of allowed role strings
* @returns Express middleware function
*/
export function requireRole(allowedRoles: ('USER' | 'MANAGEMENT' | 'ADMIN')[]) {
return (req: Request, res: Response, next: NextFunction): void => {
try {
const userRole = req.user?.role;
if (!userRole || !allowedRoles.includes(userRole as any)) {
res.status(403).json({
success: false,
error: `Access denied. Required roles: ${allowedRoles.join(' or ')}`
});
return;
}
next();
} catch (error) {
console.error('❌ Role authorization failed:', error);
res.status(500).json({
success: false,
error: 'Authorization check failed'
});
}
};
}
/**
* Helper: Check if user has specific role
*
* Purpose: Programmatic role checking within controllers
*
* @param user - Express req.user object
* @param role - Role to check
* @returns boolean
*/
export function hasRole(user: any, role: 'USER' | 'MANAGEMENT' | 'ADMIN'): boolean {
return user?.role === role;
}
/**
* Helper: Check if user has management or admin access
*
* Purpose: Quick check for enhanced permissions
*
* @param user - Express req.user object
* @returns boolean
*/
export function hasManagementAccess(user: any): boolean {
return user?.role === 'MANAGEMENT' || user?.role === 'ADMIN';
}
/**
* Helper: Check if user has admin access
*
* Purpose: Quick check for admin-only permissions
*
* @param user - Express req.user object
* @returns boolean
*/
export function hasAdminAccess(user: any): boolean {
return user?.role === 'ADMIN';
}

View File

@ -2,114 +2,236 @@ import { QueryInterface, DataTypes } from 'sequelize';
/** /**
* Migration: Create users table * Migration: Create users table
*
* Purpose: Create the main users table with all fields including RBAC and SSO fields
*
* This must run FIRST before other tables that reference users * This must run FIRST before other tables that reference users
*
* Includes:
* - Basic user information (email, name, etc.)
* - SSO/Okta fields (manager, job_title, etc.)
* - RBAC role system (USER, MANAGEMENT, ADMIN)
* - Location and AD group information
*
* Created: 2025-11-12 (Updated for fresh setup)
*/ */
export async function up(queryInterface: QueryInterface): Promise<void> { export async function up(queryInterface: QueryInterface): Promise<void> {
// Create users table console.log('📋 Creating users table with RBAC and extended SSO fields...');
await queryInterface.createTable('users', {
user_id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
field: 'user_id'
},
employee_id: {
type: DataTypes.STRING(50),
allowNull: true,
field: 'employee_id'
},
okta_sub: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
field: 'okta_sub'
},
email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
field: 'email'
},
first_name: {
type: DataTypes.STRING(100),
allowNull: true,
field: 'first_name'
},
last_name: {
type: DataTypes.STRING(100),
allowNull: true,
field: 'last_name'
},
display_name: {
type: DataTypes.STRING(200),
allowNull: true,
field: 'display_name'
},
department: {
type: DataTypes.STRING(100),
allowNull: true
},
designation: {
type: DataTypes.STRING(100),
allowNull: true
},
phone: {
type: DataTypes.STRING(20),
allowNull: true
},
location: {
type: DataTypes.JSONB,
allowNull: true
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
field: 'is_active'
},
is_admin: {
type: DataTypes.BOOLEAN,
defaultValue: false,
field: 'is_admin'
},
last_login: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'updated_at'
}
});
// Create indexes try {
await queryInterface.addIndex('users', ['email'], { // Step 1: Create ENUM type for roles
name: 'users_email_idx', console.log(' ✓ Creating user_role_enum...');
unique: true await queryInterface.sequelize.query(`
}); CREATE TYPE user_role_enum AS ENUM ('USER', 'MANAGEMENT', 'ADMIN');
`);
await queryInterface.addIndex('users', ['okta_sub'], { // Step 2: Create users table
name: 'users_okta_sub_idx', console.log(' ✓ Creating users table...');
unique: true await queryInterface.createTable('users', {
}); user_id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
field: 'user_id',
comment: 'Primary key - UUID'
},
employee_id: {
type: DataTypes.STRING(50),
allowNull: true,
field: 'employee_id',
comment: 'HR System Employee ID (optional) - some users may not have'
},
okta_sub: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
field: 'okta_sub',
comment: 'Okta user subject identifier - unique identifier from SSO'
},
email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
field: 'email',
comment: 'Primary email address - unique and required'
},
first_name: {
type: DataTypes.STRING(100),
allowNull: true,
defaultValue: '',
field: 'first_name',
comment: 'First name from SSO (optional)'
},
last_name: {
type: DataTypes.STRING(100),
allowNull: true,
defaultValue: '',
field: 'last_name',
comment: 'Last name from SSO (optional)'
},
display_name: {
type: DataTypes.STRING(200),
allowNull: true,
defaultValue: '',
field: 'display_name',
comment: 'Full display name for UI'
},
department: {
type: DataTypes.STRING(100),
allowNull: true,
comment: 'Department/Division from SSO'
},
designation: {
type: DataTypes.STRING(100),
allowNull: true,
comment: 'Job designation/position'
},
phone: {
type: DataTypes.STRING(20),
allowNull: true,
comment: 'Office phone number'
},
await queryInterface.addIndex('users', ['employee_id'], { // ============ Extended SSO/Okta Fields ============
name: 'users_employee_id_idx' manager: {
}); type: DataTypes.STRING(200),
allowNull: true,
comment: 'Reporting manager name from SSO/AD'
},
second_email: {
type: DataTypes.STRING(255),
allowNull: true,
field: 'second_email',
comment: 'Alternate email address from SSO'
},
job_title: {
type: DataTypes.TEXT,
allowNull: true,
field: 'job_title',
comment: 'Detailed job title/description from SSO'
},
employee_number: {
type: DataTypes.STRING(50),
allowNull: true,
field: 'employee_number',
comment: 'HR system employee number from SSO (e.g., "00020330")'
},
postal_address: {
type: DataTypes.STRING(500),
allowNull: true,
field: 'postal_address',
comment: 'Work location/office address from SSO'
},
mobile_phone: {
type: DataTypes.STRING(20),
allowNull: true,
field: 'mobile_phone',
comment: 'Mobile contact number from SSO'
},
ad_groups: {
type: DataTypes.JSONB,
allowNull: true,
field: 'ad_groups',
comment: 'Active Directory group memberships from SSO (memberOf array)'
},
// Users table created // ============ System Fields ============
location: {
type: DataTypes.JSONB,
allowNull: true,
comment: 'JSON object: {city, state, country, office, timezone}'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
field: 'is_active',
comment: 'Account status - true=active, false=disabled'
},
role: {
type: DataTypes.ENUM('USER', 'MANAGEMENT', 'ADMIN'),
allowNull: false,
defaultValue: 'USER',
comment: 'RBAC role: USER (default), MANAGEMENT (read all), ADMIN (full access)'
},
last_login: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login',
comment: 'Last successful login timestamp'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'updated_at'
}
});
// Step 3: Create indexes
console.log(' ✓ Creating indexes...');
await queryInterface.addIndex('users', ['email'], {
name: 'users_email_idx',
unique: true
});
await queryInterface.addIndex('users', ['okta_sub'], {
name: 'users_okta_sub_idx',
unique: true
});
await queryInterface.addIndex('users', ['employee_id'], {
name: 'users_employee_id_idx'
});
await queryInterface.addIndex('users', ['department'], {
name: 'idx_users_department'
});
await queryInterface.addIndex('users', ['is_active'], {
name: 'idx_users_is_active'
});
await queryInterface.addIndex('users', ['role'], {
name: 'idx_users_role'
});
await queryInterface.addIndex('users', ['manager'], {
name: 'idx_users_manager'
});
await queryInterface.addIndex('users', ['postal_address'], {
name: 'idx_users_postal_address'
});
// GIN indexes for JSONB fields
await queryInterface.sequelize.query(`
CREATE INDEX idx_users_location ON users USING gin(location jsonb_path_ops);
CREATE INDEX idx_users_ad_groups ON users USING gin(ad_groups);
`);
console.log('✅ Users table created successfully with all indexes!');
} catch (error) {
console.error('❌ Failed to create users table:', error);
throw error;
}
} }
export async function down(queryInterface: QueryInterface): Promise<void> { export async function down(queryInterface: QueryInterface): Promise<void> {
await queryInterface.dropTable('users'); console.log('📋 Dropping users table...');
// Users table dropped
}
await queryInterface.dropTable('users');
// Drop ENUM type
await queryInterface.sequelize.query(`
DROP TYPE IF EXISTS user_role_enum;
`);
console.log('✅ Users table dropped!');
}

View File

@ -1,166 +0,0 @@
import { QueryInterface, DataTypes } from 'sequelize';
import { v4 as uuidv4 } from 'uuid';
/**
* Migration to add AI provider configurations to admin_configurations
* Allows admins to configure AI provider and API keys through the UI
*/
export async function up(queryInterface: QueryInterface): Promise<void> {
const now = new Date();
await queryInterface.bulkInsert('admin_configurations', [
{
config_id: uuidv4(),
config_key: 'AI_PROVIDER',
config_value: 'claude',
value_type: 'STRING',
config_category: 'AI_CONFIGURATION',
description: 'Active AI provider for conclusion generation (claude, openai, or gemini)',
is_editable: true,
is_sensitive: false,
default_value: 'claude',
display_name: 'AI Provider',
validation_rules: JSON.stringify({
enum: ['claude', 'openai', 'gemini'],
required: true
}),
ui_component: 'select',
options: JSON.stringify(['claude', 'openai', 'gemini']),
sort_order: 100,
requires_restart: false,
created_at: now,
updated_at: now
},
{
config_id: uuidv4(),
config_key: 'CLAUDE_API_KEY',
config_value: '',
value_type: 'STRING',
config_category: 'AI_CONFIGURATION',
description: 'API key for Claude (Anthropic) - Get from console.anthropic.com',
is_editable: true,
is_sensitive: true,
default_value: '',
display_name: 'Claude API Key',
validation_rules: JSON.stringify({
pattern: '^sk-ant-',
minLength: 40
}),
ui_component: 'input',
sort_order: 101,
requires_restart: false,
created_at: now,
updated_at: now
},
{
config_id: uuidv4(),
config_key: 'OPENAI_API_KEY',
config_value: '',
value_type: 'STRING',
config_category: 'AI_CONFIGURATION',
description: 'API key for OpenAI (GPT-4) - Get from platform.openai.com',
is_editable: true,
is_sensitive: true,
default_value: '',
display_name: 'OpenAI API Key',
validation_rules: JSON.stringify({
pattern: '^sk-',
minLength: 40
}),
ui_component: 'input',
sort_order: 102,
requires_restart: false,
created_at: now,
updated_at: now
},
{
config_id: uuidv4(),
config_key: 'GEMINI_API_KEY',
config_value: '',
value_type: 'STRING',
config_category: 'AI_CONFIGURATION',
description: 'API key for Gemini (Google) - Get from ai.google.dev',
is_editable: true,
is_sensitive: true,
default_value: '',
display_name: 'Gemini API Key',
validation_rules: JSON.stringify({
minLength: 20
}),
ui_component: 'input',
sort_order: 103,
requires_restart: false,
created_at: now,
updated_at: now
},
{
config_id: uuidv4(),
config_key: 'AI_ENABLED',
config_value: 'true',
value_type: 'BOOLEAN',
config_category: 'AI_CONFIGURATION',
description: 'Master toggle to enable/disable all AI-powered features in the system',
is_editable: true,
is_sensitive: false,
default_value: 'true',
display_name: 'Enable AI Features',
validation_rules: JSON.stringify({
type: 'boolean'
}),
ui_component: 'toggle',
sort_order: 104,
requires_restart: false,
created_at: now,
updated_at: now
},
{
config_id: uuidv4(),
config_key: 'AI_REMARK_GENERATION_ENABLED',
config_value: 'true',
value_type: 'BOOLEAN',
config_category: 'AI_CONFIGURATION',
description: 'Enable/disable AI-powered conclusion remark generation when requests are approved',
is_editable: true,
is_sensitive: false,
default_value: 'true',
display_name: 'Enable AI Remark Generation',
validation_rules: JSON.stringify({
type: 'boolean'
}),
ui_component: 'toggle',
sort_order: 105,
requires_restart: false,
created_at: now,
updated_at: now
},
{
config_id: uuidv4(),
config_key: 'AI_MAX_REMARK_LENGTH',
config_value: '2000',
value_type: 'INTEGER',
config_category: 'AI_CONFIGURATION',
description: 'Maximum character length for AI-generated conclusion remarks (used as context for AI prompt)',
is_editable: true,
is_sensitive: false,
default_value: '2000',
display_name: 'AI Max Remark Length',
validation_rules: JSON.stringify({
type: 'number',
min: 500,
max: 5000
}),
ui_component: 'number',
sort_order: 106,
requires_restart: false,
created_at: now,
updated_at: now
}
]);
}
export async function down(queryInterface: QueryInterface): Promise<void> {
await queryInterface.bulkDelete('admin_configurations', {
config_key: ['AI_PROVIDER', 'CLAUDE_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'AI_ENABLED', 'AI_REMARK_GENERATION_ENABLED', 'AI_MAX_REMARK_LENGTH']
} as any);
}

View File

@ -1,6 +1,15 @@
import { DataTypes, Model, Optional } from 'sequelize'; import { DataTypes, Model, Optional } from 'sequelize';
import { sequelize } from '../config/database'; import { sequelize } from '../config/database';
/**
* User Role Enum
*
* USER: Default role - can create requests, view own requests, participate in workflows
* MANAGEMENT: Enhanced visibility - can view all requests, read-only access to all data
* ADMIN: Full access - can manage system configuration, users, and all workflows
*/
export type UserRole = 'USER' | 'MANAGEMENT' | 'ADMIN';
interface UserAttributes { interface UserAttributes {
userId: string; userId: string;
employeeId?: string | null; employeeId?: string | null;
@ -12,6 +21,16 @@ interface UserAttributes {
department?: string | null; department?: string | null;
designation?: string | null; designation?: string | null;
phone?: string | null; phone?: string | null;
// Extended fields from SSO/Okta (All Optional)
manager?: string | null; // Reporting manager name
secondEmail?: string | null; // Alternate email
jobTitle?: string | null; // Detailed job description (title field from Okta)
employeeNumber?: string | null; // HR system employee number (different from employeeId)
postalAddress?: string | null; // Work location/office address
mobilePhone?: string | null; // Mobile contact (different from phone)
adGroups?: string[] | null; // Active Directory group memberships
// Location Information (JSON object) // Location Information (JSON object)
location?: { location?: {
city?: string; city?: string;
@ -21,13 +40,13 @@ interface UserAttributes {
timezone?: string; timezone?: string;
}; };
isActive: boolean; isActive: boolean;
isAdmin: boolean; role: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
lastLogin?: Date; lastLogin?: Date;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
interface UserCreationAttributes extends Optional<UserAttributes, 'userId' | 'employeeId' | 'department' | 'designation' | 'phone' | 'lastLogin' | 'createdAt' | 'updatedAt'> {} interface UserCreationAttributes extends Optional<UserAttributes, 'userId' | 'employeeId' | 'department' | 'designation' | 'phone' | 'manager' | 'secondEmail' | 'jobTitle' | 'employeeNumber' | 'postalAddress' | 'mobilePhone' | 'adGroups' | 'role' | 'lastLogin' | 'createdAt' | 'updatedAt'> {}
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes { class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
public userId!: string; public userId!: string;
@ -40,6 +59,16 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
public department?: string; public department?: string;
public designation?: string; public designation?: string;
public phone?: string; public phone?: string;
// Extended fields from SSO/Okta (All Optional)
public manager?: string | null;
public secondEmail?: string | null;
public jobTitle?: string | null;
public employeeNumber?: string | null;
public postalAddress?: string | null;
public mobilePhone?: string | null;
public adGroups?: string[] | null;
// Location Information (JSON object) // Location Information (JSON object)
public location?: { public location?: {
city?: string; city?: string;
@ -49,12 +78,35 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
timezone?: string; timezone?: string;
}; };
public isActive!: boolean; public isActive!: boolean;
public isAdmin!: boolean; public role!: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
public lastLogin?: Date; public lastLogin?: Date;
public createdAt!: Date; public createdAt!: Date;
public updatedAt!: Date; public updatedAt!: Date;
// Associations // Associations
/**
* Helper Methods for Role Checking
*/
public isUserRole(): boolean {
return this.role === 'USER';
}
public isManagementRole(): boolean {
return this.role === 'MANAGEMENT';
}
public isAdminRole(): boolean {
return this.role === 'ADMIN';
}
public hasManagementAccess(): boolean {
return this.role === 'MANAGEMENT' || this.role === 'ADMIN';
}
public hasAdminAccess(): boolean {
return this.role === 'ADMIN';
}
} }
User.init( User.init(
@ -117,6 +169,53 @@ User.init(
type: DataTypes.STRING(20), type: DataTypes.STRING(20),
allowNull: true allowNull: true
}, },
// ============ Extended SSO/Okta Fields (All Optional) ============
manager: {
type: DataTypes.STRING(200),
allowNull: true,
comment: 'Reporting manager name from SSO/AD'
},
secondEmail: {
type: DataTypes.STRING(255),
allowNull: true,
field: 'second_email',
validate: {
isEmail: true
},
comment: 'Alternate email address from SSO'
},
jobTitle: {
type: DataTypes.TEXT,
allowNull: true,
field: 'job_title',
comment: 'Detailed job title/description from SSO (e.g., "Manages dealers for MotorCycle Business...")'
},
employeeNumber: {
type: DataTypes.STRING(50),
allowNull: true,
field: 'employee_number',
comment: 'HR system employee number from SSO (e.g., "00020330")'
},
postalAddress: {
type: DataTypes.STRING(500),
allowNull: true,
field: 'postal_address',
comment: 'Work location/office address from SSO (e.g., "Kolkata", "Chennai")'
},
mobilePhone: {
type: DataTypes.STRING(20),
allowNull: true,
field: 'mobile_phone',
comment: 'Mobile contact number from SSO (mobilePhone field)'
},
adGroups: {
type: DataTypes.JSONB,
allowNull: true,
field: 'ad_groups',
comment: 'Active Directory group memberships from SSO (memberOf field) - JSON array'
},
// Location Information (JSON object) // Location Information (JSON object)
location: { location: {
type: DataTypes.JSONB, // Use JSONB for PostgreSQL type: DataTypes.JSONB, // Use JSONB for PostgreSQL
@ -129,11 +228,11 @@ User.init(
field: 'is_active', field: 'is_active',
comment: 'Account status' comment: 'Account status'
}, },
isAdmin: { role: {
type: DataTypes.BOOLEAN, type: DataTypes.ENUM('USER', 'MANAGEMENT', 'ADMIN'),
defaultValue: false, allowNull: false,
field: 'is_admin', defaultValue: 'USER',
comment: 'Super user flag' comment: 'User role for access control: USER (default), MANAGEMENT (read all), ADMIN (full access)'
}, },
lastLogin: { lastLogin: {
type: DataTypes.DATE, type: DataTypes.DATE,
@ -178,11 +277,24 @@ User.init(
{ {
fields: ['is_active'] fields: ['is_active']
}, },
{
fields: ['role'], // Index for role-based queries
name: 'idx_users_role'
},
{
fields: ['manager'], // Index for org chart queries
name: 'idx_users_manager'
},
{
fields: ['postal_address'], // Index for location-based filtering
name: 'idx_users_postal_address'
},
{ {
fields: ['location'], fields: ['location'],
using: 'gin', // GIN index for JSONB queries using: 'gin', // GIN index for JSONB queries
operator: 'jsonb_path_ops' operator: 'jsonb_path_ops'
} }
// Note: ad_groups GIN index is created in migration (can't be defined here)
] ]
} }
); );

View File

@ -10,7 +10,11 @@ import {
bulkImportHolidays, bulkImportHolidays,
getAllConfigurations, getAllConfigurations,
updateConfiguration, updateConfiguration,
resetConfiguration resetConfiguration,
updateUserRole,
getUsersByRole,
getRoleStatistics,
assignRoleByEmail
} from '@controllers/admin.controller'; } from '@controllers/admin.controller';
const router = Router(); const router = Router();
@ -97,5 +101,39 @@ router.put('/configurations/:configKey', updateConfiguration);
*/ */
router.post('/configurations/:configKey/reset', resetConfiguration); router.post('/configurations/:configKey/reset', resetConfiguration);
// ==================== User Role Management Routes (RBAC) ====================
/**
* @route POST /api/admin/users/assign-role
* @desc Assign role to user by email (creates user from Okta if doesn't exist)
* @body { email: string, role: 'USER' | 'MANAGEMENT' | 'ADMIN' }
* @access Admin
*/
router.post('/users/assign-role', assignRoleByEmail);
/**
* @route PUT /api/admin/users/:userId/role
* @desc Update user's role (USER, MANAGEMENT, ADMIN)
* @params userId
* @body { role: 'USER' | 'MANAGEMENT' | 'ADMIN' }
* @access Admin
*/
router.put('/users/:userId/role', updateUserRole);
/**
* @route GET /api/admin/users/by-role
* @desc Get all users filtered by role
* @query role (optional): ADMIN | MANAGEMENT | USER
* @access Admin
*/
router.get('/users/by-role', getUsersByRole);
/**
* @route GET /api/admin/users/role-statistics
* @desc Get count of users in each role
* @access Admin
*/
router.get('/users/role-statistics', getRoleStatistics);
export default router; export default router;

View File

@ -40,7 +40,8 @@ router.get('/status', authenticateToken, async (req: Request, res: Response) =>
router.post('/reinitialize', authenticateToken, async (req: Request, res: Response): Promise<void> => { router.post('/reinitialize', authenticateToken, async (req: Request, res: Response): Promise<void> => {
try { try {
// Check if user is admin // Check if user is admin
const isAdmin = (req as any).user?.isAdmin; const userRole = (req as any).user?.role;
const isAdmin = userRole?.toLowerCase() === 'admin';
if (!isAdmin) { if (!isAdmin) {
res.status(403).json({ res.status(403).json({
success: false, success: false,

168
src/scripts/auto-setup.ts Normal file
View File

@ -0,0 +1,168 @@
/**
* Automatic Database Setup Script
* Runs before server starts to ensure database is ready
*
* This script:
* 1. Checks if database exists
* 2. Creates database if missing
* 3. Installs required extensions
* 4. Runs all pending migrations (18 total)
* 5. Configs are auto-seeded by configSeed.service.ts on server start (30 configs)
*/
import { Client } from 'pg';
import { sequelize } from '../config/database';
import { exec } from 'child_process';
import { promisify } from 'util';
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const execAsync = promisify(exec);
const DB_HOST = process.env.DB_HOST || 'localhost';
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
const DB_USER = process.env.DB_USER || 'postgres';
const DB_PASSWORD = process.env.DB_PASSWORD || '';
const DB_NAME = process.env.DB_NAME || 'royal_enfield_workflow';
async function checkAndCreateDatabase(): Promise<boolean> {
const client = new Client({
host: DB_HOST,
port: DB_PORT,
user: DB_USER,
password: DB_PASSWORD,
database: 'postgres', // Connect to default postgres database
});
try {
await client.connect();
console.log('🔍 Checking if database exists...');
// Check if database exists
const result = await client.query(
`SELECT 1 FROM pg_database WHERE datname = $1`,
[DB_NAME]
);
if (result.rows.length === 0) {
console.log(`📦 Database '${DB_NAME}' not found. Creating...`);
// Create database
await client.query(`CREATE DATABASE "${DB_NAME}"`);
console.log(`✅ Database '${DB_NAME}' created successfully!`);
await client.end();
// Connect to new database and install extensions
const newDbClient = new Client({
host: DB_HOST,
port: DB_PORT,
user: DB_USER,
password: DB_PASSWORD,
database: DB_NAME,
});
await newDbClient.connect();
console.log('📦 Installing uuid-ossp extension...');
await newDbClient.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
console.log('✅ Extension installed!');
await newDbClient.end();
return true; // Database was created
} else {
console.log(`✅ Database '${DB_NAME}' already exists.`);
await client.end();
return false; // Database already existed
}
} catch (error: any) {
console.error('❌ Database check/creation failed:', error.message);
await client.end();
throw error;
}
}
async function runMigrations(): Promise<void> {
try {
console.log('🔄 Running migrations...');
// Run migrations using npm script
const { stdout, stderr } = await execAsync('npm run migrate', {
cwd: path.resolve(__dirname, '../..'),
});
if (stdout) console.log(stdout);
if (stderr && !stderr.includes('npm WARN')) console.error(stderr);
console.log('✅ Migrations completed successfully!');
} catch (error: any) {
console.error('❌ Migration failed:', error.message);
throw error;
}
}
async function testConnection(): Promise<void> {
try {
console.log('🔌 Testing database connection...');
await sequelize.authenticate();
console.log('✅ Database connection established!');
} catch (error: any) {
console.error('❌ Unable to connect to database:', error.message);
throw error;
}
}
async function autoSetup(): Promise<void> {
console.log('\n========================================');
console.log('🚀 Royal Enfield Workflow - Auto Setup');
console.log('========================================\n');
try {
// Step 1: Check and create database if needed
const wasCreated = await checkAndCreateDatabase();
// Step 2: Test connection
await testConnection();
// Step 3: Run migrations (always, to catch any pending migrations)
await runMigrations();
console.log('\n========================================');
console.log('✅ Setup completed successfully!');
console.log('========================================\n');
console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.\n');
if (wasCreated) {
console.log('💡 Next steps:');
console.log(' 1. Server will start automatically');
console.log(' 2. Log in via SSO');
console.log(' 3. Run this SQL to make yourself admin:');
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`);
}
} catch (error: any) {
console.error('\n========================================');
console.error('❌ Setup failed!');
console.error('========================================');
console.error('Error:', error.message);
console.error('\nPlease check:');
console.error('1. PostgreSQL is running');
console.error('2. DB credentials in .env are correct');
console.error('3. User has permission to create databases\n');
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
autoSetup().then(() => {
process.exit(0);
}).catch(() => {
process.exit(1);
});
}
export default autoSetup;

View File

@ -18,7 +18,6 @@ import * as m14 from '../migrations/20251105-add-skip-fields-to-approval-levels'
import * as m15 from '../migrations/2025110501-alter-tat-days-to-generated'; import * as m15 from '../migrations/2025110501-alter-tat-days-to-generated';
import * as m16 from '../migrations/20251111-create-notifications'; import * as m16 from '../migrations/20251111-create-notifications';
import * as m17 from '../migrations/20251111-create-conclusion-remarks'; import * as m17 from '../migrations/20251111-create-conclusion-remarks';
import * as m18 from '../migrations/20251111-add-ai-provider-configs';
interface Migration { interface Migration {
name: string; name: string;
@ -51,7 +50,6 @@ const migrations: Migration[] = [
{ name: '2025110501-alter-tat-days-to-generated', module: m15 }, { name: '2025110501-alter-tat-days-to-generated', module: m15 },
{ name: '20251111-create-notifications', module: m16 }, { name: '20251111-create-notifications', module: m16 },
{ name: '20251111-create-conclusion-remarks', module: m17 }, { name: '20251111-create-conclusion-remarks', module: m17 },
{ name: '20251111-add-ai-provider-configs', module: m18 },
]; ];
/** /**

View File

@ -148,7 +148,7 @@ async function seedAdminConfigurations() {
( (
gen_random_uuid(), gen_random_uuid(),
'WORK_START_HOUR', 'WORK_START_HOUR',
'WORKING_HOURS', 'TAT_SETTINGS',
'9', '9',
'NUMBER', 'NUMBER',
'Work Day Start Hour', 'Work Day Start Hour',
@ -166,7 +166,7 @@ async function seedAdminConfigurations() {
( (
gen_random_uuid(), gen_random_uuid(),
'WORK_END_HOUR', 'WORK_END_HOUR',
'WORKING_HOURS', 'TAT_SETTINGS',
'18', '18',
'NUMBER', 'NUMBER',
'Work Day End Hour', 'Work Day End Hour',
@ -184,7 +184,7 @@ async function seedAdminConfigurations() {
( (
gen_random_uuid(), gen_random_uuid(),
'WORK_START_DAY', 'WORK_START_DAY',
'WORKING_HOURS', 'TAT_SETTINGS',
'1', '1',
'NUMBER', 'NUMBER',
'Work Week Start Day', 'Work Week Start Day',
@ -202,7 +202,7 @@ async function seedAdminConfigurations() {
( (
gen_random_uuid(), gen_random_uuid(),
'WORK_END_DAY', 'WORK_END_DAY',
'WORKING_HOURS', 'TAT_SETTINGS',
'5', '5',
'NUMBER', 'NUMBER',
'Work Week End Day', 'Work Week End Day',
@ -366,7 +366,138 @@ async function seedAdminConfigurations() {
true, true,
NOW(), NOW(),
NOW() NOW()
),
-- AI Configuration (from migration 20251111-add-ai-provider-configs)
(
gen_random_uuid(),
'AI_PROVIDER',
'AI_CONFIGURATION',
'claude',
'STRING',
'AI Provider',
'Active AI provider for conclusion generation (claude, openai, or gemini)',
'claude',
true,
false,
'{"enum": ["claude", "openai", "gemini"], "required": true}'::jsonb,
'select',
100,
false,
NOW(),
NOW()
),
(
gen_random_uuid(),
'CLAUDE_API_KEY',
'AI_CONFIGURATION',
'',
'STRING',
'Claude API Key',
'API key for Claude (Anthropic) - Get from console.anthropic.com',
'',
true,
true,
'{"pattern": "^sk-ant-", "minLength": 40}'::jsonb,
'input',
101,
false,
NOW(),
NOW()
),
(
gen_random_uuid(),
'OPENAI_API_KEY',
'AI_CONFIGURATION',
'',
'STRING',
'OpenAI API Key',
'API key for OpenAI (GPT-4) - Get from platform.openai.com',
'',
true,
true,
'{"pattern": "^sk-", "minLength": 40}'::jsonb,
'input',
102,
false,
NOW(),
NOW()
),
(
gen_random_uuid(),
'GEMINI_API_KEY',
'AI_CONFIGURATION',
'',
'STRING',
'Gemini API Key',
'API key for Gemini (Google) - Get from ai.google.dev',
'',
true,
true,
'{"minLength": 20}'::jsonb,
'input',
103,
false,
NOW(),
NOW()
),
(
gen_random_uuid(),
'AI_ENABLED',
'AI_CONFIGURATION',
'true',
'BOOLEAN',
'Enable AI Features',
'Master toggle to enable/disable all AI-powered features in the system',
'true',
true,
false,
'{"type": "boolean"}'::jsonb,
'toggle',
104,
false,
NOW(),
NOW()
),
(
gen_random_uuid(),
'AI_REMARK_GENERATION_ENABLED',
'AI_CONFIGURATION',
'true',
'BOOLEAN',
'Enable AI Remark Generation',
'Enable/disable AI-powered conclusion remark generation when requests are approved',
'true',
true,
false,
'{"type": "boolean"}'::jsonb,
'toggle',
105,
false,
NOW(),
NOW()
),
(
gen_random_uuid(),
'AI_MAX_REMARK_LENGTH',
'AI_CONFIGURATION',
'2000',
'NUMBER',
'AI Max Remark Length',
'Maximum character length for AI-generated conclusion remarks (used as context for AI prompt)',
'2000',
true,
false,
'{"type": "number", "min": 500, "max": 5000}'::jsonb,
'number',
106,
false,
NOW(),
NOW()
) )
ON CONFLICT (config_key) DO UPDATE SET
config_value = EXCLUDED.config_value,
updated_at = NOW()
`); `);
const finalCount = await sequelize.query( const finalCount = await sequelize.query(

View File

@ -89,7 +89,7 @@ export class AuthService {
designation: userData.designation || null, designation: userData.designation || null,
phone: userData.phone || null, phone: userData.phone || null,
isActive: true, isActive: true,
isAdmin: false, role: 'USER',
lastLogin: new Date() lastLogin: new Date()
}); });
@ -117,7 +117,7 @@ export class AuthService {
displayName: user.displayName || null, displayName: user.displayName || null,
department: user.department || null, department: user.department || null,
designation: user.designation || null, designation: user.designation || null,
isAdmin: user.isAdmin role: user.role
}, },
accessToken, accessToken,
refreshToken refreshToken
@ -145,7 +145,7 @@ export class AuthService {
userId: user.userId, userId: user.userId,
employeeId: user.employeeId, employeeId: user.employeeId,
email: user.email, email: user.email,
role: user.isAdmin ? 'admin' : 'user' role: user.role // Keep uppercase: USER, MANAGEMENT, ADMIN
}; };
const options: SignOptions = { const options: SignOptions = {

View File

@ -305,6 +305,111 @@ export async function seedDefaultConfigurations(): Promise<void> {
NOW(), NOW(),
NOW() NOW()
), ),
(
gen_random_uuid(),
'AI_PROVIDER',
'AI_CONFIGURATION',
'claude',
'STRING',
'AI Provider',
'Active AI provider for conclusion generation (claude, openai, or gemini)',
'claude',
true,
false,
'{"enum": ["claude", "openai", "gemini"], "required": true}'::jsonb,
'select',
'["claude", "openai", "gemini"]'::jsonb,
22,
false,
NULL,
NULL,
NOW(),
NOW()
),
(
gen_random_uuid(),
'CLAUDE_API_KEY',
'AI_CONFIGURATION',
'',
'STRING',
'Claude API Key',
'API key for Claude (Anthropic) - Get from console.anthropic.com',
'',
true,
true,
'{"pattern": "^sk-ant-", "minLength": 40}'::jsonb,
'input',
NULL,
23,
false,
NULL,
NULL,
NOW(),
NOW()
),
(
gen_random_uuid(),
'OPENAI_API_KEY',
'AI_CONFIGURATION',
'',
'STRING',
'OpenAI API Key',
'API key for OpenAI (GPT-4) - Get from platform.openai.com',
'',
true,
true,
'{"pattern": "^sk-", "minLength": 40}'::jsonb,
'input',
NULL,
24,
false,
NULL,
NULL,
NOW(),
NOW()
),
(
gen_random_uuid(),
'GEMINI_API_KEY',
'AI_CONFIGURATION',
'',
'STRING',
'Gemini API Key',
'API key for Gemini (Google) - Get from ai.google.dev',
'',
true,
true,
'{"minLength": 20}'::jsonb,
'input',
NULL,
25,
false,
NULL,
NULL,
NOW(),
NOW()
),
(
gen_random_uuid(),
'AI_ENABLED',
'AI_CONFIGURATION',
'true',
'BOOLEAN',
'Enable AI Features',
'Master toggle to enable/disable all AI-powered features in the system',
'true',
true,
false,
'{"type": "boolean"}'::jsonb,
'toggle',
NULL,
26,
false,
NULL,
NULL,
NOW(),
NOW()
),
-- Notification Rules -- Notification Rules
( (
gen_random_uuid(), gen_random_uuid(),
@ -563,7 +668,7 @@ export async function seedDefaultConfigurations(): Promise<void> {
) )
`, { type: QueryTypes.INSERT }); `, { type: QueryTypes.INSERT });
logger.info('[Config Seed] ✅ Default configurations seeded successfully (20 settings across 7 categories)'); logger.info('[Config Seed] ✅ Default configurations seeded successfully (30 settings across 7 categories)');
} catch (error) { } catch (error) {
logger.error('[Config Seed] Error seeding configurations:', error); logger.error('[Config Seed] Error seeding configurations:', error);
// Don't throw - let server start even if seeding fails // Don't throw - let server start even if seeding fails

View File

@ -103,9 +103,9 @@ export class DashboardService {
async getRequestStats(userId: string, dateRange?: string) { async getRequestStats(userId: string, dateRange?: string) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: show only requests they INITIATED (not participated in) // For regular users: show only requests they INITIATED (not participated in)
// For admin: show all requests // For admin: show all requests
@ -163,9 +163,9 @@ export class DashboardService {
async getTATEfficiency(userId: string, dateRange?: string) { async getTATEfficiency(userId: string, dateRange?: string) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: only their initiated requests // For regular users: only their initiated requests
// For admin: all requests // For admin: all requests
@ -275,9 +275,9 @@ export class DashboardService {
async getEngagementStats(userId: string, dateRange?: string) { async getEngagementStats(userId: string, dateRange?: string) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// Get work notes count - uses created_at // Get work notes count - uses created_at
// For regular users: only from requests they initiated // For regular users: only from requests they initiated
@ -340,9 +340,9 @@ export class DashboardService {
async getAIInsights(userId: string, dateRange?: string) { async getAIInsights(userId: string, dateRange?: string) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: only their initiated requests // For regular users: only their initiated requests
let whereClause = ` let whereClause = `
@ -390,9 +390,9 @@ export class DashboardService {
async getAIRemarkUtilization(userId: string, dateRange?: string) { async getAIRemarkUtilization(userId: string, dateRange?: string) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: only their initiated requests // For regular users: only their initiated requests
const userFilter = !isAdmin ? `AND cr.edited_by = :userId` : ''; const userFilter = !isAdmin ? `AND cr.edited_by = :userId` : '';
@ -451,9 +451,9 @@ export class DashboardService {
async getApproverPerformance(userId: string, dateRange?: string, page: number = 1, limit: number = 10) { async getApproverPerformance(userId: string, dateRange?: string, page: number = 1, limit: number = 10) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: return empty (only admins should see this) // For regular users: return empty (only admins should see this)
if (!isAdmin) { if (!isAdmin) {
@ -532,9 +532,9 @@ export class DashboardService {
* Get recent activity feed with pagination * Get recent activity feed with pagination
*/ */
async getRecentActivity(userId: string, page: number = 1, limit: number = 10) { async getRecentActivity(userId: string, page: number = 1, limit: number = 10) {
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: only activities from their initiated requests OR where they're a participant // For regular users: only activities from their initiated requests OR where they're a participant
let whereClause = isAdmin ? '' : ` let whereClause = isAdmin ? '' : `
@ -616,9 +616,9 @@ export class DashboardService {
* Get critical requests (breached TAT or approaching deadline) with pagination * Get critical requests (breached TAT or approaching deadline) with pagination
*/ */
async getCriticalRequests(userId: string, page: number = 1, limit: number = 10) { async getCriticalRequests(userId: string, page: number = 1, limit: number = 10) {
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: show only their initiated requests OR where they are current approver // For regular users: show only their initiated requests OR where they are current approver
let whereClause = ` let whereClause = `
@ -757,9 +757,9 @@ export class DashboardService {
* Get upcoming deadlines with pagination * Get upcoming deadlines with pagination
*/ */
async getUpcomingDeadlines(userId: string, page: number = 1, limit: number = 10) { async getUpcomingDeadlines(userId: string, page: number = 1, limit: number = 10) {
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: only show CURRENT LEVEL where they are the approver // For regular users: only show CURRENT LEVEL where they are the approver
// For admins: show all current active levels // For admins: show all current active levels
@ -871,9 +871,9 @@ export class DashboardService {
async getDepartmentStats(userId: string, dateRange?: string) { async getDepartmentStats(userId: string, dateRange?: string) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: only their initiated requests // For regular users: only their initiated requests
let whereClause = ` let whereClause = `
@ -916,9 +916,9 @@ export class DashboardService {
async getPriorityDistribution(userId: string, dateRange?: string) { async getPriorityDistribution(userId: string, dateRange?: string) {
const range = this.parseDateRange(dateRange); const range = this.parseDateRange(dateRange);
// Check if user is admin // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = (user as any)?.isAdmin || false; const isAdmin = user?.hasAdminAccess() || false;
// For regular users: only their initiated requests // For regular users: only their initiated requests
let whereClause = ` let whereClause = `

View File

@ -71,7 +71,7 @@ export class UserService {
phone: ssoData.phone || null, phone: ssoData.phone || null,
// location: (ssoData as any).location || null, // Ignored for now - schema not finalized // location: (ssoData as any).location || null, // Ignored for now - schema not finalized
isActive: true, isActive: true,
isAdmin: false, // Default to false, can be updated later role: 'USER', // Default role for new users
lastLogin: now lastLogin: now
}); });
@ -246,7 +246,7 @@ export class UserService {
designation: null, designation: null,
phone: oktaUserData.phone || null, phone: oktaUserData.phone || null,
isActive: true, isActive: true,
isAdmin: false, role: 'USER',
lastLogin: undefined, // Not logged in yet, just created for tagging lastLogin: undefined, // Not logged in yet, just created for tagging
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()

View File

@ -37,7 +37,7 @@ export interface LoginResponse {
displayName?: string | null; displayName?: string | null;
department?: string | null; department?: string | null;
designation?: string | null; designation?: string | null;
isAdmin: boolean; role: 'USER' | 'MANAGEMENT' | 'ADMIN';
}; };
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;

View File

@ -1,5 +1,7 @@
import { JwtPayload } from 'jsonwebtoken'; import { JwtPayload } from 'jsonwebtoken';
export type UserRole = 'USER' | 'MANAGEMENT' | 'ADMIN';
declare global { declare global {
namespace Express { namespace Express {
interface Request { interface Request {
@ -7,7 +9,7 @@ declare global {
userId: string; userId: string;
email: string; email: string;
employeeId?: string | null; // Optional - schema not finalized employeeId?: string | null; // Optional - schema not finalized
role?: string; role?: UserRole;
}; };
cookies?: { cookies?: {
accessToken?: string; accessToken?: string;
@ -25,7 +27,7 @@ export interface AuthenticatedRequest extends Express.Request {
userId: string; userId: string;
email: string; email: string;
employeeId?: string | null; // Optional - schema not finalized employeeId?: string | null; // Optional - schema not finalized
role: string; role: UserRole;
}; };
params: any; params: any;
body: any; body: any;

View File

@ -1,3 +1,12 @@
/**
* User Role Types (RBAC)
*
* USER: Default role - standard workflow participant
* MANAGEMENT: Enhanced visibility - read access to all data
* ADMIN: Full system access - configuration and user management
*/
export type UserRole = 'USER' | 'MANAGEMENT' | 'ADMIN';
export interface User { export interface User {
userId: string; userId: string;
employeeId: string; employeeId: string;
@ -10,7 +19,7 @@ export interface User {
phone?: string; phone?: string;
reportingManagerId?: string; reportingManagerId?: string;
isActive: boolean; isActive: boolean;
isAdmin: boolean; role: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
lastLogin?: Date; lastLogin?: Date;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
@ -26,6 +35,7 @@ export interface CreateUserData {
designation?: string; designation?: string;
phone?: string; phone?: string;
reportingManagerId?: string; reportingManagerId?: string;
role?: UserRole; // Optional on creation, defaults to 'USER'
} }
export interface UpdateUserData { export interface UpdateUserData {
@ -38,5 +48,12 @@ export interface UpdateUserData {
phone?: string; phone?: string;
reportingManagerId?: string; reportingManagerId?: string;
isActive?: boolean; isActive?: boolean;
isAdmin?: boolean; role?: UserRole; // RBAC: Can update user role (ADMIN only)
manager?: string;
secondEmail?: string;
jobTitle?: string;
employeeNumber?: string;
postalAddress?: string;
mobilePhone?: string;
adGroups?: string[];
} }