backend prepared for fresh startup added roles to user and also addd extra fieldas
This commit is contained in:
parent
cbca9d1b15
commit
b4407f59a6
263
QUICK_START.md
Normal file
263
QUICK_START.md
Normal 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!** 🎉
|
||||||
|
|
||||||
@ -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 {
|
||||||
|
|||||||
506
docs/FRESH_DATABASE_SETUP.md
Normal file
506
docs/FRESH_DATABASE_SETUP.md
Normal 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
632
docs/RBAC_IMPLEMENTATION.md
Normal 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
372
docs/RBAC_QUICK_START.md
Normal 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
13
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
55
scripts/assign-admin-user.sql
Normal file
55
scripts/assign-admin-user.sql
Normal 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;
|
||||||
|
|
||||||
123
scripts/assign-user-roles.sql
Normal file
123
scripts/assign-user-roles.sql
Normal 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;
|
||||||
136
scripts/fresh-database-setup.bat
Normal file
136
scripts/fresh-database-setup.bat
Normal 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
|
||||||
|
|
||||||
168
scripts/fresh-database-setup.sh
Normal file
168
scripts/fresh-database-setup.sh
Normal 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}"
|
||||||
|
|
||||||
@ -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
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,79 +2,162 @@ 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...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create ENUM type for roles
|
||||||
|
console.log(' ✓ Creating user_role_enum...');
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TYPE user_role_enum AS ENUM ('USER', 'MANAGEMENT', 'ADMIN');
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Step 2: Create users table
|
||||||
|
console.log(' ✓ Creating users table...');
|
||||||
await queryInterface.createTable('users', {
|
await queryInterface.createTable('users', {
|
||||||
user_id: {
|
user_id: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
field: 'user_id'
|
field: 'user_id',
|
||||||
|
comment: 'Primary key - UUID'
|
||||||
},
|
},
|
||||||
employee_id: {
|
employee_id: {
|
||||||
type: DataTypes.STRING(50),
|
type: DataTypes.STRING(50),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'employee_id'
|
field: 'employee_id',
|
||||||
|
comment: 'HR System Employee ID (optional) - some users may not have'
|
||||||
},
|
},
|
||||||
okta_sub: {
|
okta_sub: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
field: 'okta_sub'
|
field: 'okta_sub',
|
||||||
|
comment: 'Okta user subject identifier - unique identifier from SSO'
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
field: 'email'
|
field: 'email',
|
||||||
|
comment: 'Primary email address - unique and required'
|
||||||
},
|
},
|
||||||
first_name: {
|
first_name: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'first_name'
|
defaultValue: '',
|
||||||
|
field: 'first_name',
|
||||||
|
comment: 'First name from SSO (optional)'
|
||||||
},
|
},
|
||||||
last_name: {
|
last_name: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'last_name'
|
defaultValue: '',
|
||||||
|
field: 'last_name',
|
||||||
|
comment: 'Last name from SSO (optional)'
|
||||||
},
|
},
|
||||||
display_name: {
|
display_name: {
|
||||||
type: DataTypes.STRING(200),
|
type: DataTypes.STRING(200),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'display_name'
|
defaultValue: '',
|
||||||
|
field: 'display_name',
|
||||||
|
comment: 'Full display name for UI'
|
||||||
},
|
},
|
||||||
department: {
|
department: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
|
comment: 'Department/Division from SSO'
|
||||||
},
|
},
|
||||||
designation: {
|
designation: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
|
comment: 'Job designation/position'
|
||||||
},
|
},
|
||||||
phone: {
|
phone: {
|
||||||
type: DataTypes.STRING(20),
|
type: DataTypes.STRING(20),
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
|
comment: 'Office phone number'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ============ Extended SSO/Okta Fields ============
|
||||||
|
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)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============ System Fields ============
|
||||||
location: {
|
location: {
|
||||||
type: DataTypes.JSONB,
|
type: DataTypes.JSONB,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
|
comment: 'JSON object: {city, state, country, office, timezone}'
|
||||||
},
|
},
|
||||||
is_active: {
|
is_active: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
field: 'is_active'
|
field: 'is_active',
|
||||||
|
comment: 'Account status - true=active, false=disabled'
|
||||||
},
|
},
|
||||||
is_admin: {
|
role: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.ENUM('USER', 'MANAGEMENT', 'ADMIN'),
|
||||||
defaultValue: false,
|
allowNull: false,
|
||||||
field: 'is_admin'
|
defaultValue: 'USER',
|
||||||
|
comment: 'RBAC role: USER (default), MANAGEMENT (read all), ADMIN (full access)'
|
||||||
},
|
},
|
||||||
last_login: {
|
last_login: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'last_login'
|
field: 'last_login',
|
||||||
|
comment: 'Last successful login timestamp'
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
@ -90,7 +173,9 @@ export async function up(queryInterface: QueryInterface): Promise<void> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create indexes
|
// Step 3: Create indexes
|
||||||
|
console.log(' ✓ Creating indexes...');
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['email'], {
|
await queryInterface.addIndex('users', ['email'], {
|
||||||
name: 'users_email_idx',
|
name: 'users_email_idx',
|
||||||
unique: true
|
unique: true
|
||||||
@ -105,11 +190,48 @@ export async function up(queryInterface: QueryInterface): Promise<void> {
|
|||||||
name: 'users_employee_id_idx'
|
name: 'users_employee_id_idx'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Users table created
|
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!');
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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
168
src/scripts/auto-setup.ts
Normal 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;
|
||||||
|
|
||||||
@ -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 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 = `
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
6
src/types/express.d.ts
vendored
6
src/types/express.d.ts
vendored
@ -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;
|
||||||
|
|||||||
@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user