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 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);
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
Course.hasMany(models.CourseModule, {
|
||||
foreignKey: 'courseId',
|
||||
as: 'modules'
|
||||
});
|
||||
|
||||
Course.hasMany(models.Certificate, {
|
||||
foreignKey: 'courseId',
|
||||
as: 'certificates'
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
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