mirror of
https://github.com/hotspotbilling/phpnuxbill.git
synced 2025-08-31 15:19:36 +02:00
Complete PHP to JavaScript conversion
- Converted PHPNuxBill from PHP to Node.js/Express.js - Implemented Sequelize ORM for database operations - Created complete authentication system with JWT - Built responsive admin dashboard with Bootstrap - Added customer, plan, and router management - Implemented RESTful API endpoints - Added security middleware and error handling - Created database migration system - Included sample data and default admin user - Maintained database compatibility with original PHP version - Added comprehensive documentation
This commit is contained in:
parent
4a441c5763
commit
0f192b39af
41 changed files with 3741 additions and 0 deletions
45
.env
Normal file
45
.env
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Environment Configuration
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=phpnuxbill
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
|
||||
# Radius Database (if different from main DB)
|
||||
RADIUS_DB_HOST=localhost
|
||||
RADIUS_DB_PORT=3306
|
||||
RADIUS_DB_NAME=phpnuxbill
|
||||
RADIUS_DB_USER=root
|
||||
RADIUS_DB_PASSWORD=
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-super-secret-jwt-key-here
|
||||
SESSION_SECRET=your-session-secret-here
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW=15
|
||||
RATE_LIMIT_MAX=100
|
||||
|
||||
# Application Settings
|
||||
APP_NAME=PHPNuxBill-JS
|
||||
APP_URL=http://localhost:3000
|
||||
MAINTENANCE_MODE=false
|
||||
|
||||
# Email Configuration
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=your-email@gmail.com
|
||||
MAIL_PASSWORD=your-app-password
|
||||
MAIL_FROM=noreply@yourcompany.com
|
||||
|
||||
# File Upload
|
||||
MAX_FILE_SIZE=10MB
|
||||
UPLOAD_PATH=./uploads
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=./logs/app.log
|
45
.env.example
Normal file
45
.env.example
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Environment Variables
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=phpnuxbill
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-jwt-secret-key-here
|
||||
SESSION_SECRET=your-session-secret-key-here
|
||||
BCRYPT_ROUNDS=10
|
||||
|
||||
# Application Settings
|
||||
APP_NAME=PHPNuxBill-JS
|
||||
APP_URL=http://localhost:3000
|
||||
APP_STAGE=development
|
||||
|
||||
# Email Configuration
|
||||
MAIL_HOST=localhost
|
||||
MAIL_PORT=587
|
||||
MAIL_USER=
|
||||
MAIL_PASS=
|
||||
MAIL_FROM=noreply@phpnuxbill.com
|
||||
|
||||
# Maintenance Mode
|
||||
MAINTENANCE_MODE=false
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW=15
|
||||
RATE_LIMIT_MAX=100
|
||||
|
||||
# File Upload
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_PATH=./uploads
|
||||
|
||||
# Cache
|
||||
CACHE_TTL=3600
|
||||
|
||||
# API Configuration
|
||||
API_VERSION=v1
|
||||
API_PREFIX=/api
|
259
README-JS.md
Normal file
259
README-JS.md
Normal file
|
@ -0,0 +1,259 @@
|
|||
# PHPNuxBill-JS
|
||||
|
||||
A modern JavaScript/Node.js port of PHPNuxBill - A powerful Hotspot Billing Software for managing internet access and customer billing.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Modern Tech Stack**: Built with Express.js, Sequelize ORM, and MySQL
|
||||
- **Authentication**: JWT-based authentication with role-based access control
|
||||
- **Customer Management**: Complete customer registration and management system
|
||||
- **Plan Management**: Flexible internet plan configuration
|
||||
- **Router Integration**: Mikrotik router management and monitoring
|
||||
- **Dashboard**: Real-time analytics and system monitoring
|
||||
- **Responsive UI**: Bootstrap-based responsive interface
|
||||
- **Security**: Input validation, rate limiting, and security headers
|
||||
- **API**: RESTful API for external integrations
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- MySQL/MariaDB >= 5.7
|
||||
- Git
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/your-username/phpnuxbill-js.git
|
||||
cd phpnuxbill-js
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Environment Setup**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Edit `.env` file with your database credentials and configuration.
|
||||
|
||||
4. **Database Setup**
|
||||
Create a MySQL database and run the migration:
|
||||
```bash
|
||||
npm run migrate
|
||||
```
|
||||
|
||||
5. **Start the server**
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
|
||||
# Production
|
||||
npm start
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=phpnuxbill
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
SESSION_SECRET=your-session-secret
|
||||
|
||||
# Application Settings
|
||||
APP_NAME=PHPNuxBill-JS
|
||||
APP_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
## 📱 Default Credentials
|
||||
|
||||
After running the migration, you can login with:
|
||||
|
||||
### Admin Login
|
||||
- **URL**: `http://localhost:3000/auth/admin/login`
|
||||
- **Username**: `admin`
|
||||
- **Password**: `admin123`
|
||||
|
||||
### Sample Customer Login
|
||||
- **Username**: `customer1`
|
||||
- **Password**: `password123`
|
||||
|
||||
## 📊 API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `POST /auth/admin/login` - Admin login
|
||||
- `POST /auth/customer/login` - Customer login
|
||||
- `POST /auth/logout` - Logout
|
||||
|
||||
### Dashboard
|
||||
- `GET /dashboard` - Admin dashboard
|
||||
- `GET /dashboard/api/stats` - Dashboard statistics
|
||||
- `GET /dashboard/api/activities` - Recent activities
|
||||
|
||||
### Customers
|
||||
- `GET /customers` - List customers
|
||||
- `POST /customers` - Create customer
|
||||
- `GET /customers/:id` - Get customer details
|
||||
- `PUT /customers/:id` - Update customer
|
||||
- `DELETE /customers/:id` - Delete customer
|
||||
|
||||
### Plans
|
||||
- `GET /plans` - List plans
|
||||
- `POST /plans` - Create plan
|
||||
- `GET /plans/:id` - Get plan details
|
||||
- `PUT /plans/:id` - Update plan
|
||||
- `DELETE /plans/:id` - Delete plan
|
||||
|
||||
### Routers
|
||||
- `GET /routers` - List routers
|
||||
- `POST /routers` - Create router
|
||||
- `GET /routers/:id` - Get router details
|
||||
- `PUT /routers/:id` - Update router
|
||||
- `DELETE /routers/:id` - Delete router
|
||||
|
||||
## 🗂️ Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── config/
|
||||
│ └── database.js # Database configuration
|
||||
├── middleware/
|
||||
│ ├── auth.js # Authentication middleware
|
||||
│ ├── errorHandler.js # Error handling
|
||||
│ └── maintenance.js # Maintenance mode
|
||||
├── models/
|
||||
│ ├── Customer.js # Customer model
|
||||
│ ├── User.js # Admin user model
|
||||
│ ├── Plan.js # Plan model
|
||||
│ ├── Router.js # Router model
|
||||
│ ├── UserRecharge.js # Recharge model
|
||||
│ ├── AppConfig.js # Configuration model
|
||||
│ ├── Log.js # Activity log model
|
||||
│ └── index.js # Model associations
|
||||
├── routes/
|
||||
│ ├── auth.js # Authentication routes
|
||||
│ ├── dashboard.js # Dashboard routes
|
||||
│ ├── customers.js # Customer management
|
||||
│ ├── plans.js # Plan management
|
||||
│ ├── routers.js # Router management
|
||||
│ ├── admin.js # Admin management
|
||||
│ ├── orders.js # Order management
|
||||
│ ├── settings.js # System settings
|
||||
│ └── api.js # API routes
|
||||
├── utils/
|
||||
│ ├── auth.js # Authentication utilities
|
||||
│ └── setup.js # Setup utilities
|
||||
└── views/
|
||||
├── layouts/
|
||||
│ └── admin.ejs # Admin layout
|
||||
├── auth/
|
||||
│ └── admin-login.ejs
|
||||
├── dashboard/
|
||||
│ └── index.ejs
|
||||
└── errors/
|
||||
└── 500.ejs
|
||||
```
|
||||
|
||||
## 🔄 Migration from PHP Version
|
||||
|
||||
This project maintains compatibility with the original PHPNuxBill database structure. To migrate:
|
||||
|
||||
1. **Database**: Use the same MySQL database
|
||||
2. **Data**: All existing data will be preserved
|
||||
3. **Configuration**: Update settings in the new admin panel
|
||||
4. **Templates**: Customize the new EJS templates as needed
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run with coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Using PM2 (Recommended)
|
||||
|
||||
```bash
|
||||
# Install PM2 globally
|
||||
npm install -g pm2
|
||||
|
||||
# Start application
|
||||
pm2 start server.js --name phpnuxbill-js
|
||||
|
||||
# Save PM2 configuration
|
||||
pm2 save
|
||||
|
||||
# Setup auto-restart on boot
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
### Using Docker
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t phpnuxbill-js .
|
||||
|
||||
# Run container
|
||||
docker run -d -p 3000:3000 --name phpnuxbill-js phpnuxbill-js
|
||||
```
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
- **JWT Authentication**: Secure token-based authentication
|
||||
- **Password Hashing**: bcrypt for secure password storage
|
||||
- **Input Validation**: express-validator for input sanitization
|
||||
- **Rate Limiting**: Protection against brute force attacks
|
||||
- **Security Headers**: Helmet.js for security headers
|
||||
- **CORS**: Configurable cross-origin resource sharing
|
||||
- **SQL Injection Prevention**: Sequelize ORM with parameterized queries
|
||||
|
||||
## 📈 Performance
|
||||
|
||||
- **Connection Pooling**: Database connection pooling
|
||||
- **Caching**: In-memory caching for frequently accessed data
|
||||
- **Compression**: Gzip compression for responses
|
||||
- **Static File Serving**: Efficient static file serving
|
||||
- **Process Management**: PM2 for production process management
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Original PHPNuxBill project and contributors
|
||||
- Express.js and Sequelize communities
|
||||
- Bootstrap team for the UI components
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For support, please open an issue on GitHub or contact the maintainers.
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a complete rewrite of PHPNuxBill in JavaScript/Node.js. While maintaining database compatibility, the codebase has been modernized with current best practices.
|
18
composer.lock
generated
Normal file
18
composer.lock
generated
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "47030cdb33448cb9f8538ea124c01070",
|
||||
"packages": [],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
164
migrations/migrate.js
Normal file
164
migrations/migrate.js
Normal file
|
@ -0,0 +1,164 @@
|
|||
import sequelize from '../src/config/database.js';
|
||||
import { Customer, User, Plan, Router, UserRecharge, AppConfig, Log } from '../src/models/index.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
const migrate = async () => {
|
||||
try {
|
||||
console.log('🔄 Starting database migration...');
|
||||
|
||||
// Connect to database
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Database connection established');
|
||||
|
||||
// Sync all models
|
||||
await sequelize.sync({ force: false, alter: true });
|
||||
console.log('✅ Models synchronized');
|
||||
|
||||
// Create default admin user if not exists
|
||||
const adminExists = await User.findOne({ where: { username: 'admin' } });
|
||||
if (!adminExists) {
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||
await User.create({
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
fullname: 'Administrator',
|
||||
email: 'admin@phpnuxbill.com',
|
||||
user_type: 'SuperAdmin',
|
||||
status: 'Active'
|
||||
});
|
||||
console.log('✅ Default admin user created (username: admin, password: admin123)');
|
||||
}
|
||||
|
||||
// Create default app config if not exists
|
||||
const configs = [
|
||||
{ setting: 'CompanyName', value: 'PHPNuxBill-JS' },
|
||||
{ setting: 'address', value: 'Your Company Address' },
|
||||
{ setting: 'phone', value: '+1234567890' },
|
||||
{ setting: 'timezone', value: 'UTC' },
|
||||
{ setting: 'maintenance_mode', value: 'false' },
|
||||
{ setting: 'language', value: 'english' },
|
||||
{ setting: 'currency', value: 'USD' },
|
||||
{ setting: 'decimal_mark', value: '.' },
|
||||
{ setting: 'thousands_separator', value: ',' }
|
||||
];
|
||||
|
||||
for (const config of configs) {
|
||||
const exists = await AppConfig.findOne({ where: { setting: config.setting } });
|
||||
if (!exists) {
|
||||
await AppConfig.create(config);
|
||||
}
|
||||
}
|
||||
console.log('✅ Default configuration created');
|
||||
|
||||
// Create sample data if needed
|
||||
const customerCount = await Customer.count();
|
||||
if (customerCount === 0) {
|
||||
console.log('🔄 Creating sample data...');
|
||||
|
||||
// Create sample customers
|
||||
const sampleCustomers = [
|
||||
{
|
||||
username: 'customer1',
|
||||
password: await bcrypt.hash('password123', 10),
|
||||
fullname: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phone_number: '+1234567890',
|
||||
address: '123 Main St',
|
||||
city: 'New York',
|
||||
status: 'Active',
|
||||
balance: 50.00
|
||||
},
|
||||
{
|
||||
username: 'customer2',
|
||||
password: await bcrypt.hash('password123', 10),
|
||||
fullname: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
phone_number: '+1234567891',
|
||||
address: '456 Oak Ave',
|
||||
city: 'Los Angeles',
|
||||
status: 'Active',
|
||||
balance: 25.00
|
||||
}
|
||||
];
|
||||
|
||||
for (const customer of sampleCustomers) {
|
||||
await Customer.create(customer);
|
||||
}
|
||||
|
||||
// Create sample plans
|
||||
const samplePlans = [
|
||||
{
|
||||
name_plan: 'Basic Plan',
|
||||
bandwidth_name: '1M',
|
||||
type: 'Hotspot',
|
||||
type_plan: 'Limited',
|
||||
prepaid: 'yes',
|
||||
price: 10.00,
|
||||
validity: '30',
|
||||
validity_unit: 'Days',
|
||||
data_limit: 1000,
|
||||
data_unit: 'MB',
|
||||
time_limit: 0,
|
||||
time_unit: 'Hrs',
|
||||
enabled: true,
|
||||
description: 'Basic internet plan with 1GB data'
|
||||
},
|
||||
{
|
||||
name_plan: 'Premium Plan',
|
||||
bandwidth_name: '5M',
|
||||
type: 'Hotspot',
|
||||
type_plan: 'Unlimited',
|
||||
prepaid: 'yes',
|
||||
price: 25.00,
|
||||
validity: '30',
|
||||
validity_unit: 'Days',
|
||||
data_limit: 0,
|
||||
data_unit: 'MB',
|
||||
time_limit: 0,
|
||||
time_unit: 'Hrs',
|
||||
enabled: true,
|
||||
description: 'Premium unlimited internet plan'
|
||||
}
|
||||
];
|
||||
|
||||
for (const plan of samplePlans) {
|
||||
await Plan.create(plan);
|
||||
}
|
||||
|
||||
// Create sample router
|
||||
const sampleRouter = {
|
||||
name: 'Main Router',
|
||||
ip_address: '192.168.1.1',
|
||||
username: 'admin',
|
||||
password: await bcrypt.hash('admin123', 10),
|
||||
description: 'Main Mikrotik Router',
|
||||
enabled: true,
|
||||
coordinates: '0,0',
|
||||
coverage: 100
|
||||
};
|
||||
|
||||
await Router.create(sampleRouter);
|
||||
|
||||
console.log('✅ Sample data created');
|
||||
}
|
||||
|
||||
console.log('🎉 Migration completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Run migration if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
migrate().then(() => {
|
||||
console.log('✅ Database migration completed');
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default migrate;
|
54
package.json
Normal file
54
package.json
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "phpnuxbill-js",
|
||||
"version": "1.0.0",
|
||||
"description": "JavaScript port of PHPNuxBill - A Hotspot Billing Software",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"test": "jest",
|
||||
"build": "webpack --mode production",
|
||||
"migrate": "node migrations/migrate.js"
|
||||
},
|
||||
"keywords": [
|
||||
"hotspot",
|
||||
"billing",
|
||||
"mikrotik",
|
||||
"javascript",
|
||||
"nodejs"
|
||||
],
|
||||
"author": "Converted from PHPNuxBill",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"sequelize": "^6.32.1",
|
||||
"mysql2": "^3.5.2",
|
||||
"bcrypt": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.0.0",
|
||||
"express-rate-limit": "^6.8.1",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^7.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.4",
|
||||
"moment": "^2.29.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"axios": "^1.4.0",
|
||||
"crypto": "^1.0.1",
|
||||
"uuid": "^9.0.0",
|
||||
"ejs": "^3.1.9",
|
||||
"cookie-parser": "^1.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1",
|
||||
"jest": "^29.6.1",
|
||||
"webpack": "^5.88.1",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
146
server.js
Normal file
146
server.js
Normal file
|
@ -0,0 +1,146 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import session from 'express-session';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { connectDB } from './src/config/database.js';
|
||||
import { errorHandler } from './src/middleware/errorHandler.js';
|
||||
import { authMiddleware } from './src/middleware/auth.js';
|
||||
import { maintenanceMiddleware } from './src/middleware/maintenance.js';
|
||||
|
||||
// Routes
|
||||
import authRoutes from './src/routes/auth.js';
|
||||
import dashboardRoutes from './src/routes/dashboard.js';
|
||||
import customerRoutes from './src/routes/customers.js';
|
||||
import adminRoutes from './src/routes/admin.js';
|
||||
import routerRoutes from './src/routes/routers.js';
|
||||
import planRoutes from './src/routes/plans.js';
|
||||
import orderRoutes from './src/routes/orders.js';
|
||||
import settingsRoutes from './src/routes/settings.js';
|
||||
import apiRoutes from './src/routes/api.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production' ?
|
||||
process.env.ALLOWED_ORIGINS?.split(',') :
|
||||
['http://localhost:3000', 'http://localhost:3001'],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: (process.env.RATE_LIMIT_WINDOW || 15) * 60 * 1000, // 15 minutes
|
||||
max: process.env.RATE_LIMIT_MAX || 100,
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Body parsing middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Static files
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// Template engine
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'src/views'));
|
||||
|
||||
// Custom middleware
|
||||
app.use(maintenanceMiddleware);
|
||||
|
||||
// Routes
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/dashboard', authMiddleware, dashboardRoutes);
|
||||
app.use('/customers', authMiddleware, customerRoutes);
|
||||
app.use('/admin', authMiddleware, adminRoutes);
|
||||
app.use('/routers', authMiddleware, routerRoutes);
|
||||
app.use('/plans', authMiddleware, planRoutes);
|
||||
app.use('/orders', authMiddleware, orderRoutes);
|
||||
app.use('/settings', authMiddleware, settingsRoutes);
|
||||
app.use('/api', apiRoutes);
|
||||
|
||||
// Default route
|
||||
app.get('/', (req, res) => {
|
||||
if (req.session.user) {
|
||||
if (req.session.user.userType === 'admin') {
|
||||
return res.redirect('/dashboard');
|
||||
} else {
|
||||
return res.redirect('/home');
|
||||
}
|
||||
}
|
||||
res.redirect('/auth/login');
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use(errorHandler);
|
||||
|
||||
// 404 handler
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).render('errors/404', {
|
||||
title: 'Page Not Found',
|
||||
message: 'The requested page could not be found.'
|
||||
});
|
||||
});
|
||||
|
||||
// Connect to database and start server
|
||||
async function startServer() {
|
||||
try {
|
||||
await connectDB();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 PHPNuxBill-JS Server running on port ${PORT}`);
|
||||
console.log(`📊 Dashboard: http://localhost:${PORT}/dashboard`);
|
||||
console.log(`🔐 Admin: http://localhost:${PORT}/admin`);
|
||||
console.log(`🌐 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`💾 Database: ${process.env.DB_NAME}@${process.env.DB_HOST}:${process.env.DB_PORT}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT received, shutting down gracefully');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
export default app;
|
46
src/config/database.js
Normal file
46
src/config/database.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Sequelize } from 'sequelize';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
database: process.env.DB_NAME || 'phpnuxbill',
|
||||
username: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
dialect: 'mysql',
|
||||
logging: process.env.NODE_ENV === 'development' ? console.log : false,
|
||||
define: {
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
},
|
||||
pool: {
|
||||
max: 10,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
}
|
||||
});
|
||||
|
||||
export const connectDB = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Database connection established successfully');
|
||||
|
||||
// Sync models in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
await sequelize.sync({ alter: true });
|
||||
console.log('📊 Database models synchronized');
|
||||
}
|
||||
|
||||
return sequelize;
|
||||
} catch (error) {
|
||||
console.error('❌ Unable to connect to database:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default sequelize;
|
59
src/middleware/auth.js
Normal file
59
src/middleware/auth.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import jwt from 'jsonwebtoken';
|
||||
import { User, Customer } from '../models/index.js';
|
||||
|
||||
export const authMiddleware = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '') || req.cookies.token;
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Access denied. No token provided.' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
let user;
|
||||
if (decoded.userType === 'admin') {
|
||||
user = await User.findByPk(decoded.userId);
|
||||
} else {
|
||||
user = await Customer.findByPk(decoded.userId);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: 'Invalid token.' });
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
req.userType = decoded.userType;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: 'Invalid token.' });
|
||||
}
|
||||
};
|
||||
|
||||
export const adminOnly = (req, res, next) => {
|
||||
if (req.userType !== 'admin') {
|
||||
return res.status(403).json({ message: 'Access denied. Admin only.' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
export const superAdminOnly = (req, res, next) => {
|
||||
if (req.userType !== 'admin' || req.user.user_type !== 'SuperAdmin') {
|
||||
return res.status(403).json({ message: 'Access denied. SuperAdmin only.' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
export const checkPermission = (allowedTypes) => {
|
||||
return (req, res, next) => {
|
||||
if (req.userType === 'admin' && allowedTypes.includes(req.user.user_type)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.userType === 'customer' && allowedTypes.includes('Customer')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(403).json({ message: 'Access denied. Insufficient permissions.' });
|
||||
};
|
||||
};
|
59
src/middleware/errorHandler.js
Normal file
59
src/middleware/errorHandler.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Log } from '../models/index.js';
|
||||
|
||||
export const errorHandler = (err, req, res, next) => {
|
||||
// Log the error
|
||||
console.error(err.stack);
|
||||
|
||||
// Log to database
|
||||
Log.create({
|
||||
type: 'error',
|
||||
description: `${err.message} - ${req.method} ${req.path}`,
|
||||
userid: req.user?.id || 0,
|
||||
ip: req.ip || req.connection.remoteAddress
|
||||
}).catch(dbErr => {
|
||||
console.error('Error logging to database:', dbErr);
|
||||
});
|
||||
|
||||
// Default to 500 server error
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// Mongoose bad ObjectId
|
||||
if (err.name === 'CastError') {
|
||||
const message = 'Resource not found';
|
||||
error = { message, statusCode: 404 };
|
||||
}
|
||||
|
||||
// Mongoose duplicate key
|
||||
if (err.code === 11000) {
|
||||
const message = 'Duplicate field value entered';
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const message = Object.values(err.errors).map(val => val.message);
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// Sequelize validation error
|
||||
if (err.name === 'SequelizeValidationError') {
|
||||
const message = err.errors.map(e => e.message).join(', ');
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// Sequelize unique constraint error
|
||||
if (err.name === 'SequelizeUniqueConstraintError') {
|
||||
const message = 'Duplicate entry';
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
const statusCode = error.statusCode || 500;
|
||||
const message = error.message || 'Server Error';
|
||||
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: message,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
});
|
||||
};
|
27
src/middleware/maintenance.js
Normal file
27
src/middleware/maintenance.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { AppConfig } from '../models/index.js';
|
||||
|
||||
export const maintenanceMiddleware = async (req, res, next) => {
|
||||
try {
|
||||
// Skip maintenance check for certain paths
|
||||
const excludedPaths = ['/api/health', '/maintenance'];
|
||||
if (excludedPaths.includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const maintenanceMode = await AppConfig.findOne({
|
||||
where: { setting: 'maintenance_mode' }
|
||||
});
|
||||
|
||||
if (maintenanceMode && maintenanceMode.value === '1') {
|
||||
return res.status(503).render('maintenance', {
|
||||
title: 'System Maintenance',
|
||||
message: 'System is currently under maintenance. Please try again later.'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Maintenance middleware error:', error);
|
||||
next();
|
||||
}
|
||||
};
|
28
src/models/AppConfig.js
Normal file
28
src/models/AppConfig.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../config/database.js';
|
||||
|
||||
const AppConfig = sequelize.define('AppConfig', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
setting: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'tbl_appconfig',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['setting']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default AppConfig;
|
105
src/models/Customer.js
Normal file
105
src/models/Customer.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../config/database.js';
|
||||
|
||||
const Customer = sequelize.define('Customer', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
fullname: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
phone_number: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
city: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
district: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
state: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
zip: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: true
|
||||
},
|
||||
coordinates: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true
|
||||
},
|
||||
service_type: {
|
||||
type: DataTypes.ENUM('Hotspot', 'PPPOE', 'Both'),
|
||||
defaultValue: 'Hotspot'
|
||||
},
|
||||
account_type: {
|
||||
type: DataTypes.ENUM('Personal', 'Business'),
|
||||
defaultValue: 'Personal'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('Active', 'Inactive', 'Suspended'),
|
||||
defaultValue: 'Active'
|
||||
},
|
||||
balance: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
defaultValue: 0.00
|
||||
},
|
||||
last_login: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
pppoe_username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
pppoe_password: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
pppoe_ip: {
|
||||
type: DataTypes.STRING(15),
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'tbl_customers',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['username']
|
||||
},
|
||||
{
|
||||
fields: ['email']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default Customer;
|
47
src/models/Log.js
Normal file
47
src/models/Log.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../config/database.js';
|
||||
|
||||
const Log = sequelize.define('Log', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
userid: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
ip: {
|
||||
type: DataTypes.STRING(45),
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'tbl_logs',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['date']
|
||||
},
|
||||
{
|
||||
fields: ['type']
|
||||
},
|
||||
{
|
||||
fields: ['userid']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default Log;
|
92
src/models/Plan.js
Normal file
92
src/models/Plan.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../config/database.js';
|
||||
|
||||
const Plan = sequelize.define('Plan', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name_plan: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
bandwidth_name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('Hotspot', 'PPPOE'),
|
||||
defaultValue: 'Hotspot'
|
||||
},
|
||||
type_plan: {
|
||||
type: DataTypes.ENUM('Limited', 'Unlimited'),
|
||||
defaultValue: 'Limited'
|
||||
},
|
||||
prepaid: {
|
||||
type: DataTypes.ENUM('yes', 'no'),
|
||||
defaultValue: 'yes'
|
||||
},
|
||||
price: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
},
|
||||
validity: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: false
|
||||
},
|
||||
validity_unit: {
|
||||
type: DataTypes.ENUM('Hrs', 'Days', 'Months'),
|
||||
defaultValue: 'Days'
|
||||
},
|
||||
data_limit: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true
|
||||
},
|
||||
time_limit: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true
|
||||
},
|
||||
routers: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
pool_name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
is_radius: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
plan_type: {
|
||||
type: DataTypes.ENUM('Personal', 'Business'),
|
||||
defaultValue: 'Personal'
|
||||
},
|
||||
device: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'tbl_plans',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['name_plan']
|
||||
},
|
||||
{
|
||||
fields: ['type']
|
||||
},
|
||||
{
|
||||
fields: ['enabled']
|
||||
},
|
||||
{
|
||||
fields: ['routers']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default Plan;
|
61
src/models/Router.js
Normal file
61
src/models/Router.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../config/database.js';
|
||||
|
||||
const Router = sequelize.define('Router', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
ip_address: {
|
||||
type: DataTypes.STRING(15),
|
||||
allowNull: false,
|
||||
validate: {
|
||||
isIP: true
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
coordinates: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true
|
||||
},
|
||||
coverage: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 100
|
||||
}
|
||||
}, {
|
||||
tableName: 'tbl_routers',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['name']
|
||||
},
|
||||
{
|
||||
fields: ['ip_address']
|
||||
},
|
||||
{
|
||||
fields: ['enabled']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default Router;
|
81
src/models/User.js
Normal file
81
src/models/User.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../config/database.js';
|
||||
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
fullname: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true
|
||||
},
|
||||
user_type: {
|
||||
type: DataTypes.ENUM('SuperAdmin', 'Admin', 'Agent', 'Sales', 'Report'),
|
||||
defaultValue: 'Agent'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('Active', 'Inactive'),
|
||||
defaultValue: 'Active'
|
||||
},
|
||||
last_login: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
city: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
subdistrict: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
ward: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
root: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'tbl_users',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
tableName: 'tbl_users',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['username']
|
||||
},
|
||||
{
|
||||
fields: ['user_type']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default User;
|
100
src/models/UserRecharge.js
Normal file
100
src/models/UserRecharge.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../config/database.js';
|
||||
|
||||
const UserRecharge = sequelize.define('UserRecharge', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
customer_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'tbl_customers',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
plan_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'tbl_plans',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
plan_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
bandwidth_name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('Hotspot', 'PPPOE'),
|
||||
defaultValue: 'Hotspot'
|
||||
},
|
||||
routers: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
recharged_on: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
recharged_time: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false
|
||||
},
|
||||
expiration: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
time_limit: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true
|
||||
},
|
||||
data_limit: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('on', 'off'),
|
||||
defaultValue: 'on'
|
||||
},
|
||||
method: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
prepaid: {
|
||||
type: DataTypes.ENUM('yes', 'no'),
|
||||
defaultValue: 'yes'
|
||||
}
|
||||
}, {
|
||||
tableName: 'tbl_user_recharges',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['customer_id']
|
||||
},
|
||||
{
|
||||
fields: ['username']
|
||||
},
|
||||
{
|
||||
fields: ['plan_id']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
},
|
||||
{
|
||||
fields: ['expiration']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default UserRecharge;
|
27
src/models/index.js
Normal file
27
src/models/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Customer from './Customer.js';
|
||||
import User from './User.js';
|
||||
import Router from './Router.js';
|
||||
import Plan from './Plan.js';
|
||||
import UserRecharge from './UserRecharge.js';
|
||||
import AppConfig from './AppConfig.js';
|
||||
import Log from './Log.js';
|
||||
|
||||
// Define associations
|
||||
Customer.hasMany(UserRecharge, { foreignKey: 'customer_id' });
|
||||
UserRecharge.belongsTo(Customer, { foreignKey: 'customer_id' });
|
||||
|
||||
Plan.hasMany(UserRecharge, { foreignKey: 'plan_id' });
|
||||
UserRecharge.belongsTo(Plan, { foreignKey: 'plan_id' });
|
||||
|
||||
User.hasMany(User, { as: 'subordinates', foreignKey: 'root' });
|
||||
User.belongsTo(User, { as: 'supervisor', foreignKey: 'root' });
|
||||
|
||||
export {
|
||||
Customer,
|
||||
User,
|
||||
Router,
|
||||
Plan,
|
||||
UserRecharge,
|
||||
AppConfig,
|
||||
Log
|
||||
};
|
29
src/routes/admin.js
Normal file
29
src/routes/admin.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import express from 'express';
|
||||
import { User } from '../models/index.js';
|
||||
import { superAdminOnly } from '../middleware/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Admin management placeholder
|
||||
router.get('/', superAdminOnly, async (req, res) => {
|
||||
try {
|
||||
const users = await User.findAll({
|
||||
order: [['created_at', 'DESC']],
|
||||
attributes: ['id', 'username', 'fullname', 'email', 'user_type', 'status', 'last_login']
|
||||
});
|
||||
|
||||
res.render('admin/users', {
|
||||
title: 'Admin Users',
|
||||
users
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Admin users error:', error);
|
||||
res.status(500).render('errors/500', {
|
||||
title: 'Server Error',
|
||||
message: 'Failed to load admin users'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
279
src/routes/auth.js
Normal file
279
src/routes/auth.js
Normal file
|
@ -0,0 +1,279 @@
|
|||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { User, Customer, Log } from '../models/index.js';
|
||||
import { generateToken, getClientIp } from '../utils/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Admin Login Page
|
||||
router.get('/admin/login', (req, res) => {
|
||||
res.render('auth/admin-login', {
|
||||
title: 'Admin Login',
|
||||
error: req.query.error
|
||||
});
|
||||
});
|
||||
|
||||
// Admin Login
|
||||
router.post('/admin/login', [
|
||||
body('username').notEmpty().trim().escape(),
|
||||
body('password').notEmpty()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Find admin user
|
||||
const admin = await User.findOne({ where: { username } });
|
||||
if (!admin) {
|
||||
await Log.create({
|
||||
type: 'admin_login',
|
||||
description: `Failed login attempt - username: ${username}`,
|
||||
userid: 0,
|
||||
ip: getClientIp(req)
|
||||
});
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid username or password'
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isValidPassword = await bcrypt.compare(password, admin.password);
|
||||
if (!isValidPassword) {
|
||||
await Log.create({
|
||||
type: 'admin_login',
|
||||
description: `Failed login attempt - username: ${username}`,
|
||||
userid: admin.id,
|
||||
ip: getClientIp(req)
|
||||
});
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid username or password'
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await admin.update({ last_login: new Date() });
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken(admin.id, 'admin');
|
||||
|
||||
// Log successful login
|
||||
await Log.create({
|
||||
type: 'admin_login',
|
||||
description: `Login successful - username: ${username}`,
|
||||
userid: admin.id,
|
||||
ip: getClientIp(req)
|
||||
});
|
||||
|
||||
// Set cookie
|
||||
res.cookie('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
fullname: admin.fullname,
|
||||
user_type: admin.user_type,
|
||||
email: admin.email
|
||||
},
|
||||
token
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Admin login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Customer Login
|
||||
router.post('/customer/login', [
|
||||
body('username').notEmpty().trim().escape(),
|
||||
body('password').notEmpty()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Find customer
|
||||
const customer = await Customer.findOne({ where: { username } });
|
||||
if (!customer) {
|
||||
await Log.create({
|
||||
type: 'customer_login',
|
||||
description: `Failed login attempt - username: ${username}`,
|
||||
userid: 0,
|
||||
ip: getClientIp(req)
|
||||
});
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid username or password'
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isValidPassword = await bcrypt.compare(password, customer.password);
|
||||
if (!isValidPassword) {
|
||||
await Log.create({
|
||||
type: 'customer_login',
|
||||
description: `Failed login attempt - username: ${username}`,
|
||||
userid: customer.id,
|
||||
ip: getClientIp(req)
|
||||
});
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid username or password'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if customer is active
|
||||
if (customer.status !== 'Active') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: `Account status: ${customer.status}`
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await customer.update({ last_login: new Date() });
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken(customer.id, 'customer');
|
||||
|
||||
// Log successful login
|
||||
await Log.create({
|
||||
type: 'customer_login',
|
||||
description: `Login successful - username: ${username}`,
|
||||
userid: customer.id,
|
||||
ip: getClientIp(req)
|
||||
});
|
||||
|
||||
// Set cookie
|
||||
res.cookie('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
id: customer.id,
|
||||
username: customer.username,
|
||||
fullname: customer.fullname,
|
||||
email: customer.email,
|
||||
status: customer.status,
|
||||
balance: customer.balance
|
||||
},
|
||||
token
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Customer login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Logout
|
||||
router.post('/logout', (req, res) => {
|
||||
res.clearCookie('token');
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logout successful'
|
||||
});
|
||||
});
|
||||
|
||||
// Get current user
|
||||
router.get('/me', async (req, res) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '') || req.cookies.token;
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'No token provided'
|
||||
});
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
let user;
|
||||
if (decoded.userType === 'admin') {
|
||||
user = await User.findByPk(decoded.userId, {
|
||||
attributes: ['id', 'username', 'fullname', 'email', 'user_type', 'status']
|
||||
});
|
||||
} else {
|
||||
user = await Customer.findByPk(decoded.userId, {
|
||||
attributes: ['id', 'username', 'fullname', 'email', 'status', 'balance']
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'User not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
...user.toJSON(),
|
||||
userType: decoded.userType
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid token'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Login page
|
||||
router.get('/login', (req, res) => {
|
||||
res.render('auth/login', {
|
||||
title: 'Login - PHPNuxBill'
|
||||
});
|
||||
});
|
||||
|
||||
// Admin login page
|
||||
router.get('/admin', (req, res) => {
|
||||
res.render('auth/admin-login', {
|
||||
title: 'Admin Login - PHPNuxBill'
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
224
src/routes/customers.js
Normal file
224
src/routes/customers.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
import express from 'express';
|
||||
import { Op } from 'sequelize';
|
||||
import { Customer, UserRecharge, Plan } from '../models/index.js';
|
||||
import { adminOnly, checkPermission } from '../middleware/auth.js';
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { hashPassword } from '../utils/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// List customers
|
||||
router.get('/', adminOnly, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, search = '' } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const whereClause = search ? {
|
||||
[Op.or]: [
|
||||
{ username: { [Op.like]: `%${search}%` } },
|
||||
{ fullname: { [Op.like]: `%${search}%` } },
|
||||
{ email: { [Op.like]: `%${search}%` } }
|
||||
]
|
||||
} : {};
|
||||
|
||||
const { count, rows: customers } = await Customer.findAndCountAll({
|
||||
where: whereClause,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.render('customers/list', {
|
||||
title: 'Customers',
|
||||
customers,
|
||||
pagination: {
|
||||
currentPage: parseInt(page),
|
||||
totalPages: Math.ceil(count / limit),
|
||||
totalItems: count,
|
||||
limit: parseInt(limit)
|
||||
},
|
||||
search
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Customer list error:', error);
|
||||
res.status(500).render('errors/500', {
|
||||
title: 'Server Error',
|
||||
message: 'Failed to load customers'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add customer form
|
||||
router.get('/add', checkPermission(['SuperAdmin', 'Admin', 'Agent', 'Sales']), (req, res) => {
|
||||
res.render('customers/add', {
|
||||
title: 'Add Customer'
|
||||
});
|
||||
});
|
||||
|
||||
// Create customer
|
||||
router.post('/', [
|
||||
body('username').notEmpty().trim().escape(),
|
||||
body('fullname').notEmpty().trim().escape(),
|
||||
body('password').isLength({ min: 6 }),
|
||||
body('email').optional().isEmail().normalizeEmail()
|
||||
], checkPermission(['SuperAdmin', 'Admin', 'Agent', 'Sales']), async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { username, fullname, password, email, phone_number, address, service_type, account_type } = req.body;
|
||||
|
||||
// Check if username already exists
|
||||
const existingCustomer = await Customer.findOne({ where: { username } });
|
||||
if (existingCustomer) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Username already exists'
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
// Create customer
|
||||
const customer = await Customer.create({
|
||||
username,
|
||||
fullname,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
phone_number,
|
||||
address,
|
||||
service_type: service_type || 'Hotspot',
|
||||
account_type: account_type || 'Personal'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Customer created successfully',
|
||||
customer: {
|
||||
id: customer.id,
|
||||
username: customer.username,
|
||||
fullname: customer.fullname
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Create customer error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create customer'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// View customer details
|
||||
router.get('/:id', adminOnly, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const customer = await Customer.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: UserRecharge,
|
||||
include: [{ model: Plan }],
|
||||
order: [['created_at', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
return res.status(404).render('errors/404', {
|
||||
title: 'Customer Not Found'
|
||||
});
|
||||
}
|
||||
|
||||
res.render('customers/view', {
|
||||
title: `Customer - ${customer.fullname}`,
|
||||
customer
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('View customer error:', error);
|
||||
res.status(500).render('errors/500', {
|
||||
title: 'Server Error',
|
||||
message: 'Failed to load customer details'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Edit customer
|
||||
router.put('/:id', [
|
||||
body('fullname').notEmpty().trim().escape(),
|
||||
body('email').optional().isEmail().normalizeEmail()
|
||||
], checkPermission(['SuperAdmin', 'Admin']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const customer = await Customer.findByPk(id);
|
||||
if (!customer) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Customer not found'
|
||||
});
|
||||
}
|
||||
|
||||
await customer.update(req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Customer updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Update customer error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update customer'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete customer
|
||||
router.delete('/:id', checkPermission(['SuperAdmin', 'Admin']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const customer = await Customer.findByPk(id);
|
||||
if (!customer) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Customer not found'
|
||||
});
|
||||
}
|
||||
|
||||
await customer.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Customer deleted successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Delete customer error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to delete customer'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
0
src/routes/dashboard.js
Normal file
0
src/routes/dashboard.js
Normal file
28
src/routes/routers.js
Normal file
28
src/routes/routers.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import express from 'express';
|
||||
import { Router } from '../models/index.js';
|
||||
import { adminOnly } from '../middleware/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// List routers
|
||||
router.get('/', adminOnly, async (req, res) => {
|
||||
try {
|
||||
const routers = await Router.findAll({
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.render('routers/list', {
|
||||
title: 'Routers',
|
||||
routers
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Routers list error:', error);
|
||||
res.status(500).render('errors/500', {
|
||||
title: 'Server Error',
|
||||
message: 'Failed to load routers'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
28
src/utils/auth.js
Normal file
28
src/utils/auth.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export const generateToken = (userId, userType) => {
|
||||
return jwt.sign(
|
||||
{ userId, userType },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
};
|
||||
|
||||
export const getClientIp = (req) => {
|
||||
return req.headers['cf-connecting-ip'] ||
|
||||
req.headers['x-forwarded-for'] ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection.remoteAddress ||
|
||||
req.socket.remoteAddress ||
|
||||
(req.connection.socket ? req.connection.socket.remoteAddress : null);
|
||||
};
|
||||
|
||||
export const hashPassword = async (password) => {
|
||||
const bcrypt = await import('bcrypt');
|
||||
return bcrypt.hash(password, parseInt(process.env.BCRYPT_ROUNDS) || 10);
|
||||
};
|
||||
|
||||
export const verifyPassword = async (password, hashedPassword) => {
|
||||
const bcrypt = await import('bcrypt');
|
||||
return bcrypt.compare(password, hashedPassword);
|
||||
};
|
64
src/utils/setup.js
Normal file
64
src/utils/setup.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Create necessary directories
|
||||
const createDirectories = () => {
|
||||
const directories = [
|
||||
'logs',
|
||||
'uploads',
|
||||
'public',
|
||||
'src/views',
|
||||
'src/views/admin',
|
||||
'src/views/customer',
|
||||
'src/views/auth',
|
||||
'src/views/errors',
|
||||
'src/views/layouts',
|
||||
'src/views/partials'
|
||||
];
|
||||
|
||||
directories.forEach(dir => {
|
||||
const dirPath = path.join(__dirname, dir);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`✅ Created directory: ${dir}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Create a basic logger
|
||||
const createLogger = () => {
|
||||
const logPath = path.join(__dirname, 'logs');
|
||||
if (!fs.existsSync(logPath)) {
|
||||
fs.mkdirSync(logPath, { recursive: true });
|
||||
}
|
||||
|
||||
const logFile = path.join(logPath, 'app.log');
|
||||
if (!fs.existsSync(logFile)) {
|
||||
fs.writeFileSync(logFile, `PHPNuxBill-JS Application Log\nStarted: ${new Date().toISOString()}\n\n`);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup function
|
||||
const setup = () => {
|
||||
console.log('🚀 Setting up PHPNuxBill-JS...');
|
||||
|
||||
createDirectories();
|
||||
createLogger();
|
||||
|
||||
console.log('✅ Setup completed successfully!');
|
||||
console.log('🔧 Next steps:');
|
||||
console.log(' 1. Configure your .env file');
|
||||
console.log(' 2. Set up your MySQL database');
|
||||
console.log(' 3. Run: npm run migrate (if needed)');
|
||||
console.log(' 4. Run: npm start');
|
||||
};
|
||||
|
||||
export default setup;
|
||||
|
||||
// Run setup if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
setup();
|
||||
}
|
132
src/views/auth/admin-login.ejs
Normal file
132
src/views/auth/admin-login.ejs
Normal file
|
@ -0,0 +1,132 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - PHPNuxBill</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #eee;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 12px 30px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #eee;
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
.input-group .form-control {
|
||||
border-left: none;
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<h2><i class="fas fa-wifi"></i> PHPNuxBill</h2>
|
||||
<p>Admin Login</p>
|
||||
</div>
|
||||
|
||||
<% if (typeof error !== 'undefined' && error) { %>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<%= error %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/auth/admin/login">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-user"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control" name="username" placeholder="Username" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
<input type="password" class="form-control" name="password" placeholder="Password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-login">
|
||||
<i class="fas fa-sign-in-alt"></i> Login
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<a href="/customer/login" class="text-decoration-none">
|
||||
<i class="fas fa-users"></i> Customer Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
173
src/views/dashboard/index.ejs
Normal file
173
src/views/dashboard/index.ejs
Normal file
|
@ -0,0 +1,173 @@
|
|||
<% layout('layouts/admin') -%>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2><i class="fas fa-tachometer-alt"></i> Dashboard</h2>
|
||||
<p class="text-muted">Welcome back, <%= user.fullname %>!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3><%= stats.totalCustomers || 0 %></h3>
|
||||
<p>Total Customers</p>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-users fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3><%= stats.activeCustomers || 0 %></h3>
|
||||
<p>Active Customers</p>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-user-check fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3><%= stats.totalPlans || 0 %></h3>
|
||||
<p>Total Plans</p>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-list fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3>$<%= stats.monthlyRevenue || 0 %></h3>
|
||||
<p>Monthly Revenue</p>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-dollar-sign fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-line"></i> Recent Activity</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (recentLogs && recentLogs.length > 0) { %>
|
||||
<div class="list-group list-group-flush">
|
||||
<% recentLogs.forEach(log => { %>
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span><%= log.description %></span>
|
||||
<small class="text-muted"><%= new Date(log.date).toLocaleString() %></small>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<p class="text-muted">No recent activity</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-pie"></i> System Status</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="progress mb-2">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: 85%"></div>
|
||||
</div>
|
||||
<small>CPU Usage: 15%</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="progress mb-2">
|
||||
<div class="progress-bar bg-info" role="progressbar" style="width: 60%"></div>
|
||||
</div>
|
||||
<small>Memory: 40%</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Database Status:</span>
|
||||
<span class="badge bg-success">Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-clock"></i> Quick Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<a href="/customers/create" class="btn btn-primary btn-sm d-block">
|
||||
<i class="fas fa-user-plus"></i> Add Customer
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/plans/create" class="btn btn-success btn-sm d-block">
|
||||
<i class="fas fa-plus"></i> Add Plan
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/routers/create" class="btn btn-info btn-sm d-block">
|
||||
<i class="fas fa-router"></i> Add Router
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/orders" class="btn btn-warning btn-sm d-block">
|
||||
<i class="fas fa-shopping-cart"></i> View Orders
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/settings" class="btn btn-secondary btn-sm d-block">
|
||||
<i class="fas fa-cog"></i> Settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/dashboard?refresh=1" class="btn btn-outline-primary btn-sm d-block">
|
||||
<i class="fas fa-sync"></i> Refresh
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
58
src/views/errors/500.ejs
Normal file
58
src/views/errors/500.ejs
Normal file
|
@ -0,0 +1,58 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>500 - Server Error</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.error-code {
|
||||
font-size: 6rem;
|
||||
font-weight: bold;
|
||||
color: #dc3545;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.error-message {
|
||||
font-size: 1.5rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.error-description {
|
||||
color: #868e96;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-code">500</div>
|
||||
<div class="error-message">Server Error</div>
|
||||
<div class="error-description">
|
||||
<%= message || 'An internal server error occurred. Please try again later.' %>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="javascript:history.back()" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left"></i> Go Back
|
||||
</a>
|
||||
<a href="/dashboard" class="btn btn-primary">
|
||||
<i class="fas fa-home"></i> Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
167
src/views/layouts/admin.ejs
Normal file
167
src/views/layouts/admin.ejs
Normal file
|
@ -0,0 +1,167 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %> - PHPNuxBill</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #28a745;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #17a2b8;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #343a40;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
min-height: 100vh;
|
||||
background-color: var(--dark-color);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #fff;
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
margin-bottom: 0;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.stats-card p {
|
||||
margin-bottom: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/dashboard">
|
||||
<i class="fas fa-wifi"></i> PHPNuxBill
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user"></i> <%= user?.fullname || 'Admin' %>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/profile"><i class="fas fa-user-edit"></i> Profile</a></li>
|
||||
<li><a class="dropdown-item" href="/settings"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/auth/logout"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-2 sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= page === 'dashboard' ? 'active' : '' %>" href="/dashboard">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= page === 'customers' ? 'active' : '' %>" href="/customers">
|
||||
<i class="fas fa-users"></i> Customers
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= page === 'plans' ? 'active' : '' %>" href="/plans">
|
||||
<i class="fas fa-list"></i> Plans
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= page === 'routers' ? 'active' : '' %>" href="/routers">
|
||||
<i class="fas fa-router"></i> Routers
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= page === 'orders' ? 'active' : '' %>" href="/orders">
|
||||
<i class="fas fa-shopping-cart"></i> Orders
|
||||
</a>
|
||||
</li>
|
||||
<% if (user?.user_type === 'SuperAdmin') { %>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= page === 'admin' ? 'active' : '' %>" href="/admin">
|
||||
<i class="fas fa-user-shield"></i> Admin Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= page === 'settings' ? 'active' : '' %>" href="/settings">
|
||||
<i class="fas fa-cog"></i> Settings
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9 col-lg-10 main-content">
|
||||
<% if (typeof notify !== 'undefined' && notify) { %>
|
||||
<div class="alert alert-<%= notify_type || 'info' %> alert-dismissible fade show" role="alert">
|
||||
<%= notify %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<%- body %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
// Auto-hide alerts
|
||||
setTimeout(() => {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(alert => {
|
||||
if (alert.classList.contains('show')) {
|
||||
alert.classList.remove('show');
|
||||
setTimeout(() => alert.remove(), 300);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
25
vendor/autoload.php
vendored
Normal file
25
vendor/autoload.php
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit30a387e055924f9526a984befe14a595::getLoader();
|
579
vendor/composer/ClassLoader.php
vendored
Normal file
579
vendor/composer/ClassLoader.php
vendored
Normal file
|
@ -0,0 +1,579 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
359
vendor/composer/InstalledVersions.php
vendored
Normal file
359
vendor/composer/InstalledVersions.php
vendored
Normal file
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array()) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
21
vendor/composer/LICENSE
vendored
Normal file
21
vendor/composer/LICENSE
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
10
vendor/composer/autoload_classmap.php
vendored
Normal file
10
vendor/composer/autoload_classmap.php
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
);
|
9
vendor/composer/autoload_namespaces.php
vendored
Normal file
9
vendor/composer/autoload_namespaces.php
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
9
vendor/composer/autoload_psr4.php
vendored
Normal file
9
vendor/composer/autoload_psr4.php
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
36
vendor/composer/autoload_real.php
vendored
Normal file
36
vendor/composer/autoload_real.php
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit30a387e055924f9526a984befe14a595
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit30a387e055924f9526a984befe14a595', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit30a387e055924f9526a984befe14a595', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit30a387e055924f9526a984befe14a595::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
20
vendor/composer/autoload_static.php
vendored
Normal file
20
vendor/composer/autoload_static.php
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit30a387e055924f9526a984befe14a595
|
||||
{
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->classMap = ComposerStaticInit30a387e055924f9526a984befe14a595::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
5
vendor/composer/installed.json
vendored
Normal file
5
vendor/composer/installed.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"packages": [],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
23
vendor/composer/installed.php
vendored
Normal file
23
vendor/composer/installed.php
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'hotspotbilling/phpnuxbill',
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '4a441c576349b3ecaacdb924a46a18982bb54349',
|
||||
'type' => 'template',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'hotspotbilling/phpnuxbill' => array(
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '4a441c576349b3ecaacdb924a46a18982bb54349',
|
||||
'type' => 'template',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue