diff --git a/Dockerfile b/Dockerfile
index 3cd2a1b..abc64e5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,8 +16,8 @@ RUN npm ci --omit=dev && npm cache clean --force
# Copy the rest of the application code
COPY . .
-# Create uploads directory
-RUN mkdir -p public/uploads
+# Create uploads directory structure
+RUN mkdir -p public/uploads/images-driver public/uploads/images-circuit
# Expose the port the app runs on
EXPOSE 3001
diff --git a/docker-compose.yml b/docker-compose.yml
index fa9cc13..208a440 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -45,7 +45,7 @@ services:
- LOG_LEVEL=${LOG_LEVEL:-info}
- NODE_ENV=${NODE_ENV:-production}
volumes:
- - uploads_shared:/usr/src/app/public/uploads
+ - ./public/uploads:/usr/src/app/public/uploads
- ./logs:/usr/src/app/logs
depends_on:
db:
@@ -79,7 +79,7 @@ services:
- ./config/Caddyfile.http:/etc/caddy/Caddyfile:ro
# For HTTPS production mode (Let's Encrypt):
# - ./config/Caddyfile.https-production:/etc/caddy/Caddyfile:ro
- - uploads_shared:/var/www/html/uploads:ro
+ - ./public/uploads:/var/www/html/uploads:ro
- caddy_data:/data
- caddy_config:/config
- caddy_logs:/var/log/caddy
@@ -102,7 +102,6 @@ volumes:
caddy_data:
caddy_config:
caddy_logs:
- uploads_shared:
networks:
app-network:
diff --git a/package-lock.json b/package-lock.json
index c71c68e..bd2131d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.1",
"license": "ISC",
"dependencies": {
+ "connect-pg-simple": "^10.0.0",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-session": "^1.18.2",
@@ -2769,6 +2770,18 @@
"typedarray": "^0.0.6"
}
},
+ "node_modules/connect-pg-simple": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
+ "integrity": "sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==",
+ "license": "MIT",
+ "dependencies": {
+ "pg": "^8.12.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=22.0.0"
+ }
+ },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
diff --git a/package.json b/package.json
index ca69c9c..482b92f 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
+ "connect-pg-simple": "^10.0.0",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-session": "^1.18.2",
diff --git a/public/index.html b/public/index.html
index 0395e5e..fec5b7e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -108,6 +108,10 @@
ADD NEW DRIVER
@@ -142,15 +146,19 @@ EDIT DRIVER
No profile picture uploaded
+
+
![New profile picture preview]()
+
Preview - New Profile Picture
+
-
-
+
Max file size: 5MB. Supported formats: JPG, PNG, GIF, WebP
+
+
![New circuit image preview]()
+
Preview - New Circuit Image
+
UPLOAD IMAGE
@@ -200,7 +212,7 @@ RACE SETTINGS
DELETE
-
+
Max file size: 5MB. Supported formats: JPG, PNG, GIF, WebP
diff --git a/public/js/forms.js b/public/js/forms.js
index a1e74a9..775532d 100644
--- a/public/js/forms.js
+++ b/public/js/forms.js
@@ -4,6 +4,7 @@ import {
createDriver,
uploadProfilePicture,
} from './api.js'
+import { clearPreview } from './imagePreview.js'
import { loadLeaderboard } from './leaderboard.js'
import { closeAddDriverModal, closeEditDriverModal } from './modals.js'
import { showNotification } from './notifications.js'
@@ -76,6 +77,13 @@ export async function handleEditDriver(event) {
try {
await apiUpdateDriver(driverId, driverData)
+
+ // If there's a new profile picture, upload it
+ const profilePictureFile = formData.get('profilePicture')
+ if (profilePictureFile && profilePictureFile.size > 0) {
+ await uploadProfilePictureForDriver(driverId, profilePictureFile)
+ }
+
closeEditDriverModal()
await loadLeaderboard()
showNotification('Driver updated successfully!', 'success')
@@ -122,13 +130,47 @@ export async function deleteDriver(driverId) {
// Upload profile picture for a driver
async function uploadProfilePictureForDriver(driverId, file) {
try {
- await uploadProfilePicture(driverId, file)
+ const result = await uploadProfilePicture(driverId, file)
+
+ // If we're in the edit modal, update the current picture display immediately
+ const editDriverId = document.getElementById('editDriverId')
+ if (editDriverId && editDriverId.value === driverId.toString()) {
+ updateEditModalProfilePicture(result.profilePicture)
+ // Clear the preview since we now show the actual uploaded image
+ clearPreview('editPicturePreviewContainer', 'editPicturePreview')
+ }
+
+ // Also clear preview for add modal if applicable
+ const addModal = document.getElementById('addDriverModal')
+ if (addModal && addModal.style.display === 'block') {
+ clearPreview('addPicturePreviewContainer', 'addPicturePreview')
+ }
} catch (_error) {
showNotification('Failed to upload profile picture', 'warning')
// Don't throw - we don't want to prevent driver creation if picture upload fails
}
}
+// Update the profile picture display in the edit modal
+function updateEditModalProfilePicture(profilePictureUrl) {
+ const currentPicture = document.getElementById('currentPicture')
+ const noPictureText = document.getElementById('noPictureText')
+ const deletePictureBtn = document.getElementById('deletePictureBtn')
+
+ if (currentPicture && profilePictureUrl) {
+ currentPicture.src = profilePictureUrl
+ currentPicture.style.display = 'block'
+
+ if (noPictureText) {
+ noPictureText.style.display = 'none'
+ }
+
+ if (deletePictureBtn) {
+ deletePictureBtn.style.display = 'inline-flex'
+ }
+ }
+}
+
// Delete profile picture
export async function deleteProfilePicture() {
const driverId = document.getElementById('editDriverId').value
diff --git a/public/js/imagePreview.js b/public/js/imagePreview.js
new file mode 100644
index 0000000..ba458c1
--- /dev/null
+++ b/public/js/imagePreview.js
@@ -0,0 +1,114 @@
+// Image preview functionality for profile pictures
+
+import { uploadCircuitImage } from './api.js'
+import { showNotification } from './notifications.js'
+import { loadRaceSettings, populateSettingsModal } from './settings.js'
+
+/**
+ * Setup image preview for file input
+ * @param {string} fileInputId - ID of the file input element
+ * @param {string} previewContainerId - ID of the preview container
+ * @param {string} previewImageId - ID of the preview image element
+ */
+export function setupImagePreview(fileInputId, previewContainerId, previewImageId) {
+ const fileInput = document.getElementById(fileInputId)
+ const previewContainer = document.getElementById(previewContainerId)
+ const previewImage = document.getElementById(previewImageId)
+
+ if (!fileInput || !previewContainer || !previewImage) {
+ return
+ }
+
+ fileInput.addEventListener('change', async (event) => {
+ const file = event.target.files[0]
+
+ if (file) {
+ // Validate file type
+ if (!file.type.startsWith('image/')) {
+ showNotification('Please select a valid image file', 'error')
+ clearPreview(previewContainerId, previewImageId)
+ return
+ }
+
+ // Validate file size (5MB limit)
+ const maxSize = 5 * 1024 * 1024 // 5MB in bytes
+ if (file.size > maxSize) {
+ showNotification('File size must be less than 5MB', 'error')
+ clearPreview(previewContainerId, previewImageId)
+ fileInput.value = '' // Clear the input
+ return
+ }
+
+ // Create and display preview
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ previewImage.src = e.target.result
+ previewContainer.style.display = 'block'
+ }
+ reader.readAsDataURL(file)
+
+ // For circuit images, upload immediately
+ if (fileInputId === 'circuitImageUpload') {
+ try {
+ await uploadCircuitImage(file)
+ showNotification('Circuit image uploaded successfully!', 'success')
+
+ // Refresh the race settings and modal
+ await loadRaceSettings()
+ populateSettingsModal()
+
+ // Clear the preview since we now show the actual uploaded image
+ clearPreview(previewContainerId, previewImageId)
+ fileInput.value = '' // Clear the file input
+ } catch (_error) {
+ showNotification('Failed to upload circuit image. Please try again.', 'error')
+ clearPreview(previewContainerId, previewImageId)
+ fileInput.value = '' // Clear the file input
+ }
+ }
+ } else {
+ clearPreview(previewContainerId, previewImageId)
+ }
+ })
+}
+
+/**
+ * Clear image preview
+ * @param {string} previewContainerId - ID of the preview container
+ * @param {string} previewImageId - ID of the preview image element
+ */
+export function clearPreview(previewContainerId, previewImageId) {
+ const previewContainer = document.getElementById(previewContainerId)
+ const previewImage = document.getElementById(previewImageId)
+
+ if (previewContainer) {
+ previewContainer.style.display = 'none'
+ }
+
+ if (previewImage) {
+ previewImage.src = ''
+ }
+}
+
+/**
+ * Clear all previews (useful when modals are closed)
+ */
+export function clearAllPreviews() {
+ clearPreview('addPicturePreviewContainer', 'addPicturePreview')
+ clearPreview('editPicturePreviewContainer', 'editPicturePreview')
+ clearPreview('circuitImagePreviewContainer', 'circuitImagePreview')
+}
+
+/**
+ * Setup all image previews for the application
+ */
+export function initializeImagePreviews() {
+ // Setup preview for add driver modal
+ setupImagePreview('profilePictureAdd', 'addPicturePreviewContainer', 'addPicturePreview')
+
+ // Setup preview for edit driver modal
+ setupImagePreview('profilePictureEdit', 'editPicturePreviewContainer', 'editPicturePreview')
+
+ // Setup preview for circuit image upload
+ setupImagePreview('circuitImageUpload', 'circuitImagePreviewContainer', 'circuitImagePreview')
+}
diff --git a/public/js/main.js b/public/js/main.js
index a07eff7..dfbdf52 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -1,5 +1,6 @@
import { checkAuthStatus, logout } from './auth.js'
import { deleteDriver, deleteProfilePicture, setupFormEventListeners } from './forms.js'
+import { initializeImagePreviews } from './imagePreview.js'
import { loadLeaderboard } from './leaderboard.js'
import {
closeAddDriverModal,
@@ -10,6 +11,7 @@ import {
showEditDriverModal,
showSettingsModal,
triggerCircuitImageUpload,
+ triggerEditFileUpload,
triggerFileUpload,
} from './modals.js'
import {
@@ -26,6 +28,7 @@ function initializeApp() {
loadRaceSettings()
loadLeaderboard()
setupEventListeners()
+ initializeImagePreviews()
}
// Setup all event listeners
@@ -131,6 +134,9 @@ function handleAction(action) {
case 'triggerFileUpload':
triggerFileUpload()
break
+ case 'triggerEditFileUpload':
+ triggerEditFileUpload()
+ break
case 'deleteProfilePicture':
deleteProfilePicture()
break
diff --git a/public/js/modals.js b/public/js/modals.js
index e14a025..1f5f078 100644
--- a/public/js/modals.js
+++ b/public/js/modals.js
@@ -1,3 +1,4 @@
+import { clearPreview } from './imagePreview.js'
import { showNotification } from './notifications.js'
import { findDriverById, getIsAuthorized } from './state.js'
@@ -16,6 +17,7 @@ export function showAddDriverModal() {
export function closeAddDriverModal() {
document.getElementById('addDriverModal').style.display = 'none'
document.getElementById('addDriverForm').reset()
+ clearPreview('addPicturePreviewContainer', 'addPicturePreview')
}
// Show edit driver modal
@@ -56,6 +58,7 @@ export function showEditDriverModal(driverId) {
export function closeEditDriverModal() {
document.getElementById('editDriverModal').style.display = 'none'
document.getElementById('editDriverForm').reset()
+ clearPreview('editPicturePreviewContainer', 'editPicturePreview')
}
// Show settings modal
@@ -83,6 +86,7 @@ export function showSettingsModal() {
export function closeSettingsModal() {
document.getElementById('settingsModal').style.display = 'none'
document.getElementById('settingsForm').reset()
+ clearPreview('circuitImagePreviewContainer', 'circuitImagePreview')
}
// Setup modal event listeners
@@ -110,6 +114,10 @@ export function triggerFileUpload() {
document.getElementById('profilePictureInput').click()
}
+export function triggerEditFileUpload() {
+ document.getElementById('profilePictureEdit').click()
+}
+
export function triggerCircuitImageUpload() {
- document.getElementById('circuitImageInput').click()
+ document.getElementById('circuitImageUpload').click()
}
diff --git a/public/styles.css b/public/styles.css
index 417944d..a92b62d 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -817,6 +817,47 @@ html {
margin-bottom: 10px;
}
+/* Picture Preview Styles */
+.picture-preview-container {
+ margin-bottom: 15px;
+ text-align: center;
+ padding: 15px;
+ border: 2px dashed #ff1e1e;
+ border-radius: 8px;
+ background: rgba(255, 30, 30, 0.1);
+ animation: fadeIn 0.3s ease-in;
+}
+
+.picture-preview-container img {
+ max-width: 150px;
+ max-height: 150px;
+ border-radius: 8px;
+ border: 2px solid #ff1e1e;
+ object-fit: cover;
+ display: block;
+ margin: 0 auto;
+}
+
+.preview-label {
+ color: #ff1e1e;
+ font-weight: 600;
+ font-size: 0.9em;
+ margin: 8px 0 0 0;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
.form-group input:focus {
outline: none;
border-color: #ff1e1e;
diff --git a/src/middleware/upload.js b/src/middleware/upload.js
index 60b6c9e..86a07ce 100644
--- a/src/middleware/upload.js
+++ b/src/middleware/upload.js
@@ -1,15 +1,17 @@
const multer = require('multer')
const path = require('node:path')
const fs = require('node:fs')
+const { v4: uuidv4 } = require('uuid')
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: (_req, file, cb) => {
let uploadDir
if (file.fieldname === 'circuitImage') {
- uploadDir = path.join(__dirname, '../../public/uploads/circuits')
+ uploadDir = path.join(__dirname, '../../public/uploads/images-circuit')
} else {
- uploadDir = path.join(__dirname, '../../public/uploads')
+ // For profilePicture and other driver-related images
+ uploadDir = path.join(__dirname, '../../public/uploads/images-driver')
}
// Ensure upload directory exists
@@ -19,9 +21,10 @@ const storage = multer.diskStorage({
cb(null, uploadDir)
},
filename: (_req, file, cb) => {
- // Generate unique filename
- const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`
- cb(null, `${file.fieldname}-${uniqueSuffix}${path.extname(file.originalname)}`)
+ // Generate GUID-based filename to avoid weird characters
+ const guid = uuidv4()
+ const extension = path.extname(file.originalname).toLowerCase()
+ cb(null, `${guid}${extension}`)
},
})
diff --git a/src/routes/leaderboard.js b/src/routes/leaderboard.js
index 797ec85..f937866 100644
--- a/src/routes/leaderboard.js
+++ b/src/routes/leaderboard.js
@@ -74,7 +74,7 @@ router.post(
}
// Update with new profile picture path
- const profilePicturePath = `/uploads/${req.file.filename}`
+ const profilePicturePath = `/uploads/images-driver/${req.file.filename}`
await entry.update({ profilePicture: profilePicturePath })
logger.logFileOperation('upload', req.file.filename, true, {
diff --git a/src/routes/raceSettings.js b/src/routes/raceSettings.js
index af0f73f..f80889a 100644
--- a/src/routes/raceSettings.js
+++ b/src/routes/raceSettings.js
@@ -95,7 +95,7 @@ router.post('/circuit-image', isAuthenticated, upload.single('circuitImage'), as
}
// Update with new circuit image path
- const circuitImagePath = `/uploads/circuits/${req.file.filename}`
+ const circuitImagePath = `/uploads/images-circuit/${req.file.filename}`
await settings.update({ circuitImage: circuitImagePath })
res.json({
diff --git a/src/utils/appConfig.js b/src/utils/appConfig.js
index 85bdef6..1cf3b18 100644
--- a/src/utils/appConfig.js
+++ b/src/utils/appConfig.js
@@ -1,7 +1,10 @@
const express = require('express')
const path = require('node:path')
const session = require('express-session')
+const pgSession = require('connect-pg-simple')(session)
+const { Pool } = require('pg')
const { passport } = require('../config/auth')
+const logger = require('./logger')
function configureApp() {
const app = express()
@@ -13,9 +16,32 @@ function configureApp() {
app.use(express.json())
app.use(express.static(path.join(__dirname, '../../public')))
- // Session configuration
+ // Create PostgreSQL connection pool for sessions
+ const pgPool = new Pool({
+ host: process.env.DB_HOST,
+ port: process.env.DB_PORT || 5432,
+ database: process.env.DB_NAME,
+ user: process.env.DB_USER,
+ password: process.env.DB_PASSWORD,
+ })
+
+ // Handle pool errors
+ pgPool.on('error', (err) => {
+ logger.logError(err, { context: 'Session store database connection error' })
+ })
+
+ // Create session store
+ const sessionStore = new pgSession({
+ pool: pgPool,
+ tableName: 'session', // Table name for storing sessions
+ createTableIfMissing: true, // Auto-create table if it doesn't exist
+ pruneSessionInterval: 60 * 15, // Prune expired sessions every 15 minutes
+ })
+
+ // Session configuration with PostgreSQL store
app.use(
session({
+ store: sessionStore,
secret: process.env.SESSION_SECRET || 'fallback-secret-key',
resave: false,
saveUninitialized: false,
@@ -23,9 +49,12 @@ function configureApp() {
secure: false, // Set to true in production with HTTPS
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
+ name: 'sessionId', // Rename the session cookie for security
})
)
+ logger.info('Session store configured with PostgreSQL backend')
+
// Initialize Passport
app.use(passport.initialize())
app.use(passport.session())