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:
Robby Aprianto 2025-07-12 04:03:51 +00:00
parent 4a441c5763
commit 0f192b39af
41 changed files with 3741 additions and 0 deletions

45
.env Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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.' });
};
};

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

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

28
src/routes/routers.js Normal file
View 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
View 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
View 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();
}

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

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

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

@ -0,0 +1,5 @@
{
"packages": [],
"dev": true,
"dev-package-names": []
}

23
vendor/composer/installed.php vendored Normal file
View 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,
),
),
);