auth
This commit is contained in:
parent
57d8f3fd5c
commit
f9dac8cfc9
361
README.md
Normal file
361
README.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
# Cloudtopiaa Reseller Portal - Backend
|
||||||
|
|
||||||
|
A comprehensive Node.js/Express backend for the Cloudtopiaa Reseller Portal, supporting both Channel Partners and Resellers with role-based access control and advanced features.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
### **Authentication & Authorization**
|
||||||
|
- JWT-based authentication with refresh tokens
|
||||||
|
- Role-based access control (RBAC)
|
||||||
|
- Multi-factor authentication (MFA) support
|
||||||
|
- Email verification and password reset
|
||||||
|
- Session management with Redis
|
||||||
|
- Comprehensive audit logging
|
||||||
|
|
||||||
|
### **User Management**
|
||||||
|
- **Channel Partner Roles**: Admin, Manager, Sales, Support, Finance, Analyst
|
||||||
|
- **Reseller Roles**: Admin, Manager, Sales, Support, Finance, Analyst
|
||||||
|
- **System Roles**: Admin, Support, Analyst
|
||||||
|
- User hierarchy with manager-subordinate relationships
|
||||||
|
- Department and position tracking
|
||||||
|
- Onboarding workflow
|
||||||
|
|
||||||
|
### **Organization Management**
|
||||||
|
- **Channel Partners**: Manage reseller networks, territories, specializations
|
||||||
|
- **Resellers**: Manage customers, instances, billing
|
||||||
|
- Tier-based commission structures
|
||||||
|
- KYC verification workflow
|
||||||
|
- Performance metrics and analytics
|
||||||
|
|
||||||
|
### **Business Features**
|
||||||
|
- Product catalog management
|
||||||
|
- Customer lifecycle management
|
||||||
|
- Cloud instance provisioning
|
||||||
|
- Billing and invoicing
|
||||||
|
- Commission calculation and tracking
|
||||||
|
- Wallet management
|
||||||
|
- Support ticket system
|
||||||
|
- Training and certification tracking
|
||||||
|
|
||||||
|
### **Security & Compliance**
|
||||||
|
- Input validation and sanitization
|
||||||
|
- Rate limiting and DDoS protection
|
||||||
|
- CORS configuration
|
||||||
|
- Helmet security headers
|
||||||
|
- Audit trail for all actions
|
||||||
|
- Data encryption at rest and in transit
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── config/ # Configuration files
|
||||||
|
├── controllers/ # Business logic controllers
|
||||||
|
├── middleware/ # Custom middleware
|
||||||
|
├── migrations/ # Database migrations
|
||||||
|
├── models/ # Sequelize models
|
||||||
|
├── routes/ # API route definitions
|
||||||
|
├── seeders/ # Database seeders
|
||||||
|
├── utils/ # Utility functions
|
||||||
|
└── app.js # Main application file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Technology Stack
|
||||||
|
|
||||||
|
- **Runtime**: Node.js 16+
|
||||||
|
- **Framework**: Express.js
|
||||||
|
- **Database**: PostgreSQL with Sequelize ORM
|
||||||
|
- **Cache**: Redis
|
||||||
|
- **Authentication**: JWT + bcrypt
|
||||||
|
- **Validation**: express-validator
|
||||||
|
- **Email**: Nodemailer
|
||||||
|
- **Security**: Helmet, CORS, Rate limiting
|
||||||
|
- **Logging**: Morgan + custom audit logs
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
- Node.js 16.0.0 or higher
|
||||||
|
- PostgreSQL 12.0 or higher
|
||||||
|
- Redis 6.0 or higher
|
||||||
|
- npm or yarn package manager
|
||||||
|
|
||||||
|
## 🔧 Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd Cloudtopiaa_Reseller_Backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Environment Configuration**
|
||||||
|
Create a `.env` file in the root directory:
|
||||||
|
```env
|
||||||
|
# Server Configuration
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
HOST=localhost
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=cloudtopiaa_reseller
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
JWT_SECRET=your_super_secret_jwt_key_here
|
||||||
|
JWT_EXPIRES_IN=15m
|
||||||
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# Email Configuration
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USER=your_email@gmail.com
|
||||||
|
SMTP_PASS=your_app_password
|
||||||
|
SMTP_FROM=noreply@cloudtopiaa.com
|
||||||
|
|
||||||
|
# Frontend URL
|
||||||
|
FRONTEND_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_WINDOW_MS=900000
|
||||||
|
RATE_LIMIT_MAX_REQUESTS=100
|
||||||
|
|
||||||
|
# Security
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Database Setup**
|
||||||
|
```bash
|
||||||
|
# Create database
|
||||||
|
npm run db:create
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
npm run migrate
|
||||||
|
|
||||||
|
# Seed demo data
|
||||||
|
npm run seed
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Start the server**
|
||||||
|
```bash
|
||||||
|
# Development mode
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Production mode
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗄️ Database Schema
|
||||||
|
|
||||||
|
### **Users Table**
|
||||||
|
- Comprehensive user management with role-based access
|
||||||
|
- Support for Channel Partners, Resellers, and System users
|
||||||
|
- Hierarchical organization structure
|
||||||
|
- Audit trail and activity tracking
|
||||||
|
|
||||||
|
### **Channel Partners Table**
|
||||||
|
- Organization details and KYC information
|
||||||
|
- Territory and specialization management
|
||||||
|
- Performance metrics and commission structures
|
||||||
|
- Contract and approval workflow
|
||||||
|
|
||||||
|
### **Resellers Table**
|
||||||
|
- Reseller organization details
|
||||||
|
- Channel partner relationships
|
||||||
|
- Commission and margin settings
|
||||||
|
- Customer and instance management
|
||||||
|
|
||||||
|
### **Additional Tables**
|
||||||
|
- Products, Customers, Instances
|
||||||
|
- Orders, Invoices, Commissions
|
||||||
|
- Wallets, Transactions
|
||||||
|
- Support tickets, Training courses
|
||||||
|
- Audit logs, User sessions
|
||||||
|
|
||||||
|
## 🔐 Role-Based Access Control
|
||||||
|
|
||||||
|
### **Channel Partner Roles**
|
||||||
|
- **Admin**: Full access to channel partner features
|
||||||
|
- **Manager**: Management access with limited admin functions
|
||||||
|
- **Sales**: Sales-focused access to resellers and products
|
||||||
|
- **Support**: Support-focused access to customers and tickets
|
||||||
|
- **Finance**: Finance-focused access to billing and reports
|
||||||
|
- **Analyst**: Analytics and reporting access
|
||||||
|
|
||||||
|
### **Reseller Roles**
|
||||||
|
- **Admin**: Full access to reseller features
|
||||||
|
- **Manager**: Management access with limited admin functions
|
||||||
|
- **Sales**: Sales-focused access to customers and instances
|
||||||
|
- **Support**: Support-focused access to instances and tickets
|
||||||
|
- **Finance**: Finance-focused access to billing and wallet
|
||||||
|
- **Analyst**: Analytics and reporting access
|
||||||
|
|
||||||
|
### **System Roles**
|
||||||
|
- **Admin**: Full system access
|
||||||
|
- **Support**: System support access
|
||||||
|
- **Analyst**: System analytics access
|
||||||
|
|
||||||
|
## 📡 API Endpoints
|
||||||
|
|
||||||
|
### **Authentication**
|
||||||
|
- `POST /api/auth/register` - User registration
|
||||||
|
- `POST /api/auth/login` - User login
|
||||||
|
- `POST /api/auth/refresh` - Refresh access token
|
||||||
|
- `POST /api/auth/logout` - User logout
|
||||||
|
- `GET /api/auth/verify-email/:token` - Email verification
|
||||||
|
- `POST /api/auth/forgot-password` - Password reset request
|
||||||
|
- `POST /api/auth/reset-password` - Password reset
|
||||||
|
- `GET /api/auth/profile` - Get user profile
|
||||||
|
- `PUT /api/auth/profile` - Update user profile
|
||||||
|
|
||||||
|
### **Channel Partners**
|
||||||
|
- `GET /api/channel-partners` - List channel partners
|
||||||
|
- `GET /api/channel-partners/:id` - Get channel partner details
|
||||||
|
- `POST /api/channel-partners` - Create channel partner
|
||||||
|
- `PUT /api/channel-partners/:id` - Update channel partner
|
||||||
|
- `DELETE /api/channel-partners/:id` - Delete channel partner
|
||||||
|
- `POST /api/channel-partners/:id/approve` - Approve channel partner
|
||||||
|
- `POST /api/channel-partners/:id/reject` - Reject channel partner
|
||||||
|
- `GET /api/channel-partners/:id/stats` - Get channel partner statistics
|
||||||
|
- `PUT /api/channel-partners/:id/tier` - Update channel partner tier
|
||||||
|
|
||||||
|
### **Resellers**
|
||||||
|
- `GET /api/resellers` - List resellers
|
||||||
|
- `GET /api/resellers/:id` - Get reseller details
|
||||||
|
- `POST /api/resellers` - Create reseller
|
||||||
|
- `PUT /api/resellers/:id` - Update reseller
|
||||||
|
- `DELETE /api/resellers/:id` - Delete reseller
|
||||||
|
- `POST /api/resellers/:id/approve` - Approve reseller
|
||||||
|
- `POST /api/resellers/:id/reject` - Reject reseller
|
||||||
|
- `GET /api/resellers/:id/stats` - Get reseller statistics
|
||||||
|
|
||||||
|
### **Additional Endpoints**
|
||||||
|
- Users, Products, Customers, Instances
|
||||||
|
- Billing, Orders, Commissions
|
||||||
|
- Reports, Analytics, Dashboard
|
||||||
|
- Support, Training, Marketplace
|
||||||
|
- Wallet, Legal documents
|
||||||
|
|
||||||
|
## 🔧 Development
|
||||||
|
|
||||||
|
### **Running Tests**
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Database Operations**
|
||||||
|
```bash
|
||||||
|
# Create database
|
||||||
|
npm run db:create
|
||||||
|
|
||||||
|
# Drop database
|
||||||
|
npm run db:drop
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
npm run migrate
|
||||||
|
|
||||||
|
# Undo last migration
|
||||||
|
npm run migrate:undo
|
||||||
|
|
||||||
|
# Run seeders
|
||||||
|
npm run seed
|
||||||
|
|
||||||
|
# Undo seeders
|
||||||
|
npm run seed:undo
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Code Quality**
|
||||||
|
- ESLint for code linting
|
||||||
|
- Prettier for code formatting
|
||||||
|
- Input validation with express-validator
|
||||||
|
- Comprehensive error handling
|
||||||
|
- Audit logging for all operations
|
||||||
|
|
||||||
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
### **Production Setup**
|
||||||
|
1. Set `NODE_ENV=production`
|
||||||
|
2. Configure production database and Redis
|
||||||
|
3. Set up SSL certificates
|
||||||
|
4. Configure reverse proxy (nginx)
|
||||||
|
5. Set up process manager (PM2)
|
||||||
|
6. Configure monitoring and logging
|
||||||
|
|
||||||
|
### **Environment Variables**
|
||||||
|
Ensure all required environment variables are set in production:
|
||||||
|
- Database credentials
|
||||||
|
- Redis configuration
|
||||||
|
- JWT secrets
|
||||||
|
- SMTP settings
|
||||||
|
- Security configurations
|
||||||
|
|
||||||
|
## 📊 Monitoring & Logging
|
||||||
|
|
||||||
|
### **Health Check**
|
||||||
|
- `GET /health` - System health status
|
||||||
|
- Database connectivity check
|
||||||
|
- Redis connectivity check
|
||||||
|
|
||||||
|
### **Audit Logging**
|
||||||
|
- All user actions are logged
|
||||||
|
- IP address and user agent tracking
|
||||||
|
- Resource access monitoring
|
||||||
|
- Security event logging
|
||||||
|
|
||||||
|
### **Performance Monitoring**
|
||||||
|
- Request/response logging
|
||||||
|
- Database query optimization
|
||||||
|
- Redis cache utilization
|
||||||
|
- Error tracking and alerting
|
||||||
|
|
||||||
|
## 🔒 Security Features
|
||||||
|
|
||||||
|
- **Authentication**: JWT with refresh tokens
|
||||||
|
- **Authorization**: Role-based access control
|
||||||
|
- **Input Validation**: Comprehensive validation
|
||||||
|
- **Rate Limiting**: DDoS protection
|
||||||
|
- **CORS**: Cross-origin resource sharing
|
||||||
|
- **Helmet**: Security headers
|
||||||
|
- **Audit Trail**: Complete action logging
|
||||||
|
- **Data Encryption**: At rest and in transit
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests for new features
|
||||||
|
5. Ensure all tests pass
|
||||||
|
6. Submit a pull request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
For support and questions:
|
||||||
|
- Email: support@cloudtopiaa.com
|
||||||
|
- Documentation: [Link to documentation]
|
||||||
|
- Issues: [GitHub Issues]
|
||||||
|
|
||||||
|
## 🔄 Changelog
|
||||||
|
|
||||||
|
### Version 1.0.0
|
||||||
|
- Initial release
|
||||||
|
- Complete role-based access control
|
||||||
|
- Channel Partner and Reseller management
|
||||||
|
- Comprehensive API endpoints
|
||||||
|
- Security and audit features
|
||||||
6713
package-lock.json
generated
Normal file
6713
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ const redisClient = require('./config/redis');
|
|||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
const userRoutes = require('./routes/users');
|
const userRoutes = require('./routes/users');
|
||||||
const resellerRoutes = require('./routes/resellers');
|
const resellerRoutes = require('./routes/resellers');
|
||||||
|
const channelPartnerRoutes = require('./routes/channelPartners');
|
||||||
const productRoutes = require('./routes/products');
|
const productRoutes = require('./routes/products');
|
||||||
const customerRoutes = require('./routes/customers');
|
const customerRoutes = require('./routes/customers');
|
||||||
const billingRoutes = require('./routes/billing');
|
const billingRoutes = require('./routes/billing');
|
||||||
@ -106,6 +107,7 @@ app.get('/health', async (req, res) => {
|
|||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/users', authenticateToken, userRoutes);
|
app.use('/api/users', authenticateToken, userRoutes);
|
||||||
app.use('/api/resellers', authenticateToken, resellerRoutes);
|
app.use('/api/resellers', authenticateToken, resellerRoutes);
|
||||||
|
app.use('/api/channel-partners', authenticateToken, channelPartnerRoutes);
|
||||||
app.use('/api/products', authenticateToken, productRoutes);
|
app.use('/api/products', authenticateToken, productRoutes);
|
||||||
app.use('/api/customers', authenticateToken, customerRoutes);
|
app.use('/api/customers', authenticateToken, customerRoutes);
|
||||||
app.use('/api/billing', authenticateToken, billingRoutes);
|
app.use('/api/billing', authenticateToken, billingRoutes);
|
||||||
|
|||||||
@ -1,88 +1,153 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const crypto = require('crypto');
|
const bcrypt = require('bcryptjs');
|
||||||
const speakeasy = require('speakeasy');
|
const { validationResult } = require('express-validator');
|
||||||
const QRCode = require('qrcode');
|
const { User, Reseller, ChannelPartner } = require('../models');
|
||||||
const { User, UserSession, AuditLog } = require('../models');
|
|
||||||
const { sendEmail } = require('../config/email');
|
|
||||||
const redisClient = require('../config/redis');
|
const redisClient = require('../config/redis');
|
||||||
|
const { sendEmail } = require('../utils/email');
|
||||||
// Generate JWT tokens
|
const { generateToken, verifyToken } = require('../utils/jwt');
|
||||||
const generateTokens = (userId) => {
|
const { createAuditLog } = require('../utils/audit');
|
||||||
const accessToken = jwt.sign(
|
|
||||||
{ userId, type: 'access' },
|
|
||||||
process.env.JWT_SECRET,
|
|
||||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '15m' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshToken = jwt.sign(
|
|
||||||
{ userId, type: 'refresh' },
|
|
||||||
process.env.JWT_REFRESH_SECRET,
|
|
||||||
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
|
|
||||||
);
|
|
||||||
|
|
||||||
return { accessToken, refreshToken };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register new user
|
// Register new user
|
||||||
const register = async (req, res) => {
|
const register = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { email, password, firstName, lastName, phone, role = 'read_only' } = req.body;
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
// Check if user already exists
|
return res.status(400).json({
|
||||||
const existingUser = await User.findOne({ where: { email } });
|
|
||||||
if (existingUser) {
|
|
||||||
return res.status(409).json({
|
|
||||||
success: false,
|
success: false,
|
||||||
message: 'User already exists with this email'
|
message: 'Validation errors',
|
||||||
|
errors: errors.array()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate email verification token
|
const {
|
||||||
const emailVerificationToken = crypto.randomBytes(32).toString('hex');
|
|
||||||
const emailVerificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
|
|
||||||
|
|
||||||
// Create user
|
|
||||||
const user = await User.create({
|
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
phone,
|
phone,
|
||||||
role,
|
role,
|
||||||
emailVerificationToken,
|
userType,
|
||||||
emailVerificationExpires
|
companyName,
|
||||||
});
|
companyType,
|
||||||
|
address,
|
||||||
|
contactEmail,
|
||||||
|
contactPhone,
|
||||||
|
website,
|
||||||
|
channelPartnerId,
|
||||||
|
resellerId
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const existingUser = await User.findOne({ where: { email } });
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User with this email already exists'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate role based on userType
|
||||||
|
const validRoles = {
|
||||||
|
channel_partner: ['channel_partner_admin', 'channel_partner_manager', 'channel_partner_sales', 'channel_partner_support', 'channel_partner_finance', 'channel_partner_analyst'],
|
||||||
|
reseller: ['reseller_admin', 'reseller_manager', 'reseller_sales', 'reseller_support', 'reseller_finance', 'reseller_analyst'],
|
||||||
|
system: ['system_admin', 'system_support', 'system_analyst']
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!validRoles[userType] || !validRoles[userType].includes(role)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid role for the specified user type'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const userData = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
phone,
|
||||||
|
role,
|
||||||
|
userType,
|
||||||
|
status: 'pending_verification'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add organization-specific fields
|
||||||
|
if (userType === 'channel_partner') {
|
||||||
|
userData.channelPartnerId = channelPartnerId;
|
||||||
|
} else if (userType === 'reseller') {
|
||||||
|
userData.resellerId = resellerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.create(userData);
|
||||||
|
|
||||||
|
// Create organization if needed
|
||||||
|
if (userType === 'channel_partner' && !channelPartnerId) {
|
||||||
|
const channelPartner = await ChannelPartner.create({
|
||||||
|
companyName,
|
||||||
|
companyType,
|
||||||
|
address,
|
||||||
|
contactEmail: contactEmail || email,
|
||||||
|
contactPhone: contactPhone || phone,
|
||||||
|
website,
|
||||||
|
status: 'pending_approval'
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.update({ channelPartnerId: channelPartner.id });
|
||||||
|
} else if (userType === 'reseller' && !resellerId) {
|
||||||
|
const reseller = await Reseller.create({
|
||||||
|
companyName,
|
||||||
|
companyType,
|
||||||
|
address,
|
||||||
|
contactEmail: contactEmail || email,
|
||||||
|
contactPhone: contactPhone || phone,
|
||||||
|
website,
|
||||||
|
channelPartnerId,
|
||||||
|
status: 'pending_approval'
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.update({ resellerId: reseller.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate email verification token
|
||||||
|
const verificationToken = generateToken({ userId: user.id }, '1h');
|
||||||
|
user.emailVerificationToken = verificationToken;
|
||||||
|
user.emailVerificationExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
||||||
|
await user.save();
|
||||||
|
|
||||||
// Send verification email
|
// Send verification email
|
||||||
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${emailVerificationToken}`;
|
await sendEmail({
|
||||||
await sendEmail(email, 'emailVerification', {
|
to: email,
|
||||||
name: `${firstName} ${lastName}`,
|
subject: 'Verify your email address',
|
||||||
url: verificationUrl,
|
template: 'emailVerification',
|
||||||
otp: emailVerificationToken.substring(0, 6).toUpperCase()
|
data: {
|
||||||
|
name: firstName,
|
||||||
|
verificationUrl: `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log audit
|
// Create audit log
|
||||||
await AuditLog.create({
|
await createAuditLog({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
action: 'USER_REGISTERED',
|
action: 'USER_REGISTERED',
|
||||||
resource: 'user',
|
details: { userType, role },
|
||||||
resourceId: user.id,
|
ipAddress: req.ip
|
||||||
ipAddress: req.ip,
|
|
||||||
userAgent: req.get('User-Agent')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'User registered successfully. Please check your email for verification.',
|
message: 'User registered successfully. Please check your email to verify your account.',
|
||||||
data: {
|
data: {
|
||||||
user: user.toJSON()
|
user: user.toJSON(),
|
||||||
|
requiresVerification: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Registration error:', error);
|
console.error('Registration error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Registration failed',
|
message: 'Internal server error',
|
||||||
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -91,12 +156,32 @@ const register = async (req, res) => {
|
|||||||
// Login user
|
// Login user
|
||||||
const login = async (req, res) => {
|
const login = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Validation errors',
|
||||||
|
errors: errors.array()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { email, password, mfaToken } = req.body;
|
const { email, password, mfaToken } = req.body;
|
||||||
|
|
||||||
// Find user
|
// Find user with organization details
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
where: { email },
|
where: { email },
|
||||||
include: ['reseller']
|
include: [
|
||||||
|
{
|
||||||
|
model: Reseller,
|
||||||
|
as: 'reseller',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier', 'commissionRate']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ChannelPartner,
|
||||||
|
as: 'channelPartner',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier', 'commissionRate']
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -124,35 +209,44 @@ const login = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email is verified
|
// Check email verification
|
||||||
if (!user.emailVerified) {
|
if (!user.emailVerified) {
|
||||||
return res.status(401).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Please verify your email before logging in'
|
message: 'Please verify your email address before logging in',
|
||||||
|
requiresVerification: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check account status
|
// Check account status
|
||||||
if (user.status !== 'active') {
|
if (user.status !== 'active') {
|
||||||
return res.status(401).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Account is not active'
|
message: `Account is ${user.status}. Please contact support.`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check MFA if enabled
|
// Check organization status
|
||||||
|
const organization = user.reseller || user.channelPartner;
|
||||||
|
if (organization && organization.status !== 'active') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: `Your organization account is ${organization.status}. Please contact support.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MFA if enabled
|
||||||
if (user.mfaEnabled) {
|
if (user.mfaEnabled) {
|
||||||
if (!mfaToken) {
|
if (!mfaToken) {
|
||||||
return res.status(200).json({
|
return res.status(400).json({
|
||||||
success: true,
|
success: false,
|
||||||
requiresMFA: true,
|
message: 'MFA token required',
|
||||||
message: 'MFA token required'
|
requiresMfa: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMfaValid = user.verifyMfaToken(mfaToken);
|
const isValidMfa = user.verifyMfaToken(mfaToken);
|
||||||
if (!isMfaValid) {
|
if (!isValidMfa) {
|
||||||
await user.incrementLoginAttempts();
|
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Invalid MFA token'
|
message: 'Invalid MFA token'
|
||||||
@ -160,33 +254,34 @@ const login = async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate tokens
|
|
||||||
const { accessToken, refreshToken } = generateTokens(user.id);
|
|
||||||
|
|
||||||
// Create session
|
|
||||||
const session = await UserSession.create({
|
|
||||||
userId: user.id,
|
|
||||||
refreshToken,
|
|
||||||
deviceInfo: {
|
|
||||||
userAgent: req.get('User-Agent'),
|
|
||||||
platform: req.get('sec-ch-ua-platform')
|
|
||||||
},
|
|
||||||
ipAddress: req.ip,
|
|
||||||
userAgent: req.get('User-Agent'),
|
|
||||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset login attempts and update last login
|
// Reset login attempts and update last login
|
||||||
await user.resetLoginAttempts();
|
await user.resetLoginAttempts();
|
||||||
|
|
||||||
// Log audit
|
// Generate tokens
|
||||||
await AuditLog.create({
|
const accessToken = generateToken(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
role: user.role,
|
||||||
|
userType: user.userType,
|
||||||
|
organizationId: organization?.id
|
||||||
|
},
|
||||||
|
'15m'
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshToken = generateToken(
|
||||||
|
{ userId: user.id, type: 'refresh' },
|
||||||
|
'7d'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store refresh token in Redis
|
||||||
|
await redisClient.setex(`refresh_token:${user.id}`, 7 * 24 * 60 * 60, refreshToken);
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
action: 'USER_LOGIN',
|
action: 'USER_LOGIN',
|
||||||
resource: 'user',
|
details: { userType: user.userType, role: user.role },
|
||||||
resourceId: user.id,
|
ipAddress: req.ip
|
||||||
ipAddress: req.ip,
|
|
||||||
userAgent: req.get('User-Agent')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -196,147 +291,389 @@ const login = async (req, res) => {
|
|||||||
user: user.toJSON(),
|
user: user.toJSON(),
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
expiresIn: process.env.JWT_EXPIRES_IN || '15m'
|
expiresIn: 15 * 60 // 15 minutes
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Login failed',
|
message: 'Internal server error',
|
||||||
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refresh token
|
// Refresh access token
|
||||||
const refreshToken = async (req, res) => {
|
const refreshToken = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { refreshToken } = req.body;
|
const { refreshToken } = req.body;
|
||||||
|
|
||||||
const session = await UserSession.findOne({
|
if (!refreshToken) {
|
||||||
where: {
|
return res.status(400).json({
|
||||||
refreshToken,
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
include: ['user']
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session || session.isExpired()) {
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Invalid or expired refresh token'
|
message: 'Refresh token is required'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new tokens
|
// Verify refresh token
|
||||||
const { accessToken, refreshToken: newRefreshToken } = generateTokens(session.userId);
|
const decoded = verifyToken(refreshToken);
|
||||||
|
if (!decoded || decoded.type !== 'refresh') {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid refresh token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update session
|
// Check if refresh token exists in Redis
|
||||||
await session.update({
|
const storedToken = await redisClient.get(`refresh_token:${decoded.userId}`);
|
||||||
refreshToken: newRefreshToken,
|
if (!storedToken || storedToken !== refreshToken) {
|
||||||
lastUsedAt: new Date()
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid refresh token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user details
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: { id: decoded.userId },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Reseller,
|
||||||
|
as: 'reseller',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier', 'commissionRate']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ChannelPartner,
|
||||||
|
as: 'channelPartner',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier', 'commissionRate']
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!user || user.status !== 'active') {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found or inactive'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = user.reseller || user.channelPartner;
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
const newAccessToken = generateToken(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
role: user.role,
|
||||||
|
userType: user.userType,
|
||||||
|
organizationId: organization?.id
|
||||||
|
},
|
||||||
|
'15m'
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
message: 'Token refreshed successfully',
|
||||||
data: {
|
data: {
|
||||||
accessToken,
|
accessToken: newAccessToken,
|
||||||
refreshToken: newRefreshToken,
|
expiresIn: 15 * 60
|
||||||
expiresIn: process.env.JWT_EXPIRES_IN || '15m'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Token refresh error:', error);
|
console.error('Token refresh error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Token refresh failed'
|
message: 'Internal server error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Logout
|
// Logout user
|
||||||
const logout = async (req, res) => {
|
const logout = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const authHeader = req.headers['authorization'];
|
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
// Blacklist the access token
|
|
||||||
const decoded = jwt.decode(token);
|
|
||||||
if (decoded && decoded.exp) {
|
|
||||||
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
|
|
||||||
if (ttl > 0) {
|
|
||||||
await redisClient.setEx(`blacklist:${token}`, ttl, 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deactivate refresh token session
|
|
||||||
const { refreshToken } = req.body;
|
const { refreshToken } = req.body;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
await UserSession.update(
|
// Remove refresh token from Redis
|
||||||
{ isActive: false },
|
await redisClient.del(`refresh_token:${userId}`);
|
||||||
{ where: { refreshToken } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log audit
|
// Create audit log
|
||||||
await AuditLog.create({
|
await createAuditLog({
|
||||||
userId: req.user?.id,
|
userId,
|
||||||
action: 'USER_LOGOUT',
|
action: 'USER_LOGOUT',
|
||||||
resource: 'user',
|
ipAddress: req.ip
|
||||||
resourceId: req.user?.id,
|
|
||||||
ipAddress: req.ip,
|
|
||||||
userAgent: req.get('User-Agent')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Logout successful'
|
message: 'Logout successful'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error);
|
console.error('Logout error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Logout failed'
|
message: 'Internal server error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup MFA
|
// Verify email
|
||||||
const setupMFA = async (req, res) => {
|
const verifyEmail = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.user;
|
const { token } = req.params;
|
||||||
|
|
||||||
if (user.mfaEnabled) {
|
const decoded = verifyToken(token);
|
||||||
|
if (!decoded || !decoded.userId) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'MFA is already enabled'
|
message: 'Invalid verification token'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = user.generateMfaSecret();
|
const user = await User.findOne({
|
||||||
const backupCodes = user.generateMfaBackupCodes();
|
where: {
|
||||||
|
id: decoded.userId,
|
||||||
|
emailVerificationToken: token,
|
||||||
|
emailVerificationExpires: { [require('sequelize').Op.gt]: new Date() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid or expired verification token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user
|
||||||
|
user.emailVerified = true;
|
||||||
|
user.emailVerificationToken = null;
|
||||||
|
user.emailVerificationExpires = null;
|
||||||
|
user.status = 'active';
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
// Generate QR code
|
// Create audit log
|
||||||
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
|
await createAuditLog({
|
||||||
|
userId: user.id,
|
||||||
|
action: 'EMAIL_VERIFIED',
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Email verified successfully'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Email verification error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forgot password
|
||||||
|
const forgotPassword = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
const user = await User.findOne({ where: { email } });
|
||||||
|
if (!user) {
|
||||||
|
// Don't reveal if user exists or not
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'If an account with this email exists, a password reset link has been sent.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate reset token
|
||||||
|
const resetToken = generateToken({ userId: user.id }, '1h');
|
||||||
|
user.passwordResetToken = resetToken;
|
||||||
|
user.passwordResetExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
// Send reset email
|
||||||
|
await sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject: 'Reset your password',
|
||||||
|
template: 'passwordReset',
|
||||||
|
data: {
|
||||||
|
name: user.firstName,
|
||||||
|
resetUrl: `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: user.id,
|
||||||
|
action: 'PASSWORD_RESET_REQUESTED',
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'If an account with this email exists, a password reset link has been sent.'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Forgot password error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset password
|
||||||
|
const resetPassword = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { token, password } = req.body;
|
||||||
|
|
||||||
|
const decoded = verifyToken(token);
|
||||||
|
if (!decoded || !decoded.userId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid reset token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: {
|
||||||
|
id: decoded.userId,
|
||||||
|
passwordResetToken: token,
|
||||||
|
passwordResetExpires: { [require('sequelize').Op.gt]: new Date() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid or expired reset token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password
|
||||||
|
user.password = password;
|
||||||
|
user.passwordResetToken = null;
|
||||||
|
user.passwordResetExpires = null;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
// Invalidate all refresh tokens
|
||||||
|
await redisClient.del(`refresh_token:${user.id}`);
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: user.id,
|
||||||
|
action: 'PASSWORD_RESET',
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Password reset successfully'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reset password error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get current user profile
|
||||||
|
const getProfile = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: { id: req.user.id },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Reseller,
|
||||||
|
as: 'reseller',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier', 'commissionRate', 'kycStatus']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ChannelPartner,
|
||||||
|
as: 'channelPartner',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier', 'commissionRate', 'kycStatus']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
secret: secret.base32,
|
user: user.toJSON()
|
||||||
qrCode: qrCodeUrl,
|
|
||||||
backupCodes
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('MFA setup error:', error);
|
console.error('Get profile error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'MFA setup failed'
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update user profile
|
||||||
|
const updateProfile = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { firstName, lastName, phone, timezone, language, preferences } = req.body;
|
||||||
|
|
||||||
|
const user = await User.findByPk(req.user.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update allowed fields
|
||||||
|
const updates = {};
|
||||||
|
if (firstName) updates.firstName = firstName;
|
||||||
|
if (lastName) updates.lastName = lastName;
|
||||||
|
if (phone) updates.phone = phone;
|
||||||
|
if (timezone) updates.timezone = timezone;
|
||||||
|
if (language) updates.language = language;
|
||||||
|
if (preferences) updates.preferences = { ...user.preferences, ...preferences };
|
||||||
|
|
||||||
|
await user.update(updates);
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: user.id,
|
||||||
|
action: 'PROFILE_UPDATED',
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Profile updated successfully',
|
||||||
|
data: {
|
||||||
|
user: user.toJSON()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update profile error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -346,5 +683,9 @@ module.exports = {
|
|||||||
login,
|
login,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
logout,
|
logout,
|
||||||
setupMFA
|
verifyEmail,
|
||||||
|
forgotPassword,
|
||||||
|
resetPassword,
|
||||||
|
getProfile,
|
||||||
|
updateProfile
|
||||||
};
|
};
|
||||||
|
|||||||
631
src/controllers/channelPartnerController.js
Normal file
631
src/controllers/channelPartnerController.js
Normal file
@ -0,0 +1,631 @@
|
|||||||
|
const { validationResult } = require('express-validator');
|
||||||
|
const { ChannelPartner, User, Reseller, Order, Commission, Product } = require('../models');
|
||||||
|
const { createAuditLog } = require('../utils/audit');
|
||||||
|
const { sendEmail } = require('../utils/email');
|
||||||
|
|
||||||
|
// Get all channel partners
|
||||||
|
const getAllChannelPartners = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
search,
|
||||||
|
status,
|
||||||
|
tier,
|
||||||
|
sortBy = 'createdAt',
|
||||||
|
sortOrder = 'DESC'
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const whereClause = {};
|
||||||
|
|
||||||
|
// Add search filter
|
||||||
|
if (search) {
|
||||||
|
whereClause[require('sequelize').Op.or] = [
|
||||||
|
{ companyName: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
||||||
|
{ contactEmail: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
||||||
|
{ contactPhone: { [require('sequelize').Op.iLike]: `%${search}%` } }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add status filter
|
||||||
|
if (status) {
|
||||||
|
whereClause.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tier filter
|
||||||
|
if (tier) {
|
||||||
|
whereClause.tier = tier;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { count, rows: channelPartners } = await ChannelPartner.findAndCountAll({
|
||||||
|
where: whereClause,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: 'users',
|
||||||
|
attributes: ['id', 'firstName', 'lastName', 'email', 'role', 'status']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Reseller,
|
||||||
|
as: 'resellers',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [[sortBy, sortOrder]],
|
||||||
|
limit: parseInt(limit),
|
||||||
|
offset: parseInt(offset)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNERS_LISTED',
|
||||||
|
details: { filters: req.query },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
channelPartners,
|
||||||
|
pagination: {
|
||||||
|
page: parseInt(page),
|
||||||
|
limit: parseInt(limit),
|
||||||
|
total: count,
|
||||||
|
pages: Math.ceil(count / limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get channel partners error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get channel partner by ID
|
||||||
|
const getChannelPartnerById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const channelPartner = await ChannelPartner.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: 'users',
|
||||||
|
attributes: ['id', 'firstName', 'lastName', 'email', 'role', 'status', 'lastLogin']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Reseller,
|
||||||
|
as: 'resellers',
|
||||||
|
attributes: ['id', 'companyName', 'status', 'tier', 'commissionRate', 'createdAt']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Order,
|
||||||
|
as: 'orders',
|
||||||
|
attributes: ['id', 'orderNumber', 'status', 'totalAmount', 'createdAt']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Commission,
|
||||||
|
as: 'commissions',
|
||||||
|
attributes: ['id', 'amount', 'status', 'period', 'createdAt']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Product,
|
||||||
|
as: 'products',
|
||||||
|
attributes: ['id', 'name', 'category', 'status', 'basePrice']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!channelPartner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNER_VIEWED',
|
||||||
|
details: { channelPartnerId: id },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { channelPartner }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get channel partner error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new channel partner
|
||||||
|
const createChannelPartner = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Validation errors',
|
||||||
|
errors: errors.array()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
companyName,
|
||||||
|
companyType,
|
||||||
|
registrationNumber,
|
||||||
|
gstNumber,
|
||||||
|
panNumber,
|
||||||
|
address,
|
||||||
|
contactEmail,
|
||||||
|
contactPhone,
|
||||||
|
website,
|
||||||
|
tier,
|
||||||
|
commissionRate,
|
||||||
|
marginSettings,
|
||||||
|
billingDetails,
|
||||||
|
territory,
|
||||||
|
specializations,
|
||||||
|
certifications
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Check if channel partner already exists
|
||||||
|
const existingChannelPartner = await ChannelPartner.findOne({
|
||||||
|
where: {
|
||||||
|
[require('sequelize').Op.or]: [
|
||||||
|
{ contactEmail },
|
||||||
|
{ registrationNumber },
|
||||||
|
{ gstNumber }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingChannelPartner) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner with this email, registration number, or GST number already exists'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create channel partner
|
||||||
|
const channelPartner = await ChannelPartner.create({
|
||||||
|
companyName,
|
||||||
|
companyType,
|
||||||
|
registrationNumber,
|
||||||
|
gstNumber,
|
||||||
|
panNumber,
|
||||||
|
address,
|
||||||
|
contactEmail,
|
||||||
|
contactPhone,
|
||||||
|
website,
|
||||||
|
tier,
|
||||||
|
commissionRate,
|
||||||
|
marginSettings,
|
||||||
|
billingDetails,
|
||||||
|
territory,
|
||||||
|
specializations,
|
||||||
|
certifications,
|
||||||
|
status: 'pending_approval',
|
||||||
|
kycStatus: 'pending'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNER_CREATED',
|
||||||
|
details: { channelPartnerId: channelPartner.id, companyName },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send welcome email
|
||||||
|
await sendEmail({
|
||||||
|
to: contactEmail,
|
||||||
|
subject: 'Welcome to Cloudtopiaa Channel Partner Program',
|
||||||
|
template: 'channelPartnerWelcome',
|
||||||
|
data: {
|
||||||
|
companyName,
|
||||||
|
contactEmail,
|
||||||
|
status: 'pending_approval'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Channel partner created successfully',
|
||||||
|
data: { channelPartner }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create channel partner error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update channel partner
|
||||||
|
const updateChannelPartner = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Validation errors',
|
||||||
|
errors: errors.array()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const updateData = req.body;
|
||||||
|
|
||||||
|
const channelPartner = await ChannelPartner.findByPk(id);
|
||||||
|
if (!channelPartner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unique constraints
|
||||||
|
if (updateData.contactEmail || updateData.registrationNumber || updateData.gstNumber) {
|
||||||
|
const existingChannelPartner = await ChannelPartner.findOne({
|
||||||
|
where: {
|
||||||
|
id: { [require('sequelize').Op.ne]: id },
|
||||||
|
[require('sequelize').Op.or]: [
|
||||||
|
{ contactEmail: updateData.contactEmail },
|
||||||
|
{ registrationNumber: updateData.registrationNumber },
|
||||||
|
{ gstNumber: updateData.gstNumber }
|
||||||
|
].filter(Boolean)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingChannelPartner) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner with this email, registration number, or GST number already exists'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update channel partner
|
||||||
|
await channelPartner.update(updateData);
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNER_UPDATED',
|
||||||
|
details: { channelPartnerId: id, updates: updateData },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Channel partner updated successfully',
|
||||||
|
data: { channelPartner }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update channel partner error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete channel partner
|
||||||
|
const deleteChannelPartner = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const channelPartner = await ChannelPartner.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{ model: User, as: 'users' },
|
||||||
|
{ model: Reseller, as: 'resellers' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!channelPartner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if channel partner has associated users or resellers
|
||||||
|
if (channelPartner.users.length > 0 || channelPartner.resellers.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Cannot delete channel partner with associated users or resellers'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft delete by updating status
|
||||||
|
await channelPartner.update({ status: 'deleted' });
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNER_DELETED',
|
||||||
|
details: { channelPartnerId: id, companyName: channelPartner.companyName },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Channel partner deleted successfully'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete channel partner error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Approve channel partner
|
||||||
|
const approveChannelPartner = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { approvedBy } = req.body;
|
||||||
|
|
||||||
|
const channelPartner = await ChannelPartner.findByPk(id);
|
||||||
|
if (!channelPartner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelPartner.status === 'active') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner is already approved'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status and approval details
|
||||||
|
await channelPartner.update({
|
||||||
|
status: 'active',
|
||||||
|
kycStatus: 'approved',
|
||||||
|
approvedBy,
|
||||||
|
approvedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNER_APPROVED',
|
||||||
|
details: { channelPartnerId: id, approvedBy },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send approval email
|
||||||
|
await sendEmail({
|
||||||
|
to: channelPartner.contactEmail,
|
||||||
|
subject: 'Channel Partner Application Approved',
|
||||||
|
template: 'channelPartnerApproved',
|
||||||
|
data: {
|
||||||
|
companyName: channelPartner.companyName,
|
||||||
|
tier: channelPartner.tier,
|
||||||
|
commissionRate: channelPartner.commissionRate
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Channel partner approved successfully',
|
||||||
|
data: { channelPartner }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Approve channel partner error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reject channel partner
|
||||||
|
const rejectChannelPartner = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { rejectionReason } = req.body;
|
||||||
|
|
||||||
|
const channelPartner = await ChannelPartner.findByPk(id);
|
||||||
|
if (!channelPartner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
await channelPartner.update({
|
||||||
|
status: 'rejected',
|
||||||
|
kycStatus: 'rejected',
|
||||||
|
rejectionReason
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNER_REJECTED',
|
||||||
|
details: { channelPartnerId: id, rejectionReason },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send rejection email
|
||||||
|
await sendEmail({
|
||||||
|
to: channelPartner.contactEmail,
|
||||||
|
subject: 'Channel Partner Application Status',
|
||||||
|
template: 'channelPartnerRejected',
|
||||||
|
data: {
|
||||||
|
companyName: channelPartner.companyName,
|
||||||
|
rejectionReason
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Channel partner rejected successfully',
|
||||||
|
data: { channelPartner }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reject channel partner error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get channel partner statistics
|
||||||
|
const getChannelPartnerStats = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const channelPartner = await ChannelPartner.findByPk(id);
|
||||||
|
if (!channelPartner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get statistics
|
||||||
|
const [
|
||||||
|
totalResellers,
|
||||||
|
totalUsers,
|
||||||
|
totalOrders,
|
||||||
|
totalRevenue,
|
||||||
|
totalCommissions,
|
||||||
|
activeResellers
|
||||||
|
] = await Promise.all([
|
||||||
|
channelPartner.countResellers(),
|
||||||
|
channelPartner.countUsers(),
|
||||||
|
channelPartner.countOrders(),
|
||||||
|
channelPartner.getOrders({
|
||||||
|
attributes: [
|
||||||
|
[require('sequelize').fn('SUM', require('sequelize').col('totalAmount')), 'totalRevenue']
|
||||||
|
],
|
||||||
|
where: { status: 'completed' }
|
||||||
|
}),
|
||||||
|
channelPartner.getCommissions({
|
||||||
|
attributes: [
|
||||||
|
[require('sequelize').fn('SUM', require('sequelize').col('amount')), 'totalCommissions']
|
||||||
|
],
|
||||||
|
where: { status: 'paid' }
|
||||||
|
}),
|
||||||
|
channelPartner.countResellers({ where: { status: 'active' } })
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
totalResellers,
|
||||||
|
totalUsers,
|
||||||
|
totalOrders,
|
||||||
|
totalRevenue: totalRevenue[0]?.dataValues?.totalRevenue || 0,
|
||||||
|
totalCommissions: totalCommissions[0]?.dataValues?.totalCommissions || 0,
|
||||||
|
activeResellers,
|
||||||
|
tier: channelPartner.tier,
|
||||||
|
commissionRate: channelPartner.commissionRate,
|
||||||
|
status: channelPartner.status,
|
||||||
|
kycStatus: channelPartner.kycStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { stats }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get channel partner stats error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update channel partner tier
|
||||||
|
const updateChannelPartnerTier = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { tier, reason } = req.body;
|
||||||
|
|
||||||
|
const channelPartner = await ChannelPartner.findByPk(id);
|
||||||
|
if (!channelPartner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTier = channelPartner.tier;
|
||||||
|
await channelPartner.update({ tier });
|
||||||
|
|
||||||
|
// Create audit log
|
||||||
|
await createAuditLog({
|
||||||
|
userId: req.user.userId,
|
||||||
|
action: 'CHANNEL_PARTNER_TIER_UPDATED',
|
||||||
|
details: { channelPartnerId: id, oldTier, newTier: tier, reason },
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send tier update email
|
||||||
|
await sendEmail({
|
||||||
|
to: channelPartner.contactEmail,
|
||||||
|
subject: 'Channel Partner Tier Updated',
|
||||||
|
template: 'channelPartnerTierUpdate',
|
||||||
|
data: {
|
||||||
|
companyName: channelPartner.companyName,
|
||||||
|
oldTier,
|
||||||
|
newTier: tier,
|
||||||
|
reason
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Channel partner tier updated successfully',
|
||||||
|
data: { channelPartner }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update channel partner tier error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAllChannelPartners,
|
||||||
|
getChannelPartnerById,
|
||||||
|
createChannelPartner,
|
||||||
|
updateChannelPartner,
|
||||||
|
deleteChannelPartner,
|
||||||
|
approveChannelPartner,
|
||||||
|
rejectChannelPartner,
|
||||||
|
getChannelPartnerStats,
|
||||||
|
updateChannelPartnerTier
|
||||||
|
};
|
||||||
266
src/middleware/authorization.js
Normal file
266
src/middleware/authorization.js
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
const { User } = require('../models');
|
||||||
|
|
||||||
|
// Check if user has specific permission
|
||||||
|
const hasPermission = (permission) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.hasPermission(permission)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Insufficient permissions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.currentUser = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Permission check error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user has any of the specified permissions
|
||||||
|
const hasAnyPermission = (permissions) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAny = permissions.some(permission => user.hasPermission(permission));
|
||||||
|
|
||||||
|
if (!hasAny) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Insufficient permissions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.currentUser = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Permission check error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user has specific role
|
||||||
|
const hasRole = (role) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== role) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Insufficient role permissions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.currentUser = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Role check error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user has any of the specified roles
|
||||||
|
const hasAnyRole = (roles) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roles.includes(user.role)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Insufficient role permissions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.currentUser = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Role check error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user is admin
|
||||||
|
const isAdmin = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isAdmin()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Admin access required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.currentUser = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Admin check error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user is channel partner
|
||||||
|
const isChannelPartner = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isChannelPartner()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Channel partner access required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.currentUser = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Channel partner check error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user is reseller
|
||||||
|
const isReseller = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isReseller()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Reseller access required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.currentUser = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reseller check error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Predefined permission sets
|
||||||
|
const permissions = {
|
||||||
|
DASHBOARD_READ: 'dashboard:read',
|
||||||
|
DASHBOARD_WRITE: 'dashboard:write',
|
||||||
|
USERS_READ: 'users:read',
|
||||||
|
USERS_WRITE: 'users:write',
|
||||||
|
USERS_DELETE: 'users:delete',
|
||||||
|
PRODUCTS_READ: 'products:read',
|
||||||
|
PRODUCTS_WRITE: 'products:write',
|
||||||
|
PRODUCTS_DELETE: 'products:delete',
|
||||||
|
CUSTOMERS_READ: 'customers:read',
|
||||||
|
CUSTOMERS_WRITE: 'customers:write',
|
||||||
|
CUSTOMERS_DELETE: 'customers:delete',
|
||||||
|
BILLING_READ: 'billing:read',
|
||||||
|
BILLING_WRITE: 'billing:write',
|
||||||
|
REPORTS_READ: 'reports:read',
|
||||||
|
REPORTS_WRITE: 'reports:write',
|
||||||
|
ANALYTICS_READ: 'analytics:read',
|
||||||
|
ANALYTICS_WRITE: 'analytics:write',
|
||||||
|
SETTINGS_READ: 'settings:read',
|
||||||
|
SETTINGS_WRITE: 'settings:write',
|
||||||
|
SUPPORT_READ: 'support:read',
|
||||||
|
SUPPORT_WRITE: 'support:write',
|
||||||
|
WALLET_READ: 'wallet:read',
|
||||||
|
WALLET_WRITE: 'wallet:write',
|
||||||
|
TRAINING_READ: 'training:read',
|
||||||
|
TRAINING_WRITE: 'training:write',
|
||||||
|
MARKETPLACE_READ: 'marketplace:read',
|
||||||
|
MARKETPLACE_WRITE: 'marketplace:write',
|
||||||
|
INSTANCES_READ: 'instances:read',
|
||||||
|
INSTANCES_WRITE: 'instances:write',
|
||||||
|
INSTANCES_DELETE: 'instances:delete'
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hasPermission,
|
||||||
|
hasAnyPermission,
|
||||||
|
hasRole,
|
||||||
|
hasAnyRole,
|
||||||
|
isAdmin,
|
||||||
|
isChannelPartner,
|
||||||
|
isReseller,
|
||||||
|
permissions
|
||||||
|
};
|
||||||
262
src/migrations/20241201000001-create-channel-partners.js
Normal file
262
src/migrations/20241201000001-create-channel-partners.js
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
// Create channel_partners table
|
||||||
|
await queryInterface.createTable('channel_partners', {
|
||||||
|
id: {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
defaultValue: Sequelize.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
companyName: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
companyType: {
|
||||||
|
type: Sequelize.ENUM('individual', 'partnership', 'private_limited', 'public_limited', 'llp', 'corporation'),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
registrationNumber: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
gstNumber: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
panNumber: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
contactEmail: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
contactPhone: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
website: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
tier: {
|
||||||
|
type: Sequelize.ENUM('bronze', 'silver', 'gold', 'platinum', 'diamond'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'bronze'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: Sequelize.ENUM('active', 'inactive', 'suspended', 'pending_approval', 'rejected'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'pending_approval'
|
||||||
|
},
|
||||||
|
commissionRate: {
|
||||||
|
type: Sequelize.DECIMAL(5, 2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 15.00
|
||||||
|
},
|
||||||
|
marginSettings: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
defaultMargin: 25,
|
||||||
|
serviceMargins: {},
|
||||||
|
resellerMargins: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
billingDetails: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
billingCycle: 'monthly',
|
||||||
|
paymentTerms: 30,
|
||||||
|
currency: 'INR',
|
||||||
|
taxRate: 18.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
kycStatus: {
|
||||||
|
type: Sequelize.ENUM('pending', 'submitted', 'approved', 'rejected', 'expired'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'pending'
|
||||||
|
},
|
||||||
|
kycDocuments: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
approvedBy: {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
approvedAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
rejectionReason: {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
contractStartDate: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
contractEndDate: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
territory: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
regions: [],
|
||||||
|
countries: [],
|
||||||
|
states: [],
|
||||||
|
cities: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
specializations: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
certifications: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
performanceMetrics: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
totalRevenue: 0,
|
||||||
|
totalResellers: 0,
|
||||||
|
totalCustomers: 0,
|
||||||
|
averageDealSize: 0,
|
||||||
|
customerSatisfaction: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: Sequelize.NOW
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: Sequelize.NOW
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add indexes
|
||||||
|
await queryInterface.addIndex('channel_partners', ['registration_number'], { unique: true });
|
||||||
|
await queryInterface.addIndex('channel_partners', ['gst_number'], { unique: true });
|
||||||
|
await queryInterface.addIndex('channel_partners', ['tier']);
|
||||||
|
await queryInterface.addIndex('channel_partners', ['status']);
|
||||||
|
await queryInterface.addIndex('channel_partners', ['kyc_status']);
|
||||||
|
await queryInterface.addIndex('channel_partners', ['contact_email']);
|
||||||
|
|
||||||
|
// Update users table to add new fields
|
||||||
|
await queryInterface.addColumn('users', 'userType', {
|
||||||
|
type: Sequelize.ENUM('channel_partner', 'reseller', 'system'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'reseller'
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('users', 'permissions', {
|
||||||
|
type: Sequelize.JSON,
|
||||||
|
defaultValue: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('users', 'department', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('users', 'position', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('users', 'managerId', {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('users', 'onboardingCompleted', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('users', 'lastActivity', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('users', 'channelPartnerId', {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'channel_partners',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update resellers table to add channelPartnerId
|
||||||
|
await queryInterface.addColumn('resellers', 'channelPartnerId', {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'channel_partners',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add indexes for new user fields
|
||||||
|
await queryInterface.addIndex('users', ['userType']);
|
||||||
|
await queryInterface.addIndex('users', ['managerId']);
|
||||||
|
await queryInterface.addIndex('users', ['channelPartnerId']);
|
||||||
|
|
||||||
|
// Add index for resellers channelPartnerId
|
||||||
|
await queryInterface.addIndex('resellers', ['channelPartnerId']);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
// Remove indexes
|
||||||
|
await queryInterface.removeIndex('users', ['userType']);
|
||||||
|
await queryInterface.removeIndex('users', ['managerId']);
|
||||||
|
await queryInterface.removeIndex('users', ['channelPartnerId']);
|
||||||
|
await queryInterface.removeIndex('resellers', ['channelPartnerId']);
|
||||||
|
|
||||||
|
// Remove columns from users table
|
||||||
|
await queryInterface.removeColumn('users', 'userType');
|
||||||
|
await queryInterface.removeColumn('users', 'permissions');
|
||||||
|
await queryInterface.removeColumn('users', 'department');
|
||||||
|
await queryInterface.removeColumn('users', 'position');
|
||||||
|
await queryInterface.removeColumn('users', 'managerId');
|
||||||
|
await queryInterface.removeColumn('users', 'onboardingCompleted');
|
||||||
|
await queryInterface.removeColumn('users', 'lastActivity');
|
||||||
|
await queryInterface.removeColumn('users', 'channelPartnerId');
|
||||||
|
|
||||||
|
// Remove column from resellers table
|
||||||
|
await queryInterface.removeColumn('resellers', 'channelPartnerId');
|
||||||
|
|
||||||
|
// Drop channel_partners table
|
||||||
|
await queryInterface.dropTable('channel_partners');
|
||||||
|
}
|
||||||
|
};
|
||||||
303
src/models/ChannelPartner.js
Normal file
303
src/models/ChannelPartner.js
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const ChannelPartner = sequelize.define('ChannelPartner', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
companyName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [2, 100]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
companyType: {
|
||||||
|
type: DataTypes.ENUM('individual', 'partnership', 'private_limited', 'public_limited', 'llp', 'corporation'),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
registrationNumber: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
gstNumber: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
validate: {
|
||||||
|
is: /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
panNumber: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
validate: {
|
||||||
|
is: /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
hasRequiredFields(value) {
|
||||||
|
if (!value.street || !value.city || !value.state || !value.country || !value.zipCode) {
|
||||||
|
throw new Error('Address must include street, city, state, country, and zipCode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contactEmail: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
isEmail: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contactPhone: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
is: /^[\+]?[1-9][\d]{0,15}$/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
website: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
validate: {
|
||||||
|
isUrl: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tier: {
|
||||||
|
type: DataTypes.ENUM('bronze', 'silver', 'gold', 'platinum', 'diamond'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'bronze'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_approval', 'rejected'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'pending_approval'
|
||||||
|
},
|
||||||
|
commissionRate: {
|
||||||
|
type: DataTypes.DECIMAL(5, 2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 15.00,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
marginSettings: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
defaultMargin: 25,
|
||||||
|
serviceMargins: {},
|
||||||
|
resellerMargins: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
billingDetails: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
billingCycle: 'monthly',
|
||||||
|
paymentTerms: 30,
|
||||||
|
currency: 'INR',
|
||||||
|
taxRate: 18.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
kycStatus: {
|
||||||
|
type: DataTypes.ENUM('pending', 'submitted', 'approved', 'rejected', 'expired'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'pending'
|
||||||
|
},
|
||||||
|
kycDocuments: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
approvedBy: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
approvedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
rejectionReason: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
contractStartDate: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
contractEndDate: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
territory: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
regions: [],
|
||||||
|
countries: [],
|
||||||
|
states: [],
|
||||||
|
cities: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
specializations: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
certifications: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
performanceMetrics: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: {
|
||||||
|
totalRevenue: 0,
|
||||||
|
totalResellers: 0,
|
||||||
|
totalCustomers: 0,
|
||||||
|
averageDealSize: 0,
|
||||||
|
customerSatisfaction: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'channel_partners',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ['registration_number']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ['gst_number']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['tier']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['status']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['kyc_status']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['contact_email']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Instance methods
|
||||||
|
ChannelPartner.prototype.calculateCommission = function(amount, serviceType = null, resellerId = null) {
|
||||||
|
let rate = this.commissionRate;
|
||||||
|
|
||||||
|
if (serviceType && this.marginSettings.serviceMargins[serviceType]) {
|
||||||
|
rate = this.marginSettings.serviceMargins[serviceType];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resellerId && this.marginSettings.resellerMargins[resellerId]) {
|
||||||
|
rate = this.marginSettings.resellerMargins[resellerId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (amount * rate) / 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelPartner.prototype.isActive = function() {
|
||||||
|
return this.status === 'active' && this.kycStatus === 'approved';
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelPartner.prototype.canUpgradeTier = function() {
|
||||||
|
const tierOrder = ['bronze', 'silver', 'gold', 'platinum', 'diamond'];
|
||||||
|
const currentIndex = tierOrder.indexOf(this.tier);
|
||||||
|
return currentIndex < tierOrder.length - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelPartner.prototype.getNextTier = function() {
|
||||||
|
const tierOrder = ['bronze', 'silver', 'gold', 'platinum', 'diamond'];
|
||||||
|
const currentIndex = tierOrder.indexOf(this.tier);
|
||||||
|
return currentIndex < tierOrder.length - 1 ? tierOrder[currentIndex + 1] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelPartner.prototype.updateKycStatus = async function(status, documents = null, rejectionReason = null) {
|
||||||
|
const updates = { kycStatus: status };
|
||||||
|
|
||||||
|
if (documents) {
|
||||||
|
updates.kycDocuments = documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'rejected' && rejectionReason) {
|
||||||
|
updates.rejectionReason = rejectionReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'approved') {
|
||||||
|
updates.approvedAt = new Date();
|
||||||
|
updates.status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.update(updates);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelPartner.prototype.updatePerformanceMetrics = async function(metrics) {
|
||||||
|
const currentMetrics = this.performanceMetrics || {};
|
||||||
|
const updatedMetrics = { ...currentMetrics, ...metrics };
|
||||||
|
|
||||||
|
return this.update({ performanceMetrics: updatedMetrics });
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelPartner.prototype.addResellerMargin = async function(resellerId, margin) {
|
||||||
|
const currentMargins = this.marginSettings.resellerMargins || {};
|
||||||
|
currentMargins[resellerId] = margin;
|
||||||
|
|
||||||
|
return this.update({
|
||||||
|
marginSettings: {
|
||||||
|
...this.marginSettings,
|
||||||
|
resellerMargins: currentMargins
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Class methods
|
||||||
|
ChannelPartner.associate = function(models) {
|
||||||
|
ChannelPartner.hasMany(models.User, {
|
||||||
|
foreignKey: 'channelPartnerId',
|
||||||
|
as: 'users'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChannelPartner.belongsTo(models.User, {
|
||||||
|
foreignKey: 'approvedBy',
|
||||||
|
as: 'approver'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChannelPartner.hasMany(models.Reseller, {
|
||||||
|
foreignKey: 'channelPartnerId',
|
||||||
|
as: 'resellers'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChannelPartner.hasMany(models.Order, {
|
||||||
|
foreignKey: 'channelPartnerId',
|
||||||
|
as: 'orders'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChannelPartner.hasMany(models.Commission, {
|
||||||
|
foreignKey: 'channelPartnerId',
|
||||||
|
as: 'commissions'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChannelPartner.hasMany(models.Product, {
|
||||||
|
foreignKey: 'channelPartnerId',
|
||||||
|
as: 'products'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return ChannelPartner;
|
||||||
|
};
|
||||||
@ -179,11 +179,6 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
as: 'enrollments'
|
as: 'enrollments'
|
||||||
});
|
});
|
||||||
|
|
||||||
Course.hasMany(models.CourseModule, {
|
|
||||||
foreignKey: 'courseId',
|
|
||||||
as: 'modules'
|
|
||||||
});
|
|
||||||
|
|
||||||
Course.hasMany(models.Certificate, {
|
Course.hasMany(models.Certificate, {
|
||||||
foreignKey: 'courseId',
|
foreignKey: 'courseId',
|
||||||
as: 'certificates'
|
as: 'certificates'
|
||||||
|
|||||||
@ -197,10 +197,7 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
as: 'reviewer'
|
as: 'reviewer'
|
||||||
});
|
});
|
||||||
|
|
||||||
KnowledgeArticle.hasMany(models.ArticleFeedback, {
|
// ArticleFeedback association removed - model doesn't exist
|
||||||
foreignKey: 'articleId',
|
|
||||||
as: 'feedback'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return KnowledgeArticle;
|
return KnowledgeArticle;
|
||||||
|
|||||||
@ -141,6 +141,14 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {}
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
channelPartnerId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'channel_partners',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'resellers',
|
tableName: 'resellers',
|
||||||
@ -226,6 +234,11 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
as: 'approver'
|
as: 'approver'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reseller.belongsTo(models.ChannelPartner, {
|
||||||
|
foreignKey: 'channelPartnerId',
|
||||||
|
as: 'channelPartner'
|
||||||
|
});
|
||||||
|
|
||||||
Reseller.hasMany(models.Customer, {
|
Reseller.hasMany(models.Customer, {
|
||||||
foreignKey: 'resellerId',
|
foreignKey: 'resellerId',
|
||||||
as: 'customers'
|
as: 'customers'
|
||||||
|
|||||||
@ -45,12 +45,39 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
role: {
|
role: {
|
||||||
type: DataTypes.ENUM('reseller_admin', 'sales_agent', 'support_agent', 'read_only'),
|
type: DataTypes.ENUM(
|
||||||
|
// Channel Partner Roles
|
||||||
|
'channel_partner_admin',
|
||||||
|
'channel_partner_manager',
|
||||||
|
'channel_partner_sales',
|
||||||
|
'channel_partner_support',
|
||||||
|
'channel_partner_finance',
|
||||||
|
'channel_partner_analyst',
|
||||||
|
|
||||||
|
// Reseller Roles
|
||||||
|
'reseller_admin',
|
||||||
|
'reseller_manager',
|
||||||
|
'reseller_sales',
|
||||||
|
'reseller_support',
|
||||||
|
'reseller_finance',
|
||||||
|
'reseller_analyst',
|
||||||
|
|
||||||
|
// System Roles
|
||||||
|
'system_admin',
|
||||||
|
'system_support',
|
||||||
|
'system_analyst',
|
||||||
|
'read_only'
|
||||||
|
),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'read_only'
|
defaultValue: 'read_only'
|
||||||
},
|
},
|
||||||
|
userType: {
|
||||||
|
type: DataTypes.ENUM('channel_partner', 'reseller', 'system'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'reseller'
|
||||||
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_verification'),
|
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_verification', 'locked'),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'pending_verification'
|
defaultValue: 'pending_verification'
|
||||||
},
|
},
|
||||||
@ -113,6 +140,54 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
preferences: {
|
preferences: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {}
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: {},
|
||||||
|
comment: 'JSON object containing specific permissions for the user'
|
||||||
|
},
|
||||||
|
department: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Department within the organization'
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Job position/title'
|
||||||
|
},
|
||||||
|
managerId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
comment: 'Reference to manager/supervisor'
|
||||||
|
},
|
||||||
|
onboardingCompleted: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
lastActivity: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
resellerId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'resellers',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channelPartnerId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'channel_partners',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'users',
|
tableName: 'users',
|
||||||
@ -124,6 +199,9 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
{
|
{
|
||||||
fields: ['role']
|
fields: ['role']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fields: ['userType']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fields: ['status']
|
fields: ['status']
|
||||||
},
|
},
|
||||||
@ -132,6 +210,9 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
fields: ['password_reset_token']
|
fields: ['password_reset_token']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['managerId']
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
@ -140,12 +221,32 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
|
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
|
||||||
user.password = await bcrypt.hash(user.password, salt);
|
user.password = await bcrypt.hash(user.password, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set userType based on role
|
||||||
|
if (user.role.startsWith('channel_partner')) {
|
||||||
|
user.userType = 'channel_partner';
|
||||||
|
} else if (user.role.startsWith('reseller')) {
|
||||||
|
user.userType = 'reseller';
|
||||||
|
} else if (user.role.startsWith('system')) {
|
||||||
|
user.userType = 'system';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeUpdate: async (user) => {
|
beforeUpdate: async (user) => {
|
||||||
if (user.changed('password')) {
|
if (user.changed('password')) {
|
||||||
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
|
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
|
||||||
user.password = await bcrypt.hash(user.password, salt);
|
user.password = await bcrypt.hash(user.password, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update userType if role changes
|
||||||
|
if (user.changed('role')) {
|
||||||
|
if (user.role.startsWith('channel_partner')) {
|
||||||
|
user.userType = 'channel_partner';
|
||||||
|
} else if (user.role.startsWith('reseller')) {
|
||||||
|
user.userType = 'reseller';
|
||||||
|
} else if (user.role.startsWith('system')) {
|
||||||
|
user.userType = 'system';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -224,10 +325,194 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
return this.update({
|
return this.update({
|
||||||
loginAttempts: 0,
|
loginAttempts: 0,
|
||||||
lockUntil: null,
|
lockUntil: null,
|
||||||
lastLogin: new Date()
|
lastLogin: new Date(),
|
||||||
|
lastActivity: new Date()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.prototype.hasPermission = function(permission) {
|
||||||
|
if (this.permissions && this.permissions[permission]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role-based permissions
|
||||||
|
const rolePermissions = this.getRolePermissions();
|
||||||
|
return rolePermissions.includes(permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.getRolePermissions = function() {
|
||||||
|
const permissions = {
|
||||||
|
// Channel Partner Admin - Full access to channel partner features
|
||||||
|
channel_partner_admin: [
|
||||||
|
'dashboard:read', 'dashboard:write',
|
||||||
|
'resellers:read', 'resellers:write', 'resellers:delete',
|
||||||
|
'products:read', 'products:write', 'products:delete',
|
||||||
|
'customers:read', 'customers:write', 'customers:delete',
|
||||||
|
'billing:read', 'billing:write',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'analytics:read', 'analytics:write',
|
||||||
|
'users:read', 'users:write', 'users:delete',
|
||||||
|
'settings:read', 'settings:write'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Channel Partner Manager - Management access
|
||||||
|
channel_partner_manager: [
|
||||||
|
'dashboard:read', 'dashboard:write',
|
||||||
|
'resellers:read', 'resellers:write',
|
||||||
|
'products:read', 'products:write',
|
||||||
|
'customers:read', 'customers:write',
|
||||||
|
'billing:read', 'billing:write',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'analytics:read',
|
||||||
|
'users:read', 'users:write',
|
||||||
|
'settings:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Channel Partner Sales - Sales focused
|
||||||
|
channel_partner_sales: [
|
||||||
|
'dashboard:read',
|
||||||
|
'resellers:read', 'resellers:write',
|
||||||
|
'products:read',
|
||||||
|
'customers:read', 'customers:write',
|
||||||
|
'billing:read',
|
||||||
|
'reports:read',
|
||||||
|
'analytics:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Channel Partner Support - Support focused
|
||||||
|
channel_partner_support: [
|
||||||
|
'dashboard:read',
|
||||||
|
'resellers:read',
|
||||||
|
'customers:read', 'customers:write',
|
||||||
|
'support:read', 'support:write',
|
||||||
|
'reports:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Channel Partner Finance - Finance focused
|
||||||
|
channel_partner_finance: [
|
||||||
|
'dashboard:read',
|
||||||
|
'billing:read', 'billing:write',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'analytics:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Channel Partner Analyst - Analytics focused
|
||||||
|
channel_partner_analyst: [
|
||||||
|
'dashboard:read',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'analytics:read', 'analytics:write'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reseller Admin - Full access to reseller features
|
||||||
|
reseller_admin: [
|
||||||
|
'dashboard:read', 'dashboard:write',
|
||||||
|
'customers:read', 'customers:write', 'customers:delete',
|
||||||
|
'instances:read', 'instances:write', 'instances:delete',
|
||||||
|
'billing:read', 'billing:write',
|
||||||
|
'support:read', 'support:write',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'wallet:read', 'wallet:write',
|
||||||
|
'training:read', 'training:write',
|
||||||
|
'marketplace:read', 'marketplace:write',
|
||||||
|
'users:read', 'users:write',
|
||||||
|
'settings:read', 'settings:write'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reseller Manager - Management access
|
||||||
|
reseller_manager: [
|
||||||
|
'dashboard:read', 'dashboard:write',
|
||||||
|
'customers:read', 'customers:write',
|
||||||
|
'instances:read', 'instances:write',
|
||||||
|
'billing:read', 'billing:write',
|
||||||
|
'support:read', 'support:write',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'wallet:read',
|
||||||
|
'training:read',
|
||||||
|
'marketplace:read',
|
||||||
|
'users:read', 'users:write',
|
||||||
|
'settings:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reseller Sales - Sales focused
|
||||||
|
reseller_sales: [
|
||||||
|
'dashboard:read',
|
||||||
|
'customers:read', 'customers:write',
|
||||||
|
'instances:read', 'instances:write',
|
||||||
|
'billing:read',
|
||||||
|
'reports:read',
|
||||||
|
'marketplace:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reseller Support - Support focused
|
||||||
|
reseller_support: [
|
||||||
|
'dashboard:read',
|
||||||
|
'customers:read',
|
||||||
|
'instances:read',
|
||||||
|
'support:read', 'support:write',
|
||||||
|
'reports:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reseller Finance - Finance focused
|
||||||
|
reseller_finance: [
|
||||||
|
'dashboard:read',
|
||||||
|
'billing:read', 'billing:write',
|
||||||
|
'wallet:read', 'wallet:write',
|
||||||
|
'reports:read', 'reports:write'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reseller Analyst - Analytics focused
|
||||||
|
reseller_analyst: [
|
||||||
|
'dashboard:read',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'analytics:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// System Admin - Full system access
|
||||||
|
system_admin: [
|
||||||
|
'*'
|
||||||
|
],
|
||||||
|
|
||||||
|
// System Support - System support access
|
||||||
|
system_support: [
|
||||||
|
'dashboard:read',
|
||||||
|
'users:read', 'users:write',
|
||||||
|
'support:read', 'support:write',
|
||||||
|
'reports:read'
|
||||||
|
],
|
||||||
|
|
||||||
|
// System Analyst - System analytics access
|
||||||
|
system_analyst: [
|
||||||
|
'dashboard:read',
|
||||||
|
'reports:read', 'reports:write',
|
||||||
|
'analytics:read', 'analytics:write'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Read Only - Limited access
|
||||||
|
read_only: [
|
||||||
|
'dashboard:read',
|
||||||
|
'reports:read'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return permissions[this.role] || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.isChannelPartner = function() {
|
||||||
|
return this.userType === 'channel_partner';
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.isReseller = function() {
|
||||||
|
return this.userType === 'reseller';
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.isSystemUser = function() {
|
||||||
|
return this.userType === 'system';
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.isAdmin = function() {
|
||||||
|
return this.role.includes('admin');
|
||||||
|
};
|
||||||
|
|
||||||
User.prototype.toJSON = function() {
|
User.prototype.toJSON = function() {
|
||||||
const values = Object.assign({}, this.get());
|
const values = Object.assign({}, this.get());
|
||||||
delete values.password;
|
delete values.password;
|
||||||
@ -245,6 +530,21 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
as: 'reseller'
|
as: 'reseller'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
User.belongsTo(models.ChannelPartner, {
|
||||||
|
foreignKey: 'channelPartnerId',
|
||||||
|
as: 'channelPartner'
|
||||||
|
});
|
||||||
|
|
||||||
|
User.belongsTo(User, {
|
||||||
|
foreignKey: 'managerId',
|
||||||
|
as: 'manager'
|
||||||
|
});
|
||||||
|
|
||||||
|
User.hasMany(User, {
|
||||||
|
foreignKey: 'managerId',
|
||||||
|
as: 'subordinates'
|
||||||
|
});
|
||||||
|
|
||||||
User.hasMany(models.UserSession, {
|
User.hasMany(models.UserSession, {
|
||||||
foreignKey: 'userId',
|
foreignKey: 'userId',
|
||||||
as: 'sessions'
|
as: 'sessions'
|
||||||
|
|||||||
@ -42,8 +42,16 @@ const registerValidation = [
|
|||||||
.withMessage('Invalid phone number format'),
|
.withMessage('Invalid phone number format'),
|
||||||
body('role')
|
body('role')
|
||||||
.optional()
|
.optional()
|
||||||
.isIn(['reseller_admin', 'sales_agent', 'support_agent', 'read_only'])
|
.isIn([
|
||||||
.withMessage('Invalid role')
|
'channel_partner_admin', 'channel_partner_manager', 'channel_partner_sales', 'channel_partner_support', 'channel_partner_finance', 'channel_partner_analyst',
|
||||||
|
'reseller_admin', 'reseller_manager', 'reseller_sales', 'reseller_support', 'reseller_finance', 'reseller_analyst',
|
||||||
|
'system_admin', 'system_support', 'system_analyst', 'read_only'
|
||||||
|
])
|
||||||
|
.withMessage('Invalid role'),
|
||||||
|
body('userType')
|
||||||
|
.optional()
|
||||||
|
.isIn(['channel_partner', 'reseller', 'system'])
|
||||||
|
.withMessage('Invalid user type')
|
||||||
];
|
];
|
||||||
|
|
||||||
// Login validation
|
// Login validation
|
||||||
@ -100,13 +108,12 @@ router.post('/register', registerValidation, handleValidationErrors, authControl
|
|||||||
router.post('/login', loginValidation, handleValidationErrors, authController.login);
|
router.post('/login', loginValidation, handleValidationErrors, authController.login);
|
||||||
router.post('/refresh-token', validateRefreshToken, authController.refreshToken);
|
router.post('/refresh-token', validateRefreshToken, authController.refreshToken);
|
||||||
router.post('/logout', authController.logout);
|
router.post('/logout', authController.logout);
|
||||||
router.post('/verify-email', verifyEmailValidation, handleValidationErrors, authController.verifyEmail);
|
router.get('/verify-email/:token', authController.verifyEmail);
|
||||||
router.post('/forgot-password', forgotPasswordValidation, handleValidationErrors, authController.forgotPassword);
|
router.post('/forgot-password', forgotPasswordValidation, handleValidationErrors, authController.forgotPassword);
|
||||||
router.post('/reset-password', resetPasswordValidation, handleValidationErrors, authController.resetPassword);
|
router.post('/reset-password', resetPasswordValidation, handleValidationErrors, authController.resetPassword);
|
||||||
|
|
||||||
// MFA routes (protected)
|
// Profile routes (protected)
|
||||||
router.post('/setup-mfa', authenticateToken, authController.setupMFA);
|
router.get('/profile', authenticateToken, authController.getProfile);
|
||||||
router.post('/verify-mfa', authenticateToken, mfaTokenValidation, handleValidationErrors, authController.verifyMFA);
|
router.put('/profile', authenticateToken, authController.updateProfile);
|
||||||
router.post('/disable-mfa', authenticateToken, mfaTokenValidation, handleValidationErrors, authController.disableMFA);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
191
src/routes/channelPartners.js
Normal file
191
src/routes/channelPartners.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { body, param, query } = require('express-validator');
|
||||||
|
const channelPartnerController = require('../controllers/channelPartnerController');
|
||||||
|
const { hasPermission, hasAnyRole, isAdmin, isChannelPartner, permissions } = require('../middleware/authorization');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Validation middleware
|
||||||
|
const validateChannelPartner = [
|
||||||
|
body('companyName')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 2, max: 100 })
|
||||||
|
.withMessage('Company name must be between 2 and 100 characters'),
|
||||||
|
body('companyType')
|
||||||
|
.isIn(['individual', 'partnership', 'private_limited', 'public_limited', 'llp', 'corporation'])
|
||||||
|
.withMessage('Invalid company type'),
|
||||||
|
body('registrationNumber')
|
||||||
|
.optional()
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 50 })
|
||||||
|
.withMessage('Registration number must be between 1 and 50 characters'),
|
||||||
|
body('gstNumber')
|
||||||
|
.optional()
|
||||||
|
.matches(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/)
|
||||||
|
.withMessage('Invalid GST number format'),
|
||||||
|
body('panNumber')
|
||||||
|
.optional()
|
||||||
|
.matches(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
|
||||||
|
.withMessage('Invalid PAN number format'),
|
||||||
|
body('address')
|
||||||
|
.isObject()
|
||||||
|
.withMessage('Address must be an object'),
|
||||||
|
body('address.street')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 200 })
|
||||||
|
.withMessage('Street address is required and must be between 1 and 200 characters'),
|
||||||
|
body('address.city')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 100 })
|
||||||
|
.withMessage('City is required and must be between 1 and 100 characters'),
|
||||||
|
body('address.state')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 100 })
|
||||||
|
.withMessage('State is required and must be between 1 and 100 characters'),
|
||||||
|
body('address.country')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 100 })
|
||||||
|
.withMessage('Country is required and must be between 1 and 100 characters'),
|
||||||
|
body('address.zipCode')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 1, max: 20 })
|
||||||
|
.withMessage('Zip code is required and must be between 1 and 20 characters'),
|
||||||
|
body('contactEmail')
|
||||||
|
.isEmail()
|
||||||
|
.normalizeEmail()
|
||||||
|
.withMessage('Valid contact email is required'),
|
||||||
|
body('contactPhone')
|
||||||
|
.matches(/^[\+]?[1-9][\d]{0,15}$/)
|
||||||
|
.withMessage('Valid contact phone number is required'),
|
||||||
|
body('website')
|
||||||
|
.optional()
|
||||||
|
.isURL()
|
||||||
|
.withMessage('Valid website URL is required'),
|
||||||
|
body('tier')
|
||||||
|
.optional()
|
||||||
|
.isIn(['bronze', 'silver', 'gold', 'platinum', 'diamond'])
|
||||||
|
.withMessage('Invalid tier'),
|
||||||
|
body('commissionRate')
|
||||||
|
.optional()
|
||||||
|
.isFloat({ min: 0, max: 100 })
|
||||||
|
.withMessage('Commission rate must be between 0 and 100'),
|
||||||
|
body('marginSettings')
|
||||||
|
.optional()
|
||||||
|
.isObject()
|
||||||
|
.withMessage('Margin settings must be an object'),
|
||||||
|
body('billingDetails')
|
||||||
|
.optional()
|
||||||
|
.isObject()
|
||||||
|
.withMessage('Billing details must be an object'),
|
||||||
|
body('territory')
|
||||||
|
.optional()
|
||||||
|
.isObject()
|
||||||
|
.withMessage('Territory must be an object'),
|
||||||
|
body('specializations')
|
||||||
|
.optional()
|
||||||
|
.isArray()
|
||||||
|
.withMessage('Specializations must be an array'),
|
||||||
|
body('certifications')
|
||||||
|
.optional()
|
||||||
|
.isArray()
|
||||||
|
.withMessage('Certifications must be an array')
|
||||||
|
];
|
||||||
|
|
||||||
|
const validatePagination = [
|
||||||
|
query('page')
|
||||||
|
.optional()
|
||||||
|
.isInt({ min: 1 })
|
||||||
|
.withMessage('Page must be a positive integer'),
|
||||||
|
query('limit')
|
||||||
|
.optional()
|
||||||
|
.isInt({ min: 1, max: 100 })
|
||||||
|
.withMessage('Limit must be between 1 and 100'),
|
||||||
|
query('sortBy')
|
||||||
|
.optional()
|
||||||
|
.isIn(['companyName', 'status', 'tier', 'createdAt', 'updatedAt'])
|
||||||
|
.withMessage('Invalid sort field'),
|
||||||
|
query('sortOrder')
|
||||||
|
.optional()
|
||||||
|
.isIn(['ASC', 'DESC'])
|
||||||
|
.withMessage('Sort order must be ASC or DESC')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
|
||||||
|
// GET /api/channel-partners - Get all channel partners
|
||||||
|
router.get('/',
|
||||||
|
validatePagination,
|
||||||
|
hasPermission(permissions.USERS_READ),
|
||||||
|
channelPartnerController.getAllChannelPartners
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /api/channel-partners/:id - Get channel partner by ID
|
||||||
|
router.get('/:id',
|
||||||
|
param('id').isUUID().withMessage('Invalid channel partner ID'),
|
||||||
|
hasPermission(permissions.USERS_READ),
|
||||||
|
channelPartnerController.getChannelPartnerById
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /api/channel-partners - Create new channel partner
|
||||||
|
router.post('/',
|
||||||
|
validateChannelPartner,
|
||||||
|
hasPermission(permissions.USERS_WRITE),
|
||||||
|
channelPartnerController.createChannelPartner
|
||||||
|
);
|
||||||
|
|
||||||
|
// PUT /api/channel-partners/:id - Update channel partner
|
||||||
|
router.put('/:id',
|
||||||
|
param('id').isUUID().withMessage('Invalid channel partner ID'),
|
||||||
|
validateChannelPartner,
|
||||||
|
hasPermission(permissions.USERS_WRITE),
|
||||||
|
channelPartnerController.updateChannelPartner
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE /api/channel-partners/:id - Delete channel partner
|
||||||
|
router.delete('/:id',
|
||||||
|
param('id').isUUID().withMessage('Invalid channel partner ID'),
|
||||||
|
hasPermission(permissions.USERS_DELETE),
|
||||||
|
channelPartnerController.deleteChannelPartner
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /api/channel-partners/:id/approve - Approve channel partner
|
||||||
|
router.post('/:id/approve',
|
||||||
|
param('id').isUUID().withMessage('Invalid channel partner ID'),
|
||||||
|
body('approvedBy').isUUID().withMessage('Invalid approver ID'),
|
||||||
|
hasAnyRole(['system_admin', 'channel_partner_admin']),
|
||||||
|
channelPartnerController.approveChannelPartner
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /api/channel-partners/:id/reject - Reject channel partner
|
||||||
|
router.post('/:id/reject',
|
||||||
|
param('id').isUUID().withMessage('Invalid channel partner ID'),
|
||||||
|
body('rejectionReason')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 10, max: 1000 })
|
||||||
|
.withMessage('Rejection reason must be between 10 and 1000 characters'),
|
||||||
|
hasAnyRole(['system_admin', 'channel_partner_admin']),
|
||||||
|
channelPartnerController.rejectChannelPartner
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /api/channel-partners/:id/stats - Get channel partner statistics
|
||||||
|
router.get('/:id/stats',
|
||||||
|
param('id').isUUID().withMessage('Invalid channel partner ID'),
|
||||||
|
hasPermission(permissions.REPORTS_READ),
|
||||||
|
channelPartnerController.getChannelPartnerStats
|
||||||
|
);
|
||||||
|
|
||||||
|
// PUT /api/channel-partners/:id/tier - Update channel partner tier
|
||||||
|
router.put('/:id/tier',
|
||||||
|
param('id').isUUID().withMessage('Invalid channel partner ID'),
|
||||||
|
body('tier')
|
||||||
|
.isIn(['bronze', 'silver', 'gold', 'platinum', 'diamond'])
|
||||||
|
.withMessage('Invalid tier'),
|
||||||
|
body('reason')
|
||||||
|
.trim()
|
||||||
|
.isLength({ min: 10, max: 500 })
|
||||||
|
.withMessage('Reason must be between 10 and 500 characters'),
|
||||||
|
hasAnyRole(['system_admin', 'channel_partner_admin']),
|
||||||
|
channelPartnerController.updateChannelPartnerTier
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
281
src/seeders/20241201000001-demo-data.js
Normal file
281
src/seeders/20241201000001-demo-data.js
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
'use strict';
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
// Create demo channel partners
|
||||||
|
const channelPartners = [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
companyName: 'Cloud Solutions India Pvt Ltd',
|
||||||
|
companyType: 'private_limited',
|
||||||
|
registrationNumber: 'CIN123456789',
|
||||||
|
gstNumber: '27AABCC1234Z1Z5',
|
||||||
|
panNumber: 'AABCC1234Z',
|
||||||
|
address: {
|
||||||
|
street: '123 Tech Park, Phase 1',
|
||||||
|
city: 'Mumbai',
|
||||||
|
state: 'Maharashtra',
|
||||||
|
country: 'India',
|
||||||
|
zipCode: '400001'
|
||||||
|
},
|
||||||
|
contactEmail: 'admin@cloudsolutions.in',
|
||||||
|
contactPhone: '+919876543210',
|
||||||
|
website: 'https://cloudsolutions.in',
|
||||||
|
tier: 'platinum',
|
||||||
|
status: 'active',
|
||||||
|
commissionRate: 20.00,
|
||||||
|
kycStatus: 'approved',
|
||||||
|
approvedAt: new Date(),
|
||||||
|
contractStartDate: new Date('2024-01-01'),
|
||||||
|
contractEndDate: new Date('2025-12-31'),
|
||||||
|
territory: {
|
||||||
|
regions: ['West India', 'North India'],
|
||||||
|
countries: ['India'],
|
||||||
|
states: ['Maharashtra', 'Gujarat', 'Delhi', 'Punjab'],
|
||||||
|
cities: ['Mumbai', 'Pune', 'Ahmedabad', 'Delhi', 'Chandigarh']
|
||||||
|
},
|
||||||
|
specializations: ['Cloud Migration', 'DevOps', 'Security'],
|
||||||
|
certifications: ['AWS Advanced Consulting Partner', 'Azure Gold Partner'],
|
||||||
|
performanceMetrics: {
|
||||||
|
totalRevenue: 25000000,
|
||||||
|
totalResellers: 45,
|
||||||
|
totalCustomers: 1200,
|
||||||
|
averageDealSize: 208333,
|
||||||
|
customerSatisfaction: 4.8
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
companyName: 'Digital Innovations Corp',
|
||||||
|
companyType: 'corporation',
|
||||||
|
registrationNumber: 'CIN987654321',
|
||||||
|
gstNumber: '29XYZAB5678Z2Z6',
|
||||||
|
panNumber: 'XYZAB5678Z',
|
||||||
|
address: {
|
||||||
|
street: '456 Innovation Hub',
|
||||||
|
city: 'Bangalore',
|
||||||
|
state: 'Karnataka',
|
||||||
|
country: 'India',
|
||||||
|
zipCode: '560001'
|
||||||
|
},
|
||||||
|
contactEmail: 'info@digitalinnovations.com',
|
||||||
|
contactPhone: '+919876543211',
|
||||||
|
website: 'https://digitalinnovations.com',
|
||||||
|
tier: 'gold',
|
||||||
|
status: 'active',
|
||||||
|
commissionRate: 18.00,
|
||||||
|
kycStatus: 'approved',
|
||||||
|
approvedAt: new Date(),
|
||||||
|
contractStartDate: new Date('2024-02-01'),
|
||||||
|
contractEndDate: new Date('2025-01-31'),
|
||||||
|
territory: {
|
||||||
|
regions: ['South India'],
|
||||||
|
countries: ['India'],
|
||||||
|
states: ['Karnataka', 'Tamil Nadu', 'Kerala'],
|
||||||
|
cities: ['Bangalore', 'Chennai', 'Kochi', 'Hyderabad']
|
||||||
|
},
|
||||||
|
specializations: ['AI/ML', 'Data Analytics', 'IoT'],
|
||||||
|
certifications: ['Google Cloud Premier Partner', 'Oracle Platinum Partner'],
|
||||||
|
performanceMetrics: {
|
||||||
|
totalRevenue: 18000000,
|
||||||
|
totalResellers: 32,
|
||||||
|
totalCustomers: 850,
|
||||||
|
averageDealSize: 211765,
|
||||||
|
customerSatisfaction: 4.6
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('channel_partners', channelPartners, {});
|
||||||
|
|
||||||
|
// Create demo resellers
|
||||||
|
const resellers = [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
companyName: 'Tech Solutions Inc',
|
||||||
|
companyType: 'private_limited',
|
||||||
|
registrationNumber: 'CIN111222333',
|
||||||
|
gstNumber: '27TECH1234Z1Z5',
|
||||||
|
panNumber: 'TECH1234Z',
|
||||||
|
address: {
|
||||||
|
street: '789 Business Park',
|
||||||
|
city: 'Pune',
|
||||||
|
state: 'Maharashtra',
|
||||||
|
country: 'India',
|
||||||
|
zipCode: '411001'
|
||||||
|
},
|
||||||
|
contactEmail: 'john@techsolutions.com',
|
||||||
|
contactPhone: '+919876543212',
|
||||||
|
website: 'https://techsolutions.com',
|
||||||
|
tier: 'silver',
|
||||||
|
status: 'active',
|
||||||
|
commissionRate: 12.00,
|
||||||
|
channelPartnerId: channelPartners[0].id,
|
||||||
|
kycStatus: 'approved',
|
||||||
|
approvedAt: new Date(),
|
||||||
|
contractStartDate: new Date('2024-03-01'),
|
||||||
|
contractEndDate: new Date('2025-02-28'),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
companyName: 'Cloud Masters Ltd',
|
||||||
|
companyType: 'private_limited',
|
||||||
|
registrationNumber: 'CIN444555666',
|
||||||
|
gstNumber: '29CLOUD5678Z2Z6',
|
||||||
|
panNumber: 'CLOUD5678Z',
|
||||||
|
address: {
|
||||||
|
street: '321 Tech Street',
|
||||||
|
city: 'Chennai',
|
||||||
|
state: 'Tamil Nadu',
|
||||||
|
country: 'India',
|
||||||
|
zipCode: '600001'
|
||||||
|
},
|
||||||
|
contactEmail: 'sarah@cloudmasters.com',
|
||||||
|
contactPhone: '+919876543213',
|
||||||
|
website: 'https://cloudmasters.com',
|
||||||
|
tier: 'bronze',
|
||||||
|
status: 'active',
|
||||||
|
commissionRate: 10.00,
|
||||||
|
channelPartnerId: channelPartners[1].id,
|
||||||
|
kycStatus: 'approved',
|
||||||
|
approvedAt: new Date(),
|
||||||
|
contractStartDate: new Date('2024-04-01'),
|
||||||
|
contractEndDate: new Date('2025-03-31'),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('resellers', resellers, {});
|
||||||
|
|
||||||
|
// Create demo users
|
||||||
|
const hashedPassword = await bcrypt.hash('Password123!', 12);
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
// System Admin
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
email: 'admin@cloudtopiaa.com',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'System',
|
||||||
|
lastName: 'Administrator',
|
||||||
|
phone: '+919876543214',
|
||||||
|
role: 'system_admin',
|
||||||
|
userType: 'system',
|
||||||
|
status: 'active',
|
||||||
|
emailVerified: true,
|
||||||
|
onboardingCompleted: true,
|
||||||
|
lastLogin: new Date(),
|
||||||
|
lastActivity: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
// Channel Partner Admin
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
email: 'admin@cloudsolutions.in',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'Rajesh',
|
||||||
|
lastName: 'Kumar',
|
||||||
|
phone: '+919876543215',
|
||||||
|
role: 'channel_partner_admin',
|
||||||
|
userType: 'channel_partner',
|
||||||
|
status: 'active',
|
||||||
|
emailVerified: true,
|
||||||
|
channelPartnerId: channelPartners[0].id,
|
||||||
|
onboardingCompleted: true,
|
||||||
|
lastLogin: new Date(),
|
||||||
|
lastActivity: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
// Channel Partner Manager
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
email: 'manager@cloudsolutions.in',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'Priya',
|
||||||
|
lastName: 'Sharma',
|
||||||
|
phone: '+919876543216',
|
||||||
|
role: 'channel_partner_manager',
|
||||||
|
userType: 'channel_partner',
|
||||||
|
status: 'active',
|
||||||
|
emailVerified: true,
|
||||||
|
channelPartnerId: channelPartners[0].id,
|
||||||
|
managerId: channelPartners[0].id, // Reference to channel partner admin
|
||||||
|
onboardingCompleted: true,
|
||||||
|
lastLogin: new Date(),
|
||||||
|
lastActivity: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
// Reseller Admin
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
email: 'john@techsolutions.com',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Reseller',
|
||||||
|
phone: '+919876543217',
|
||||||
|
role: 'reseller_admin',
|
||||||
|
userType: 'reseller',
|
||||||
|
status: 'active',
|
||||||
|
emailVerified: true,
|
||||||
|
resellerId: resellers[0].id,
|
||||||
|
onboardingCompleted: true,
|
||||||
|
lastLogin: new Date(),
|
||||||
|
lastActivity: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
// Reseller Manager
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
email: 'sarah@cloudmasters.com',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'Sarah',
|
||||||
|
lastName: 'Wilson',
|
||||||
|
phone: '+919876543218',
|
||||||
|
role: 'reseller_manager',
|
||||||
|
userType: 'reseller',
|
||||||
|
status: 'active',
|
||||||
|
emailVerified: true,
|
||||||
|
resellerId: resellers[1].id,
|
||||||
|
onboardingCompleted: true,
|
||||||
|
lastLogin: new Date(),
|
||||||
|
lastActivity: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('users', users, {});
|
||||||
|
|
||||||
|
// Update channel partners with approvedBy
|
||||||
|
await queryInterface.bulkUpdate('channel_partners',
|
||||||
|
{ approvedBy: users[0].id }, // System admin approved them
|
||||||
|
{ id: { [Sequelize.Op.in]: channelPartners.map(cp => cp.id) } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update resellers with approvedBy
|
||||||
|
await queryInterface.bulkUpdate('resellers',
|
||||||
|
{ approvedBy: users[1].id }, // Channel partner admin approved them
|
||||||
|
{ id: { [Sequelize.Op.in]: resellers.map(r => r.id) } }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
// Remove all demo data
|
||||||
|
await queryInterface.bulkDelete('users', null, {});
|
||||||
|
await queryInterface.bulkDelete('resellers', null, {});
|
||||||
|
await queryInterface.bulkDelete('channel_partners', null, {});
|
||||||
|
}
|
||||||
|
};
|
||||||
33
src/utils/audit.js
Normal file
33
src/utils/audit.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const { AuditLog } = require('../models');
|
||||||
|
|
||||||
|
const createAuditLog = async ({ userId, action, details = {}, ipAddress, userAgent, resource, resourceId }) => {
|
||||||
|
try {
|
||||||
|
await AuditLog.create({
|
||||||
|
userId,
|
||||||
|
action,
|
||||||
|
details,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
resource,
|
||||||
|
resourceId,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Audit log creation failed:', error);
|
||||||
|
// Don't throw error as audit logging shouldn't break main functionality
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logUserAction = async (userId, action, details = {}) => {
|
||||||
|
return createAuditLog({ userId, action, details });
|
||||||
|
};
|
||||||
|
|
||||||
|
const logSystemAction = async (action, details = {}) => {
|
||||||
|
return createAuditLog({ userId: null, action, details });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createAuditLog,
|
||||||
|
logUserAction,
|
||||||
|
logSystemAction
|
||||||
|
};
|
||||||
131
src/utils/email.js
Normal file
131
src/utils/email.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
// Create transporter
|
||||||
|
const createTransporter = () => {
|
||||||
|
return nodemailer.createTransporter({
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
port: process.env.SMTP_PORT,
|
||||||
|
secure: process.env.SMTP_SECURE === 'true',
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Email templates
|
||||||
|
const emailTemplates = {
|
||||||
|
emailVerification: (data) => ({
|
||||||
|
subject: 'Verify your email address',
|
||||||
|
html: `
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2>Welcome to Cloudtopiaa Reseller Portal!</h2>
|
||||||
|
<p>Hi ${data.name},</p>
|
||||||
|
<p>Thank you for registering with us. Please verify your email address by clicking the button below:</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="${data.verificationUrl}"
|
||||||
|
style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
||||||
|
Verify Email Address
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p>If the button doesn't work, you can copy and paste this link into your browser:</p>
|
||||||
|
<p>${data.verificationUrl}</p>
|
||||||
|
<p>This link will expire in 1 hour.</p>
|
||||||
|
<p>Best regards,<br>The Cloudtopiaa Team</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}),
|
||||||
|
|
||||||
|
passwordReset: (data) => ({
|
||||||
|
subject: 'Reset your password',
|
||||||
|
html: `
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2>Password Reset Request</h2>
|
||||||
|
<p>Hi ${data.name},</p>
|
||||||
|
<p>We received a request to reset your password. Click the button below to create a new password:</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="${data.resetUrl}"
|
||||||
|
style="background-color: #dc3545; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
||||||
|
Reset Password
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p>If you didn't request this password reset, you can safely ignore this email.</p>
|
||||||
|
<p>This link will expire in 1 hour.</p>
|
||||||
|
<p>Best regards,<br>The Cloudtopiaa Team</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}),
|
||||||
|
|
||||||
|
welcomeEmail: (data) => ({
|
||||||
|
subject: 'Welcome to Cloudtopiaa Reseller Portal',
|
||||||
|
html: `
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2>Welcome to Cloudtopiaa!</h2>
|
||||||
|
<p>Hi ${data.name},</p>
|
||||||
|
<p>Your account has been successfully verified and activated. You can now access all features of the Cloudtopiaa Reseller Portal.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="${data.loginUrl}"
|
||||||
|
style="background-color: #28a745; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
||||||
|
Login to Portal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p>If you have any questions, please don't hesitate to contact our support team.</p>
|
||||||
|
<p>Best regards,<br>The Cloudtopiaa Team</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send email function
|
||||||
|
const sendEmail = async ({ to, subject, template, data, html, text }) => {
|
||||||
|
try {
|
||||||
|
const transporter = createTransporter();
|
||||||
|
|
||||||
|
let emailContent = {};
|
||||||
|
|
||||||
|
if (template && emailTemplates[template]) {
|
||||||
|
emailContent = emailTemplates[template](data);
|
||||||
|
} else {
|
||||||
|
emailContent = { subject, html, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.SMTP_FROM || process.env.SMTP_USER,
|
||||||
|
to,
|
||||||
|
subject: emailContent.subject,
|
||||||
|
html: emailContent.html,
|
||||||
|
text: emailContent.text
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await transporter.sendMail(mailOptions);
|
||||||
|
console.log('Email sent successfully:', result.messageId);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Email sending failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send bulk email
|
||||||
|
const sendBulkEmail = async (emails) => {
|
||||||
|
const transporter = createTransporter();
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const email of emails) {
|
||||||
|
try {
|
||||||
|
const result = await sendEmail(email);
|
||||||
|
results.push({ success: true, email: email.to, messageId: result.messageId });
|
||||||
|
} catch (error) {
|
||||||
|
results.push({ success: false, email: email.to, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendEmail,
|
||||||
|
sendBulkEmail,
|
||||||
|
emailTemplates
|
||||||
|
};
|
||||||
27
src/utils/jwt.js
Normal file
27
src/utils/jwt.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
const generateToken = (payload, expiresIn = '15m') => {
|
||||||
|
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn });
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyToken = (token) => {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeToken = (token) => {
|
||||||
|
try {
|
||||||
|
return jwt.decode(token);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateToken,
|
||||||
|
verifyToken,
|
||||||
|
decodeToken
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user