Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# GSM Theme Clone (PHP 8.1+, No Composer)

A lightweight PHP MVC web app with Bootstrap 5 for admin and user panels, and a web-based installer at `/install`.

## Requirements
- PHP 8.1+
- MySQL 5.7+/MariaDB 10+
- Extensions: mysqli, pdo_mysql, curl, openssl, json, mbstring, zip, gd, fileinfo
- Apache with mod_rewrite (for pretty URLs)

## Quick Install on cPanel
1. Upload the ZIP to your domain root (e.g., `public_html`).
2. Extract the ZIP.
3. Ensure `public/.htaccess` is present and your document root points to `public` (or move contents of `public` to the root if needed).
4. Visit `https://yourdomain.com/install`.
5. Follow the wizard:
- Environment check
- License agreement
- Database credentials
- Site name, URL, admin email & password
- Auto-generate `config.php`
- Import `database.sql`
- Create admin user
- Auto-rename `/install`

If you skip the installer, default admin is `admin@example.com / Password@123` (update after first login). Copy `config.php.sample` to `config.php` and edit.

## Cron Jobs (cPanel > Cron Jobs)
Use the following examples, adjusting paths and PHP binary as needed.

- Hourly API sync:
```
/usr/bin/php -q /home/USER/public_html/cron/sync_apis.php
```
- Every 2 min order updates:
```
/usr/bin/php -q /home/USER/public_html/cron/update_orders.php
```

Configure Dhru-like API paths and keys in Admin > Settings > Settings. Default keys assume responses like `{ "order_id": "123", "status": "processing", "result": {...} }` and services under `data`. Adjust if your provider uses different keys.
- Daily subscription checks:
```
/usr/bin/php -q /home/USER/public_html/cron/subscriptions_check.php
```
- Hourly failed job alerts:
```
/usr/bin/php -q /home/USER/public_html/cron/failed_jobs_alert.php
```

Alternatively, you can trigger via HTTP with a secret:
```
https://yourdomain.com/cron/sync_apis.php?secret=YOUR_CRON_SECRET
```

## SMTP (PHPMailer)
Place PHPMailer sources under `lib/PHPMailer/src/{PHPMailer.php,SMTP.php,Exception.php}` to enable SMTP. Otherwise the app will fallback to PHP `mail()`.

## Security
- CSRF tokens on all POST forms
- Prepared statements via PDO
- Basic XSS escaping via `View::e()`
- Optional TOTP 2FA foundation

## Development
- No Composer required
- MVC structure under `app/`
- Routes in `app/routes.php`

## License
See `license.txt`. This project is provided as-is.
41 changes: 41 additions & 0 deletions app/Controllers/Admin/ApisController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
namespace App\Controllers\Admin;

use App\Core\Controller;
use App\Core\DB;
use App\Core\CSRF;

class ApisController extends Controller
{
public function index(): void
{
$this->requireRole(['admin', 'super_admin']);
$apis = DB::fetchAll('SELECT * FROM apis ORDER BY name');
$this->render('admin/apis/index', [
'title' => 'APIs',
'apis' => $apis,
], 'admin');
}

public function createForm(): void
{
$this->requireRole(['admin', 'super_admin']);
$this->render('admin/apis/create', ['title' => 'Add API'], 'admin');
}

public function create(): void
{
$this->requireRole(['admin', 'super_admin']);
if (!CSRF::validate($_POST['_token'] ?? '')) { http_response_code(419); echo 'Invalid token'; return; }
$name = trim($_POST['name'] ?? '');
$base = trim($_POST['base_url'] ?? '');
$key = trim($_POST['api_key'] ?? '');
$type = trim($_POST['type'] ?? 'generic');
if ($name && $base && $key) {
DB::insert('INSERT INTO apis (name, base_url, api_key, type, status, created_at, updated_at) VALUES (:n,:b,:k,:t,1,NOW(),NOW())', [
'n' => $name, 'b' => $base, 'k' => $key, 't' => $type,
]);
}
$this->redirect('/admin/apis');
}
}
25 changes: 25 additions & 0 deletions app/Controllers/Admin/DashboardController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
namespace App\Controllers\Admin;

use App\Core\Controller;
use App\Core\Auth;
use App\Core\DB;

class DashboardController extends Controller
{
public function index(): void
{
$this->requireRole(['admin', 'super_admin']);

$totals = [
'users' => (int) DB::fetch('SELECT COUNT(*) AS c FROM users')['c'] ?? 0,
'orders' => (int) DB::fetch('SELECT COUNT(*) AS c FROM orders')['c'] ?? 0,
'revenue' => (float) (DB::fetch('SELECT COALESCE(SUM(CASE WHEN amount < 0 THEN 0 ELSE amount END),0) AS s FROM wallet_transactions')['s'] ?? 0),
];

$this->render('admin/dashboard', [
'title' => 'Admin Dashboard',
'totals' => $totals,
], 'admin');
}
}
18 changes: 18 additions & 0 deletions app/Controllers/Admin/LogsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
namespace App\Controllers\Admin;

use App\Core\Controller;
use App\Core\DB;

class LogsController extends Controller
{
public function index(): void
{
$this->requireRole(['admin', 'super_admin']);
$logs = DB::fetchAll('SELECT * FROM cron_logs ORDER BY ran_at DESC, id DESC LIMIT 200');
$this->render('admin/logs/index', [
'title' => 'Logs',
'logs' => $logs,
], 'admin');
}
}
18 changes: 18 additions & 0 deletions app/Controllers/Admin/OrdersController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
namespace App\Controllers\Admin;

use App\Core\Controller;
use App\Core\DB;

class OrdersController extends Controller
{
public function index(): void
{
$this->requireRole(['admin', 'super_admin']);
$orders = DB::fetchAll('SELECT o.*, u.email, s.name AS service_name FROM orders o JOIN users u ON u.id = o.user_id JOIN services s ON s.id = o.service_id ORDER BY o.id DESC LIMIT 200');
$this->render('admin/orders/index', [
'title' => 'Orders',
'orders' => $orders,
], 'admin');
}
}
24 changes: 24 additions & 0 deletions app/Controllers/Admin/ReportsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
namespace App\Controllers\Admin;

use App\Core\Controller;
use App\Core\DB;

class ReportsController extends Controller
{
public function index(): void
{
$this->requireRole(['admin', 'super_admin']);
$totals = [
'credits' => (float) (DB::fetch("SELECT COALESCE(SUM(amount),0) AS s FROM wallet_transactions WHERE type = 'credit'")['s'] ?? 0),
'debits' => (float) (DB::fetch("SELECT COALESCE(SUM(amount),0) AS s FROM wallet_transactions WHERE type = 'debit'")['s'] ?? 0),
'orders' => (int) (DB::fetch('SELECT COUNT(*) AS c FROM orders')['c'] ?? 0),
];
$recent = DB::fetchAll("SELECT DATE(created_at) d, SUM(CASE WHEN type='credit' THEN amount ELSE 0 END) credits, SUM(CASE WHEN type='debit' THEN amount ELSE 0 END) debits FROM wallet_transactions GROUP BY DATE(created_at) ORDER BY d DESC LIMIT 14");
$this->render('admin/reports/index', [
'title' => 'Reports',
'totals' => $totals,
'recent' => $recent,
], 'admin');
}
}
72 changes: 72 additions & 0 deletions app/Controllers/Admin/ServiceCategoriesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
namespace App\Controllers\Admin;

use App\Core\Controller;
use App\Core\DB;
use App\Core\CSRF;

class ServiceCategoriesController extends Controller
{
public function index(): void
{
$this->requireRole(['admin', 'super_admin']);
$cats = DB::fetchAll('SELECT * FROM service_categories ORDER BY sort_order, name');
$this->render('admin/categories/index', ['title' => 'Service Categories', 'cats' => $cats], 'admin');
}

public function createForm(): void
{
$this->requireRole(['admin', 'super_admin']);
$this->render('admin/categories/create', ['title' => 'Create Category'], 'admin');
}

public function create(): void
{
$this->requireRole(['admin', 'super_admin']);
if (!CSRF::validate($_POST['_token'] ?? '')) { http_response_code(419); echo 'Invalid token'; return; }
$name = trim($_POST['name'] ?? '');
$sort = (int)($_POST['sort_order'] ?? 0);
$status = (int)($_POST['status'] ?? 1);
if ($name !== '') {
DB::insert('INSERT INTO service_categories (name, status, sort_order, created_at, updated_at) VALUES (:n,:s,:o,NOW(),NOW())', [
'n' => $name, 's' => $status, 'o' => $sort,
]);
}
$this->redirect('/admin/services');
}

public function editForm(): void
{
$this->requireRole(['admin', 'super_admin']);
$id = (int)($_GET['id'] ?? 0);
$cat = DB::fetch('SELECT * FROM service_categories WHERE id = :id', ['id' => $id]);
$this->render('admin/categories/edit', ['title' => 'Edit Category', 'cat' => $cat], 'admin');
}

public function update(): void
{
$this->requireRole(['admin', 'super_admin']);
if (!CSRF::validate($_POST['_token'] ?? '')) { http_response_code(419); echo 'Invalid token'; return; }
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$sort = (int)($_POST['sort_order'] ?? 0);
$status = (int)($_POST['status'] ?? 1);
if ($id > 0 && $name !== '') {
DB::query('UPDATE service_categories SET name = :n, status = :s, sort_order = :o, updated_at = NOW() WHERE id = :id', [
'n' => $name, 's' => $status, 'o' => $sort, 'id' => $id,
]);
}
$this->redirect('/admin/services');
}

public function delete(): void
{
$this->requireRole(['admin', 'super_admin']);
if (!CSRF::validate($_POST['_token'] ?? '')) { http_response_code(419); echo 'Invalid token'; return; }
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
DB::query('DELETE FROM service_categories WHERE id = :id', ['id' => $id]);
}
$this->redirect('/admin/services');
}
}
61 changes: 61 additions & 0 deletions app/Controllers/Admin/ServicesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
namespace App\Controllers\Admin;

use App\Core\Controller;
use App\Core\DB;
use App\Core\CSRF;

class ServicesController extends Controller
{
public function index(): void
{
$this->requireRole(['admin', 'super_admin']);
$categories = DB::fetchAll('SELECT * FROM service_categories ORDER BY sort_order, name');
$services = DB::fetchAll('SELECT s.*, c.name AS category_name FROM services s JOIN service_categories c ON c.id = s.category_id ORDER BY c.sort_order, s.name');
$this->render('admin/services/index', [
'title' => 'Services',
'categories' => $categories,
'services' => $services,
], 'admin');
}

public function mapForm(): void
{
$this->requireRole(['admin', 'super_admin']);
$apis = DB::fetchAll('SELECT * FROM apis WHERE status = 1 ORDER BY name');
$apiServices = DB::fetchAll('SELECT * FROM api_services ORDER BY api_id, name');
$services = DB::fetchAll('SELECT id, name FROM services ORDER BY name');
$this->render('admin/services/map', [
'title' => 'Map API Services',
'apis' => $apis,
'apiServices' => $apiServices,
'services' => $services,
], 'admin');
}

public function mapSave(): void
{
$this->requireRole(['admin', 'super_admin']);
if (!CSRF::validate($_POST['_token'] ?? '')) { http_response_code(419); echo 'Invalid token'; return; }
$pairs = $_POST['map'] ?? [];
foreach ($pairs as $apiServiceId => $serviceId) {
$apiServiceId = (int)$apiServiceId; $serviceId = (int)$serviceId;
if ($apiServiceId > 0 && $serviceId > 0) {
DB::query('UPDATE services SET api_service_id = :asid WHERE id = :sid', ['asid' => $apiServiceId, 'sid' => $serviceId]);
}
}
$this->redirect('/admin/services');
}

public function syncPrices(): void
{
$this->requireRole(['admin', 'super_admin']);
if (!CSRF::validate($_POST['_token'] ?? '')) { http_response_code(419); echo 'Invalid token'; return; }
// For each mapped service, pull price from api_services
$mapped = DB::fetchAll('SELECT s.id, a.price FROM services s JOIN api_services a ON a.id = s.api_service_id');
foreach ($mapped as $m) {
DB::query('UPDATE services SET price = :p WHERE id = :id', ['p' => $m['price'], 'id' => $m['id']]);
}
$this->redirect('/admin/services');
}
}
Loading