diff --git a/.gitignore b/.gitignore
index e427ff3..f6334fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
node_modules/
*/node_modules/
*/build/
+
+# Composer
+vendor/
+composer.lock
+composer.phar
diff --git a/analyze.php b/analyze.php
new file mode 100644
index 0000000..954333a
--- /dev/null
+++ b/analyze.php
@@ -0,0 +1,92 @@
+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();
+?>
\ No newline at end of file
diff --git a/assets/css/custom.css b/assets/css/custom.css
index f1ce07e..3a27347 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -32,3 +32,108 @@ body {
border-radius: 0.5rem;
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 */
+}
diff --git a/billing.php b/billing.php
new file mode 100644
index 0000000..b7455e9
--- /dev/null
+++ b/billing.php
@@ -0,0 +1,81 @@
+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);
+
+?>
+
+
+
+
Billing & Credits
+
View your credit balance and purchase history.
+
+
+
+
+
+
+
+
Purchase History
+
+
+
+
+ | Date |
+ Package |
+ Credits |
+ Amount |
+
+
+
+
+
+ | = date('M d, Y', strtotime($purchase['created_at'])) ?> |
+ = htmlspecialchars($purchase['plan_name']) ?> |
+ += htmlspecialchars($purchase['credits_purchased']) ?> |
+ $= htmlspecialchars(number_format($purchase['amount_paid'], 2)) ?> |
+
+
+
+
+
+
You have not made any purchases yet.
+
+
+
+
+
+
+
+
diff --git a/checkout.php b/checkout.php
new file mode 100644
index 0000000..10dfaac
--- /dev/null
+++ b/checkout.php
@@ -0,0 +1,40 @@
+ ['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);
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..c46e7df
--- /dev/null
+++ b/composer.json
@@ -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"
+ }
+}
diff --git a/create-portal-session.php b/create-portal-session.php
new file mode 100644
index 0000000..f69004d
--- /dev/null
+++ b/create-portal-session.php
@@ -0,0 +1,57 @@
+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());
+}
diff --git a/db/config.php b/db/config.php
index 75ce244..d51597c 100644
--- a/db/config.php
+++ b/db/config.php
@@ -15,3 +15,16 @@ function db() {
}
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;
+ }
+}
diff --git a/db/migrate.php b/db/migrate.php
new file mode 100644
index 0000000..86354e9
--- /dev/null
+++ b/db/migrate.php
@@ -0,0 +1,49 @@
+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);
+}
\ No newline at end of file
diff --git a/db/migrations/001_create_users_table.sql b/db/migrations/001_create_users_table.sql
new file mode 100644
index 0000000..f6028a2
--- /dev/null
+++ b/db/migrations/001_create_users_table.sql
@@ -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
+);
\ No newline at end of file
diff --git a/db/migrations/002_create_uploads_table.sql b/db/migrations/002_create_uploads_table.sql
new file mode 100644
index 0000000..f8ae255
--- /dev/null
+++ b/db/migrations/002_create_uploads_table.sql
@@ -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
+);
\ No newline at end of file
diff --git a/db/migrations/003_add_analysis_columns.sql b/db/migrations/003_add_analysis_columns.sql
new file mode 100644
index 0000000..af8bc93
--- /dev/null
+++ b/db/migrations/003_add_analysis_columns.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `uploads`
+ADD COLUMN `status` VARCHAR(50) NOT NULL DEFAULT 'pending',
+ADD COLUMN `analysis_result` TEXT DEFAULT NULL;
diff --git a/db/migrations/004_create_billing_tables.sql b/db/migrations/004_create_billing_tables.sql
new file mode 100644
index 0000000..63aba63
--- /dev/null
+++ b/db/migrations/004_create_billing_tables.sql
@@ -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`)
+);
diff --git a/db/migrations/005_add_credits_system.sql b/db/migrations/005_add_credits_system.sql
new file mode 100644
index 0000000..d28ceb6
--- /dev/null
+++ b/db/migrations/005_add_credits_system.sql
@@ -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;
diff --git a/db/migrations/006_create_purchases_table.sql b/db/migrations/006_create_purchases_table.sql
new file mode 100644
index 0000000..a305946
--- /dev/null
+++ b/db/migrations/006_create_purchases_table.sql
@@ -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;
diff --git a/db/seed_plans.php b/db/seed_plans.php
new file mode 100644
index 0000000..e3b9a92
--- /dev/null
+++ b/db/seed_plans.php
@@ -0,0 +1,31 @@
+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());
+}
diff --git a/index.php b/index.php
index 3602744..3c930fe 100644
--- a/index.php
+++ b/index.php
@@ -1,9 +1,24 @@
prepare("SELECT credits FROM users WHERE id = ?");
+$stmt->execute([$_SESSION['user_id']]);
+$user = $stmt->fetch();
+$user_credits = $user ? $user['credits'] : 0;
+
$upload_dir = 'uploads/';
$uploaded_file_path = 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) {
$tmp_name = $_FILES['vehicleImage']['tmp_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)) {
$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 {
$error_message = 'Failed to move uploaded file.';
}
@@ -28,81 +51,145 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['vehicleImage'])) {
}
}
?>
-
-
-
-
-
- Acyfer Vision
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Upload Vehicle Photo
-
Get an instant AI-powered damage analysis. Drag and drop an image or click to select a file.
-
-
-
-
-
-
-
-
+
+
My Dashboard
+
+
Credits: = $user_credits ?>
+
Buy More
+
+
-
-
-
Analysis Result
-
-
; ?>)
-
-
Image Received
-
- Status: Pending Analysis
-
+ 0): ?>
+
+
+
+
Upload Vehicle Photo
+
Get an instant AI-powered damage analysis. Drag and drop an image or click to select a file.
+
+
+
+
+
+
+
+
+
+
+
+
Analysis Result
+
+
; ?>)
+
+
Image Received
+
+ Status: Pending Analysis
+
+
-
-
-
+
+
+
+
+
+
You're Out of Credits!
+
You need to buy more credits to upload and analyze images.
+
Buy Credits
+
+
+
+
+
My Uploads
+
+ 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';
+ }
+ ?>
+
+
+
; ?>)
+
+
+ Uploaded:
+
+
+ Status:
+
+
+
+
+ Analysis Result:
+ Damage Detected:
+ Confidence: %
+
+
+
+ Analysis Failed:
+
+
+
+
+
+
+
+
+
+
+
You haven't uploaded any images yet.
+
+
+
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..936278b
--- /dev/null
+++ b/login.php
@@ -0,0 +1,70 @@
+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();
+ }
+ }
+}
+?>
+
+
+
+
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..95db42c
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,6 @@
+
+
+
+