Add multi-user support with 24-hour image expiration

Implemented complete session management system for multiple users:

**New Features:**
- Isolated user sessions with unique session IDs
- File-based image storage (not in PHP session)
- Automatic 24-hour image expiration
- Session directories: uploads/sessions/{session_id}/
- Images stored with metadata (creation time, expiry, MIME type)

**New Files:**
- session_manager.php - Complete session management class
- cleanup.php - Cron script to delete expired images
- uploads/.htaccess - Security: prevent direct file access

**Updated Files:**
- api.php - Uses SessionManager for file-based storage
- index.php - Loads images from disk via SessionManager
- .gitignore - Exclude user uploads from repository

**Usage:**
- Each user gets isolated session automatically
- Images auto-delete after 24 hours
- Run cleanup.php via cron: `0 * * * * php cleanup.php`

**Security:**
- Session IDs regenerated on first access
- Upload directory protected by .htaccess
- User images isolated by session

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DJP 2025-12-16 08:56:26 -05:00
parent bf5fa7fd48
commit 7d1cd03a32
6 changed files with 448 additions and 79 deletions

4
.gitignore vendored
View file

@ -4,6 +4,10 @@ config.php
# PHP session files
sessions/
# User uploaded images (24-hour storage)
uploads/sessions/
!uploads/.htaccess
# IDE files
.vscode/
.idea/

65
api.php
View file

@ -1,9 +1,12 @@
<?php
session_start();
header('Content-Type: application/json');
// Load configuration
// Load configuration and session manager
require_once 'config.php';
require_once 'session_manager.php';
// Initialize session manager for multi-user support
$sessionManager = new SessionManager();
class NanoBananaProAPI {
private $apiKey;
@ -184,10 +187,7 @@ try {
// Handle reset action
if ($action === 'reset') {
$_SESSION['conversation_history'] = [];
$_SESSION['current_image'] = null;
$_SESSION['current_image_mime'] = 'image/png';
$_SESSION['image_history'] = [];
$sessionManager->reset();
echo json_encode(['success' => true]);
exit;
@ -207,13 +207,10 @@ try {
}
// Handle uploaded image
if ($uploadedImage && !$_SESSION['current_image']) {
$currentImage = $sessionManager->getCurrentImage();
if ($uploadedImage && !$currentImage) {
error_log("Processing uploaded image (type: $uploadedImageType)");
// Store the uploaded image directly in session
$_SESSION['current_image'] = $uploadedImage;
$_SESSION['current_image_mime'] = $uploadedImageType;
// If there's a prompt, apply it to the uploaded image
if ($prompt) {
error_log("Applying prompt to uploaded image: $prompt");
@ -225,23 +222,19 @@ try {
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $uploadedImage);
$imageData = $api->extractImageData($response);
// Update session with the edited image
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
// Save edited image to disk
$filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']);
$sessionManager->setCurrentImage($filename, $imageData['mime_type']);
// Add to conversation history
$_SESSION['conversation_history'][] = [
'prompt' => 'Uploaded image + ' . $prompt,
'timestamp' => time(),
'type' => 'upload_edit'
];
$sessionManager->addToHistory('Uploaded image + ' . $prompt, 'upload_edit');
} else {
// Just uploaded, no prompt yet
$_SESSION['conversation_history'][] = [
'prompt' => 'Image uploaded',
'timestamp' => time(),
'type' => 'upload'
];
// Just uploaded, no prompt yet - save to disk
$filename = $sessionManager->saveImage($uploadedImage, $uploadedImageType);
$sessionManager->setCurrentImage($filename, $uploadedImageType);
// Add to conversation history
$sessionManager->addToHistory('Image uploaded', 'upload');
}
echo json_encode([
@ -260,29 +253,19 @@ try {
$api = new NanoBananaProAPI(GEMINI_API_KEY);
// Get current image if editing
$inputImage = $_SESSION['current_image'] ?? null;
$currentImage = $sessionManager->getCurrentImage();
$inputImage = $currentImage ? $currentImage['data'] : null;
// Generate or edit image
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $inputImage);
$imageData = $api->extractImageData($response);
// Save to session
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
// Save to disk
$filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']);
$sessionManager->setCurrentImage($filename, $imageData['mime_type']);
// Add to conversation history
$_SESSION['conversation_history'][] = [
'prompt' => $prompt,
'timestamp' => time(),
'type' => $inputImage ? 'edit' : 'generate'
];
// Add to image history
$_SESSION['image_history'][] = [
'image' => $imageData['base64'],
'prompt' => $prompt,
'timestamp' => time()
];
$sessionManager->addToHistory($prompt, $inputImage ? 'edit' : 'generate');
echo json_encode([
'success' => true,

64
cleanup.php Normal file
View file

@ -0,0 +1,64 @@
<?php
/**
* Cleanup Script for Expired Images
* Run this via cron every hour to delete images older than 24 hours
*
* Cron example (runs every hour):
* 0 * * * * /usr/bin/php /path/to/cleanup.php >> /path/to/cleanup.log 2>&1
*/
require_once 'session_manager.php';
// Check if running from command line or web
$isCLI = php_sapi_name() === 'cli';
if (!$isCLI) {
// If accessed via web, require authentication or disable
// For now, we'll allow it but you should add authentication in production
header('Content-Type: application/json');
}
try {
$result = SessionManager::cleanupExpiredImages();
$output = [
'success' => true,
'message' => "Cleanup completed successfully",
'cleaned_images' => $result['cleaned'],
'errors' => $result['errors'],
'timestamp' => $result['timestamp']
];
if ($isCLI) {
echo "=== Image Cleanup Report ===\n";
echo "Timestamp: {$result['timestamp']}\n";
echo "Images cleaned: {$result['cleaned']}\n";
if (!empty($result['errors'])) {
echo "Errors encountered: " . count($result['errors']) . "\n";
foreach ($result['errors'] as $error) {
echo " - $error\n";
}
}
echo "===========================\n\n";
} else {
echo json_encode($output, JSON_PRETTY_PRINT);
}
exit(0);
} catch (Exception $e) {
$output = [
'success' => false,
'error' => $e->getMessage(),
'timestamp' => date('Y-m-d H:i:s')
];
if ($isCLI) {
echo "ERROR: {$e->getMessage()}\n";
exit(1);
} else {
http_response_code(500);
echo json_encode($output, JSON_PRETTY_PRINT);
exit(1);
}
}

View file

@ -1,19 +1,11 @@
<?php
session_start();
// Initialize session manager for multi-user support
require_once 'session_manager.php';
$sessionManager = new SessionManager();
// Initialize session variables if not set
if (!isset($_SESSION['conversation_history'])) {
$_SESSION['conversation_history'] = [];
}
if (!isset($_SESSION['current_image'])) {
$_SESSION['current_image'] = null;
}
if (!isset($_SESSION['current_image_mime'])) {
$_SESSION['current_image_mime'] = 'image/png';
}
if (!isset($_SESSION['image_history'])) {
$_SESSION['image_history'] = [];
}
// Get current image from disk
$currentImage = $sessionManager->getCurrentImage();
$conversationHistory = $sessionManager->getHistory();
?>
<!DOCTYPE html>
<html lang="en">
@ -774,7 +766,7 @@ if (!isset($_SESSION['image_history'])) {
<h2>Create or Edit Image</h2>
<form id="imageForm">
<?php if (!$_SESSION['current_image']): ?>
<?php if (!$currentImage): ?>
<div class="form-group">
<label for="uploadImage">Upload Your Own Image (Optional)</label>
<input
@ -791,11 +783,11 @@ if (!isset($_SESSION['image_history'])) {
<?php endif; ?>
<div class="form-group">
<label for="prompt">Prompt <?php echo !$_SESSION['current_image'] ? '(Optional if uploading)' : ''; ?></label>
<label for="prompt">Prompt <?php echo !$currentImage ? '(Optional if uploading)' : ''; ?></label>
<textarea
id="prompt"
name="prompt"
placeholder="<?php echo $_SESSION['current_image'] ? 'Enter edit instructions (e.g., "make it red", "add sunset", "remove background")' : 'Describe the image you want to create or edit... Leave empty to just upload. Examples: "A cyberpunk city at night with neon signs", "Make this photo look like a painting"'; ?>"
placeholder="<?php echo $currentImage ? 'Enter edit instructions (e.g., "make it red", "add sunset", "remove background")' : 'Describe the image you want to create or edit... Leave empty to just upload. Examples: "A cyberpunk city at night with neon signs", "Make this photo look like a painting"'; ?>"
></textarea>
</div>
@ -822,17 +814,17 @@ if (!isset($_SESSION['image_history'])) {
</div>
<button type="submit" class="btn" id="generateBtn">
<?php echo $_SESSION['current_image'] ? 'Edit Image' : 'Generate Image'; ?>
<?php echo $currentImage ? 'Edit Image' : 'Generate Image'; ?>
</button>
<?php if ($_SESSION['current_image']): ?>
<?php if ($currentImage): ?>
<button type="button" class="btn btn-secondary" id="resetBtn">
Start New Image
</button>
<?php endif; ?>
</form>
<?php if ($_SESSION['current_image']): ?>
<?php if ($currentImage): ?>
<div class="quick-actions">
<button class="quick-btn" onclick="quickEdit('Add dramatic lighting')">Add Lighting</button>
<button class="quick-btn" onclick="quickEdit('Add sunset in background')">Add Sunset</button>
@ -843,10 +835,10 @@ if (!isset($_SESSION['image_history'])) {
</div>
<?php endif; ?>
<?php if (!empty($_SESSION['conversation_history'])): ?>
<?php if (!empty($conversationHistory)): ?>
<div class="history">
<h2>Conversation History</h2>
<?php foreach (array_reverse($_SESSION['conversation_history']) as $item): ?>
<?php foreach (array_reverse($conversationHistory) as $item): ?>
<div class="history-item">
<div class="history-prompt"><?php echo htmlspecialchars($item['prompt']); ?></div>
<div class="history-time"><?php echo date('g:i A', $item['timestamp']); ?></div>
@ -860,9 +852,9 @@ if (!isset($_SESSION['image_history'])) {
<div class="panel">
<h2>Generated Image</h2>
<div class="image-display <?php echo $_SESSION['current_image'] ? 'has-image' : ''; ?>" id="imageDisplay">
<?php if ($_SESSION['current_image']): ?>
<img src="data:<?php echo $_SESSION['current_image_mime']; ?>;base64,<?php echo $_SESSION['current_image']; ?>" alt="Generated Image" id="currentImage">
<div class="image-display <?php echo $currentImage ? 'has-image' : ''; ?>" id="imageDisplay">
<?php if ($currentImage): ?>
<img src="data:<?php echo $currentImage['mime_type']; ?>;base64,<?php echo $currentImage['data']; ?>" alt="Generated Image" id="currentImage">
<?php else: ?>
<div class="placeholder">
<div class="placeholder-icon">🎨</div>
@ -871,7 +863,7 @@ if (!isset($_SESSION['image_history'])) {
<?php endif; ?>
</div>
<?php if ($_SESSION['current_image']): ?>
<?php if ($currentImage): ?>
<div class="image-actions">
<button class="btn" onclick="downloadImage()">Download Image</button>
</div>
@ -887,11 +879,12 @@ if (!isset($_SESSION['image_history'])) {
<div class="debug-section">
<h4>Session Status</h4>
<pre>Has Current Image: <?php echo $_SESSION['current_image'] ? 'YES' : 'NO'; ?>
Image MIME Type: <?php echo $_SESSION['current_image_mime'] ?? 'Not Set'; ?>
Image Data Length: <?php echo $_SESSION['current_image'] ? strlen($_SESSION['current_image']) . ' chars' : '0'; ?>
Conversation History: <?php echo count($_SESSION['conversation_history']); ?> items
Image History: <?php echo count($_SESSION['image_history']); ?> items</pre>
<pre>Session ID: <?php echo $sessionManager->getSessionId(); ?>
Has Current Image: <?php echo $currentImage ? 'YES' : 'NO'; ?>
Image MIME Type: <?php echo $currentImage ? $currentImage['mime_type'] : 'Not Set'; ?>
Image Data Length: <?php echo $currentImage ? strlen($currentImage['data']) . ' chars' : '0'; ?>
Conversation History: <?php echo count($conversationHistory); ?> items
Session Directory: <?php echo basename($sessionManager->getSessionDir()); ?></pre>
<button class="quick-btn" onclick="loadServerLogs()" style="margin-top: 10px;">📋 Load Server Logs</button>
</div>
@ -900,11 +893,11 @@ Image History: <?php echo count($_SESSION['image_history']); ?> items</pre>
<pre id="serverLogsData"></pre>
</div>
<?php if (!empty($_SESSION['conversation_history'])): ?>
<?php if (!empty($conversationHistory)): ?>
<div class="debug-section">
<h4>Recent Prompts</h4>
<pre><?php
$recent = array_slice($_SESSION['conversation_history'], -3);
$recent = array_slice($conversationHistory, -3);
foreach ($recent as $item) {
echo date('H:i:s', $item['timestamp']) . ' [' . $item['type'] . ']: ' . htmlspecialchars($item['prompt']) . "\n";
}
@ -948,7 +941,7 @@ Image History: <?php echo count($_SESSION['image_history']); ?> items</pre>
// Check if we have either a prompt or an upload
const hasUploadFile = uploadInput && uploadInput.files && uploadInput.files[0];
const hasExistingImage = <?php echo $_SESSION['current_image'] ? 'true' : 'false'; ?>;
const hasExistingImage = <?php echo $currentImage ? 'true' : 'false'; ?>;
if (!prompt && !hasUploadFile && !hasExistingImage) {
showError('Please enter a prompt or upload an image');
@ -991,7 +984,7 @@ Image History: <?php echo count($_SESSION['image_history']); ?> items</pre>
prompt: prompt,
aspectRatio: aspectRatio,
imageSize: imageSize,
hasExistingImage: <?php echo $_SESSION['current_image'] ? 'true' : 'false'; ?>,
hasExistingImage: <?php echo $currentImage ? 'true' : 'false'; ?>,
hasUpload: hasUpload
});
@ -1014,17 +1007,17 @@ Image History: <?php echo count($_SESSION['image_history']); ?> items</pre>
showError(result.error || 'Failed to generate image');
logDebug('error', result.error || 'Failed to generate image');
generateBtn.disabled = false;
generateBtn.textContent = '<?php echo $_SESSION['current_image'] ? 'Edit Image' : 'Generate Image'; ?>';
generateBtn.textContent = '<?php echo $currentImage ? 'Edit Image' : 'Generate Image'; ?>';
}
} catch (error) {
showError('Network error: ' + error.message);
logDebug('error', 'Network error: ' + error.message);
generateBtn.disabled = false;
generateBtn.textContent = '<?php echo $_SESSION['current_image'] ? 'Edit Image' : 'Generate Image'; ?>';
generateBtn.textContent = '<?php echo $currentImage ? 'Edit Image' : 'Generate Image'; ?>';
}
});
<?php if ($_SESSION['current_image']): ?>
<?php if ($currentImage): ?>
resetBtn.addEventListener('click', async () => {
if (confirm('Are you sure you want to start a new image? This will clear your current image and history.')) {
const formData = new FormData();

314
session_manager.php Normal file
View file

@ -0,0 +1,314 @@
<?php
/**
* Session Manager for Multi-User Support
* Handles isolated sessions, file-based image storage, and automatic cleanup
*/
class SessionManager {
private $sessionId;
private $uploadDir;
private $sessionDir;
public function __construct() {
// Configure session for better security
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_lifetime', 86400); // 24 hours
ini_set('session.gc_maxlifetime', 86400); // 24 hours
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Regenerate session ID on first access for security
if (!isset($_SESSION['initialized'])) {
session_regenerate_id(true);
$_SESSION['initialized'] = true;
$_SESSION['created_at'] = time();
}
$this->sessionId = session_id();
$this->uploadDir = __DIR__ . '/uploads/sessions';
$this->sessionDir = $this->uploadDir . '/' . $this->sessionId;
// Create session directory if it doesn't exist
if (!is_dir($this->sessionDir)) {
mkdir($this->sessionDir, 0755, true);
mkdir($this->sessionDir . '/images', 0755, true);
}
// Initialize session data
$this->initializeSessionData();
}
private function initializeSessionData() {
if (!isset($_SESSION['conversation_history'])) {
$_SESSION['conversation_history'] = [];
}
if (!isset($_SESSION['image_history'])) {
$_SESSION['image_history'] = [];
}
if (!isset($_SESSION['current_image_path'])) {
$_SESSION['current_image_path'] = null;
}
}
public function getSessionId() {
return $this->sessionId;
}
public function getSessionDir() {
return $this->sessionDir;
}
public function getImagesDir() {
return $this->sessionDir . '/images';
}
/**
* Save image to disk and return file path
*/
public function saveImage($imageData, $mimeType = 'image/png') {
$extension = $this->getExtensionFromMime($mimeType);
$timestamp = time();
$filename = 'image_' . $timestamp . '_' . uniqid() . '.' . $extension;
$filepath = $this->getImagesDir() . '/' . $filename;
// Decode base64 if needed
if (base64_decode($imageData, true) !== false) {
$imageData = base64_decode($imageData);
}
// Save image
if (file_put_contents($filepath, $imageData) === false) {
throw new Exception('Failed to save image to disk');
}
// Save metadata
$metadataFile = $filepath . '.meta';
$metadata = [
'created_at' => $timestamp,
'mime_type' => $mimeType,
'session_id' => $this->sessionId,
'expires_at' => $timestamp + 86400 // 24 hours
];
file_put_contents($metadataFile, json_encode($metadata));
return $filename;
}
/**
* Get image data from disk
*/
public function getImage($filename) {
$filepath = $this->getImagesDir() . '/' . $filename;
if (!file_exists($filepath)) {
return null;
}
// Check if image has expired
$metadataFile = $filepath . '.meta';
if (file_exists($metadataFile)) {
$metadata = json_decode(file_get_contents($metadataFile), true);
if ($metadata && $metadata['expires_at'] < time()) {
// Image expired, delete it
$this->deleteImage($filename);
return null;
}
}
return [
'data' => base64_encode(file_get_contents($filepath)),
'mime_type' => $this->getMimeTypeFromFile($filepath)
];
}
/**
* Delete image from disk
*/
public function deleteImage($filename) {
$filepath = $this->getImagesDir() . '/' . $filename;
$metadataFile = $filepath . '.meta';
if (file_exists($filepath)) {
unlink($filepath);
}
if (file_exists($metadataFile)) {
unlink($metadataFile);
}
}
/**
* Set current image
*/
public function setCurrentImage($filename, $mimeType = 'image/png') {
$_SESSION['current_image_path'] = $filename;
$_SESSION['current_image_mime'] = $mimeType;
}
/**
* Get current image
*/
public function getCurrentImage() {
if (!isset($_SESSION['current_image_path']) || !$_SESSION['current_image_path']) {
return null;
}
return $this->getImage($_SESSION['current_image_path']);
}
/**
* Clear current image
*/
public function clearCurrentImage() {
if (isset($_SESSION['current_image_path']) && $_SESSION['current_image_path']) {
$this->deleteImage($_SESSION['current_image_path']);
}
$_SESSION['current_image_path'] = null;
$_SESSION['current_image_mime'] = 'image/png';
}
/**
* Add to conversation history
*/
public function addToHistory($prompt, $type = 'edit') {
$_SESSION['conversation_history'][] = [
'prompt' => $prompt,
'type' => $type,
'timestamp' => time()
];
// Keep only last 50 items
if (count($_SESSION['conversation_history']) > 50) {
$_SESSION['conversation_history'] = array_slice($_SESSION['conversation_history'], -50);
}
}
/**
* Get conversation history
*/
public function getHistory() {
return $_SESSION['conversation_history'] ?? [];
}
/**
* Clear all session data and images
*/
public function reset() {
// Delete all images in session directory
$imagesDir = $this->getImagesDir();
if (is_dir($imagesDir)) {
$files = glob($imagesDir . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
// Clear session data
$_SESSION['conversation_history'] = [];
$_SESSION['image_history'] = [];
$_SESSION['current_image_path'] = null;
$_SESSION['current_image_mime'] = 'image/png';
}
/**
* Get file extension from MIME type
*/
private function getExtensionFromMime($mimeType) {
$mimeMap = [
'image/jpeg' => 'jpg',
'image/jpg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif'
];
return $mimeMap[$mimeType] ?? 'png';
}
/**
* Get MIME type from file
*/
private function getMimeTypeFromFile($filepath) {
$extension = strtolower(pathinfo($filepath, PATHINFO_EXTENSION));
$extMap = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif'
];
return $extMap[$extension] ?? 'image/png';
}
/**
* Static method to clean up expired images across all sessions
*/
public static function cleanupExpiredImages($uploadDir = null) {
if ($uploadDir === null) {
$uploadDir = __DIR__ . '/uploads/sessions';
}
if (!is_dir($uploadDir)) {
return ['cleaned' => 0, 'errors' => []];
}
$cleaned = 0;
$errors = [];
$currentTime = time();
// Scan all session directories
$sessionDirs = glob($uploadDir . '/*', GLOB_ONLYDIR);
foreach ($sessionDirs as $sessionDir) {
$imagesDir = $sessionDir . '/images';
if (!is_dir($imagesDir)) {
continue;
}
// Find all metadata files
$metadataFiles = glob($imagesDir . '/*.meta');
foreach ($metadataFiles as $metadataFile) {
try {
$metadata = json_decode(file_get_contents($metadataFile), true);
// Check if expired
if ($metadata && isset($metadata['expires_at']) && $metadata['expires_at'] < $currentTime) {
// Delete image and metadata
$imageFile = str_replace('.meta', '', $metadataFile);
if (file_exists($imageFile)) {
unlink($imageFile);
}
unlink($metadataFile);
$cleaned++;
}
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
}
// Remove empty session directories
$remainingFiles = glob($imagesDir . '/*');
if (empty($remainingFiles)) {
rmdir($imagesDir);
rmdir($sessionDir);
}
}
return [
'cleaned' => $cleaned,
'errors' => $errors,
'timestamp' => date('Y-m-d H:i:s')
];
}
}

11
uploads/.htaccess Normal file
View file

@ -0,0 +1,11 @@
# Secure uploads directory
# Prevent direct access to uploaded images via browser
# Deny access to all files
Order Deny,Allow
Deny from all
# Allow PHP scripts to access files
<FilesMatch "\.php$">
Allow from all
</FilesMatch>