seck
This commit is contained in:
parent
df3eb075fa
commit
011d28fa8c
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,8 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*/node_modules/
|
*/node_modules/
|
||||||
*/build/
|
*/build/
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
vendor/
|
||||||
|
composer.lock
|
||||||
|
composer.phar
|
||||||
|
|||||||
92
analyze.php
Normal file
92
analyze.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for sufficient credits
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$user || $user['credits'] <= 0) {
|
||||||
|
// Redirect or show an error if credits are insufficient
|
||||||
|
$_SESSION['error_message'] = 'You have no credits left. Please purchase more to continue.';
|
||||||
|
header('Location: pricing.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload_id'])) {
|
||||||
|
$uploadId = $_POST['upload_id'];
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
// Re-verify the upload belongs to the user
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM uploads WHERE id = ? AND user_id = ?");
|
||||||
|
$stmt->execute([$uploadId, $userId]);
|
||||||
|
$upload = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($upload) {
|
||||||
|
// Deduct one credit BEFORE starting the analysis
|
||||||
|
$pdo->prepare("UPDATE users SET credits = credits - 1 WHERE id = ?")->execute([$userId]);
|
||||||
|
|
||||||
|
// Update status to 'analyzing'
|
||||||
|
$updateStmt = $pdo->prepare("UPDATE uploads SET status = 'analyzing' WHERE id = ?");
|
||||||
|
$updateStmt->execute([$uploadId]);
|
||||||
|
|
||||||
|
// --- Real CV Service Integration ---
|
||||||
|
$bearerToken = getenv('INTERNAL_CV_BEARER_TOKEN');
|
||||||
|
$cvServiceUrl = 'https://internal-model/analyze';
|
||||||
|
|
||||||
|
$analysisResult = null;
|
||||||
|
$newStatus = 'failed';
|
||||||
|
|
||||||
|
if (!$bearerToken) {
|
||||||
|
$analysisResult = ['error' => 'Internal server configuration error: CV service token not set.'];
|
||||||
|
} elseif (empty($upload['file_path']) || !file_exists($upload['file_path'])) {
|
||||||
|
$analysisResult = ['error' => 'File not found for analysis.'];
|
||||||
|
} else {
|
||||||
|
// Prepare cURL request
|
||||||
|
$ch = curl_init();
|
||||||
|
$cfile = new CURLFile($upload['file_path'], mime_content_type($upload['file_path']), basename($upload['file_path']));
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $cvServiceUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, ['image' => $cfile]);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bearer ' . $bearerToken,
|
||||||
|
'Accept: application/json',
|
||||||
|
]);
|
||||||
|
// IMPORTANT: In a real production environment, you would not disable SSL verification.
|
||||||
|
// This is included for local/dev environments with self-signed certificates.
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
$analysisResult = ['error' => 'cURL Error: ' . $curlError];
|
||||||
|
} elseif ($httpCode >= 200 && $httpCode < 300) {
|
||||||
|
$analysisResult = json_decode($response, true);
|
||||||
|
$newStatus = 'completed';
|
||||||
|
} else {
|
||||||
|
$analysisResult = ['error' => 'CV service returned HTTP ' . $httpCode, 'response' => $response];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the result and update status
|
||||||
|
$resultStmt = $pdo->prepare("UPDATE uploads SET status = ?, analysis_result = ? WHERE id = ?");
|
||||||
|
$resultStmt->execute([$newStatus, json_encode($analysisResult), $uploadId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: index.php');
|
||||||
|
exit();
|
||||||
|
?>
|
||||||
@ -32,3 +32,108 @@ body {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Auth Forms */
|
||||||
|
.auth-form {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 5rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #0069d9;
|
||||||
|
border-color: #0062cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #721c24;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
color: #155724;
|
||||||
|
background-color: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card .card-img-top {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-result {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 3px solid #007BFF;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounding-box {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid #ff4d4d;
|
||||||
|
background-color: rgba(255, 77, 77, 0.2);
|
||||||
|
box-shadow: 0 0 5px rgba(255, 77, 77, 0.5);
|
||||||
|
pointer-events: none; /* So it doesn't interfere with image interactions */
|
||||||
|
}
|
||||||
|
|||||||
81
billing.php
Normal file
81
billing.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/partials/header.php';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Fetch user's current credit balance
|
||||||
|
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
$user_credits = $user ? $user['credits'] : 0;
|
||||||
|
|
||||||
|
// Fetch purchase history
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT p.credits_purchased, p.amount_paid, p.created_at, pl.name as plan_name " .
|
||||||
|
"FROM purchases p " .
|
||||||
|
"JOIN plans pl ON p.plan_id = pl.id " .
|
||||||
|
"WHERE p.user_id = ? ORDER BY p.created_at DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$purchases = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center my-5">
|
||||||
|
<h1>Billing & Credits</h1>
|
||||||
|
<p class="lead">View your credit balance and purchase history.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Your Credits</h5>
|
||||||
|
<p class="display-4 fw-bold"><?= $user_credits ?></p>
|
||||||
|
<a href="pricing.php" class="btn btn-primary">Buy More Credits</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Purchase History</h5>
|
||||||
|
<?php if ($purchases): ?>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Package</th>
|
||||||
|
<th>Credits</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($purchases as $purchase): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= date('M d, Y', strtotime($purchase['created_at'])) ?></td>
|
||||||
|
<td><?= htmlspecialchars($purchase['plan_name']) ?></td>
|
||||||
|
<td>+<?= htmlspecialchars($purchase['credits_purchased']) ?></td>
|
||||||
|
<td>$<?= htmlspecialchars(number_format($purchase['amount_paid'], 2)) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
|
<p>You have not made any purchases yet.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/partials/footer.php'; ?>
|
||||||
40
checkout.php
Normal file
40
checkout.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stripe_secret_key = getenv('STRIPE_SECRET_KEY');
|
||||||
|
if (!$stripe_secret_key) {
|
||||||
|
die('Stripe secret key is not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
\Stripe\Stripe::setApiKey($stripe_secret_key);
|
||||||
|
|
||||||
|
$price_id = $_GET['price_id'] ?? null;
|
||||||
|
if (!$price_id) {
|
||||||
|
header('Location: pricing.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_email = $_SESSION['user_email']; // Assuming user_email is stored in session from login
|
||||||
|
|
||||||
|
$checkout_session = \Stripe\Checkout\Session::create([
|
||||||
|
'payment_method_types' => ['card'],
|
||||||
|
'line_items' => [[
|
||||||
|
'price' => $price_id,
|
||||||
|
'quantity' => 1,
|
||||||
|
]],
|
||||||
|
'mode' => 'subscription',
|
||||||
|
'success_url' => 'http://' . $_SERVER['HTTP_HOST'] . '/index.php?payment=success',
|
||||||
|
'cancel_url' => 'http://' . $_SERVER['HTTP_HOST'] . '/pricing.php?payment=cancel',
|
||||||
|
'customer_email' => $user_email,
|
||||||
|
'client_reference_id' => $_SESSION['user_id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
header("Location: " . $checkout_session->url);
|
||||||
13
composer.json
Normal file
13
composer.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "flatlogic/vda",
|
||||||
|
"description": "Vehicle Damage Analysis",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Flatlogic Bot",
|
||||||
|
"email": "support@flatlogic.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"stripe/stripe-php": "^18.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
create-portal-session.php
Normal file
57
create-portal-session.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Stripe API key from .env
|
||||||
|
$stripeSecretKey = getenv('STRIPE_SECRET_KEY');
|
||||||
|
if (!$stripeSecretKey) {
|
||||||
|
die('Stripe secret key is not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
\Stripe\Stripe::setApiKey($stripeSecretKey);
|
||||||
|
|
||||||
|
// Get the user's Stripe Customer ID from your database
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
$customerId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT stripe_customer_id FROM subscriptions WHERE user_id = ? ORDER BY created_at DESC LIMIT 1");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$customerId = $stmt->fetchColumn();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die('Could not retrieve customer data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$customerId) {
|
||||||
|
// This can happen if the subscription was created but the webhook failed.
|
||||||
|
// Or if the user has no subscription.
|
||||||
|
header('Location: billing.php?error=nocustomer');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The return URL to which the user will be redirected after managing their billing
|
||||||
|
$returnUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . '/billing.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a Billing Portal session
|
||||||
|
$portalSession = \Stripe\BillingPortal\Session::create([
|
||||||
|
'customer' => $customerId,
|
||||||
|
'return_url' => $returnUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Redirect to the session URL
|
||||||
|
header("Location: " . $portalSession->url);
|
||||||
|
exit();
|
||||||
|
} catch (\Stripe\Exception\ApiErrorException $e) {
|
||||||
|
// Handle Stripe API errors
|
||||||
|
// You might want to log this error and show a generic message
|
||||||
|
die('Stripe API error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@ -15,3 +15,16 @@ function db() {
|
|||||||
}
|
}
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasActiveSubscription($user_id) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM subscriptions WHERE user_id = ? AND status = 'active' AND current_period_end > NOW()");
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
return $stmt->fetch() !== false;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Log error if needed
|
||||||
|
error_log('Subscription check failed: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
49
db/migrate.php
Normal file
49
db/migrate.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// 1. Create migrations table if it doesn't exist
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS `migrations` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `migration` VARCHAR(255) NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP );");
|
||||||
|
|
||||||
|
// 2. Get all executed migrations
|
||||||
|
$stmt = $pdo->query("SELECT `migration` FROM `migrations`");
|
||||||
|
$executed_migrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// 3. Find and execute new migrations
|
||||||
|
$migration_files = glob(__DIR__ . '/migrations/*.sql');
|
||||||
|
sort($migration_files);
|
||||||
|
|
||||||
|
$new_migrations_found = false;
|
||||||
|
foreach ($migration_files as $migration_file) {
|
||||||
|
$migration_name = basename($migration_file);
|
||||||
|
|
||||||
|
if (!in_array($migration_name, $executed_migrations)) {
|
||||||
|
$new_migrations_found = true;
|
||||||
|
$sql = file_get_contents($migration_file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec($sql);
|
||||||
|
|
||||||
|
// 4. Log the new migration
|
||||||
|
$insert_stmt = $pdo->prepare("INSERT INTO `migrations` (`migration`) VALUES (?)");
|
||||||
|
$insert_stmt->execute([$migration_name]);
|
||||||
|
|
||||||
|
echo "Executed migration: " . $migration_name . PHP_EOL;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// If a specific migration fails, output the error and stop.
|
||||||
|
die("Migration failed on " . $migration_name . ": " . $e->getMessage() . PHP_EOL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$new_migrations_found) {
|
||||||
|
echo "No new migrations to execute." . PHP_EOL;
|
||||||
|
} else {
|
||||||
|
echo "All new migrations executed successfully." . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Database operation failed: " . $e->getMessage() . PHP_EOL);
|
||||||
|
}
|
||||||
6
db/migrations/001_create_users_table.sql
Normal file
6
db/migrations/001_create_users_table.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `users` (
|
||||||
|
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`email` VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
`password` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
7
db/migrations/002_create_uploads_table.sql
Normal file
7
db/migrations/002_create_uploads_table.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `uploads` (
|
||||||
|
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL,
|
||||||
|
`file_path` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
3
db/migrations/003_add_analysis_columns.sql
Normal file
3
db/migrations/003_add_analysis_columns.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `uploads`
|
||||||
|
ADD COLUMN `status` VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||||||
|
ADD COLUMN `analysis_result` TEXT DEFAULT NULL;
|
||||||
21
db/migrations/004_create_billing_tables.sql
Normal file
21
db/migrations/004_create_billing_tables.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `plans` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`name` VARCHAR(255) NOT NULL,
|
||||||
|
`stripe_price_id` VARCHAR(255) NOT NULL,
|
||||||
|
`price` DECIMAL(10, 2) NOT NULL,
|
||||||
|
`features` TEXT,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `subscriptions` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL,
|
||||||
|
`plan_id` INT NOT NULL,
|
||||||
|
`stripe_subscription_id` VARCHAR(255) NOT NULL,
|
||||||
|
`status` VARCHAR(50) NOT NULL,
|
||||||
|
`current_period_end` TIMESTAMP,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
|
||||||
|
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`)
|
||||||
|
);
|
||||||
2
db/migrations/005_add_credits_system.sql
Normal file
2
db/migrations/005_add_credits_system.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `users` ADD `credits` INT UNSIGNED NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE `plans` ADD `credits_awarded` INT UNSIGNED NOT NULL DEFAULT 0;
|
||||||
15
db/migrations/006_create_purchases_table.sql
Normal file
15
db/migrations/006_create_purchases_table.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
-- Drop the now-obsolete subscriptions table
|
||||||
|
DROP TABLE IF EXISTS `subscriptions`;
|
||||||
|
|
||||||
|
-- Create a table to log credit purchases
|
||||||
|
CREATE TABLE IF NOT EXISTS `purchases` (
|
||||||
|
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL,
|
||||||
|
`plan_id` INT NOT NULL,
|
||||||
|
`stripe_charge_id` VARCHAR(255) NOT NULL,
|
||||||
|
`credits_purchased` INT(11) NOT NULL,
|
||||||
|
`amount_paid` DECIMAL(10, 2) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
31
db/seed_plans.php
Normal file
31
db/seed_plans.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
// Check if plans already exist
|
||||||
|
$stmt = $pdo->query("SELECT COUNT(*) FROM plans");
|
||||||
|
if ($stmt->fetchColumn() > 0) {
|
||||||
|
echo "Plans table is already seeded." . PHP_EOL;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the "Starter" credit pack
|
||||||
|
// IMPORTANT: Replace 'price_12345' with your actual Stripe Price ID for the credit pack
|
||||||
|
$starterPack = [
|
||||||
|
'name' => 'Starter Pack',
|
||||||
|
'stripe_price_id' => 'price_1PeP3QRpH4kRz8A8e25a25fA', // Placeholder - REPLACE THIS
|
||||||
|
'price' => 5.00,
|
||||||
|
'credits_awarded' => 50,
|
||||||
|
'features' => json_encode(['50 analysis credits', 'Standard support']),
|
||||||
|
];
|
||||||
|
|
||||||
|
$sql = "INSERT INTO plans (name, stripe_price_id, price, credits_awarded, features) VALUES (:name, :stripe_price_id, :price, :credits_awarded, :features)";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($starterPack);
|
||||||
|
|
||||||
|
echo "Successfully seeded the 'plans' table with the Starter Pack." . PHP_EOL;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Database error: " . $e->getMessage());
|
||||||
|
}
|
||||||
159
index.php
159
index.php
@ -1,9 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
|
require_once 'partials/header.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// If the user is not logged in, redirect to the login page.
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
$user_credits = $user ? $user['credits'] : 0;
|
||||||
|
|
||||||
$upload_dir = 'uploads/';
|
$upload_dir = 'uploads/';
|
||||||
$uploaded_file_path = null;
|
$uploaded_file_path = null;
|
||||||
$error_message = null;
|
$error_message = null;
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['vehicleImage'])) {
|
if ($user_credits > 0 && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['vehicleImage'])) {
|
||||||
if ($_FILES['vehicleImage']['error'] === UPLOAD_ERR_OK) {
|
if ($_FILES['vehicleImage']['error'] === UPLOAD_ERR_OK) {
|
||||||
$tmp_name = $_FILES['vehicleImage']['tmp_name'];
|
$tmp_name = $_FILES['vehicleImage']['tmp_name'];
|
||||||
$name = basename($_FILES['vehicleImage']['name']);
|
$name = basename($_FILES['vehicleImage']['name']);
|
||||||
@ -17,6 +32,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['vehicleImage'])) {
|
|||||||
|
|
||||||
if (move_uploaded_file($tmp_name, $destination)) {
|
if (move_uploaded_file($tmp_name, $destination)) {
|
||||||
$uploaded_file_path = $destination;
|
$uploaded_file_path = $destination;
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("INSERT INTO uploads (user_id, file_path) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$_SESSION['user_id'], $uploaded_file_path]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$error_message = "Database error: " . $e->getMessage();
|
||||||
|
// Optionally, delete the uploaded file if DB insertion fails
|
||||||
|
unlink($destination);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$error_message = 'Failed to move uploaded file.';
|
$error_message = 'Failed to move uploaded file.';
|
||||||
}
|
}
|
||||||
@ -28,34 +51,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['vehicleImage'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Acyfer Vision</title>
|
|
||||||
<meta name="description" content="Vehicle damage analysis powered by AI. Upload photos to get instant reports.">
|
|
||||||
<meta name="keywords" content="car damage analysis, vehicle inspection, AI image recognition, automotive diagnostics, collision repair estimate, auto body assessment, vehicle photo upload, Acyfer Vision, Built with Flatlogic Generator">
|
|
||||||
<meta property="og:title" content="Acyfer Vision">
|
|
||||||
<meta property="og:description" content="Vehicle damage analysis powered by AI. Upload photos to get instant reports.">
|
|
||||||
<meta property="og:image" content="">
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
|
||||||
<meta name="twitter:image" content="">
|
|
||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
<script src="https://unpkg.com/feather-icons"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<header class="p-3 mb-4 text-white header-gradient">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="h3">Acyfer Vision</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<div class="card p-4 p-md-5 border-0 shadow-sm">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h2">My Dashboard</h1>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-primary fs-6">Credits: <?= $user_credits ?></span>
|
||||||
|
<a href="pricing.php" class="btn btn-sm btn-outline-primary ms-2">Buy More</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($user_credits > 0): ?>
|
||||||
|
<div class="card p-4 p-md-5 border-0 shadow-sm mb-5">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-lg-6 mb-4 mb-lg-0">
|
<div class="col-lg-6 mb-4 mb-lg-0">
|
||||||
<h2 class="h1 mb-3">Upload Vehicle Photo</h2>
|
<h2 class="h1 mb-3">Upload Vehicle Photo</h2>
|
||||||
@ -94,15 +102,94 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['vehicleImage'])) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="card p-4 p-md-5 border-0 shadow-sm mb-5 text-center">
|
||||||
|
<h2 class="h1 mb-3">You're Out of Credits!</h2>
|
||||||
|
<p class="lead mb-4">You need to buy more credits to upload and analyze images.</p>
|
||||||
|
<a href="pricing.php" class="btn btn-primary btn-lg">Buy Credits</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="dashboard">
|
||||||
|
<h2 class="h3 mb-4">My Uploads</h2>
|
||||||
|
<div class="row">
|
||||||
|
<?php
|
||||||
|
$stmt = db()->prepare("SELECT * FROM uploads WHERE user_id = ? ORDER BY created_at DESC");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
$uploads = $stmt->fetchAll();
|
||||||
|
|
||||||
|
if (count($uploads) > 0):
|
||||||
|
foreach ($uploads as $upload):
|
||||||
|
$status = htmlspecialchars($upload['status']);
|
||||||
|
$status_badge_class = 'bg-secondary';
|
||||||
|
if ($status === 'pending') {
|
||||||
|
$status_badge_class = 'bg-warning text-dark';
|
||||||
|
} elseif ($status === 'analyzing') {
|
||||||
|
$status_badge_class = 'bg-info text-dark';
|
||||||
|
} elseif ($status === 'completed') {
|
||||||
|
$status_badge_class = 'bg-success';
|
||||||
|
} elseif ($status === 'failed') {
|
||||||
|
$status_badge_class = 'bg-danger';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card upload-card h-100">
|
||||||
|
<img src="<?php echo htmlspecialchars($upload['file_path']); ?>" class="card-img-top" alt="Uploaded Image">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<p class="card-text small text-muted mb-2">
|
||||||
|
Uploaded: <?php echo date("M d, Y", strtotime($upload['created_at'])); ?>
|
||||||
|
</p>
|
||||||
|
<p class="card-text mb-3">
|
||||||
|
Status: <span class="badge <?php echo $status_badge_class; ?>"><?php echo ucfirst($status); ?></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$result = $upload['analysis_result'] ? json_decode($upload['analysis_result'], true) : null;
|
||||||
|
|
||||||
|
if ($status === 'completed' && $result && !isset($result['error'])):
|
||||||
|
?>
|
||||||
|
<div class="analysis-result small mb-3">
|
||||||
|
<strong>Analysis Result:</strong><br>
|
||||||
|
Damage Detected: <?php echo isset($result['damage_detected']) && $result['damage_detected'] ? 'Yes' : 'No'; ?><br>
|
||||||
|
Confidence: <?php echo $result['confidence'] ?? 'N/A'; ?>%<br>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($status === 'failed' && $result && isset($result['error'])):
|
||||||
|
?>
|
||||||
|
<div class="analysis-result small text-danger mb-3">
|
||||||
|
<strong>Analysis Failed:</strong><br>
|
||||||
|
<?php echo htmlspecialchars($result['error']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mt-auto">
|
||||||
|
<?php if ($user_credits > 0): ?>
|
||||||
|
<?php if ($status === 'pending' || $status === 'failed'): ?>
|
||||||
|
<form action="analyze.php" method="post" class="d-grid">
|
||||||
|
<input type="hidden" name="upload_id" value="<?php echo $upload['id']; ?>">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm"><?php echo ($status === 'failed') ? 'Retry Analysis' : 'Analyze'; ?></button>
|
||||||
|
</form>
|
||||||
|
<?php elseif ($status === 'analyzing'): ?>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm w-100" disabled>Analyzing...</button>
|
||||||
|
<?php elseif ($status === 'completed'): ?>
|
||||||
|
<a href="report.php?id=<?php echo $upload['id']; ?>" class="btn btn-outline-secondary btn-sm">View Report</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="pricing.php" class="btn btn-primary btn-sm disabled" title="Buy more credits">Analyze</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
endforeach;
|
||||||
|
else:
|
||||||
|
?>
|
||||||
|
<div class="col">
|
||||||
|
<p>You haven't uploaded any images yet.</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="text-center text-muted py-4 mt-5">
|
<?php require_once 'partials/footer.php'; ?>
|
||||||
<p>© <?php echo date("Y"); ?> Acyfer Vision. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
|
||||||
<script>
|
|
||||||
feather.replace();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
70
login.php
Normal file
70
login.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'partials/header.php';
|
||||||
|
|
||||||
|
// If user is already logged in, redirect to home
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$email = $_POST['email'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($email) || empty($password)) {
|
||||||
|
$errors[] = 'Email and password are required.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password'])) {
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['user_email'] = $user['email'];
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Invalid email or password.';
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$errors[] = 'Database error: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="auth-form">
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
<?php if (!empty($errors)):
|
||||||
|
?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<?php foreach ($errors as $error):
|
||||||
|
?>
|
||||||
|
<p><?php echo htmlspecialchars($error); ?></p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form action="login.php" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Login</button>
|
||||||
|
</form>
|
||||||
|
<p>Don't have an account? <a href="register.php">Register here</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once 'partials/footer.php';
|
||||||
|
?>
|
||||||
6
logout.php
Normal file
6
logout.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
5
partials/footer.php
Normal file
5
partials/footer.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
</div> <!-- /container -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
partials/header.php
Normal file
48
partials/header.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vehicle Damage Analysis</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="index.php">Vehicle Damage AI</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="index.php">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="pricing.php">Pricing</a>
|
||||||
|
</li>
|
||||||
|
<?php if (isset($_SESSION['user_id'])): ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="billing.php">Billing</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="logout.php">Logout</a>
|
||||||
|
</li>
|
||||||
|
<?php else: ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="login.php">Login</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="register.php">Register</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
39
pricing.php
Normal file
39
pricing.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/partials/header.php';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT * FROM plans ORDER BY price");
|
||||||
|
$plans = $stmt->fetchAll();
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center my-5">
|
||||||
|
<h1>Purchase Credits</h1>
|
||||||
|
<p class="lead">One credit buys you one image analysis.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<?php foreach ($plans as $plan): ?>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4 shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="my-0 fw-normal"><?= htmlspecialchars($plan['name']) ?></h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title pricing-card-title">$<?= htmlspecialchars(number_format($plan['price'], 2)) ?></h1>
|
||||||
|
<ul class="list-unstyled mt-3 mb-4">
|
||||||
|
<li><strong><?= htmlspecialchars($plan['credits_awarded']) ?></strong> credits</li>
|
||||||
|
<li>Never expire</li>
|
||||||
|
<li>Use them anytime</li>
|
||||||
|
</ul>
|
||||||
|
<a href="checkout.php?price_id=<?= htmlspecialchars($plan['stripe_price_id']) ?>" class="w-100 btn btn-lg btn-primary">Buy Now</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/partials/footer.php'; ?>
|
||||||
77
register.php
Normal file
77
register.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'partials/header.php';
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$email = $_POST['email'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errors[] = 'A valid email is required.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($password) || strlen($password) < 8) {
|
||||||
|
$errors[] = 'Password must be at least 8 characters long.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$errors[] = 'Email already exists.';
|
||||||
|
} else {
|
||||||
|
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO users (email, password) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$email, $hashed_password]);
|
||||||
|
$success = 'Registration successful! You can now <a href="login.php">log in</a>.';
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$errors[] = 'Database error: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="auth-form">
|
||||||
|
<h1>Register</h1>
|
||||||
|
|
||||||
|
<?php if (!empty($errors)):
|
||||||
|
?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<?php foreach ($errors as $error):
|
||||||
|
?>
|
||||||
|
<p><?php echo htmlspecialchars($error); ?></p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success):
|
||||||
|
?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<p><?php echo $success; ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else:
|
||||||
|
?>
|
||||||
|
<form action="register.php" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Register</button>
|
||||||
|
</form>
|
||||||
|
<p>Already have an account? <a href="login.php">Login here</a>.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once 'partials/footer.php';
|
||||||
|
?>
|
||||||
94
report.php
Normal file
94
report.php
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for active subscription
|
||||||
|
if (!hasActiveSubscription($_SESSION['user_id'])) {
|
||||||
|
// Redirect to billing page if no active subscription
|
||||||
|
header('Location: billing.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_GET['id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadId = $_GET['id'];
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM uploads WHERE id = ? AND user_id = ? AND status = 'completed'");
|
||||||
|
$stmt->execute([$uploadId, $userId]);
|
||||||
|
$upload = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$upload) {
|
||||||
|
// Redirect if the upload doesn't exist, doesn't belong to the user, or isn't completed
|
||||||
|
header('Location: index.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$analysisResult = json_decode($upload['analysis_result'], true);
|
||||||
|
$imagePath = htmlspecialchars($upload['file_path']);
|
||||||
|
list($width, $height) = getimagesize($upload['file_path']);
|
||||||
|
|
||||||
|
include 'partials/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="h3">Analysis Report</h2>
|
||||||
|
<a href="index.php" class="btn btn-outline-secondary">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<h4 class="mb-3">Vehicle Image</h4>
|
||||||
|
<div id="image-container" class="position-relative" style="width: <?php echo $width; ?>px; max-width: 100%;">
|
||||||
|
<img src="<?php echo $imagePath; ?>" class="img-fluid" alt="Analyzed Vehicle Image">
|
||||||
|
<?php if (isset($analysisResult['bounding_box'])):
|
||||||
|
$box = $analysisResult['bounding_box'];
|
||||||
|
// Calculate percentages for responsive scaling
|
||||||
|
$left = ($box['x'] / $width) * 100;
|
||||||
|
$top = ($box['y'] / $height) * 100;
|
||||||
|
$boxWidth = ($box['width'] / $width) * 100;
|
||||||
|
$boxHeight = ($box['height'] / $height) * 100;
|
||||||
|
?>
|
||||||
|
<div class="bounding-box" style="left: <?php echo $left; ?>%; top: <?php echo $top; ?>%; width: <?php echo $boxWidth; ?>%; height: <?php echo $boxHeight; ?>%;"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<h4 class="mb-3">Report Details</h4>
|
||||||
|
<?php if ($analysisResult): ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<?php foreach ($analysisResult as $key => $value):
|
||||||
|
if (is_array($value)) continue; // Skip arrays like bounding_box
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th class="w-50"><strong><?php echo ucfirst(str_replace('_', ' ', htmlspecialchars($key))); ?></strong></th>
|
||||||
|
<td><?php echo is_bool($value) ? ($value ? 'Yes' : 'No') : htmlspecialchars($value); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h5 class="mt-4">Raw JSON Output</h5>
|
||||||
|
<pre class="bg-light p-3 rounded"><code><?php echo json_encode($analysisResult, JSON_PRETTY_PRINT); ?></code></pre>
|
||||||
|
<?php else: ?>
|
||||||
|
<p>No analysis data available.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php include 'partials/footer.php'; ?>
|
||||||
BIN
uploads/69161db8714608.50876253.jpg
Normal file
BIN
uploads/69161db8714608.50876253.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
BIN
uploads/69161dc883b4f3.88736172.jpg
Normal file
BIN
uploads/69161dc883b4f3.88736172.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
92
webhook.php
Normal file
92
webhook.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// Get Stripe keys from environment
|
||||||
|
$stripeSecretKey = getenv('STRIPE_SECRET_KEY');
|
||||||
|
$webhookSecret = getenv('STRIPE_WEBHOOK_SECRET');
|
||||||
|
|
||||||
|
if (!$stripeSecretKey || !$webhookSecret) {
|
||||||
|
http_response_code(500);
|
||||||
|
error_log('Stripe keys are not configured.');
|
||||||
|
exit('Configuration error.');
|
||||||
|
}
|
||||||
|
|
||||||
|
\Stripe\Stripe::setApiKey($stripeSecretKey);
|
||||||
|
|
||||||
|
$payload = @file_get_contents('php://input');
|
||||||
|
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
|
||||||
|
$event = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$event = \Stripe\Webhook::constructEvent(
|
||||||
|
$payload, $sig_header, $webhookSecret
|
||||||
|
);
|
||||||
|
} catch(\UnexpectedValueException $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit(); // Invalid payload
|
||||||
|
} catch(\Stripe\Exception\SignatureVerificationException $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit(); // Invalid signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the event
|
||||||
|
switch ($event->type) {
|
||||||
|
case 'checkout.session.completed':
|
||||||
|
$session = $event->data->object;
|
||||||
|
handleCheckoutSession($session);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unexpected event type
|
||||||
|
error_log('Received unknown event type ' . $event->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(200);
|
||||||
|
|
||||||
|
function handleCheckoutSession($session) {
|
||||||
|
$userId = $session->client_reference_id;
|
||||||
|
$stripeChargeId = $session->payment_intent; // Using payment_intent as a proxy for charge ID
|
||||||
|
|
||||||
|
if (!$userId) {
|
||||||
|
error_log('Webhook Error: No client_reference_id in checkout.session.completed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Retrieve the line items to find out what was purchased
|
||||||
|
$line_items = \Stripe\Checkout\Session::allLineItems($session->id, ['limit' => 1]);
|
||||||
|
$priceId = $line_items->data[0]->price->id;
|
||||||
|
|
||||||
|
// Get plan details from our database
|
||||||
|
$stmt = $pdo->prepare("SELECT id, credits_awarded, price FROM plans WHERE stripe_price_id = ?");
|
||||||
|
$stmt->execute([$priceId]);
|
||||||
|
$plan = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$plan) {
|
||||||
|
error_log("Webhook Error: Plan with price ID {$priceId} not found in database.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$planId = $plan['id'];
|
||||||
|
$creditsPurchased = $plan['credits_awarded'];
|
||||||
|
$amountPaid = $plan['price'];
|
||||||
|
|
||||||
|
// Record the purchase
|
||||||
|
$sql = "INSERT INTO purchases (user_id, plan_id, stripe_charge_id, credits_purchased, amount_paid) VALUES (?, ?, ?, ?, ?)";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$userId, $planId, $stripeChargeId, $creditsPurchased, $amountPaid]);
|
||||||
|
|
||||||
|
// Add credits to the user's account
|
||||||
|
$sql = "UPDATE users SET credits = credits + ? WHERE id = ?";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$creditsPurchased, $userId]);
|
||||||
|
|
||||||
|
} catch (\Stripe\Exception\ApiErrorException $e) {
|
||||||
|
error_log("Stripe API Error in webhook: " . $e->getMessage());
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Database error in webhook: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user