This commit is contained in:
rohit 2025-08-05 18:22:31 +05:30
parent 57d8f3fd5c
commit f9dac8cfc9
18 changed files with 10030 additions and 176 deletions

361
README.md Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ const redisClient = require('./config/redis');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const resellerRoutes = require('./routes/resellers');
const channelPartnerRoutes = require('./routes/channelPartners');
const productRoutes = require('./routes/products');
const customerRoutes = require('./routes/customers');
const billingRoutes = require('./routes/billing');
@ -106,6 +107,7 @@ app.get('/health', async (req, res) => {
app.use('/api/auth', authRoutes);
app.use('/api/users', authenticateToken, userRoutes);
app.use('/api/resellers', authenticateToken, resellerRoutes);
app.use('/api/channel-partners', authenticateToken, channelPartnerRoutes);
app.use('/api/products', authenticateToken, productRoutes);
app.use('/api/customers', authenticateToken, customerRoutes);
app.use('/api/billing', authenticateToken, billingRoutes);

View File

@ -1,88 +1,153 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const { User, UserSession, AuditLog } = require('../models');
const { sendEmail } = require('../config/email');
const bcrypt = require('bcryptjs');
const { validationResult } = require('express-validator');
const { User, Reseller, ChannelPartner } = require('../models');
const redisClient = require('../config/redis');
// Generate JWT tokens
const generateTokens = (userId) => {
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 };
};
const { sendEmail } = require('../utils/email');
const { generateToken, verifyToken } = require('../utils/jwt');
const { createAuditLog } = require('../utils/audit');
// Register new user
const register = async (req, res) => {
try {
const { email, password, firstName, lastName, phone, role = 'read_only' } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(409).json({
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'User already exists with this email'
message: 'Validation errors',
errors: errors.array()
});
}
// Generate email verification token
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({
const {
email,
password,
firstName,
lastName,
phone,
role,
emailVerificationToken,
emailVerificationExpires
});
userType,
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
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${emailVerificationToken}`;
await sendEmail(email, 'emailVerification', {
name: `${firstName} ${lastName}`,
url: verificationUrl,
otp: emailVerificationToken.substring(0, 6).toUpperCase()
await sendEmail({
to: email,
subject: 'Verify your email address',
template: 'emailVerification',
data: {
name: firstName,
verificationUrl: `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`
}
});
// Log audit
await AuditLog.create({
// Create audit log
await createAuditLog({
userId: user.id,
action: 'USER_REGISTERED',
resource: 'user',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
details: { userType, role },
ipAddress: req.ip
});
res.status(201).json({
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: {
user: user.toJSON()
user: user.toJSON(),
requiresVerification: true
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Registration failed',
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
@ -91,12 +156,32 @@ const register = async (req, res) => {
// Login user
const login = 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 { email, password, mfaToken } = req.body;
// Find user
// Find user with organization details
const user = await User.findOne({
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) {
@ -124,35 +209,44 @@ const login = async (req, res) => {
});
}
// Check if email is verified
// Check email verification
if (!user.emailVerified) {
return res.status(401).json({
return res.status(403).json({
success: false,
message: 'Please verify your email before logging in'
message: 'Please verify your email address before logging in',
requiresVerification: true
});
}
// Check account status
if (user.status !== 'active') {
return res.status(401).json({
return res.status(403).json({
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 (!mfaToken) {
return res.status(200).json({
success: true,
requiresMFA: true,
message: 'MFA token required'
return res.status(400).json({
success: false,
message: 'MFA token required',
requiresMfa: true
});
}
const isMfaValid = user.verifyMfaToken(mfaToken);
if (!isMfaValid) {
await user.incrementLoginAttempts();
const isValidMfa = user.verifyMfaToken(mfaToken);
if (!isValidMfa) {
return res.status(401).json({
success: false,
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
await user.resetLoginAttempts();
// Log audit
await AuditLog.create({
// Generate tokens
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,
action: 'USER_LOGIN',
resource: 'user',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
details: { userType: user.userType, role: user.role },
ipAddress: req.ip
});
res.json({
@ -196,147 +291,389 @@ const login = async (req, res) => {
user: user.toJSON(),
accessToken,
refreshToken,
expiresIn: process.env.JWT_EXPIRES_IN || '15m'
expiresIn: 15 * 60 // 15 minutes
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Login failed',
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// Refresh token
// Refresh access token
const refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
const session = await UserSession.findOne({
where: {
refreshToken,
isActive: true
},
include: ['user']
});
if (!session || session.isExpired()) {
return res.status(401).json({
if (!refreshToken) {
return res.status(400).json({
success: false,
message: 'Invalid or expired refresh token'
message: 'Refresh token is required'
});
}
// Generate new tokens
const { accessToken, refreshToken: newRefreshToken } = generateTokens(session.userId);
// Verify refresh token
const decoded = verifyToken(refreshToken);
if (!decoded || decoded.type !== 'refresh') {
return res.status(401).json({
success: false,
message: 'Invalid refresh token'
});
}
// Update session
await session.update({
refreshToken: newRefreshToken,
lastUsedAt: new Date()
// Check if refresh token exists in Redis
const storedToken = await redisClient.get(`refresh_token:${decoded.userId}`);
if (!storedToken || storedToken !== refreshToken) {
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({
success: true,
message: 'Token refreshed successfully',
data: {
accessToken,
refreshToken: newRefreshToken,
expiresIn: process.env.JWT_EXPIRES_IN || '15m'
accessToken: newAccessToken,
expiresIn: 15 * 60
}
});
} catch (error) {
console.error('Token refresh error:', error);
res.status(500).json({
success: false,
message: 'Token refresh failed'
message: 'Internal server error'
});
}
};
// Logout
// Logout user
const logout = async (req, res) => {
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 userId = req.user.id;
if (refreshToken) {
await UserSession.update(
{ isActive: false },
{ where: { refreshToken } }
);
// Remove refresh token from Redis
await redisClient.del(`refresh_token:${userId}`);
}
// Log audit
await AuditLog.create({
userId: req.user?.id,
// Create audit log
await createAuditLog({
userId,
action: 'USER_LOGOUT',
resource: 'user',
resourceId: req.user?.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
ipAddress: req.ip
});
res.json({
success: true,
message: 'Logout successful'
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({
success: false,
message: 'Logout failed'
message: 'Internal server error'
});
}
};
// Setup MFA
const setupMFA = async (req, res) => {
// Verify email
const verifyEmail = async (req, res) => {
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({
success: false,
message: 'MFA is already enabled'
message: 'Invalid verification token'
});
}
const secret = user.generateMfaSecret();
const backupCodes = user.generateMfaBackupCodes();
const user = await User.findOne({
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();
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
// Create audit log
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({
success: true,
data: {
secret: secret.base32,
qrCode: qrCodeUrl,
backupCodes
user: user.toJSON()
}
});
} catch (error) {
console.error('MFA setup error:', error);
console.error('Get profile error:', error);
res.status(500).json({
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,
refreshToken,
logout,
setupMFA
verifyEmail,
forgotPassword,
resetPassword,
getProfile,
updateProfile
};

View 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
};

View 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
};

View 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');
}
};

View 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;
};

View File

@ -179,11 +179,6 @@ module.exports = (sequelize, DataTypes) => {
as: 'enrollments'
});
Course.hasMany(models.CourseModule, {
foreignKey: 'courseId',
as: 'modules'
});
Course.hasMany(models.Certificate, {
foreignKey: 'courseId',
as: 'certificates'

View File

@ -197,10 +197,7 @@ module.exports = (sequelize, DataTypes) => {
as: 'reviewer'
});
KnowledgeArticle.hasMany(models.ArticleFeedback, {
foreignKey: 'articleId',
as: 'feedback'
});
// ArticleFeedback association removed - model doesn't exist
};
return KnowledgeArticle;

View File

@ -141,6 +141,14 @@ module.exports = (sequelize, DataTypes) => {
metadata: {
type: DataTypes.JSON,
defaultValue: {}
},
channelPartnerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'channel_partners',
key: 'id'
}
}
}, {
tableName: 'resellers',
@ -226,6 +234,11 @@ module.exports = (sequelize, DataTypes) => {
as: 'approver'
});
Reseller.belongsTo(models.ChannelPartner, {
foreignKey: 'channelPartnerId',
as: 'channelPartner'
});
Reseller.hasMany(models.Customer, {
foreignKey: 'resellerId',
as: 'customers'

View File

@ -45,12 +45,39 @@ module.exports = (sequelize, DataTypes) => {
}
},
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,
defaultValue: 'read_only'
},
userType: {
type: DataTypes.ENUM('channel_partner', 'reseller', 'system'),
allowNull: false,
defaultValue: 'reseller'
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_verification'),
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_verification', 'locked'),
allowNull: false,
defaultValue: 'pending_verification'
},
@ -113,6 +140,54 @@ module.exports = (sequelize, DataTypes) => {
preferences: {
type: DataTypes.JSON,
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',
@ -124,6 +199,9 @@ module.exports = (sequelize, DataTypes) => {
{
fields: ['role']
},
{
fields: ['userType']
},
{
fields: ['status']
},
@ -132,6 +210,9 @@ module.exports = (sequelize, DataTypes) => {
},
{
fields: ['password_reset_token']
},
{
fields: ['managerId']
}
],
hooks: {
@ -140,12 +221,32 @@ module.exports = (sequelize, DataTypes) => {
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
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) => {
if (user.changed('password')) {
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
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({
loginAttempts: 0,
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() {
const values = Object.assign({}, this.get());
delete values.password;
@ -245,6 +530,21 @@ module.exports = (sequelize, DataTypes) => {
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, {
foreignKey: 'userId',
as: 'sessions'

View File

@ -42,8 +42,16 @@ const registerValidation = [
.withMessage('Invalid phone number format'),
body('role')
.optional()
.isIn(['reseller_admin', 'sales_agent', 'support_agent', 'read_only'])
.withMessage('Invalid role')
.isIn([
'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
@ -100,13 +108,12 @@ router.post('/register', registerValidation, handleValidationErrors, authControl
router.post('/login', loginValidation, handleValidationErrors, authController.login);
router.post('/refresh-token', validateRefreshToken, authController.refreshToken);
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('/reset-password', resetPasswordValidation, handleValidationErrors, authController.resetPassword);
// MFA routes (protected)
router.post('/setup-mfa', authenticateToken, authController.setupMFA);
router.post('/verify-mfa', authenticateToken, mfaTokenValidation, handleValidationErrors, authController.verifyMFA);
router.post('/disable-mfa', authenticateToken, mfaTokenValidation, handleValidationErrors, authController.disableMFA);
// Profile routes (protected)
router.get('/profile', authenticateToken, authController.getProfile);
router.put('/profile', authenticateToken, authController.updateProfile);
module.exports = router;

View 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;

View 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
View 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
View 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
View 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
};