mirror of
https://github.com/myronblair/epic-download
synced 2026-06-30 17:51:00 -05:00
auto-commit for f3b04df9-f563-4cb2-9a0a-69756e09f838
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Download Deployment Packages
|
||||
*/
|
||||
|
||||
if ($method === 'GET') {
|
||||
if ($id === 'php-package') {
|
||||
// Serve PHP/cPanel package
|
||||
$file = '/app/cpanel_php/epic-travel-php-cpanel.zip';
|
||||
|
||||
if (!file_exists($file)) {
|
||||
jsonResponse(['error' => 'PHP package not found'], 404);
|
||||
}
|
||||
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="epic-travel-php-cpanel.zip"');
|
||||
header('Content-Length: ' . filesize($file));
|
||||
readfile($file);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id === 'list') {
|
||||
jsonResponse([
|
||||
'packages' => [
|
||||
[
|
||||
'name' => 'PHP/cPanel Package',
|
||||
'description' => 'Standard cPanel hosting with PHP & MySQL (No SSH/Python required)',
|
||||
'size' => '790 KB',
|
||||
'download_url' => '/api/download/php-package',
|
||||
'requirements' => ['PHP 7.4+', 'MySQL 5.7+', 'cPanel', 'FTP/File Manager access']
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid download endpoint'], 404);
|
||||
@@ -0,0 +1,336 @@
|
||||
# Epic Travel & Expeditions - cPanel PHP Installation Guide
|
||||
|
||||
## 📋 Overview
|
||||
This guide will help you install Epic Travel & Expeditions on standard cPanel hosting using only FTP or File Manager - no SSH, Python, or root access required!
|
||||
|
||||
## ✅ Requirements
|
||||
- cPanel hosting account
|
||||
- PHP 7.4 or higher
|
||||
- MySQL 5.7+ or MariaDB 10.3+
|
||||
- At least 100MB disk space
|
||||
- FTP client (FileZilla, WinSCP) OR cPanel File Manager access
|
||||
|
||||
## 📦 Package Contents
|
||||
```
|
||||
epic-travel-php/
|
||||
├── frontend/ # React production build
|
||||
│ ├── index.html
|
||||
│ ├── .htaccess
|
||||
│ └── static/
|
||||
├── api/ # PHP backend
|
||||
│ ├── index.php # Main API router
|
||||
│ ├── config.php # Configuration file
|
||||
│ ├── .htaccess
|
||||
│ ├── setup_password.php
|
||||
│ ├── includes/
|
||||
│ └── api/
|
||||
├── database_schema.sql # MySQL database structure
|
||||
└── INSTALLATION.md # This file
|
||||
```
|
||||
|
||||
## 🚀 Installation Steps
|
||||
|
||||
### Step 1: Create MySQL Database
|
||||
|
||||
1. **Log into cPanel**
|
||||
2. **Go to "MySQL® Databases"**
|
||||
3. **Create New Database:**
|
||||
- Database name: `epictravel` (or your choice)
|
||||
- Click "Create Database"
|
||||
|
||||
4. **Create Database User:**
|
||||
- Username: Choose a username
|
||||
- Password: Generate a strong password
|
||||
- Click "Create User"
|
||||
|
||||
5. **Add User to Database:**
|
||||
- Select your user
|
||||
- Select your database
|
||||
- Grant **ALL PRIVILEGES**
|
||||
- Click "Make Changes"
|
||||
|
||||
6. **Note Down:**
|
||||
- Database name: `username_epictravel`
|
||||
- Username: `username_dbuser`
|
||||
- Password: `your_password`
|
||||
- Host: `localhost`
|
||||
|
||||
### Step 2: Import Database Schema
|
||||
|
||||
**Option A: Using phpMyAdmin**
|
||||
1. Go to cPanel → phpMyAdmin
|
||||
2. Select your database from the left sidebar
|
||||
3. Click "Import" tab
|
||||
4. Click "Choose File" and select `database_schema.sql`
|
||||
5. Click "Go" at the bottom
|
||||
6. Wait for "Import has been successfully finished" message
|
||||
|
||||
**Option B: Using MySQL Databases Tool**
|
||||
1. Go to cPanel → MySQL Databases
|
||||
2. Find "Run SQL on Database" section (if available)
|
||||
3. Copy contents of `database_schema.sql`
|
||||
4. Paste and execute
|
||||
|
||||
### Step 3: Upload Files via FTP
|
||||
|
||||
**Using FTP Client (FileZilla, WinSCP, etc.):**
|
||||
|
||||
1. **Connect to your server:**
|
||||
- Host: `ftp.yourdomain.com` (or your server IP)
|
||||
- Username: Your cPanel username
|
||||
- Password: Your cPanel password
|
||||
- Port: 21 (or 22 for SFTP if available)
|
||||
|
||||
2. **Upload Frontend:**
|
||||
- Navigate to `public_html/` (or your domain's folder)
|
||||
- Upload all files from `frontend/` folder
|
||||
- Make sure `.htaccess` is uploaded
|
||||
|
||||
3. **Upload Backend:**
|
||||
- Create folder: `public_html/api/`
|
||||
- Upload all files from `api/` folder to `public_html/api/`
|
||||
- Make sure `.htaccess` is uploaded
|
||||
- Create folder: `public_html/api/uploads/` (empty folder for image uploads)
|
||||
|
||||
### Step 3 (Alternative): Upload via File Manager
|
||||
|
||||
**Using cPanel File Manager:**
|
||||
|
||||
1. **Open File Manager** in cPanel
|
||||
2. **Navigate to** `public_html/`
|
||||
3. **Upload Frontend:**
|
||||
- Click "Upload" button
|
||||
- Select all files from `frontend/` folder
|
||||
- Wait for upload to complete
|
||||
4. **Create API Folder:**
|
||||
- Click "New Folder"
|
||||
- Name it `api`
|
||||
5. **Navigate to** `public_html/api/`
|
||||
6. **Upload Backend:**
|
||||
- Click "Upload"
|
||||
- Select all files from `api/` folder
|
||||
7. **Create Uploads Folder:**
|
||||
- Inside `/api/`, click "New Folder"
|
||||
- Name it `uploads`
|
||||
- Right-click → "Change Permissions" → Set to `755`
|
||||
|
||||
### Step 4: Configure Database Connection
|
||||
|
||||
1. **Navigate to** `public_html/api/`
|
||||
2. **Edit** `config.php` (right-click → Edit or Code Editor)
|
||||
3. **Update these lines:**
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'username_epictravel'); // Your database name
|
||||
define('DB_USER', 'username_dbuser'); // Your database user
|
||||
define('DB_PASS', 'your_password'); // Your database password
|
||||
```
|
||||
|
||||
4. **Generate JWT Secret Key:**
|
||||
- Visit: https://www.grc.com/passwords.htm
|
||||
- Copy the "63 random alpha-numeric characters" key
|
||||
- Update in config.php:
|
||||
```php
|
||||
define('JWT_SECRET_KEY', 'paste_your_generated_key_here');
|
||||
```
|
||||
|
||||
5. **Update CORS Origin:**
|
||||
```php
|
||||
define('ALLOWED_ORIGINS', 'https://yourdomain.com');
|
||||
```
|
||||
|
||||
6. **Save the file**
|
||||
|
||||
### Step 5: Setup Admin Password
|
||||
|
||||
**Method A: Using Browser (Recommended)**
|
||||
|
||||
1. Visit: `https://yourdomain.com/api/setup_password.php`
|
||||
2. Enter your desired admin password (e.g., `Joker1974!!!`)
|
||||
3. Check "Update password in database"
|
||||
4. Click "Generate Hash"
|
||||
5. Verify success message
|
||||
6. **IMPORTANT:** Delete `setup_password.php` file immediately
|
||||
|
||||
**Method B: Manual Database Update**
|
||||
|
||||
1. Go to phpMyAdmin
|
||||
2. Select your database
|
||||
3. Find `admin_users` table
|
||||
4. Click "Edit" (pencil icon) for the admin row
|
||||
5. In `password_hash` field, paste the generated hash
|
||||
6. Click "Go"
|
||||
|
||||
### Step 6: Set Folder Permissions
|
||||
|
||||
**Via File Manager:**
|
||||
1. Right-click `api/uploads/` folder
|
||||
2. Select "Change Permissions"
|
||||
3. Set to `755` (or `775` if needed)
|
||||
4. Click "Change Permissions"
|
||||
|
||||
**Via FTP Client:**
|
||||
1. Right-click `api/uploads/` folder
|
||||
2. File Permissions → `755`
|
||||
3. Apply
|
||||
|
||||
### Step 7: Test Installation
|
||||
|
||||
1. **Test Backend API:**
|
||||
- Visit: `https://yourdomain.com/api/`
|
||||
- Should see: `{"message":"Epic Travel API is running","status":"healthy"}`
|
||||
|
||||
2. **Test Frontend:**
|
||||
- Visit: `https://yourdomain.com`
|
||||
- Should see the Epic Travel homepage
|
||||
|
||||
3. **Test Admin Login:**
|
||||
- Visit: `https://yourdomain.com/admin`
|
||||
- Email: `admin@epictravel.com`
|
||||
- Password: `Joker1974!!!` (or your chosen password)
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### "Internal Server Error" (500)
|
||||
|
||||
**Check PHP Version:**
|
||||
1. cPanel → MultiPHP Manager
|
||||
2. Ensure PHP 7.4 or higher is selected
|
||||
3. Apply changes
|
||||
|
||||
**Check .htaccess:**
|
||||
1. Ensure `.htaccess` files are uploaded
|
||||
2. Check if they're hidden (Show Hidden Files in File Manager)
|
||||
|
||||
**Check Permissions:**
|
||||
- Folders: `755`
|
||||
- Files: `644`
|
||||
- uploads/ folder: `755` or `775`
|
||||
|
||||
### Database Connection Failed
|
||||
|
||||
1. **Verify credentials in config.php**
|
||||
2. **Check user privileges in cPanel → MySQL Databases**
|
||||
3. **Try localhost vs 127.0.0.1 in DB_HOST**
|
||||
4. **Contact hosting support if issue persists**
|
||||
|
||||
### Frontend Shows Blank Page
|
||||
|
||||
1. **Check browser console (F12) for errors**
|
||||
2. **Verify API is working** (`/api/` endpoint)
|
||||
3. **Check `.htaccess` in public_html**
|
||||
4. **Clear browser cache (Ctrl+Shift+Del)**
|
||||
|
||||
### CORS Errors
|
||||
|
||||
1. **Update ALLOWED_ORIGINS in config.php**
|
||||
2. **Use your actual domain (with https://)**
|
||||
3. **Restart by saving config.php again**
|
||||
|
||||
### Can't Upload Images
|
||||
|
||||
1. **Check uploads/ folder exists**
|
||||
2. **Set permissions to 755 or 775**
|
||||
3. **Check PHP upload_max_filesize**:
|
||||
- cPanel → MultiPHP INI Editor
|
||||
- Increase upload_max_filesize to 10M
|
||||
- Increase post_max_size to 10M
|
||||
|
||||
### Admin Login Not Working
|
||||
|
||||
1. **Verify password was set correctly**
|
||||
2. **Run setup_password.php again**
|
||||
3. **Check admin_users table in database**
|
||||
4. **Clear browser cookies**
|
||||
|
||||
## 📁 File Structure After Installation
|
||||
|
||||
```
|
||||
public_html/
|
||||
├── index.html # Frontend entry point
|
||||
├── .htaccess # Frontend routing
|
||||
├── static/ # CSS, JS files
|
||||
│ ├── css/
|
||||
│ └── js/
|
||||
├── api/ # Backend
|
||||
│ ├── index.php # API router
|
||||
│ ├── config.php # Configuration
|
||||
│ ├── .htaccess # API routing
|
||||
│ ├── includes/ # Core files
|
||||
│ │ ├── database.php
|
||||
│ │ ├── jwt.php
|
||||
│ │ └── functions.php
|
||||
│ ├── api/ # Endpoints
|
||||
│ │ ├── auth.php
|
||||
│ │ ├── destinations.php
|
||||
│ │ ├── specials.php
|
||||
│ │ ├── contact.php
|
||||
│ │ ├── newsletter.php
|
||||
│ │ └── upload.php
|
||||
│ └── uploads/ # Image uploads (empty)
|
||||
└── favicon.ico
|
||||
```
|
||||
|
||||
## 🔐 Security Checklist
|
||||
|
||||
After installation:
|
||||
- [ ] Changed admin password from default
|
||||
- [ ] Updated JWT_SECRET_KEY in config.php
|
||||
- [ ] Deleted setup_password.php
|
||||
- [ ] Set correct folder permissions
|
||||
- [ ] Enabled SSL certificate (HTTPS)
|
||||
- [ ] Updated ALLOWED_ORIGINS to your domain
|
||||
- [ ] Verified config.php is not web-accessible
|
||||
- [ ] Regular backups scheduled
|
||||
|
||||
## 🔄 Updating the Application
|
||||
|
||||
1. **Backup first!**
|
||||
- Download current files via FTP
|
||||
- Export database via phpMyAdmin
|
||||
|
||||
2. **Upload new files:**
|
||||
- Don't overwrite `config.php`
|
||||
- Upload updated files only
|
||||
|
||||
3. **Run database migrations (if any)**
|
||||
|
||||
4. **Test thoroughly**
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Application:** Epic Travel & Expeditions
|
||||
**Contact:** advisor@epictravelexpeditions.com
|
||||
**Phone:** +1 (817) 266-2022
|
||||
|
||||
**For Hosting Issues:**
|
||||
- Contact your hosting provider's support
|
||||
- Share error logs from cPanel → Error Logs
|
||||
|
||||
**Common Hosting Providers with cPanel:**
|
||||
- Bluehost, HostGator, SiteGround, A2 Hosting, InMotion
|
||||
- GoDaddy, Namecheap, DreamHost
|
||||
|
||||
## ✨ Success!
|
||||
|
||||
If everything is working:
|
||||
1. Visit your website
|
||||
2. Browse destinations
|
||||
3. Try the contact form
|
||||
4. Subscribe to newsletter
|
||||
5. Login to admin panel
|
||||
6. Add/edit destinations
|
||||
|
||||
**Congratulations! Your Epic Travel website is now live! 🎉**
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
- Customize destination content
|
||||
- Add your own images
|
||||
- Update contact information
|
||||
- Set up email forwarding for contact forms
|
||||
- Enable SSL certificate for HTTPS
|
||||
- Submit to search engines
|
||||
|
||||
Need help? Email us at advisor@epictravelexpeditions.com
|
||||
@@ -0,0 +1,38 @@
|
||||
# Epic Travel & Expeditions - API .htaccess
|
||||
# Place this file in the /api/ directory
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /api/
|
||||
|
||||
# Route all requests to index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php/$1 [L,QSA]
|
||||
</IfModule>
|
||||
|
||||
# Security Headers
|
||||
<IfModule mod_headers.c>
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
Header set X-Frame-Options "SAMEORIGIN"
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
</IfModule>
|
||||
|
||||
# Protect sensitive files
|
||||
<FilesMatch "^(config\.php|\.env)">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Enable compression
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE application/json text/plain
|
||||
</IfModule>
|
||||
|
||||
# PHP Settings
|
||||
<IfModule mod_php7.c>
|
||||
php_value upload_max_filesize 10M
|
||||
php_value post_max_size 10M
|
||||
php_value max_execution_time 300
|
||||
php_value max_input_time 300
|
||||
</IfModule>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Authentication Endpoints
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
// Login endpoint
|
||||
if ($method === 'POST' && $id === 'login') {
|
||||
$input = getJsonInput();
|
||||
|
||||
// Validate input
|
||||
$errors = validateRequired($input, ['email', 'password']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
$email = sanitizeString($input['email']);
|
||||
$password = $input['password'];
|
||||
|
||||
// Find admin user
|
||||
$stmt = $db->prepare("SELECT * FROM admin_users WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$admin = $stmt->fetch();
|
||||
|
||||
if (!$admin) {
|
||||
jsonResponse(['error' => 'Invalid email or password'], 401);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if (!password_verify($password, $admin['password_hash'])) {
|
||||
jsonResponse(['error' => 'Invalid email or password'], 401);
|
||||
}
|
||||
|
||||
// Create token
|
||||
$token = JWT::createToken($email);
|
||||
|
||||
jsonResponse([
|
||||
'access_token' => $token,
|
||||
'token_type' => 'bearer',
|
||||
'email' => $email
|
||||
]);
|
||||
}
|
||||
|
||||
// Verify token endpoint
|
||||
if ($method === 'POST' && $id === 'verify') {
|
||||
$payload = requireAuth();
|
||||
|
||||
jsonResponse([
|
||||
'valid' => true,
|
||||
'email' => $payload['sub']
|
||||
]);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid auth endpoint'], 404);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Contact Form Endpoint
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
if ($method === 'POST') {
|
||||
$input = getJsonInput();
|
||||
|
||||
$errors = validateRequired($input, ['name', 'email', 'message']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
if (!isValidEmail($input['email'])) {
|
||||
jsonResponse(['error' => 'Invalid email address'], 400);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO contacts (id, name, email, message, created_at)
|
||||
VALUES (?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$id,
|
||||
sanitizeString($input['name']),
|
||||
sanitizeString($input['email']),
|
||||
$input['message']
|
||||
]);
|
||||
|
||||
jsonResponse(['message' => 'Contact form submitted successfully']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Destinations CRUD Endpoints
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
// GET all destinations or single destination
|
||||
if ($method === 'GET') {
|
||||
if ($id) {
|
||||
// Get single destination
|
||||
$stmt = $db->prepare("SELECT * FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$destination = $stmt->fetch();
|
||||
|
||||
if (!$destination) {
|
||||
jsonResponse(['error' => 'Destination not found'], 404);
|
||||
}
|
||||
|
||||
jsonResponse($destination);
|
||||
} else {
|
||||
// Get all destinations with optional filtering
|
||||
$category = isset($_GET['category']) ? sanitizeString($_GET['category']) : null;
|
||||
$search = isset($_GET['search']) ? sanitizeString($_GET['search']) : null;
|
||||
|
||||
$sql = "SELECT * FROM destinations WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($category && $category !== 'All') {
|
||||
$sql .= " AND category = ?";
|
||||
$params[] = $category;
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$sql .= " AND (name LIKE ? OR location LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$sql .= " LIMIT 100";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$destinations = $stmt->fetchAll();
|
||||
|
||||
jsonResponse($destinations);
|
||||
}
|
||||
}
|
||||
|
||||
// POST create new destination (admin only)
|
||||
if ($method === 'POST') {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
$errors = validateRequired($input, ['name', 'location', 'description', 'image', 'category', 'rating', 'price']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO destinations (id, name, location, description, image, category, rating, price, currency, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$id,
|
||||
sanitizeString($input['name']),
|
||||
sanitizeString($input['location']),
|
||||
$input['description'],
|
||||
$input['image'],
|
||||
$input['category'],
|
||||
$input['rating'],
|
||||
$input['price'],
|
||||
isset($input['currency']) ? $input['currency'] : 'USD'
|
||||
]);
|
||||
|
||||
// Fetch created destination
|
||||
$stmt = $db->prepare("SELECT * FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$destination = $stmt->fetch();
|
||||
|
||||
jsonResponse($destination, 201);
|
||||
}
|
||||
|
||||
// PUT update destination (admin only)
|
||||
if ($method === 'PUT' && $id) {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
// Build update query dynamically
|
||||
$updates = [];
|
||||
$params = [];
|
||||
|
||||
$allowedFields = ['name', 'location', 'description', 'image', 'category', 'rating', 'price', 'currency'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
$updates[] = "$field = ?";
|
||||
$params[] = $field === 'description' ? $input[$field] : sanitizeString($input[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
jsonResponse(['error' => 'No fields to update'], 400);
|
||||
}
|
||||
|
||||
$params[] = $id;
|
||||
|
||||
$sql = "UPDATE destinations SET " . implode(', ', $updates) . " WHERE id = ?";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
// Fetch updated destination
|
||||
$stmt = $db->prepare("SELECT * FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$destination = $stmt->fetch();
|
||||
|
||||
jsonResponse($destination);
|
||||
}
|
||||
|
||||
// DELETE destination (admin only)
|
||||
if ($method === 'DELETE' && $id) {
|
||||
requireAuth();
|
||||
|
||||
// Delete destination (cascades to specials)
|
||||
$stmt = $db->prepare("DELETE FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(['error' => 'Destination not found'], 404);
|
||||
}
|
||||
|
||||
jsonResponse(['message' => 'Destination deleted successfully']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid destinations endpoint'], 404);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Newsletter Subscription Endpoint
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
if ($method === 'POST' && $id === 'subscribe') {
|
||||
$input = getJsonInput();
|
||||
|
||||
if (!isset($input['email']) || !isValidEmail($input['email'])) {
|
||||
jsonResponse(['error' => 'Valid email address is required'], 400);
|
||||
}
|
||||
|
||||
$email = sanitizeString($input['email']);
|
||||
|
||||
// Check if already subscribed
|
||||
$stmt = $db->prepare("SELECT id FROM newsletter_subscribers WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
jsonResponse(['message' => 'Email already subscribed']);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO newsletter_subscribers (id, email, subscribed_at)
|
||||
VALUES (?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([$id, $email]);
|
||||
|
||||
jsonResponse(['message' => 'Successfully subscribed to newsletter']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid newsletter endpoint'], 404);
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Weekly Specials CRUD Endpoints
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
// GET all specials
|
||||
if ($method === 'GET' && !$id) {
|
||||
$stmt = $db->query("SELECT * FROM specials LIMIT 100");
|
||||
$specials = $stmt->fetchAll();
|
||||
|
||||
// Parse JSON highlights
|
||||
foreach ($specials as &$special) {
|
||||
$special['highlights'] = json_decode($special['highlights'], true);
|
||||
}
|
||||
|
||||
jsonResponse($specials);
|
||||
}
|
||||
|
||||
// POST create special (admin only)
|
||||
if ($method === 'POST') {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
$errors = validateRequired($input, ['destination_id', 'discount', 'end_date', 'highlights']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
// Check if destination exists
|
||||
$stmt = $db->prepare("SELECT id FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$input['destination_id']]);
|
||||
if (!$stmt->fetch()) {
|
||||
jsonResponse(['error' => 'Destination not found'], 404);
|
||||
}
|
||||
|
||||
// Check if special already exists for this destination
|
||||
$stmt = $db->prepare("SELECT id FROM specials WHERE destination_id = ?");
|
||||
$stmt->execute([$input['destination_id']]);
|
||||
if ($stmt->fetch()) {
|
||||
jsonResponse(['error' => 'Special already exists for this destination'], 400);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
$highlights = json_encode($input['highlights']);
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO specials (id, destination_id, discount, end_date, highlights, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$id,
|
||||
$input['destination_id'],
|
||||
$input['discount'],
|
||||
$input['end_date'],
|
||||
$highlights
|
||||
]);
|
||||
|
||||
// Fetch created special
|
||||
$stmt = $db->prepare("SELECT * FROM specials WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$special = $stmt->fetch();
|
||||
$special['highlights'] = json_decode($special['highlights'], true);
|
||||
|
||||
jsonResponse($special, 201);
|
||||
}
|
||||
|
||||
// PUT update special (admin only)
|
||||
if ($method === 'PUT' && $id) {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
$updates = [];
|
||||
$params = [];
|
||||
|
||||
if (isset($input['discount'])) {
|
||||
$updates[] = "discount = ?";
|
||||
$params[] = $input['discount'];
|
||||
}
|
||||
|
||||
if (isset($input['end_date'])) {
|
||||
$updates[] = "end_date = ?";
|
||||
$params[] = $input['end_date'];
|
||||
}
|
||||
|
||||
if (isset($input['highlights'])) {
|
||||
$updates[] = "highlights = ?";
|
||||
$params[] = json_encode($input['highlights']);
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
jsonResponse(['error' => 'No fields to update'], 400);
|
||||
}
|
||||
|
||||
$params[] = $id;
|
||||
|
||||
$sql = "UPDATE specials SET " . implode(', ', $updates) . " WHERE id = ?";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
// Fetch updated special
|
||||
$stmt = $db->prepare("SELECT * FROM specials WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$special = $stmt->fetch();
|
||||
$special['highlights'] = json_decode($special['highlights'], true);
|
||||
|
||||
jsonResponse($special);
|
||||
}
|
||||
|
||||
// DELETE special by destination_id (admin only)
|
||||
if ($method === 'DELETE' && isset($pathParts[1]) && $pathParts[1] === 'destination' && isset($pathParts[2])) {
|
||||
requireAuth();
|
||||
|
||||
$destinationId = $pathParts[2];
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM specials WHERE destination_id = ?");
|
||||
$stmt->execute([$destinationId]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(['error' => 'Special not found for this destination'], 404);
|
||||
}
|
||||
|
||||
jsonResponse(['message' => 'Special removed successfully']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid specials endpoint'], 404);
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Image Upload Endpoint
|
||||
*/
|
||||
|
||||
requireAuth(); // Only authenticated users can upload
|
||||
|
||||
if ($method === 'POST' && $id === 'image') {
|
||||
if (!isset($_FILES['file'])) {
|
||||
jsonResponse(['error' => 'No file uploaded'], 400);
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
|
||||
// Validate file
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
jsonResponse(['error' => 'File upload failed'], 400);
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if ($file['size'] > MAX_UPLOAD_SIZE) {
|
||||
jsonResponse(['error' => 'File too large. Maximum size is 5MB'], 400);
|
||||
}
|
||||
|
||||
// Check file type
|
||||
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mimeType, $allowedTypes)) {
|
||||
jsonResponse(['error' => 'Invalid file type. Only JPG, PNG, and WebP allowed'], 400);
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$filename = generateUuid() . '.' . $extension;
|
||||
$filepath = UPLOAD_DIR . $filename;
|
||||
|
||||
// Move uploaded file
|
||||
if (!move_uploaded_file($file['tmp_name'], $filepath)) {
|
||||
jsonResponse(['error' => 'Failed to save file'], 500);
|
||||
}
|
||||
|
||||
$fileUrl = '/api/uploads/' . $filename;
|
||||
|
||||
jsonResponse([
|
||||
'url' => $fileUrl,
|
||||
'filename' => $filename
|
||||
]);
|
||||
}
|
||||
|
||||
// Serve uploaded images
|
||||
if ($method === 'GET' && isset($pathParts[1]) && $pathParts[1] === 'uploads' && isset($pathParts[2])) {
|
||||
$filename = basename($pathParts[2]);
|
||||
$filepath = UPLOAD_DIR . $filename;
|
||||
|
||||
if (!file_exists($filepath)) {
|
||||
jsonResponse(['error' => 'Image not found'], 404);
|
||||
}
|
||||
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $filepath);
|
||||
finfo_close($finfo);
|
||||
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . filesize($filepath));
|
||||
readfile($filepath);
|
||||
exit;
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid upload endpoint'], 404);
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Epic Travel & Expeditions - Configuration File
|
||||
* Update these settings with your cPanel MySQL database credentials
|
||||
*/
|
||||
|
||||
// Database Configuration
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'your_database_name');
|
||||
define('DB_USER', 'your_database_user');
|
||||
define('DB_PASS', 'your_database_password');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// Security Configuration
|
||||
define('JWT_SECRET_KEY', 'CHANGE_THIS_TO_A_RANDOM_SECRET_KEY_32_CHARACTERS_OR_MORE');
|
||||
define('JWT_EXPIRY', 86400); // 24 hours in seconds
|
||||
define('ADMIN_PASSWORD_HASH', '$2y$10$PLACEHOLDER'); // Generate using setup_password.php
|
||||
|
||||
// CORS Configuration
|
||||
define('ALLOWED_ORIGINS', 'https://yourdomain.com');
|
||||
|
||||
// Application Settings
|
||||
define('DEBUG_MODE', false);
|
||||
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
||||
define('MAX_UPLOAD_SIZE', 5242880); // 5MB in bytes
|
||||
|
||||
// API Settings
|
||||
define('API_PREFIX', '/api');
|
||||
|
||||
// Error Reporting
|
||||
if (DEBUG_MODE) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
} else {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
}
|
||||
|
||||
// Timezone
|
||||
date_default_timezone_set('America/Chicago');
|
||||
|
||||
// Create uploads directory if it doesn't exist
|
||||
if (!file_exists(UPLOAD_DIR)) {
|
||||
mkdir(UPLOAD_DIR, 0755, true);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Connection Class
|
||||
* Uses PDO for secure MySQL connections
|
||||
*/
|
||||
|
||||
class Database {
|
||||
private static $instance = null;
|
||||
private $conn;
|
||||
|
||||
private function __construct() {
|
||||
try {
|
||||
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
$this->conn = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
} catch (PDOException $e) {
|
||||
$this->handleError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getConnection() {
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
private function handleError($message) {
|
||||
if (DEBUG_MODE) {
|
||||
die(json_encode(['error' => 'Database Error: ' . $message]));
|
||||
} else {
|
||||
die(json_encode(['error' => 'Database connection failed']));
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent cloning
|
||||
private function __clone() {}
|
||||
|
||||
// Prevent unserialization
|
||||
public function __wakeup() {
|
||||
throw new Exception("Cannot unserialize singleton");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* Helper Functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set CORS headers
|
||||
*/
|
||||
function setCorsHeaders() {
|
||||
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
|
||||
|
||||
if ($origin && (ALLOWED_ORIGINS === '*' || strpos(ALLOWED_ORIGINS, $origin) !== false)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
} else {
|
||||
header("Access-Control-Allow-Origin: " . ALLOWED_ORIGINS);
|
||||
}
|
||||
|
||||
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
||||
header("Access-Control-Allow-Credentials: true");
|
||||
|
||||
// Handle preflight requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send JSON response
|
||||
*/
|
||||
function jsonResponse($data, $statusCode = 200) {
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON input
|
||||
*/
|
||||
function getJsonInput() {
|
||||
$input = file_get_contents('php://input');
|
||||
return json_decode($input, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email
|
||||
*/
|
||||
function isValidEmail($email) {
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUID v4
|
||||
*/
|
||||
function generateUuid() {
|
||||
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000,
|
||||
mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize string
|
||||
*/
|
||||
function sanitizeString($string) {
|
||||
return htmlspecialchars(strip_tags(trim($string)), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required fields
|
||||
*/
|
||||
function validateRequired($data, $requiredFields) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!isset($data[$field]) || empty(trim($data[$field]))) {
|
||||
$errors[] = "$field is required";
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* JWT Authentication Functions
|
||||
* Simple JWT implementation for PHP
|
||||
*/
|
||||
|
||||
class JWT {
|
||||
|
||||
/**
|
||||
* Create a JWT token
|
||||
*/
|
||||
public static function encode($payload, $secret) {
|
||||
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
||||
$payload = json_encode($payload);
|
||||
|
||||
$base64UrlHeader = self::base64UrlEncode($header);
|
||||
$base64UrlPayload = self::base64UrlEncode($payload);
|
||||
|
||||
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
|
||||
$base64UrlSignature = self::base64UrlEncode($signature);
|
||||
|
||||
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and verify a JWT token
|
||||
*/
|
||||
public static function decode($jwt, $secret) {
|
||||
$parts = explode('.', $jwt);
|
||||
|
||||
if (count($parts) !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($base64UrlHeader, $base64UrlPayload, $base64UrlSignature) = $parts;
|
||||
|
||||
$signature = self::base64UrlDecode($base64UrlSignature);
|
||||
$expectedSignature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
|
||||
|
||||
if (!hash_equals($signature, $expectedSignature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = json_decode(self::base64UrlDecode($base64UrlPayload), true);
|
||||
|
||||
// Check expiration
|
||||
if (isset($payload['exp']) && $payload['exp'] < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create authentication token
|
||||
*/
|
||||
public static function createToken($email) {
|
||||
$payload = [
|
||||
'sub' => $email,
|
||||
'iat' => time(),
|
||||
'exp' => time() + JWT_EXPIRY
|
||||
];
|
||||
|
||||
return self::encode($payload, JWT_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify token from Authorization header
|
||||
*/
|
||||
public static function verifyToken() {
|
||||
$headers = getallheaders();
|
||||
|
||||
if (!isset($headers['Authorization'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authHeader = $headers['Authorization'];
|
||||
|
||||
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $matches[1];
|
||||
$payload = self::decode($token, JWT_SECRET_KEY);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL encode
|
||||
*/
|
||||
private static function base64UrlEncode($data) {
|
||||
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL decode
|
||||
*/
|
||||
private static function base64UrlDecode($data) {
|
||||
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication middleware
|
||||
*/
|
||||
function requireAuth() {
|
||||
$payload = JWT::verifyToken();
|
||||
|
||||
if (!$payload) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* Epic Travel & Expeditions - Main API Entry Point
|
||||
* This file routes all API requests to appropriate handlers
|
||||
*/
|
||||
|
||||
// Load configuration and dependencies
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/database.php';
|
||||
require_once __DIR__ . '/includes/jwt.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
// Set CORS headers
|
||||
setCorsHeaders();
|
||||
|
||||
// Get request method and path
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/';
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Parse path
|
||||
$pathParts = explode('/', $path);
|
||||
$resource = isset($pathParts[0]) ? $pathParts[0] : '';
|
||||
$id = isset($pathParts[1]) ? $pathParts[1] : null;
|
||||
|
||||
// Health check endpoint
|
||||
if ($path === '' || $path === 'api') {
|
||||
jsonResponse([
|
||||
'message' => 'Epic Travel API is running',
|
||||
'status' => 'healthy'
|
||||
]);
|
||||
}
|
||||
|
||||
// Route to appropriate handler
|
||||
try {
|
||||
switch ($resource) {
|
||||
case 'auth':
|
||||
require __DIR__ . '/api/auth.php';
|
||||
break;
|
||||
|
||||
case 'destinations':
|
||||
require __DIR__ . '/api/destinations.php';
|
||||
break;
|
||||
|
||||
case 'specials':
|
||||
require __DIR__ . '/api/specials.php';
|
||||
break;
|
||||
|
||||
case 'contact':
|
||||
require __DIR__ . '/api/contact.php';
|
||||
break;
|
||||
|
||||
case 'newsletter':
|
||||
require __DIR__ . '/api/newsletter.php';
|
||||
break;
|
||||
|
||||
case 'upload':
|
||||
require __DIR__ . '/api/upload.php';
|
||||
break;
|
||||
|
||||
case 'download':
|
||||
require __DIR__ . '/api/download.php';
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Endpoint not found'], 404);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
if (DEBUG_MODE) {
|
||||
jsonResponse(['error' => $e->getMessage()], 500);
|
||||
} else {
|
||||
jsonResponse(['error' => 'Internal server error'], 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
/**
|
||||
* Setup Password Hash Generator
|
||||
* Run this file once to generate a password hash for your admin account
|
||||
*
|
||||
* HOW TO USE:
|
||||
* 1. Upload this file to your server
|
||||
* 2. Visit it in your browser: https://yourdomain.com/api/setup_password.php
|
||||
* 3. Enter your desired password
|
||||
* 4. Copy the generated hash
|
||||
* 5. Update config.php with the hash
|
||||
* 6. DELETE this file for security
|
||||
*/
|
||||
|
||||
$generated_hash = '';
|
||||
$password = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
|
||||
$password = $_POST['password'];
|
||||
$generated_hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
// Update database
|
||||
if (isset($_POST['update_db']) && $_POST['update_db'] === '1') {
|
||||
try {
|
||||
require_once 'config.php';
|
||||
require_once 'includes/database.php';
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
$stmt = $db->prepare("UPDATE admin_users SET password_hash = ? WHERE email = 'admin@epictravel.com'");
|
||||
$stmt->execute([$generated_hash]);
|
||||
|
||||
$message = "✅ Password updated in database successfully!";
|
||||
} catch (Exception $e) {
|
||||
$error = "❌ Database update failed: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Password Setup - Epic Travel</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #0891b2;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="password"],
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
background: #0891b2;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0e7490;
|
||||
}
|
||||
.result {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #0891b2;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.warning {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #f59e0b;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #059669;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
color: #065f46;
|
||||
}
|
||||
.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #dc2626;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
color: #991b1b;
|
||||
}
|
||||
.checkbox-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
code {
|
||||
background: #f3f4f6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Admin Password Setup</h1>
|
||||
|
||||
<div class="warning">
|
||||
<strong>⚠️ Security Warning:</strong> Delete this file after use!
|
||||
</div>
|
||||
|
||||
<?php if (isset($message)): ?>
|
||||
<div class="success"><?php echo $message; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="error"><?php echo $error; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="password">Enter Admin Password:</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
placeholder="Enter a strong password" value="<?php echo htmlspecialchars($password); ?>">
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" name="update_db" value="1">
|
||||
Update password in database (requires config.php to be configured)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Generate Hash</button>
|
||||
</form>
|
||||
|
||||
<?php if ($generated_hash): ?>
|
||||
<div class="result">
|
||||
<strong>Generated Password Hash:</strong><br>
|
||||
<code><?php echo $generated_hash; ?></code>
|
||||
|
||||
<h3>Next Steps:</h3>
|
||||
<ol>
|
||||
<li>Copy the hash above</li>
|
||||
<li>Open <code>config.php</code></li>
|
||||
<li>Find the line with <code>ADMIN_PASSWORD_HASH</code></li>
|
||||
<li>Replace the placeholder with your hash</li>
|
||||
<li><strong>DELETE this file immediately!</strong></li>
|
||||
</ol>
|
||||
|
||||
<h3>Or run this SQL:</h3>
|
||||
<code style="display:block; padding:10px; margin-top:10px;">
|
||||
UPDATE admin_users SET password_hash = '<?php echo $generated_hash; ?>' WHERE email = 'admin@epictravel.com';
|
||||
</code>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px;">
|
||||
Epic Travel & Expeditions | admin@epictravel.com
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
# Epic Travel & Expeditions - PHP/cPanel Quick Start
|
||||
|
||||
## 🎯 What You Get
|
||||
A complete travel website with admin dashboard that works on ANY cPanel hosting - no SSH, Python, or special permissions needed!
|
||||
|
||||
## ⚡ Quick Installation (15 minutes)
|
||||
|
||||
### 1️⃣ Create Database (2 min)
|
||||
- cPanel → MySQL Databases
|
||||
- Create database & user
|
||||
- Grant ALL privileges
|
||||
- Note credentials
|
||||
|
||||
### 2️⃣ Import SQL (1 min)
|
||||
- cPanel → phpMyAdmin
|
||||
- Select database
|
||||
- Import → Choose `database_schema.sql`
|
||||
- Click Go
|
||||
|
||||
### 3️⃣ Upload Files (5 min)
|
||||
**Via FTP or File Manager:**
|
||||
- Upload `frontend/*` to `public_html/`
|
||||
- Upload `api/*` to `public_html/api/`
|
||||
- Create `public_html/api/uploads/` folder
|
||||
|
||||
### 4️⃣ Configure (3 min)
|
||||
Edit `api/config.php`:
|
||||
```php
|
||||
DB_NAME = 'your_database'
|
||||
DB_USER = 'your_username'
|
||||
DB_PASS = 'your_password'
|
||||
JWT_SECRET_KEY = 'generate_random_key'
|
||||
ALLOWED_ORIGINS = 'https://yourdomain.com'
|
||||
```
|
||||
|
||||
### 5️⃣ Set Password (2 min)
|
||||
- Visit: `yourdomain.com/api/setup_password.php`
|
||||
- Enter password
|
||||
- Check "Update database"
|
||||
- Click Generate
|
||||
- DELETE the setup_password.php file
|
||||
|
||||
### 6️⃣ Set Permissions (1 min)
|
||||
- Folder `api/uploads/` → `755`
|
||||
- Files → `644`
|
||||
|
||||
### 7️⃣ Test (1 min)
|
||||
✅ Visit: `yourdomain.com` → See website
|
||||
✅ Visit: `yourdomain.com/api/` → See API health
|
||||
✅ Visit: `yourdomain.com/admin` → Login works
|
||||
|
||||
## 🎉 Done!
|
||||
Your Epic Travel website is live!
|
||||
|
||||
## 📦 Package Contents
|
||||
- **frontend/** - React production build
|
||||
- **api/** - PHP backend (no Python needed!)
|
||||
- **database_schema.sql** - MySQL structure
|
||||
- **README.md** - Full installation guide
|
||||
|
||||
## 🔐 Default Login
|
||||
- Email: `admin@epictravel.com`
|
||||
- Password: Set during installation
|
||||
|
||||
## ❓ Need Help?
|
||||
Read the full `README.md` for detailed instructions and troubleshooting.
|
||||
|
||||
Contact: advisor@epictravelexpeditions.com
|
||||
Phone: +1 (817) 266-2022
|
||||
@@ -0,0 +1,336 @@
|
||||
# Epic Travel & Expeditions - cPanel PHP Installation Guide
|
||||
|
||||
## 📋 Overview
|
||||
This guide will help you install Epic Travel & Expeditions on standard cPanel hosting using only FTP or File Manager - no SSH, Python, or root access required!
|
||||
|
||||
## ✅ Requirements
|
||||
- cPanel hosting account
|
||||
- PHP 7.4 or higher
|
||||
- MySQL 5.7+ or MariaDB 10.3+
|
||||
- At least 100MB disk space
|
||||
- FTP client (FileZilla, WinSCP) OR cPanel File Manager access
|
||||
|
||||
## 📦 Package Contents
|
||||
```
|
||||
epic-travel-php/
|
||||
├── frontend/ # React production build
|
||||
│ ├── index.html
|
||||
│ ├── .htaccess
|
||||
│ └── static/
|
||||
├── api/ # PHP backend
|
||||
│ ├── index.php # Main API router
|
||||
│ ├── config.php # Configuration file
|
||||
│ ├── .htaccess
|
||||
│ ├── setup_password.php
|
||||
│ ├── includes/
|
||||
│ └── api/
|
||||
├── database_schema.sql # MySQL database structure
|
||||
└── INSTALLATION.md # This file
|
||||
```
|
||||
|
||||
## 🚀 Installation Steps
|
||||
|
||||
### Step 1: Create MySQL Database
|
||||
|
||||
1. **Log into cPanel**
|
||||
2. **Go to "MySQL® Databases"**
|
||||
3. **Create New Database:**
|
||||
- Database name: `epictravel` (or your choice)
|
||||
- Click "Create Database"
|
||||
|
||||
4. **Create Database User:**
|
||||
- Username: Choose a username
|
||||
- Password: Generate a strong password
|
||||
- Click "Create User"
|
||||
|
||||
5. **Add User to Database:**
|
||||
- Select your user
|
||||
- Select your database
|
||||
- Grant **ALL PRIVILEGES**
|
||||
- Click "Make Changes"
|
||||
|
||||
6. **Note Down:**
|
||||
- Database name: `username_epictravel`
|
||||
- Username: `username_dbuser`
|
||||
- Password: `your_password`
|
||||
- Host: `localhost`
|
||||
|
||||
### Step 2: Import Database Schema
|
||||
|
||||
**Option A: Using phpMyAdmin**
|
||||
1. Go to cPanel → phpMyAdmin
|
||||
2. Select your database from the left sidebar
|
||||
3. Click "Import" tab
|
||||
4. Click "Choose File" and select `database_schema.sql`
|
||||
5. Click "Go" at the bottom
|
||||
6. Wait for "Import has been successfully finished" message
|
||||
|
||||
**Option B: Using MySQL Databases Tool**
|
||||
1. Go to cPanel → MySQL Databases
|
||||
2. Find "Run SQL on Database" section (if available)
|
||||
3. Copy contents of `database_schema.sql`
|
||||
4. Paste and execute
|
||||
|
||||
### Step 3: Upload Files via FTP
|
||||
|
||||
**Using FTP Client (FileZilla, WinSCP, etc.):**
|
||||
|
||||
1. **Connect to your server:**
|
||||
- Host: `ftp.yourdomain.com` (or your server IP)
|
||||
- Username: Your cPanel username
|
||||
- Password: Your cPanel password
|
||||
- Port: 21 (or 22 for SFTP if available)
|
||||
|
||||
2. **Upload Frontend:**
|
||||
- Navigate to `public_html/` (or your domain's folder)
|
||||
- Upload all files from `frontend/` folder
|
||||
- Make sure `.htaccess` is uploaded
|
||||
|
||||
3. **Upload Backend:**
|
||||
- Create folder: `public_html/api/`
|
||||
- Upload all files from `api/` folder to `public_html/api/`
|
||||
- Make sure `.htaccess` is uploaded
|
||||
- Create folder: `public_html/api/uploads/` (empty folder for image uploads)
|
||||
|
||||
### Step 3 (Alternative): Upload via File Manager
|
||||
|
||||
**Using cPanel File Manager:**
|
||||
|
||||
1. **Open File Manager** in cPanel
|
||||
2. **Navigate to** `public_html/`
|
||||
3. **Upload Frontend:**
|
||||
- Click "Upload" button
|
||||
- Select all files from `frontend/` folder
|
||||
- Wait for upload to complete
|
||||
4. **Create API Folder:**
|
||||
- Click "New Folder"
|
||||
- Name it `api`
|
||||
5. **Navigate to** `public_html/api/`
|
||||
6. **Upload Backend:**
|
||||
- Click "Upload"
|
||||
- Select all files from `api/` folder
|
||||
7. **Create Uploads Folder:**
|
||||
- Inside `/api/`, click "New Folder"
|
||||
- Name it `uploads`
|
||||
- Right-click → "Change Permissions" → Set to `755`
|
||||
|
||||
### Step 4: Configure Database Connection
|
||||
|
||||
1. **Navigate to** `public_html/api/`
|
||||
2. **Edit** `config.php` (right-click → Edit or Code Editor)
|
||||
3. **Update these lines:**
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'username_epictravel'); // Your database name
|
||||
define('DB_USER', 'username_dbuser'); // Your database user
|
||||
define('DB_PASS', 'your_password'); // Your database password
|
||||
```
|
||||
|
||||
4. **Generate JWT Secret Key:**
|
||||
- Visit: https://www.grc.com/passwords.htm
|
||||
- Copy the "63 random alpha-numeric characters" key
|
||||
- Update in config.php:
|
||||
```php
|
||||
define('JWT_SECRET_KEY', 'paste_your_generated_key_here');
|
||||
```
|
||||
|
||||
5. **Update CORS Origin:**
|
||||
```php
|
||||
define('ALLOWED_ORIGINS', 'https://yourdomain.com');
|
||||
```
|
||||
|
||||
6. **Save the file**
|
||||
|
||||
### Step 5: Setup Admin Password
|
||||
|
||||
**Method A: Using Browser (Recommended)**
|
||||
|
||||
1. Visit: `https://yourdomain.com/api/setup_password.php`
|
||||
2. Enter your desired admin password (e.g., `Joker1974!!!`)
|
||||
3. Check "Update password in database"
|
||||
4. Click "Generate Hash"
|
||||
5. Verify success message
|
||||
6. **IMPORTANT:** Delete `setup_password.php` file immediately
|
||||
|
||||
**Method B: Manual Database Update**
|
||||
|
||||
1. Go to phpMyAdmin
|
||||
2. Select your database
|
||||
3. Find `admin_users` table
|
||||
4. Click "Edit" (pencil icon) for the admin row
|
||||
5. In `password_hash` field, paste the generated hash
|
||||
6. Click "Go"
|
||||
|
||||
### Step 6: Set Folder Permissions
|
||||
|
||||
**Via File Manager:**
|
||||
1. Right-click `api/uploads/` folder
|
||||
2. Select "Change Permissions"
|
||||
3. Set to `755` (or `775` if needed)
|
||||
4. Click "Change Permissions"
|
||||
|
||||
**Via FTP Client:**
|
||||
1. Right-click `api/uploads/` folder
|
||||
2. File Permissions → `755`
|
||||
3. Apply
|
||||
|
||||
### Step 7: Test Installation
|
||||
|
||||
1. **Test Backend API:**
|
||||
- Visit: `https://yourdomain.com/api/`
|
||||
- Should see: `{"message":"Epic Travel API is running","status":"healthy"}`
|
||||
|
||||
2. **Test Frontend:**
|
||||
- Visit: `https://yourdomain.com`
|
||||
- Should see the Epic Travel homepage
|
||||
|
||||
3. **Test Admin Login:**
|
||||
- Visit: `https://yourdomain.com/admin`
|
||||
- Email: `admin@epictravel.com`
|
||||
- Password: `Joker1974!!!` (or your chosen password)
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### "Internal Server Error" (500)
|
||||
|
||||
**Check PHP Version:**
|
||||
1. cPanel → MultiPHP Manager
|
||||
2. Ensure PHP 7.4 or higher is selected
|
||||
3. Apply changes
|
||||
|
||||
**Check .htaccess:**
|
||||
1. Ensure `.htaccess` files are uploaded
|
||||
2. Check if they're hidden (Show Hidden Files in File Manager)
|
||||
|
||||
**Check Permissions:**
|
||||
- Folders: `755`
|
||||
- Files: `644`
|
||||
- uploads/ folder: `755` or `775`
|
||||
|
||||
### Database Connection Failed
|
||||
|
||||
1. **Verify credentials in config.php**
|
||||
2. **Check user privileges in cPanel → MySQL Databases**
|
||||
3. **Try localhost vs 127.0.0.1 in DB_HOST**
|
||||
4. **Contact hosting support if issue persists**
|
||||
|
||||
### Frontend Shows Blank Page
|
||||
|
||||
1. **Check browser console (F12) for errors**
|
||||
2. **Verify API is working** (`/api/` endpoint)
|
||||
3. **Check `.htaccess` in public_html**
|
||||
4. **Clear browser cache (Ctrl+Shift+Del)**
|
||||
|
||||
### CORS Errors
|
||||
|
||||
1. **Update ALLOWED_ORIGINS in config.php**
|
||||
2. **Use your actual domain (with https://)**
|
||||
3. **Restart by saving config.php again**
|
||||
|
||||
### Can't Upload Images
|
||||
|
||||
1. **Check uploads/ folder exists**
|
||||
2. **Set permissions to 755 or 775**
|
||||
3. **Check PHP upload_max_filesize**:
|
||||
- cPanel → MultiPHP INI Editor
|
||||
- Increase upload_max_filesize to 10M
|
||||
- Increase post_max_size to 10M
|
||||
|
||||
### Admin Login Not Working
|
||||
|
||||
1. **Verify password was set correctly**
|
||||
2. **Run setup_password.php again**
|
||||
3. **Check admin_users table in database**
|
||||
4. **Clear browser cookies**
|
||||
|
||||
## 📁 File Structure After Installation
|
||||
|
||||
```
|
||||
public_html/
|
||||
├── index.html # Frontend entry point
|
||||
├── .htaccess # Frontend routing
|
||||
├── static/ # CSS, JS files
|
||||
│ ├── css/
|
||||
│ └── js/
|
||||
├── api/ # Backend
|
||||
│ ├── index.php # API router
|
||||
│ ├── config.php # Configuration
|
||||
│ ├── .htaccess # API routing
|
||||
│ ├── includes/ # Core files
|
||||
│ │ ├── database.php
|
||||
│ │ ├── jwt.php
|
||||
│ │ └── functions.php
|
||||
│ ├── api/ # Endpoints
|
||||
│ │ ├── auth.php
|
||||
│ │ ├── destinations.php
|
||||
│ │ ├── specials.php
|
||||
│ │ ├── contact.php
|
||||
│ │ ├── newsletter.php
|
||||
│ │ └── upload.php
|
||||
│ └── uploads/ # Image uploads (empty)
|
||||
└── favicon.ico
|
||||
```
|
||||
|
||||
## 🔐 Security Checklist
|
||||
|
||||
After installation:
|
||||
- [ ] Changed admin password from default
|
||||
- [ ] Updated JWT_SECRET_KEY in config.php
|
||||
- [ ] Deleted setup_password.php
|
||||
- [ ] Set correct folder permissions
|
||||
- [ ] Enabled SSL certificate (HTTPS)
|
||||
- [ ] Updated ALLOWED_ORIGINS to your domain
|
||||
- [ ] Verified config.php is not web-accessible
|
||||
- [ ] Regular backups scheduled
|
||||
|
||||
## 🔄 Updating the Application
|
||||
|
||||
1. **Backup first!**
|
||||
- Download current files via FTP
|
||||
- Export database via phpMyAdmin
|
||||
|
||||
2. **Upload new files:**
|
||||
- Don't overwrite `config.php`
|
||||
- Upload updated files only
|
||||
|
||||
3. **Run database migrations (if any)**
|
||||
|
||||
4. **Test thoroughly**
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Application:** Epic Travel & Expeditions
|
||||
**Contact:** advisor@epictravelexpeditions.com
|
||||
**Phone:** +1 (817) 266-2022
|
||||
|
||||
**For Hosting Issues:**
|
||||
- Contact your hosting provider's support
|
||||
- Share error logs from cPanel → Error Logs
|
||||
|
||||
**Common Hosting Providers with cPanel:**
|
||||
- Bluehost, HostGator, SiteGround, A2 Hosting, InMotion
|
||||
- GoDaddy, Namecheap, DreamHost
|
||||
|
||||
## ✨ Success!
|
||||
|
||||
If everything is working:
|
||||
1. Visit your website
|
||||
2. Browse destinations
|
||||
3. Try the contact form
|
||||
4. Subscribe to newsletter
|
||||
5. Login to admin panel
|
||||
6. Add/edit destinations
|
||||
|
||||
**Congratulations! Your Epic Travel website is now live! 🎉**
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
- Customize destination content
|
||||
- Add your own images
|
||||
- Update contact information
|
||||
- Set up email forwarding for contact forms
|
||||
- Enable SSL certificate for HTTPS
|
||||
- Submit to search engines
|
||||
|
||||
Need help? Email us at advisor@epictravelexpeditions.com
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Authentication Endpoints
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
// Login endpoint
|
||||
if ($method === 'POST' && $id === 'login') {
|
||||
$input = getJsonInput();
|
||||
|
||||
// Validate input
|
||||
$errors = validateRequired($input, ['email', 'password']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
$email = sanitizeString($input['email']);
|
||||
$password = $input['password'];
|
||||
|
||||
// Find admin user
|
||||
$stmt = $db->prepare("SELECT * FROM admin_users WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$admin = $stmt->fetch();
|
||||
|
||||
if (!$admin) {
|
||||
jsonResponse(['error' => 'Invalid email or password'], 401);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if (!password_verify($password, $admin['password_hash'])) {
|
||||
jsonResponse(['error' => 'Invalid email or password'], 401);
|
||||
}
|
||||
|
||||
// Create token
|
||||
$token = JWT::createToken($email);
|
||||
|
||||
jsonResponse([
|
||||
'access_token' => $token,
|
||||
'token_type' => 'bearer',
|
||||
'email' => $email
|
||||
]);
|
||||
}
|
||||
|
||||
// Verify token endpoint
|
||||
if ($method === 'POST' && $id === 'verify') {
|
||||
$payload = requireAuth();
|
||||
|
||||
jsonResponse([
|
||||
'valid' => true,
|
||||
'email' => $payload['sub']
|
||||
]);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid auth endpoint'], 404);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Contact Form Endpoint
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
if ($method === 'POST') {
|
||||
$input = getJsonInput();
|
||||
|
||||
$errors = validateRequired($input, ['name', 'email', 'message']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
if (!isValidEmail($input['email'])) {
|
||||
jsonResponse(['error' => 'Invalid email address'], 400);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO contacts (id, name, email, message, created_at)
|
||||
VALUES (?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$id,
|
||||
sanitizeString($input['name']),
|
||||
sanitizeString($input['email']),
|
||||
$input['message']
|
||||
]);
|
||||
|
||||
jsonResponse(['message' => 'Contact form submitted successfully']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Destinations CRUD Endpoints
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
// GET all destinations or single destination
|
||||
if ($method === 'GET') {
|
||||
if ($id) {
|
||||
// Get single destination
|
||||
$stmt = $db->prepare("SELECT * FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$destination = $stmt->fetch();
|
||||
|
||||
if (!$destination) {
|
||||
jsonResponse(['error' => 'Destination not found'], 404);
|
||||
}
|
||||
|
||||
jsonResponse($destination);
|
||||
} else {
|
||||
// Get all destinations with optional filtering
|
||||
$category = isset($_GET['category']) ? sanitizeString($_GET['category']) : null;
|
||||
$search = isset($_GET['search']) ? sanitizeString($_GET['search']) : null;
|
||||
|
||||
$sql = "SELECT * FROM destinations WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($category && $category !== 'All') {
|
||||
$sql .= " AND category = ?";
|
||||
$params[] = $category;
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$sql .= " AND (name LIKE ? OR location LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$sql .= " LIMIT 100";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$destinations = $stmt->fetchAll();
|
||||
|
||||
jsonResponse($destinations);
|
||||
}
|
||||
}
|
||||
|
||||
// POST create new destination (admin only)
|
||||
if ($method === 'POST') {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
$errors = validateRequired($input, ['name', 'location', 'description', 'image', 'category', 'rating', 'price']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO destinations (id, name, location, description, image, category, rating, price, currency, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$id,
|
||||
sanitizeString($input['name']),
|
||||
sanitizeString($input['location']),
|
||||
$input['description'],
|
||||
$input['image'],
|
||||
$input['category'],
|
||||
$input['rating'],
|
||||
$input['price'],
|
||||
isset($input['currency']) ? $input['currency'] : 'USD'
|
||||
]);
|
||||
|
||||
// Fetch created destination
|
||||
$stmt = $db->prepare("SELECT * FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$destination = $stmt->fetch();
|
||||
|
||||
jsonResponse($destination, 201);
|
||||
}
|
||||
|
||||
// PUT update destination (admin only)
|
||||
if ($method === 'PUT' && $id) {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
// Build update query dynamically
|
||||
$updates = [];
|
||||
$params = [];
|
||||
|
||||
$allowedFields = ['name', 'location', 'description', 'image', 'category', 'rating', 'price', 'currency'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
$updates[] = "$field = ?";
|
||||
$params[] = $field === 'description' ? $input[$field] : sanitizeString($input[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
jsonResponse(['error' => 'No fields to update'], 400);
|
||||
}
|
||||
|
||||
$params[] = $id;
|
||||
|
||||
$sql = "UPDATE destinations SET " . implode(', ', $updates) . " WHERE id = ?";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
// Fetch updated destination
|
||||
$stmt = $db->prepare("SELECT * FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$destination = $stmt->fetch();
|
||||
|
||||
jsonResponse($destination);
|
||||
}
|
||||
|
||||
// DELETE destination (admin only)
|
||||
if ($method === 'DELETE' && $id) {
|
||||
requireAuth();
|
||||
|
||||
// Delete destination (cascades to specials)
|
||||
$stmt = $db->prepare("DELETE FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(['error' => 'Destination not found'], 404);
|
||||
}
|
||||
|
||||
jsonResponse(['message' => 'Destination deleted successfully']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid destinations endpoint'], 404);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Newsletter Subscription Endpoint
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
if ($method === 'POST' && $id === 'subscribe') {
|
||||
$input = getJsonInput();
|
||||
|
||||
if (!isset($input['email']) || !isValidEmail($input['email'])) {
|
||||
jsonResponse(['error' => 'Valid email address is required'], 400);
|
||||
}
|
||||
|
||||
$email = sanitizeString($input['email']);
|
||||
|
||||
// Check if already subscribed
|
||||
$stmt = $db->prepare("SELECT id FROM newsletter_subscribers WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
jsonResponse(['message' => 'Email already subscribed']);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO newsletter_subscribers (id, email, subscribed_at)
|
||||
VALUES (?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([$id, $email]);
|
||||
|
||||
jsonResponse(['message' => 'Successfully subscribed to newsletter']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid newsletter endpoint'], 404);
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Weekly Specials CRUD Endpoints
|
||||
*/
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
|
||||
// GET all specials
|
||||
if ($method === 'GET' && !$id) {
|
||||
$stmt = $db->query("SELECT * FROM specials LIMIT 100");
|
||||
$specials = $stmt->fetchAll();
|
||||
|
||||
// Parse JSON highlights
|
||||
foreach ($specials as &$special) {
|
||||
$special['highlights'] = json_decode($special['highlights'], true);
|
||||
}
|
||||
|
||||
jsonResponse($specials);
|
||||
}
|
||||
|
||||
// POST create special (admin only)
|
||||
if ($method === 'POST') {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
$errors = validateRequired($input, ['destination_id', 'discount', 'end_date', 'highlights']);
|
||||
if (!empty($errors)) {
|
||||
jsonResponse(['error' => implode(', ', $errors)], 400);
|
||||
}
|
||||
|
||||
// Check if destination exists
|
||||
$stmt = $db->prepare("SELECT id FROM destinations WHERE id = ?");
|
||||
$stmt->execute([$input['destination_id']]);
|
||||
if (!$stmt->fetch()) {
|
||||
jsonResponse(['error' => 'Destination not found'], 404);
|
||||
}
|
||||
|
||||
// Check if special already exists for this destination
|
||||
$stmt = $db->prepare("SELECT id FROM specials WHERE destination_id = ?");
|
||||
$stmt->execute([$input['destination_id']]);
|
||||
if ($stmt->fetch()) {
|
||||
jsonResponse(['error' => 'Special already exists for this destination'], 400);
|
||||
}
|
||||
|
||||
$id = generateUuid();
|
||||
$highlights = json_encode($input['highlights']);
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO specials (id, destination_id, discount, end_date, highlights, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$id,
|
||||
$input['destination_id'],
|
||||
$input['discount'],
|
||||
$input['end_date'],
|
||||
$highlights
|
||||
]);
|
||||
|
||||
// Fetch created special
|
||||
$stmt = $db->prepare("SELECT * FROM specials WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$special = $stmt->fetch();
|
||||
$special['highlights'] = json_decode($special['highlights'], true);
|
||||
|
||||
jsonResponse($special, 201);
|
||||
}
|
||||
|
||||
// PUT update special (admin only)
|
||||
if ($method === 'PUT' && $id) {
|
||||
requireAuth();
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
$updates = [];
|
||||
$params = [];
|
||||
|
||||
if (isset($input['discount'])) {
|
||||
$updates[] = "discount = ?";
|
||||
$params[] = $input['discount'];
|
||||
}
|
||||
|
||||
if (isset($input['end_date'])) {
|
||||
$updates[] = "end_date = ?";
|
||||
$params[] = $input['end_date'];
|
||||
}
|
||||
|
||||
if (isset($input['highlights'])) {
|
||||
$updates[] = "highlights = ?";
|
||||
$params[] = json_encode($input['highlights']);
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
jsonResponse(['error' => 'No fields to update'], 400);
|
||||
}
|
||||
|
||||
$params[] = $id;
|
||||
|
||||
$sql = "UPDATE specials SET " . implode(', ', $updates) . " WHERE id = ?";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
// Fetch updated special
|
||||
$stmt = $db->prepare("SELECT * FROM specials WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$special = $stmt->fetch();
|
||||
$special['highlights'] = json_decode($special['highlights'], true);
|
||||
|
||||
jsonResponse($special);
|
||||
}
|
||||
|
||||
// DELETE special by destination_id (admin only)
|
||||
if ($method === 'DELETE' && isset($pathParts[1]) && $pathParts[1] === 'destination' && isset($pathParts[2])) {
|
||||
requireAuth();
|
||||
|
||||
$destinationId = $pathParts[2];
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM specials WHERE destination_id = ?");
|
||||
$stmt->execute([$destinationId]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(['error' => 'Special not found for this destination'], 404);
|
||||
}
|
||||
|
||||
jsonResponse(['message' => 'Special removed successfully']);
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid specials endpoint'], 404);
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Image Upload Endpoint
|
||||
*/
|
||||
|
||||
requireAuth(); // Only authenticated users can upload
|
||||
|
||||
if ($method === 'POST' && $id === 'image') {
|
||||
if (!isset($_FILES['file'])) {
|
||||
jsonResponse(['error' => 'No file uploaded'], 400);
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
|
||||
// Validate file
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
jsonResponse(['error' => 'File upload failed'], 400);
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if ($file['size'] > MAX_UPLOAD_SIZE) {
|
||||
jsonResponse(['error' => 'File too large. Maximum size is 5MB'], 400);
|
||||
}
|
||||
|
||||
// Check file type
|
||||
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mimeType, $allowedTypes)) {
|
||||
jsonResponse(['error' => 'Invalid file type. Only JPG, PNG, and WebP allowed'], 400);
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$filename = generateUuid() . '.' . $extension;
|
||||
$filepath = UPLOAD_DIR . $filename;
|
||||
|
||||
// Move uploaded file
|
||||
if (!move_uploaded_file($file['tmp_name'], $filepath)) {
|
||||
jsonResponse(['error' => 'Failed to save file'], 500);
|
||||
}
|
||||
|
||||
$fileUrl = '/api/uploads/' . $filename;
|
||||
|
||||
jsonResponse([
|
||||
'url' => $fileUrl,
|
||||
'filename' => $filename
|
||||
]);
|
||||
}
|
||||
|
||||
// Serve uploaded images
|
||||
if ($method === 'GET' && isset($pathParts[1]) && $pathParts[1] === 'uploads' && isset($pathParts[2])) {
|
||||
$filename = basename($pathParts[2]);
|
||||
$filepath = UPLOAD_DIR . $filename;
|
||||
|
||||
if (!file_exists($filepath)) {
|
||||
jsonResponse(['error' => 'Image not found'], 404);
|
||||
}
|
||||
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $filepath);
|
||||
finfo_close($finfo);
|
||||
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . filesize($filepath));
|
||||
readfile($filepath);
|
||||
exit;
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid upload endpoint'], 404);
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Epic Travel & Expeditions - Configuration File
|
||||
* Update these settings with your cPanel MySQL database credentials
|
||||
*/
|
||||
|
||||
// Database Configuration
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'your_database_name');
|
||||
define('DB_USER', 'your_database_user');
|
||||
define('DB_PASS', 'your_database_password');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// Security Configuration
|
||||
define('JWT_SECRET_KEY', 'CHANGE_THIS_TO_A_RANDOM_SECRET_KEY_32_CHARACTERS_OR_MORE');
|
||||
define('JWT_EXPIRY', 86400); // 24 hours in seconds
|
||||
define('ADMIN_PASSWORD_HASH', '$2y$10$PLACEHOLDER'); // Generate using setup_password.php
|
||||
|
||||
// CORS Configuration
|
||||
define('ALLOWED_ORIGINS', 'https://yourdomain.com');
|
||||
|
||||
// Application Settings
|
||||
define('DEBUG_MODE', false);
|
||||
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
||||
define('MAX_UPLOAD_SIZE', 5242880); // 5MB in bytes
|
||||
|
||||
// API Settings
|
||||
define('API_PREFIX', '/api');
|
||||
|
||||
// Error Reporting
|
||||
if (DEBUG_MODE) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
} else {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
}
|
||||
|
||||
// Timezone
|
||||
date_default_timezone_set('America/Chicago');
|
||||
|
||||
// Create uploads directory if it doesn't exist
|
||||
if (!file_exists(UPLOAD_DIR)) {
|
||||
mkdir(UPLOAD_DIR, 0755, true);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Connection Class
|
||||
* Uses PDO for secure MySQL connections
|
||||
*/
|
||||
|
||||
class Database {
|
||||
private static $instance = null;
|
||||
private $conn;
|
||||
|
||||
private function __construct() {
|
||||
try {
|
||||
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
$this->conn = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
} catch (PDOException $e) {
|
||||
$this->handleError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getConnection() {
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
private function handleError($message) {
|
||||
if (DEBUG_MODE) {
|
||||
die(json_encode(['error' => 'Database Error: ' . $message]));
|
||||
} else {
|
||||
die(json_encode(['error' => 'Database connection failed']));
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent cloning
|
||||
private function __clone() {}
|
||||
|
||||
// Prevent unserialization
|
||||
public function __wakeup() {
|
||||
throw new Exception("Cannot unserialize singleton");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* Helper Functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set CORS headers
|
||||
*/
|
||||
function setCorsHeaders() {
|
||||
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
|
||||
|
||||
if ($origin && (ALLOWED_ORIGINS === '*' || strpos(ALLOWED_ORIGINS, $origin) !== false)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
} else {
|
||||
header("Access-Control-Allow-Origin: " . ALLOWED_ORIGINS);
|
||||
}
|
||||
|
||||
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
||||
header("Access-Control-Allow-Credentials: true");
|
||||
|
||||
// Handle preflight requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send JSON response
|
||||
*/
|
||||
function jsonResponse($data, $statusCode = 200) {
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON input
|
||||
*/
|
||||
function getJsonInput() {
|
||||
$input = file_get_contents('php://input');
|
||||
return json_decode($input, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email
|
||||
*/
|
||||
function isValidEmail($email) {
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUID v4
|
||||
*/
|
||||
function generateUuid() {
|
||||
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000,
|
||||
mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize string
|
||||
*/
|
||||
function sanitizeString($string) {
|
||||
return htmlspecialchars(strip_tags(trim($string)), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required fields
|
||||
*/
|
||||
function validateRequired($data, $requiredFields) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!isset($data[$field]) || empty(trim($data[$field]))) {
|
||||
$errors[] = "$field is required";
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* JWT Authentication Functions
|
||||
* Simple JWT implementation for PHP
|
||||
*/
|
||||
|
||||
class JWT {
|
||||
|
||||
/**
|
||||
* Create a JWT token
|
||||
*/
|
||||
public static function encode($payload, $secret) {
|
||||
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
||||
$payload = json_encode($payload);
|
||||
|
||||
$base64UrlHeader = self::base64UrlEncode($header);
|
||||
$base64UrlPayload = self::base64UrlEncode($payload);
|
||||
|
||||
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
|
||||
$base64UrlSignature = self::base64UrlEncode($signature);
|
||||
|
||||
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and verify a JWT token
|
||||
*/
|
||||
public static function decode($jwt, $secret) {
|
||||
$parts = explode('.', $jwt);
|
||||
|
||||
if (count($parts) !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($base64UrlHeader, $base64UrlPayload, $base64UrlSignature) = $parts;
|
||||
|
||||
$signature = self::base64UrlDecode($base64UrlSignature);
|
||||
$expectedSignature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
|
||||
|
||||
if (!hash_equals($signature, $expectedSignature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = json_decode(self::base64UrlDecode($base64UrlPayload), true);
|
||||
|
||||
// Check expiration
|
||||
if (isset($payload['exp']) && $payload['exp'] < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create authentication token
|
||||
*/
|
||||
public static function createToken($email) {
|
||||
$payload = [
|
||||
'sub' => $email,
|
||||
'iat' => time(),
|
||||
'exp' => time() + JWT_EXPIRY
|
||||
];
|
||||
|
||||
return self::encode($payload, JWT_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify token from Authorization header
|
||||
*/
|
||||
public static function verifyToken() {
|
||||
$headers = getallheaders();
|
||||
|
||||
if (!isset($headers['Authorization'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authHeader = $headers['Authorization'];
|
||||
|
||||
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $matches[1];
|
||||
$payload = self::decode($token, JWT_SECRET_KEY);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL encode
|
||||
*/
|
||||
private static function base64UrlEncode($data) {
|
||||
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL decode
|
||||
*/
|
||||
private static function base64UrlDecode($data) {
|
||||
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication middleware
|
||||
*/
|
||||
function requireAuth() {
|
||||
$payload = JWT::verifyToken();
|
||||
|
||||
if (!$payload) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* Epic Travel & Expeditions - Main API Entry Point
|
||||
* This file routes all API requests to appropriate handlers
|
||||
*/
|
||||
|
||||
// Load configuration and dependencies
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/database.php';
|
||||
require_once __DIR__ . '/includes/jwt.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
// Set CORS headers
|
||||
setCorsHeaders();
|
||||
|
||||
// Get request method and path
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/';
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Parse path
|
||||
$pathParts = explode('/', $path);
|
||||
$resource = isset($pathParts[0]) ? $pathParts[0] : '';
|
||||
$id = isset($pathParts[1]) ? $pathParts[1] : null;
|
||||
|
||||
// Health check endpoint
|
||||
if ($path === '' || $path === 'api') {
|
||||
jsonResponse([
|
||||
'message' => 'Epic Travel API is running',
|
||||
'status' => 'healthy'
|
||||
]);
|
||||
}
|
||||
|
||||
// Route to appropriate handler
|
||||
try {
|
||||
switch ($resource) {
|
||||
case 'auth':
|
||||
require __DIR__ . '/api/auth.php';
|
||||
break;
|
||||
|
||||
case 'destinations':
|
||||
require __DIR__ . '/api/destinations.php';
|
||||
break;
|
||||
|
||||
case 'specials':
|
||||
require __DIR__ . '/api/specials.php';
|
||||
break;
|
||||
|
||||
case 'contact':
|
||||
require __DIR__ . '/api/contact.php';
|
||||
break;
|
||||
|
||||
case 'newsletter':
|
||||
require __DIR__ . '/api/newsletter.php';
|
||||
break;
|
||||
|
||||
case 'upload':
|
||||
require __DIR__ . '/api/upload.php';
|
||||
break;
|
||||
|
||||
case 'download':
|
||||
require __DIR__ . '/api/download.php';
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Endpoint not found'], 404);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
if (DEBUG_MODE) {
|
||||
jsonResponse(['error' => $e->getMessage()], 500);
|
||||
} else {
|
||||
jsonResponse(['error' => 'Internal server error'], 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
/**
|
||||
* Setup Password Hash Generator
|
||||
* Run this file once to generate a password hash for your admin account
|
||||
*
|
||||
* HOW TO USE:
|
||||
* 1. Upload this file to your server
|
||||
* 2. Visit it in your browser: https://yourdomain.com/api/setup_password.php
|
||||
* 3. Enter your desired password
|
||||
* 4. Copy the generated hash
|
||||
* 5. Update config.php with the hash
|
||||
* 6. DELETE this file for security
|
||||
*/
|
||||
|
||||
$generated_hash = '';
|
||||
$password = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
|
||||
$password = $_POST['password'];
|
||||
$generated_hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
// Update database
|
||||
if (isset($_POST['update_db']) && $_POST['update_db'] === '1') {
|
||||
try {
|
||||
require_once 'config.php';
|
||||
require_once 'includes/database.php';
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
$stmt = $db->prepare("UPDATE admin_users SET password_hash = ? WHERE email = 'admin@epictravel.com'");
|
||||
$stmt->execute([$generated_hash]);
|
||||
|
||||
$message = "✅ Password updated in database successfully!";
|
||||
} catch (Exception $e) {
|
||||
$error = "❌ Database update failed: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Password Setup - Epic Travel</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #0891b2;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="password"],
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
background: #0891b2;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0e7490;
|
||||
}
|
||||
.result {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #0891b2;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.warning {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #f59e0b;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #059669;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
color: #065f46;
|
||||
}
|
||||
.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #dc2626;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
color: #991b1b;
|
||||
}
|
||||
.checkbox-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
code {
|
||||
background: #f3f4f6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Admin Password Setup</h1>
|
||||
|
||||
<div class="warning">
|
||||
<strong>⚠️ Security Warning:</strong> Delete this file after use!
|
||||
</div>
|
||||
|
||||
<?php if (isset($message)): ?>
|
||||
<div class="success"><?php echo $message; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="error"><?php echo $error; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="password">Enter Admin Password:</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
placeholder="Enter a strong password" value="<?php echo htmlspecialchars($password); ?>">
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" name="update_db" value="1">
|
||||
Update password in database (requires config.php to be configured)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Generate Hash</button>
|
||||
</form>
|
||||
|
||||
<?php if ($generated_hash): ?>
|
||||
<div class="result">
|
||||
<strong>Generated Password Hash:</strong><br>
|
||||
<code><?php echo $generated_hash; ?></code>
|
||||
|
||||
<h3>Next Steps:</h3>
|
||||
<ol>
|
||||
<li>Copy the hash above</li>
|
||||
<li>Open <code>config.php</code></li>
|
||||
<li>Find the line with <code>ADMIN_PASSWORD_HASH</code></li>
|
||||
<li>Replace the placeholder with your hash</li>
|
||||
<li><strong>DELETE this file immediately!</strong></li>
|
||||
</ol>
|
||||
|
||||
<h3>Or run this SQL:</h3>
|
||||
<code style="display:block; padding:10px; margin-top:10px;">
|
||||
UPDATE admin_users SET password_hash = '<?php echo $generated_hash; ?>' WHERE email = 'admin@epictravel.com';
|
||||
</code>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px;">
|
||||
Epic Travel & Expeditions | admin@epictravel.com
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,95 @@
|
||||
-- Epic Travel & Expeditions Database Schema for MySQL
|
||||
-- Run this script to create the database structure
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS epic_travel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE epic_travel;
|
||||
|
||||
-- Destinations Table
|
||||
CREATE TABLE IF NOT EXISTS destinations (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
location VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
image VARCHAR(500) NOT NULL,
|
||||
category VARCHAR(50) NOT NULL,
|
||||
rating DECIMAL(2,1) NOT NULL DEFAULT 4.5,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_location (location)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Specials Table
|
||||
CREATE TABLE IF NOT EXISTS specials (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
destination_id VARCHAR(36) NOT NULL,
|
||||
discount DECIMAL(5,2) NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
highlights JSON NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (destination_id) REFERENCES destinations(id) ON DELETE CASCADE,
|
||||
INDEX idx_destination (destination_id),
|
||||
INDEX idx_end_date (end_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Admin Users Table
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Contacts Table
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Newsletter Subscribers Table
|
||||
CREATE TABLE IF NOT EXISTS newsletter_subscribers (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
subscribed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Insert default admin user (password: Joker1974!!!)
|
||||
-- Note: Replace the password_hash with the actual bcrypt hash
|
||||
INSERT INTO admin_users (id, email, password_hash, created_at)
|
||||
VALUES (
|
||||
'admin-1',
|
||||
'admin@epictravel.com',
|
||||
'$2b$12$PLACEHOLDER_HASH_WILL_BE_GENERATED',
|
||||
NOW()
|
||||
) ON DUPLICATE KEY UPDATE email=email;
|
||||
|
||||
-- Insert sample destinations
|
||||
INSERT INTO destinations (id, name, location, description, image, category, rating, price, currency) VALUES
|
||||
('1', 'Paris', 'France', 'Experience the romance and elegance of the City of Light. Visit iconic landmarks like the Eiffel Tower, Louvre Museum, and stroll along the Champs-Élysées.', 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&q=80', 'City', 4.9, 1299, 'USD'),
|
||||
('2', 'Bali', 'Indonesia', 'Discover tropical paradise with stunning beaches, ancient temples, lush rice terraces, and vibrant culture in this Indonesian gem.', 'https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&q=80', 'Beach', 4.8, 899, 'USD'),
|
||||
('3', 'Tokyo', 'Japan', 'Immerse yourself in the perfect blend of ancient tradition and cutting-edge technology in Japan\'s bustling capital city.', 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800&q=80', 'City', 4.9, 1499, 'USD'),
|
||||
('4', 'Santorini', 'Greece', 'Marvel at breathtaking sunsets, whitewashed buildings, and crystal-clear waters in this stunning Greek island paradise.', 'https://images.unsplash.com/photo-1613395877344-13d4a8e0d49e?w=800&q=80', 'Beach', 4.9, 1199, 'USD'),
|
||||
('5', 'Iceland', 'Iceland', 'Witness the Northern Lights, explore glaciers, geysers, and volcanic landscapes in this land of fire and ice.', 'https://images.unsplash.com/photo-1504829857797-ddff29c27927?w=800&q=80', 'Adventure', 4.8, 1699, 'USD'),
|
||||
('6', 'Dubai', 'UAE', 'Experience luxury and innovation in the desert with world-class shopping, stunning architecture, and endless entertainment.', 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=800&q=80', 'City', 4.7, 1399, 'USD'),
|
||||
('7', 'Maldives', 'Maldives', 'Relax in overwater bungalows, dive in pristine coral reefs, and enjoy the ultimate tropical island getaway.', 'https://images.unsplash.com/photo-1514282401047-d79a71a590e8?w=800&q=80', 'Beach', 5.0, 2199, 'USD'),
|
||||
('8', 'New York', 'USA', 'Explore the city that never sleeps with iconic landmarks, world-class museums, Broadway shows, and diverse neighborhoods.', 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&q=80', 'City', 4.8, 1099, 'USD'),
|
||||
('9', 'Machu Picchu', 'Peru', 'Trek to the ancient Incan citadel nestled high in the Andes Mountains, one of the New Seven Wonders of the World.', 'https://images.unsplash.com/photo-1587595431973-160d0d94add1?w=800&q=80', 'Adventure', 4.9, 1299, 'USD'),
|
||||
('10', 'Swiss Alps', 'Switzerland', 'Ski pristine slopes, hike mountain trails, and enjoy charming alpine villages with breathtaking mountain vistas.', 'https://images.unsplash.com/photo-1531366936337-7c912a4589a7?w=800&q=80', 'Adventure', 4.9, 1799, 'USD'),
|
||||
('11', 'Venice', 'Italy', 'Glide through romantic canals, admire Renaissance architecture, and savor authentic Italian cuisine in this unique floating city.', 'https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=800&q=80', 'City', 4.8, 1149, 'USD'),
|
||||
('12', 'Safari Kenya', 'Kenya', 'Witness the Great Migration, spot the Big Five, and experience the raw beauty of African wilderness.', 'https://images.unsplash.com/photo-1516426122078-c23e76319801?w=800&q=80', 'Adventure', 4.9, 2499, 'USD')
|
||||
ON DUPLICATE KEY UPDATE name=name;
|
||||
|
||||
-- Insert sample specials
|
||||
INSERT INTO specials (id, destination_id, discount, end_date, highlights) VALUES
|
||||
('special-1', '2', 25, DATE_ADD(CURDATE(), INTERVAL 30 DAY), JSON_ARRAY('Free spa treatment', 'Complimentary airport transfer', 'Sunset dinner cruise')),
|
||||
('special-2', '4', 30, DATE_ADD(CURDATE(), INTERVAL 45 DAY), JSON_ARRAY('Wine tasting tour', 'Private yacht excursion', 'Luxury accommodation upgrade')),
|
||||
('special-3', '7', 20, DATE_ADD(CURDATE(), INTERVAL 20 DAY), JSON_ARRAY('Snorkeling adventure', 'Couples massage', 'Romantic beach dinner'))
|
||||
ON DUPLICATE KEY UPDATE discount=discount;
|
||||
@@ -0,0 +1,50 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Force HTTPS
|
||||
RewriteCond %{HTTPS} off
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# API Proxy - Forward /api requests to Python backend
|
||||
# Update the proxy URL to match your cPanel Python app configuration
|
||||
RewriteCond %{REQUEST_URI} ^/api/(.*)$
|
||||
RewriteRule ^api/(.*)$ http://localhost:YOUR_PYTHON_APP_PORT/api/$1 [P,L]
|
||||
|
||||
# React Router - Serve index.html for all non-file requests
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-l
|
||||
RewriteRule ^ index.html [L]
|
||||
</IfModule>
|
||||
|
||||
# Enable Gzip Compression
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
|
||||
</IfModule>
|
||||
|
||||
# Browser Caching
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType image/jpg "access plus 1 year"
|
||||
ExpiresByType image/jpeg "access plus 1 year"
|
||||
ExpiresByType image/png "access plus 1 year"
|
||||
ExpiresByType image/gif "access plus 1 year"
|
||||
ExpiresByType image/webp "access plus 1 year"
|
||||
ExpiresByType text/css "access plus 1 month"
|
||||
ExpiresByType application/javascript "access plus 1 month"
|
||||
ExpiresByType application/json "access plus 1 week"
|
||||
</IfModule>
|
||||
|
||||
# Security Headers
|
||||
<IfModule mod_headers.c>
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
Header set X-Frame-Options "SAMEORIGIN"
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
</IfModule>
|
||||
|
||||
# Protect .env file
|
||||
<FilesMatch "^\.env">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.7791f349.css",
|
||||
"main.js": "/static/js/main.df6117d1.js",
|
||||
"index.html": "/index.html",
|
||||
"main.7791f349.css.map": "/static/css/main.7791f349.css.map",
|
||||
"main.df6117d1.js.map": "/static/js/main.df6117d1.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.7791f349.css",
|
||||
"static/js/main.df6117d1.js"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A product of emergent.sh"/><link rel="preconnect" href="https://fonts.googleapis.com"/><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/><link href="https://fonts.googleapis.com/css2?family=Inter:wght@600&display=swap" rel="stylesheet"/><title>Emergent | Fullstack App</title><script>window.addEventListener("error",function(e){e.error instanceof DOMException&&"DataCloneError"===e.error.name&&e.message&&e.message.includes("PerformanceServerTiming")&&(e.stopImmediatePropagation(),e.preventDefault())},!0)</script><script src="https://assets.emergent.sh/scripts/emergent-main.js"></script><script defer="defer" src="/static/js/main.df6117d1.js"></script><link href="/static/css/main.7791f349.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><a id="emergent-badge" target="_blank" href="https://app.emergent.sh/?utm_source=emergent-badge" style="display:inline-flex!important;box-sizing:border-box;width:178px;height:40px;padding:8px 12px 8px 12px;align-items:center!important;gap:8px;border-radius:50px!important;background:#000!important;position:fixed!important;bottom:16px;right:16px;text-decoration:none;font-family:-apple-system,BlinkMacSystemFont,"z-index:9999!important"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M15.5702 8.13142C15.7729 8.0412 16.0007 8.18878 15.9892 8.4103C15.8374 11.3192 14.0965 14.0405 11.2531 15.3065C8.40964 16.5725 5.2224 16.0453 2.95912 14.2117C2.78676 14.072 2.82955 13.804 3.03219 13.7137L4.95677 12.8568C5.04866 12.8159 5.15446 12.823 5.24204 12.8725C6.73377 13.7153 8.59176 13.8649 10.2772 13.1145C11.9626 12.3641 13.0947 10.8833 13.4665 9.21075C13.4883 9.11256 13.5539 9.02918 13.6457 8.98827L15.5702 8.13142Z" fill="white"/><path fill-rule="evenodd" clip-rule="evenodd" d="M15.3066 4.74698L15.5067 5.19653C15.5759 5.35178 15.5061 5.53366 15.3508 5.60278L1.29992 11.8586C1.14467 11.9278 0.962794 11.8579 0.893675 11.7027L0.701732 11.2716L0.693457 11.2531C-1.10317 7.21778 0.711626 2.49007 4.74692 0.693443C8.78221 -1.10318 13.51 0.711693 15.3066 4.74698ZM2.82356 8.55367C2.63552 8.63739 2.41991 8.51617 2.40853 8.31065C2.28373 6.05724 3.53858 3.85787 5.72286 2.88536C7.90715 1.91286 10.3813 2.45199 11.9724 4.05256C12.1175 4.19854 12.0633 4.43988 11.8753 4.5236L2.82356 8.55367Z" fill="white"/></svg><p style="color:#fff!important;font-family:Inter,sans-serif!important;font-size:13px!important;font-style:normal!important;font-weight:600!important;line-height:20px!important;margin:0!important;white-space:nowrap!important">Made with Emergent</p></a><script>!function(e,t){var r,s,o,i;t.__SV||(window.posthog=t,t._i=[],t.init=function(n,a,p){function c(e,t){var r=t.split(".");2==r.length&&(e=e[r[0]],t=r[1]),e[t]=function(){e.push([t].concat(Array.prototype.slice.call(arguments,0)))}}(o=e.createElement("script")).type="text/javascript",o.crossOrigin="anonymous",o.async=!0,o.src=a.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(i=e.getElementsByTagName("script")[0]).parentNode.insertBefore(o,i);var g=t;for(void 0!==p?g=t[p]=[]:p="posthog",g.people=g.people||[],g.toString=function(e){var t="posthog";return"posthog"!==p&&(t+="."+p),e||(t+=" (stub)"),t},g.people.toString=function(){return g.toString(1)+".people (stub)"},r="init me ws ys ps bs capture je Di ks register register_once register_for_session unregister unregister_for_session Ps getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Es $s createPersonProfile Is opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing Ss debug xs getPageViewId captureTraceFeedback captureTraceMetric".split(" "),s=0;s<r.length;s++)c(g,r[s]);t._i.push([n,a,p])},t.__SV=1)}(document,window.posthog||[]),posthog.init("phc_xAvL2Iq4tFmANRE7kzbKwaSqp1HJjN7x48s3vr0CMjs",{api_host:"https://us.i.posthog.com",person_profiles:"identified_only",session_recording:{recordCrossOriginIframes:!0,capturePerformance:!1}})</script></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @license React
|
||||
* react-dom-client.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license lucide-react v0.507.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* react-router v7.11.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user