From 7cb7888319498f44093ecfa957e9fd865d79d7a4 Mon Sep 17 00:00:00 2001 From: q-pie Date: Mon, 14 Jul 2025 06:47:11 +0300 Subject: [PATCH 1/4] Implement complete backend property management system - Restructured project with separate backend/ and public/ directories - Added Express.js backend with SQLite database for property management - Implemented full CRUD operations for properties (lands, cars, apartments) - Added admin authentication system with JWT tokens - Created contact form functionality with message management - Added image upload system with category-based storage - Implemented admin dashboard with property and message management - Added proper static file serving and API endpoints - Fixed all file paths and references after restructuring - Added comprehensive error handling and validation Dependencies installed: - express: Web application framework - cors: Cross-origin resource sharing - dotenv: Environment variable management - sqlite3: Database management - multer: File upload handling - bcryptjs: Password hashing - jsonwebtoken: JWT authentication - nodemon: Development auto-restart (dev dependency) The application now has: - Frontend: HTML/CSS/JS property listings with dynamic loading - Backend API: RESTful endpoints for all operations - Admin panel: Complete property and message management - Database: SQLite with proper schema and relationships - File uploads: Organized by property category - Authentication: Secure admin access with JWT Amp-Thread: https://ampcode.com/threads/T-1f1e5d13-066a-49eb-aea3-845a79b68df5 Co-authored-by: Amp --- .gitignore | 57 + admin.html | 36 - backend/controllers/authController.js | 161 + backend/controllers/contactController.js | 253 ++ .../controllers/contactController.js.broken | 431 +++ backend/controllers/propertyController.js | 350 ++ backend/middleware/auth.js | 67 + backend/models/database.js | 173 + backend/routes/auth.js | 15 + backend/routes/contact.js | 16 + backend/routes/properties.js | 49 + backend/server.js | 174 + backend/uploads/.gitkeep | 0 backend/utils/errorHandler.js | 18 + package-lock.json | 2918 +++++++++++++++++ package.json | 31 + public/admin/admin.css | 706 ++++ public/admin/admin.html | 336 ++ public/admin/admin.js | 1049 ++++++ {assets => public/assets}/goodroads.jpeg | Bin {assets => public/assets}/icon.jpeg | Bin {assets => public/assets}/land1.jpeg | Bin {assets => public/assets}/land2.jpeg | Bin {assets => public/assets}/land3.jpeg | Bin {assets => public/assets}/land4.jpeg | Bin {assets => public/assets}/land5.jpeg | Bin {assets => public/assets}/land6.jpeg | Bin {assets => public/assets}/location.jpg | Bin {assets => public/assets}/office1.jpeg | Bin {assets => public/assets}/office2.jpeg | Bin {assets => public/assets}/office3.jpeg | Bin {assets => public/assets}/phone.svg | 0 {assets => public/assets}/staff1.jpeg | Bin {assets => public/assets}/staff2.jpeg | Bin {assets => public/assets}/staff3.jpeg | Bin {assets => public/assets}/whatsapp.svg | 0 contact.html => public/contact.html | 15 +- style.css => public/css/style.css | 0 index.html => public/index.html | 79 +- public/js/script.js | 67 + public/js/script.js.backup | 132 + public/js/script.js.temp | 132 + public/js/temp_stats_fix.js | 41 + script.js | 61 - 44 files changed, 7228 insertions(+), 139 deletions(-) create mode 100644 .gitignore delete mode 100644 admin.html create mode 100644 backend/controllers/authController.js create mode 100644 backend/controllers/contactController.js create mode 100644 backend/controllers/contactController.js.broken create mode 100644 backend/controllers/propertyController.js create mode 100644 backend/middleware/auth.js create mode 100644 backend/models/database.js create mode 100644 backend/routes/auth.js create mode 100644 backend/routes/contact.js create mode 100644 backend/routes/properties.js create mode 100644 backend/server.js create mode 100644 backend/uploads/.gitkeep create mode 100644 backend/utils/errorHandler.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/admin/admin.css create mode 100644 public/admin/admin.html create mode 100644 public/admin/admin.js rename {assets => public/assets}/goodroads.jpeg (100%) rename {assets => public/assets}/icon.jpeg (100%) rename {assets => public/assets}/land1.jpeg (100%) rename {assets => public/assets}/land2.jpeg (100%) rename {assets => public/assets}/land3.jpeg (100%) rename {assets => public/assets}/land4.jpeg (100%) rename {assets => public/assets}/land5.jpeg (100%) rename {assets => public/assets}/land6.jpeg (100%) rename {assets => public/assets}/location.jpg (100%) rename {assets => public/assets}/office1.jpeg (100%) rename {assets => public/assets}/office2.jpeg (100%) rename {assets => public/assets}/office3.jpeg (100%) rename {assets => public/assets}/phone.svg (100%) rename {assets => public/assets}/staff1.jpeg (100%) rename {assets => public/assets}/staff2.jpeg (100%) rename {assets => public/assets}/staff3.jpeg (100%) rename {assets => public/assets}/whatsapp.svg (100%) rename contact.html => public/contact.html (91%) rename style.css => public/css/style.css (100%) rename index.html => public/index.html (76%) create mode 100644 public/js/script.js create mode 100644 public/js/script.js.backup create mode 100644 public/js/script.js.temp create mode 100644 public/js/temp_stats_fix.js delete mode 100644 script.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01810e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Database +database/*.db +*.sqlite +*.sqlite3 + +# Uploads (keep structure, ignore files) +backend/uploads/* +!backend/uploads/.gitkeep +!backend/uploads/lands/.gitkeep +!backend/uploads/cars/.gitkeep +!backend/uploads/apartments/.gitkeep + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +tmp/ +temp/ diff --git a/admin.html b/admin.html deleted file mode 100644 index 12a797a..0000000 --- a/admin.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Admin Login - Good Foot Properties - - - - - - - - diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js new file mode 100644 index 0000000..5dc244c --- /dev/null +++ b/backend/controllers/authController.js @@ -0,0 +1,161 @@ +const bcrypt = require('bcryptjs'); +const database = require('../models/database'); +const { generateToken } = require('../middleware/auth'); + +class AuthController { + // Admin login + async login(req, res) { + try { + const { username, password } = req.body; + + // Validate input + if (!username || !password) { + return res.status(400).json({ + error: 'Missing credentials', + message: 'Username and password are required' + }); + } + + // Find admin user + const admin = await database.get( + 'SELECT * FROM admin_users WHERE username = ?', + [username] + ); + + if (!admin) { + return res.status(401).json({ + error: 'Invalid credentials', + message: 'Username or password is incorrect' + }); + } + + // Verify password + const isValidPassword = bcrypt.compareSync(password, admin.password_hash); + + if (!isValidPassword) { + return res.status(401).json({ + error: 'Invalid credentials', + message: 'Username or password is incorrect' + }); + } + + // Update last login + await database.run( + 'UPDATE admin_users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', + [admin.id] + ); + + // Generate JWT token + const token = generateToken({ + id: admin.id, + username: admin.username, + role: 'admin' + }); + + res.json({ + success: true, + message: 'Login successful', + token, + user: { + id: admin.id, + username: admin.username, + role: 'admin' + } + }); + + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + error: 'Login failed', + message: 'An error occurred during login. Please try again.' + }); + } + } + + // Verify token endpoint + async verifyToken(req, res) { + try { + // If we reach here, token is valid (middleware passed) + res.json({ + success: true, + message: 'Token is valid', + user: req.user + }); + } catch (error) { + console.error('Token verification error:', error); + res.status(500).json({ + error: 'Verification failed', + message: 'Unable to verify token' + }); + } + } + + // Change password + async changePassword(req, res) { + try { + const { currentPassword, newPassword } = req.body; + const userId = req.user.id; + + // Validate input + if (!currentPassword || !newPassword) { + return res.status(400).json({ + error: 'Missing passwords', + message: 'Current password and new password are required' + }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ + error: 'Password too short', + message: 'New password must be at least 6 characters long' + }); + } + + // Get current admin + const admin = await database.get( + 'SELECT * FROM admin_users WHERE id = ?', + [userId] + ); + + if (!admin) { + return res.status(404).json({ + error: 'User not found', + message: 'Admin user not found' + }); + } + + // Verify current password + const isValidPassword = bcrypt.compareSync(currentPassword, admin.password_hash); + + if (!isValidPassword) { + return res.status(401).json({ + error: 'Invalid current password', + message: 'Current password is incorrect' + }); + } + + // Hash new password + const newPasswordHash = bcrypt.hashSync(newPassword, 10); + + // Update password + await database.run( + 'UPDATE admin_users SET password_hash = ? WHERE id = ?', + [newPasswordHash, userId] + ); + + res.json({ + success: true, + message: 'Password changed successfully' + }); + + } catch (error) { + console.error('Change password error:', error); + res.status(500).json({ + error: 'Password change failed', + message: 'An error occurred while changing password' + }); + } + } +} + +module.exports = new AuthController(); diff --git a/backend/controllers/contactController.js b/backend/controllers/contactController.js new file mode 100644 index 0000000..f8277af --- /dev/null +++ b/backend/controllers/contactController.js @@ -0,0 +1,253 @@ +const database = require('../models/database'); + +// Submit contact message (public endpoint) +const submitMessage = async (req, res) => { + try { + const { name, email, phone, message } = req.body; + + // Validation + if (!name || !email || !message) { + return res.status(400).json({ + success: false, + message: 'Name, email, and message are required' + }); + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ + success: false, + message: 'Please provide a valid email address' + }); + } + + // Insert message + const result = await database.run( + 'INSERT INTO contact_messages (name, email, phone, message, status, created_at) VALUES (?, ?, ?, ?, ?, ?)', + [name, email, phone || null, message, 'unread', new Date().toISOString()] + ); + + res.json({ + success: true, + message: 'Message sent successfully', + id: result.lastID + }); + + } catch (error) { + console.error('Contact submission error:', error); + res.status(500).json({ + success: false, + message: 'Failed to send message' + }); + } +}; + +// Get all messages (admin endpoint) +const getAllMessages = async (req, res) => { + try { + const { status, page = 1, limit = 20 } = req.query; + const offset = (page - 1) * limit; + + let query = 'SELECT * FROM contact_messages'; + let countQuery = 'SELECT COUNT(*) as count FROM contact_messages'; + let params = []; + let countParams = []; + + if (status && status !== 'all') { + query += ' WHERE status = ?'; + countQuery += ' WHERE status = ?'; + params.push(status); + countParams.push(status); + } + + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; + params.push(parseInt(limit), parseInt(offset)); + + const [messages, countResult] = await Promise.all([ + database.query(query, params), + database.get(countQuery, countParams) + ]); + + const totalMessages = countResult.count; + const totalPages = Math.ceil(totalMessages / limit); + + res.json({ + success: true, + data: messages, + pagination: { + currentPage: parseInt(page), + totalPages, + totalMessages, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + + } catch (error) { + console.error('Get messages error:', error); + res.status(500).json({ + success: false, + message: 'Failed to retrieve messages' + }); + } +}; + +// Get message by ID (admin endpoint) +const getMessageById = async (req, res) => { + try { + const { id } = req.params; + + const message = await database.get( + 'SELECT * FROM contact_messages WHERE id = ?', + [id] + ); + + if (!message) { + return res.status(404).json({ + success: false, + message: 'Message not found' + }); + } + + res.json({ + success: true, + data: message + }); + + } catch (error) { + console.error('Get message by ID error:', error); + res.status(500).json({ + success: false, + message: 'Failed to retrieve message' + }); + } +}; + +// Update message status (admin endpoint) +const updateMessageStatus = async (req, res) => { + try { + const { id } = req.params; + const { status } = req.body; + + // Validate status + const validStatuses = ['unread', 'read', 'replied']; + if (!validStatuses.includes(status)) { + return res.status(400).json({ + success: false, + message: 'Invalid status. Must be: unread, read, or replied' + }); + } + + // Check if message exists + const existingMessage = await database.get( + 'SELECT id FROM contact_messages WHERE id = ?', + [id] + ); + + if (!existingMessage) { + return res.status(404).json({ + success: false, + message: 'Message not found' + }); + } + + // Update status + await database.run( + 'UPDATE contact_messages SET status = ?, updated_at = ? WHERE id = ?', + [status, new Date().toISOString(), id] + ); + + res.json({ + success: true, + message: 'Message status updated successfully' + }); + + } catch (error) { + console.error('Update message status error:', error); + res.status(500).json({ + success: false, + message: 'Failed to update message status' + }); + } +}; + +// Delete message (admin endpoint) +const deleteMessage = async (req, res) => { + try { + const { id } = req.params; + + // Check if message exists + const message = await database.get( + 'SELECT id FROM contact_messages WHERE id = ?', + [id] + ); + + if (!message) { + return res.status(404).json({ + success: false, + message: 'Message not found' + }); + } + + // Delete message + await database.run('DELETE FROM contact_messages WHERE id = ?', [id]); + + res.json({ + success: true, + message: 'Message deleted successfully' + }); + + } catch (error) { + console.error('Delete message error:', error); + res.status(500).json({ + success: false, + message: 'Failed to delete message' + }); + } +}; + +// Get contact statistics (admin endpoint) +const getContactStats = async (req, res) => { + try { + const [ + totalMessages, + unreadMessages, + readMessages, + repliedMessages + ] = await Promise.all([ + database.get('SELECT COUNT(*) as count FROM contact_messages'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "unread"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "read"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "replied"') + ]); + + res.json({ + success: true, + data: { + total: totalMessages.count, + messagesByStatus: { + unread: unreadMessages.count, + read: readMessages.count, + replied: repliedMessages.count + } + } + }); + + } catch (error) { + console.error('Contact stats error:', error); + res.status(500).json({ + success: false, + message: 'Failed to get contact stats' + }); + } +}; + +module.exports = { + submitMessage, + getAllMessages, + getMessageById, + updateMessageStatus, + deleteMessage, + getContactStats +}; diff --git a/backend/controllers/contactController.js.broken b/backend/controllers/contactController.js.broken new file mode 100644 index 0000000..de2ff93 --- /dev/null +++ b/backend/controllers/contactController.js.broken @@ -0,0 +1,431 @@ +const database = require('../models/database'); + +class ContactController { + // Submit contact message (public endpoint) + async submitMessage(req, res) { + try { + const { name, email, phone, message } = req.body; + + // Validate required fields + if (!name || !email || !message) { + return res.status(400).json({ + error: 'Missing required fields', + message: 'Name, email, and message are required' + }); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ + error: 'Invalid email format', + message: 'Please provide a valid email address' + }); + } + + // Validate phone if provided + if (phone && phone.length < 10) { + return res.status(400).json({ + error: 'Invalid phone number', + message: 'Phone number must be at least 10 digits' + }); + } + + // Insert contact message + const result = await database.run( + 'INSERT INTO contact_messages (name, email, phone, message) VALUES (?, ?, ?, ?)', + [name, email, phone || null, message] + ); + + // Get the created message + const newMessage = await database.get( + 'SELECT * FROM contact_messages WHERE id = ?', + [result.id] + ); + + res.status(201).json({ + success: true, + message: 'Thank you! Your message has been sent successfully. We will contact you soon.', + data: { + id: newMessage.id, + name: newMessage.name, + email: newMessage.email, + created_at: newMessage.created_at + } + }); + + } catch (error) { + console.error('Submit message error:', error); + res.status(500).json({ + error: 'Failed to send message', + message: 'An error occurred while sending your message. Please try again.' + }); + } + } + + // Get all contact messages (admin only) + async getAllMessages(req, res) { + try { + const { status, page = 1, limit = 20 } = req.query; + + let query = 'SELECT * FROM contact_messages'; + let params = []; + + if (status && ['unread', 'read', 'replied'].includes(status)) { + query += ' WHERE status = ?'; + params.push(status); + } + + query += ' ORDER BY created_at DESC'; + + // Add pagination + const offset = (page - 1) * limit; + query += ' LIMIT ? OFFSET ?'; + params.push(parseInt(limit), parseInt(offset)); + + const messages = await database.query(query, params); + + // Get total count + let countQuery = 'SELECT COUNT(*) as count FROM contact_messages'; + let countParams = []; + + if (status && ['unread', 'read', 'replied'].includes(status)) { + countQuery += ' WHERE status = ?'; + countParams.push(status); + } + + const countResult = await database.get(countQuery, countParams); + const totalMessages = countResult.total; + const totalPages = Math.ceil(totalMessages / limit); + + res.json({ + success: true, + data: messages, + pagination: { + currentPage: parseInt(page), + totalPages, + totalMessages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1 + } + }); + + } catch (error) { + console.error('Get messages error:', error); + res.status(500).json({ + error: 'Failed to fetch messages', + message: 'An error occurred while fetching contact messages' + }); + } + } + + // Get single contact message (admin only) + async getMessageById(req, res) { + try { + const { id } = req.params; + + const message = await database.get( + 'SELECT * FROM contact_messages WHERE id = ?', + [id] + ); + + if (!message) { + return res.status(404).json({ + error: 'Message not found', + message: 'The requested contact message does not exist' + }); + } + + // Mark as read if it was unread + if (message.status === 'unread') { + await database.run( + 'UPDATE contact_messages SET status = ? WHERE id = ?', + ['read', id] + ); + message.status = 'read'; + } + + res.json({ + success: true, + data: message + }); + + } catch (error) { + console.error('Get message by ID error:', error); + res.status(500).json({ + error: 'Failed to fetch message', + message: 'An error occurred while fetching the contact message' + }); + } + } + + // Update message status (admin only) + async updateMessageStatus(req, res) { + try { + const { id } = req.params; + const { status } = req.body; + + // Validate status + if (!status || !['unread', 'read', 'replied'].includes(status)) { + return res.status(400).json({ + error: 'Invalid status', + message: 'Status must be one of: unread, read, replied' + }); + } + + // Check if message exists + const existingMessage = await database.get( + 'SELECT * FROM contact_messages WHERE id = ?', + [id] + ); + + if (!existingMessage) { + return res.status(404).json({ + error: 'Message not found', + message: 'The contact message you want to update does not exist' + }); + } + + // Update message status + await database.run( + 'UPDATE contact_messages SET status = ? WHERE id = ?', + [status, id] + ); + + // Get updated message + const updatedMessage = await database.get( + 'SELECT * FROM contact_messages WHERE id = ?', + [id] + ); + + res.json({ + success: true, + message: 'Message status updated successfully', + data: updatedMessage + }); + + } catch (error) { + console.error('Update message status error:', error); + res.status(500).json({ + error: 'Failed to update message status', + message: 'An error occurred while updating the message status' + }); + } + } + + // Delete contact message (admin only) + async deleteMessage(req, res) { + try { + const { id } = req.params; + + // Check if message exists + const message = await database.get( + 'SELECT * FROM contact_messages WHERE id = ?', + [id] + ); + + if (!message) { + return res.status(404).json({ + error: 'Message not found', + message: 'The contact message you want to delete does not exist' + }); + } + + // Delete message from database + await database.run('DELETE FROM contact_messages WHERE id = ?', [id]); + + res.json({ + success: true, + message: 'Contact message deleted successfully' + }); + + } catch (error) { + console.error('Delete message error:', error); + res.status(500).json({ + error: 'Failed to delete message', + message: 'An error occurred while deleting the contact message' + }); + } + } + + // Get contact statistics (admin only) + async getContactStats(req, res) { + try { + const stats = await Promise.all([ + database.get('SELECT COUNT(*) as count FROM contact_messages'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "unread"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "read"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "replied"'), + database.get(` + SELECT COUNT(*) as count + FROM contact_messages + WHERE DATE(created_at) = DATE('now') + `), + database.get(` + SELECT COUNT(*) as count + FROM contact_messages + WHERE created_at >= DATE('now', '-7 days') + `) + ]); + + res.json({ + success: true, + data: { + totalMessages: stats[0].total, + messagesByStatus: { + unread: stats[1].unread, + read: stats[2].read, + replied: stats[3].replied + }, + recentActivity: { + today: stats[4].today, + thisWeek: stats[5].thisWeek + } + } + }); + + } catch (error) { + console.error('Contact stats error:', error); + res.status(500).json({ + error: 'Failed to fetch contact stats', + message: 'An error occurred while fetching contact statistics' + }); + } + } +} + +module.exports = new ContactController(); + +// Get contact statistics for admin dashboard +const getContactStats = async (req, res) => { + try { + const [ + totalMessages, + unreadMessages, + readMessages, + repliedMessages + ] = await Promise.all([ + database.get('SELECT COUNT(*) as count FROM contact_messages'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "unread"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "read"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "replied"') + ]); + + res.json({ + success: true, + data: { + total: totalMessages.count, + unread: unreadMessages.count, + read: readMessages.count, + replied: repliedMessages.count + } + }); + } catch (error) { + console.error('Contact stats error:', error); + res.status(500).json({ success: false, message: 'Failed to get contact stats' }); + } +}; + + +module.exports = { + submitContact: async (req, res) => { + try { + const { name, email, message } = req.body; + + if (!name || !email || !message) { + return res.status(400).json({ success: false, message: 'All fields are required' }); + } + + const result = await database.run( + 'INSERT INTO contact_messages (name, email, message, status, created_at) VALUES (?, ?, ?, ?, ?)', + [name, email, message, 'unread', new Date().toISOString()] + ); + + res.json({ success: true, message: 'Message sent successfully', id: result.lastID }); + } catch (error) { + console.error('Contact submission error:', error); + res.status(500).json({ success: false, message: 'Failed to send message' }); + } + }, + + getMessages: async (req, res) => { + try { + const { status, page = 1, limit = 10 } = req.query; + const offset = (page - 1) * limit; + + let query = 'SELECT * FROM contact_messages'; + let params = []; + + if (status && status !== 'all') { + query += ' WHERE status = ?'; + params.push(status); + } + + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; + params.push(parseInt(limit), parseInt(offset)); + + const messages = await database.all(query, params); + + res.json({ success: true, data: messages }); + } catch (error) { + console.error('Get messages error:', error); + res.status(500).json({ success: false, message: 'Failed to get messages' }); + } + }, + + updateMessageStatus: async (req, res) => { + try { + const { id } = req.params; + const { status } = req.body; + + await database.run('UPDATE contact_messages SET status = ? WHERE id = ?', [status, id]); + + res.json({ success: true, message: 'Message status updated' }); + } catch (error) { + console.error('Update message status error:', error); + res.status(500).json({ success: false, message: 'Failed to update message status' }); + } + }, + + deleteMessage: async (req, res) => { + try { + const { id } = req.params; + + await database.run('DELETE FROM contact_messages WHERE id = ?', [id]); + + res.json({ success: true, message: 'Message deleted' }); + } catch (error) { + console.error('Delete message error:', error); + res.status(500).json({ success: false, message: 'Failed to delete message' }); + } + }, + + getContactStats: async (req, res) => { + try { + const [ + totalMessages, + unreadMessages, + readMessages, + repliedMessages + ] = await Promise.all([ + database.get('SELECT COUNT(*) as count FROM contact_messages'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "unread"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "read"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "replied"') + ]); + + res.json({ + success: true, + data: { + total: totalMessages.count, + unread: unreadMessages.count, + read: readMessages.count, + replied: repliedMessages.count + } + }); + } catch (error) { + console.error('Contact stats error:', error); + res.status(500).json({ success: false, message: 'Failed to get contact stats' }); + } + } +}; diff --git a/backend/controllers/propertyController.js b/backend/controllers/propertyController.js new file mode 100644 index 0000000..77cc66f --- /dev/null +++ b/backend/controllers/propertyController.js @@ -0,0 +1,350 @@ +const database = require('../models/database'); +const path = require('path'); +const fs = require('fs').promises; + +class PropertyController { + // Get all properties (public endpoint) + async getAllProperties(req, res) { + try { + const { category, status = 'available' } = req.query; + + let query = 'SELECT * FROM properties WHERE status = ?'; + let params = [status]; + + if (category && ['lands', 'cars', 'apartments'].includes(category)) { + query += ' AND category = ?'; + params.push(category); + } + + query += ' ORDER BY created_at DESC'; + + const properties = await database.query(query, params); + + res.json({ + success: true, + data: properties, + count: properties.length + }); + + } catch (error) { + console.error('Get properties error:', error); + res.status(500).json({ + error: 'Failed to fetch properties', + message: 'An error occurred while fetching properties' + }); + } + } + + // Get properties by category (public endpoint) + async getPropertiesByCategory(req, res) { + try { + const { category } = req.params; + + if (!['lands', 'cars', 'apartments'].includes(category)) { + return res.status(400).json({ + error: 'Invalid category', + message: 'Category must be one of: lands, cars, apartments' + }); + } + + const properties = await database.query( + 'SELECT * FROM properties WHERE category = ? AND status = ? ORDER BY created_at DESC', + [category, 'available'] + ); + + res.json({ + success: true, + category, + data: properties, + count: properties.length + }); + + } catch (error) { + console.error('Get properties by category error:', error); + res.status(500).json({ + error: 'Failed to fetch properties', + message: 'An error occurred while fetching properties' + }); + } + } + + // Get single property (public endpoint) + async getPropertyById(req, res) { + try { + const { id } = req.params; + + const property = await database.get( + 'SELECT * FROM properties WHERE id = ?', + [id] + ); + + if (!property) { + return res.status(404).json({ + error: 'Property not found', + message: 'The requested property does not exist' + }); + } + + res.json({ + success: true, + data: property + }); + + } catch (error) { + console.error('Get property by ID error:', error); + res.status(500).json({ + error: 'Failed to fetch property', + message: 'An error occurred while fetching the property' + }); + } + } + + // Add new property (admin only) + async addProperty(req, res) { + try { + const { + title, + description, + price, + location, + category, + features, + size, + status = 'available' + } = req.body; + + // Validate required fields + if (!title || !price || !location || !category) { + return res.status(400).json({ + error: 'Missing required fields', + message: 'Title, price, location, and category are required' + }); + } + + // Validate category + if (!['lands', 'cars', 'apartments'].includes(category)) { + return res.status(400).json({ + error: 'Invalid category', + message: 'Category must be one of: lands, cars, apartments' + }); + } + + // Handle image upload + let imageUrl = null; + if (req.file) { + imageUrl = `/uploads/${category}/${req.file.filename}`; + } + + // Insert property + const result = await database.run( + `INSERT INTO properties + (title, description, price, location, category, image_url, features, size, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [title, description, price, location, category, imageUrl, features, size, status] + ); + + // Get the created property + const newProperty = await database.get( + 'SELECT * FROM properties WHERE id = ?', + [result.id] + ); + + res.status(201).json({ + success: true, + message: 'Property added successfully', + data: newProperty + }); + + } catch (error) { + console.error('Add property error:', error); + res.status(500).json({ + error: 'Failed to add property', + message: 'An error occurred while adding the property' + }); + } + } + + // Update property (admin only) + async updateProperty(req, res) { + try { + const { id } = req.params; + const { + title, + description, + price, + location, + category, + features, + size, + status + } = req.body; + + // Check if property exists + const existingProperty = await database.get( + 'SELECT * FROM properties WHERE id = ?', + [id] + ); + + if (!existingProperty) { + return res.status(404).json({ + error: 'Property not found', + message: 'The property you want to update does not exist' + }); + } + + // Validate category if provided + if (category && !['lands', 'cars', 'apartments'].includes(category)) { + return res.status(400).json({ + error: 'Invalid category', + message: 'Category must be one of: lands, cars, apartments' + }); + } + + // Handle image upload + let imageUrl = existingProperty.image_url; + if (req.file) { + // Delete old image if exists + if (existingProperty.image_url) { + try { + const oldImagePath = path.join(__dirname, '../../', existingProperty.image_url); + await fs.unlink(oldImagePath); + } catch (err) { + console.log('Could not delete old image:', err.message); + } + } + imageUrl = `/uploads/${category || existingProperty.category}/${req.file.filename}`; + } + + // Update property + await database.run( + `UPDATE properties SET + title = COALESCE(?, title), + description = COALESCE(?, description), + price = COALESCE(?, price), + location = COALESCE(?, location), + category = COALESCE(?, category), + image_url = COALESCE(?, image_url), + features = COALESCE(?, features), + size = COALESCE(?, size), + status = COALESCE(?, status), + updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, + [title, description, price, location, category, imageUrl, features, size, status, id] + ); + + // Get updated property + const updatedProperty = await database.get( + 'SELECT * FROM properties WHERE id = ?', + [id] + ); + + res.json({ + success: true, + message: 'Property updated successfully', + data: updatedProperty + }); + + } catch (error) { + console.error('Update property error:', error); + res.status(500).json({ + error: 'Failed to update property', + message: 'An error occurred while updating the property' + }); + } + } + + // Delete property (admin only) + async deleteProperty(req, res) { + try { + const { id } = req.params; + + // Check if property exists + const property = await database.get( + 'SELECT * FROM properties WHERE id = ?', + [id] + ); + + if (!property) { + return res.status(404).json({ + error: 'Property not found', + message: 'The property you want to delete does not exist' + }); + } + + // Delete image file if exists + if (property.image_url) { + try { + const imagePath = path.join(__dirname, '../../', property.image_url); + await fs.unlink(imagePath); + } catch (err) { + console.log('Could not delete image file:', err.message); + } + } + + // Delete property from database + await database.run('DELETE FROM properties WHERE id = ?', [id]); + + res.json({ + success: true, + message: 'Property deleted successfully' + }); + + } catch (error) { + console.error('Delete property error:', error); + res.status(500).json({ + error: 'Failed to delete property', + message: 'An error occurred while deleting the property' + }); + } + } + + // Get admin dashboard stats + + async getDashboardStats(req, res) { + try { + const [ + totalProperties, + landsCount, + carsCount, + apartmentsCount, + availableCount, + soldCount, + unreadMessages + ] = await Promise.all([ + database.get('SELECT COUNT(*) as count FROM properties'), + database.get('SELECT COUNT(*) as count FROM properties WHERE category = "lands"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE category = "cars"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE category = "apartments"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE status = "available"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE status = "sold"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "unread"') + ]); + + res.json({ + success: true, + data: { + total: totalProperties.count, + propertiesByCategory: { + lands: landsCount.count, + cars: carsCount.count, + apartments: apartmentsCount.count + }, + propertiesByStatus: { + available: availableCount.count, + sold: soldCount.count + }, + messages: unreadMessages.count + } + }); + } catch (error) { + console.error('Dashboard stats error:', error); + res.status(500).json({ + success: false, + message: 'Failed to get dashboard stats' + }); + } + } + +} + +module.exports = new PropertyController(); diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..7247d23 --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,67 @@ +const jwt = require('jsonwebtoken'); +require('dotenv').config(); + +const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production'; + +// Middleware to verify JWT token +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + return res.status(401).json({ + error: 'Access token required', + message: 'Please provide a valid authentication token' + }); + } + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) { + console.error('Token verification failed:', err.message); + + if (err.name === 'TokenExpiredError') { + return res.status(401).json({ + error: 'Token expired', + message: 'Your session has expired. Please login again.' + }); + } + + if (err.name === 'JsonWebTokenError') { + return res.status(403).json({ + error: 'Invalid token', + message: 'The provided token is invalid.' + }); + } + + return res.status(403).json({ + error: 'Token verification failed', + message: 'Unable to verify authentication token.' + }); + } + + req.user = user; + next(); + }); +}; + +// Generate JWT token +const generateToken = (payload) => { + return jwt.sign(payload, JWT_SECRET, { + expiresIn: '24h' // Token expires in 24 hours + }); +}; + +// Verify token without middleware (for manual checks) +const verifyToken = (token) => { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + return null; + } +}; + +module.exports = { + authenticateToken, + generateToken, + verifyToken +}; diff --git a/backend/models/database.js b/backend/models/database.js new file mode 100644 index 0000000..f2e1ea3 --- /dev/null +++ b/backend/models/database.js @@ -0,0 +1,173 @@ +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +require('dotenv').config(); + +const dbPath = process.env.DB_PATH || './database/properties.db'; + +class Database { + constructor() { + this.db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error('Error opening database:', err.message); + } else { + console.log('Connected to SQLite database'); + this.initializeTables(); + } + }); + } + + initializeTables() { + // Properties table with categories + this.db.run(` + CREATE TABLE IF NOT EXISTS properties ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title VARCHAR(255) NOT NULL, + description TEXT, + price DECIMAL(15,2) NOT NULL, + location VARCHAR(255) NOT NULL, + category VARCHAR(50) NOT NULL CHECK(category IN ('lands', 'cars', 'apartments')), + image_url VARCHAR(500), + features TEXT, + size VARCHAR(100), + status VARCHAR(20) DEFAULT 'available' CHECK(status IN ('available', 'sold', 'pending')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `, (err) => { + if (err) { + console.error('Error creating properties table:', err.message); + } else { + console.log('Properties table ready'); + } + }); + + // Contact messages table + this.db.run(` + CREATE TABLE IF NOT EXISTS contact_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(20), + message TEXT NOT NULL, + status VARCHAR(20) DEFAULT 'unread' CHECK(status IN ('unread', 'read', 'replied')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `, (err) => { + if (err) { + console.error('Error creating contact_messages table:', err.message); + } else { + console.log('Contact messages table ready'); + + // updated_at for message updates + this.db.run(`ALTER TABLE contact_messages ADD COLUMN updated_at DATETIME`, (alterErr) => { + if (alterErr && !/duplicate column/.test(alterErr.message)) { + console.error('Error adding updated_at column:', alterErr.message); + } + }); + } + }); + + // Admin users table + this.db.run(` + CREATE TABLE IF NOT EXISTS admin_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME + ) + `, (err) => { + if (err) { + console.error('Error creating admin_users table:', err.message); + } else { + console.log('Admin users table ready'); + this.createDefaultAdmin(); + } + }); + } + + createDefaultAdmin() { + const bcrypt = require('bcryptjs'); + const username = process.env.ADMIN_USERNAME || 'admin'; + const password = process.env.ADMIN_PASSWORD || 'admin123'; + + // Check if admin exists + this.db.get('SELECT id FROM admin_users WHERE username = ?', [username], (err, row) => { + if (err) { + console.error('Error checking admin user:', err.message); + return; + } + + if (!row) { + // Create default admin + const passwordHash = bcrypt.hashSync(password, 10); + this.db.run( + 'INSERT INTO admin_users (username, password_hash) VALUES (?, ?)', + [username, passwordHash], + (err) => { + if (err) { + console.error('Error creating default admin:', err.message); + } else { + console.log(`Default admin created: ${username}`); + } + } + ); + } + }); + } + + // Helper method to run queries with promises + query(sql, params = []) { + return new Promise((resolve, reject) => { + this.db.all(sql, params, (err, rows) => { + if (err) { + reject(err); + } else { + resolve(rows); + } + }); + }); + } + + // Helper method to run single queries + run(sql, params = []) { + return new Promise((resolve, reject) => { + this.db.run(sql, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ id: this.lastID, changes: this.changes }); + } + }); + }); + } + + // Helper method to get single row + get(sql, params = []) { + return new Promise((resolve, reject) => { + this.db.get(sql, params, (err, row) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); + } + + close() { + return new Promise((resolve, reject) => { + this.db.close((err) => { + if (err) { + reject(err); + } else { + console.log('Database connection closed'); + resolve(); + } + }); + }); + } +} + +module.exports = new Database(); diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..9aa501f --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/authController'); +const { authenticateToken } = require('../middleware/auth'); + +// POST /api/auth/login - Admin login +router.post('/login', authController.login); + +// GET /api/auth/verify - Verify token +router.get('/verify', authenticateToken, authController.verifyToken); + +// POST /api/auth/change-password - Change password +router.post('/change-password', authenticateToken, authController.changePassword); + +module.exports = router; diff --git a/backend/routes/contact.js b/backend/routes/contact.js new file mode 100644 index 0000000..e2ff85f --- /dev/null +++ b/backend/routes/contact.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const contactController = require('../controllers/contactController'); +const { authenticateToken } = require('../middleware/auth'); + +// Public routes +router.post('/submit', contactController.submitMessage); + +// Admin routes (protected) +router.get('/', authenticateToken, contactController.getAllMessages); +router.get('/stats', authenticateToken, contactController.getContactStats); +router.get('/:id', authenticateToken, contactController.getMessageById); +router.put('/:id/status', authenticateToken, contactController.updateMessageStatus); +router.delete('/:id', authenticateToken, contactController.deleteMessage); + +module.exports = router; diff --git a/backend/routes/properties.js b/backend/routes/properties.js new file mode 100644 index 0000000..99ece93 --- /dev/null +++ b/backend/routes/properties.js @@ -0,0 +1,49 @@ +const express = require('express'); +const router = express.Router(); +const multer = require('multer'); +const path = require('path'); +const propertyController = require('../controllers/propertyController'); +const { authenticateToken } = require('../middleware/auth'); + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const category = req.body.category || 'lands'; + const uploadPath = path.join(__dirname, '../uploads', category); + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); + } +}); + +const fileFilter = (req, file, cb) => { + // Accept images only + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed!'), false); + } +}; + +const upload = multer({ + storage: storage, + limits: { + fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024 // 5MB default + }, + fileFilter: fileFilter +}); + +// Public routes +router.get('/', propertyController.getAllProperties); +router.get('/category/:category', propertyController.getPropertiesByCategory); +router.get('/:id', propertyController.getPropertyById); + +// Admin routes (protected) +router.post('/', authenticateToken, upload.single('image'), propertyController.addProperty); +router.put('/:id', authenticateToken, upload.single('image'), propertyController.updateProperty); +router.delete('/:id', authenticateToken, propertyController.deleteProperty); +router.get('/admin/stats', authenticateToken, propertyController.getDashboardStats); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..c9e5775 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,174 @@ +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const multer = require('multer'); +require('dotenv').config(); + +// Import routes +const propertyRoutes = require('./routes/properties'); +const contactRoutes = require('./routes/contact'); +const authRoutes = require('./routes/auth'); + +// Import database to initialize +const database = require('./models/database'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true +})); + +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// Serve static files (uploaded images) +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +// Serve frontend static files +app.use(express.static(path.join(__dirname, '../public'))); + +// API Routes +app.use('/api/properties', propertyRoutes); +app.use('/api/contact', contactRoutes); +app.use('/api/auth', authRoutes); + +// Health check endpoint +app.get('/api/health', (req, res) => { + res.json({ + success: true, + message: 'Good Foot Properties API is running', + timestamp: new Date().toISOString(), + version: '1.0.0' + }); +}); + +// API documentation endpoint +app.get('/api', (req, res) => { + res.json({ + success: true, + message: 'Good Foot Properties API', + version: '1.0.0', + endpoints: { + properties: { + 'GET /api/properties': 'Get all properties', + 'GET /api/properties/category/:category': 'Get properties by category (lands, cars, apartments)', + 'GET /api/properties/:id': 'Get single property', + 'POST /api/properties': 'Add new property (admin only)', + 'PUT /api/properties/:id': 'Update property (admin only)', + 'DELETE /api/properties/:id': 'Delete property (admin only)', + 'GET /api/properties/admin/stats': 'Get dashboard stats (admin only)' + }, + contact: { + 'POST /api/contact/submit': 'Submit contact message', + 'GET /api/contact': 'Get all messages (admin only)', + 'GET /api/contact/stats': 'Get contact stats (admin only)', + 'GET /api/contact/:id': 'Get single message (admin only)', + 'PUT /api/contact/:id/status': 'Update message status (admin only)', + 'DELETE /api/contact/:id': 'Delete message (admin only)' + }, + auth: { + 'POST /api/auth/login': 'Admin login', + 'GET /api/auth/verify': 'Verify token (admin only)', + 'POST /api/auth/change-password': 'Change password (admin only)' + } + } + }); +}); + +// Serve frontend pages +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '../public/index.html')); +}); + +app.get('/contact', (req, res) => { + res.sendFile(path.join(__dirname, '../public/contact.html')); +}); + +app.get('/admin', (req, res) => { + res.sendFile(path.join(__dirname, '../public/admin/admin.html')); +}); + +// Handle 404 for API routes +app.use('/api/*', (req, res) => { + res.status(404).json({ + error: 'API endpoint not found', + message: `The endpoint ${req.originalUrl} does not exist`, + availableEndpoints: '/api' + }); +}); + +// Handle 404 for other routes (serve index.html for SPA) +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../public/index.html')); +}); + +// Global error handler +app.use((error, req, res, next) => { + console.error('Global error handler:', error); + + // Multer errors + if (error instanceof multer.MulterError) { + if (error.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ + error: 'File too large', + message: 'The uploaded file exceeds the maximum size limit' + }); + } + } + + // File type errors + if (error.message === 'Only image files are allowed!') { + return res.status(400).json({ + error: 'Invalid file type', + message: 'Only image files are allowed' + }); + } + + res.status(500).json({ + error: 'Internal server error', + message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong' + }); +}); + +// Graceful shutdown +process.on('SIGINT', async () => { + console.log('\nShutting down server gracefully...'); + try { + await database.close(); + console.log('Database connection closed'); + process.exit(0); + } catch (error) { + console.error('Error during shutdown:', error); + process.exit(1); + } +}); + +process.on('SIGTERM', async () => { + console.log('SIGTERM received, shutting down gracefully...'); + try { + await database.close(); + console.log('Database connection closed'); + process.exit(0); + } catch (error) { + console.error('Error during shutdown:', error); + process.exit(1); + } +}); + +// Start server +app.listen(PORT, () => { + console.log(` +🚀 Good Foot Properties API Server Started! +📍 Server running on: http://localhost:${PORT} +🌐 Environment: ${process.env.NODE_ENV || 'development'} +�� API Documentation: http://localhost:${PORT}/api +🏠 Frontend: http://localhost:${PORT} +📧 Contact: http://localhost:${PORT}/contact +🔐 Admin: http://localhost:${PORT}/admin + `); +}); + +module.exports = app; diff --git a/backend/uploads/.gitkeep b/backend/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/errorHandler.js b/backend/utils/errorHandler.js new file mode 100644 index 0000000..c1f01f0 --- /dev/null +++ b/backend/utils/errorHandler.js @@ -0,0 +1,18 @@ +const handleDatabaseError = (error, operation) => { + console.error(`Database Error in ${operation}:`, error); + + // Log to file in production + if (process.env.NODE_ENV === 'production') { + const fs = require('fs'); + const logEntry = `${new Date().toISOString()} - ${operation}: ${error.message}\n`; + fs.appendFileSync('error.log', logEntry); + } + + return { + success: false, + message: 'Database operation failed', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }; +}; + +module.exports = { handleDatabaseError }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4954a62 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2918 @@ +{ + "name": "goodfootproperties", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "goodfootproperties", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.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", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..73fa27c --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "goodfootproperties", + "version": "1.0.0", + "description": "Good Foot Properties Limited - Backend API", + "main": "backend/server.js", + "scripts": { + "start": "node backend/server.js", + "dev": "nodemon backend/server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "real-estate", + "properties", + "kenya", + "api" + ], + "author": "Good Foot Properties Limited", + "license": "ISC", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/public/admin/admin.css b/public/admin/admin.css new file mode 100644 index 0000000..b8afa34 --- /dev/null +++ b/public/admin/admin.css @@ -0,0 +1,706 @@ +/* Admin Dashboard Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f5f5f5; + color: #333; +} + +/* Utility Classes */ +.hidden { + display: none !important; +} + +.active { + display: block !important; +} + +/* Modal Styles */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal.active { + display: flex; +} + +.modal-content { + background: white; + border-radius: 8px; + padding: 2rem; + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; +} + +.modal-content.large { + max-width: 800px; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid #eee; +} + +.close-btn { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #666; + padding: 0.5rem; +} + +.close-btn:hover { + color: #333; +} + +/* Login Form */ +.login-form { + text-align: center; +} + +.login-form h2 { + margin-bottom: 2rem; + color: #2c3e50; +} + +.login-form h2 i { + margin-right: 0.5rem; +} + +/* Dashboard Layout */ +.dashboard { + display: grid; + grid-template-areas: + "header header" + "nav main"; + grid-template-rows: auto 1fr; + grid-template-columns: 250px 1fr; + min-height: 100vh; +} + +/* Header */ +.dashboard-header { + grid-area: header; + background: #2c3e50; + color: white; + padding: 1rem 2rem; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-content h1 { + font-size: 1.5rem; +} + +.header-content h1 i { + margin-right: 0.5rem; +} + +.header-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.welcome-text { + font-size: 0.9rem; + opacity: 0.9; +} + +/* Navigation */ +.dashboard-nav { + grid-area: nav; + background: #34495e; + padding: 1rem 0; +} + +.dashboard-nav ul { + list-style: none; +} + +.dashboard-nav li { + margin-bottom: 0.5rem; +} + +.nav-link { + display: flex; + align-items: center; + padding: 1rem 1.5rem; + color: #bdc3c7; + text-decoration: none; + transition: all 0.3s ease; + position: relative; +} + +.nav-link:hover { + background: #2c3e50; + color: white; +} + +.nav-link.active { + background: #3498db; + color: white; +} + +.nav-link i { + margin-right: 0.75rem; + width: 20px; +} + +.badge { + background: #e74c3c; + color: white; + border-radius: 50%; + padding: 0.2rem 0.5rem; + font-size: 0.7rem; + margin-left: auto; +} + +/* Main Content */ +.dashboard-main { + grid-area: main; + padding: 2rem; + overflow-y: auto; +} + +.dashboard-section { + display: none; +} + +.dashboard-section.active { + display: block; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.section-header h2 { + color: #2c3e50; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: white; + border-radius: 8px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + display: flex; + align-items: center; + gap: 1rem; +} + +.stat-icon { + background: #3498db; + color: white; + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; +} + +.stat-content h3 { + font-size: 2rem; + color: #2c3e50; + margin-bottom: 0.5rem; +} + +.stat-content p { + color: #7f8c8d; + font-size: 0.9rem; +} + +/* Forms */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #2c3e50; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + transition: border-color 0.3s ease; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #3498db; +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.form-group.full-width { + grid-column: 1 / -1; +} + +/* Buttons */ +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.5rem; + transition: all 0.3s ease; +} + +.btn-primary { + background: #3498db; + color: white; +} + +.btn-primary:hover { + background: #2980b9; +} + +.btn-secondary { + background: #95a5a6; + color: white; +} + +.btn-secondary:hover { + background: #7f8c8d; +} + +.btn-danger { + background: #e74c3c; + color: white; +} + +.btn-danger:hover { + background: #c0392b; +} + +.btn-success { + background: #27ae60; + color: white; +} + +.btn-success:hover { + background: #229954; +} + +.btn-small { + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +/* Filters */ +.filters { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.filters select { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; +} + +.message-filters { + display: flex; + gap: 1rem; +} + +/* Tables */ +.table-container { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.data-table { + width: 100%; + border-collapse: collapse; +} + +.data-table th, +.data-table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #eee; +} + +.data-table th { + background: #f8f9fa; + font-weight: 600; + color: #2c3e50; +} + +.data-table tr:hover { + background: #f8f9fa; +} + +.data-table img { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: 4px; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 500; +} + +.status-available { + background: #d4edda; + color: #155724; +} + +.status-sold { + background: #f8d7da; + color: #721c24; +} + +.status-pending { + background: #fff3cd; + color: #856404; +} + +.status-unread { + background: #cce5ff; + color: #004085; +} + +.status-read { + background: #e2e3e5; + color: #383d41; +} + +.status-replied { + background: #d1ecf1; + color: #0c5460; +} + +/* Action Buttons */ +.action-buttons { + display: flex; + gap: 0.5rem; +} + +/* Settings */ +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 2rem; +} + +.settings-card { + background: white; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.settings-card h3 { + margin-bottom: 1.5rem; + color: #2c3e50; +} + +.settings-card h3 i { + margin-right: 0.5rem; +} + +/* Modal Actions */ +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #eee; +} + +/* Current Image */ +.current-image { + margin-top: 1rem; +} + +.current-image img { + max-width: 200px; + height: auto; + border-radius: 4px; + border: 1px solid #ddd; +} + +/* Loading Spinner */ +.loading-spinner { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.spinner { + width: 50px; + height: 50px; + border: 5px solid #f3f3f3; + border-top: 5px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Toast Notifications */ +.toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; +} + +.toast { + background: #2c3e50; + color: white; + padding: 1rem 1.5rem; + border-radius: 4px; + margin-bottom: 0.5rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + animation: slideIn 0.3s ease; +} + +.toast.success { + background: #27ae60; +} + +.toast.error { + background: #e74c3c; +} + +.toast.warning { + background: #f39c12; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Error Messages */ +.error-message { + color: #e74c3c; + font-size: 0.9rem; + margin-top: 1rem; + padding: 0.5rem; + background: #fdf2f2; + border: 1px solid #fecaca; + border-radius: 4px; + display: none; +} + +.error-message.show { + display: block; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .dashboard { + grid-template-areas: + "header" + "nav" + "main"; + grid-template-columns: 1fr; + grid-template-rows: auto auto 1fr; + } + + .dashboard-nav { + padding: 0.5rem 0; + } + + .dashboard-nav ul { + display: flex; + overflow-x: auto; + } + + .dashboard-nav li { + margin-bottom: 0; + margin-right: 0.5rem; + } + + .nav-link { + padding: 0.75rem 1rem; + white-space: nowrap; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .filters { + flex-direction: column; + } + + .section-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .header-content { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .header-actions { + width: 100%; + justify-content: space-between; + } + + .modal-content { + width: 95%; + padding: 1rem; + } + .settings-grid { + grid-template-columns: 1fr; + } + + .action-buttons { + flex-direction: column; + } + + .data-table { + font-size: 0.9rem; + } + + .data-table th, + .data-table td { + padding: 0.5rem; + } + + .modal-actions { + flex-direction: column; + } +} + +@media (max-width: 480px) { + .dashboard-main { + padding: 1rem; + } + + .stat-card { + flex-direction: column; + text-align: center; + } + + .data-table { + display: block; + overflow-x: auto; + white-space: nowrap; + } + + .btn { + width: 100%; + justify-content: center; + } + + .toast-container { + left: 10px; + right: 10px; + } +} + +/* Print Styles */ +@media print { + .dashboard-nav, + .header-actions, + .action-buttons, + .modal { + display: none !important; + } + + .dashboard { + grid-template-areas: + "header" + "main"; + grid-template-columns: 1fr; + } + + .dashboard-main { + padding: 0; + } +} diff --git a/public/admin/admin.html b/public/admin/admin.html new file mode 100644 index 0000000..27d58ae --- /dev/null +++ b/public/admin/admin.html @@ -0,0 +1,336 @@ + + + + + + + Admin Dashboard - Good Foot Properties + + + + + + + + + + + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/public/admin/admin.js b/public/admin/admin.js new file mode 100644 index 0000000..295b3c8 --- /dev/null +++ b/public/admin/admin.js @@ -0,0 +1,1049 @@ +// Admin Dashboard JavaScript +class AdminDashboard { + constructor() { + this.token = localStorage.getItem('adminToken'); + this.baseURL = '/api'; + this.currentSection = 'dashboard'; + this.properties = []; + this.messages = []; + + this.init(); + } + + init() { + this.setupEventListeners(); + + if (this.token) { + this.verifyToken(); + } else { + this.showLoginModal(); + } + } + + setupEventListeners() { + // Login form + document.getElementById('loginForm').addEventListener('submit', (e) => { + e.preventDefault(); + this.handleLogin(); + }); + + // Logout button + document.getElementById('logoutBtn').addEventListener('click', () => { + this.handleLogout(); + }); + + // Navigation links + document.querySelectorAll('.nav-link').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const section = link.dataset.section; + this.showSection(section); + }); + }); + + // Add property button + document.getElementById('addPropertyBtn').addEventListener('click', () => { + this.showPropertyModal(); + }); + + // Property form + document.getElementById('propertyForm').addEventListener('submit', (e) => { + e.preventDefault(); + this.handlePropertySubmit(); + }); + + // Filters + document.getElementById('categoryFilter').addEventListener('change', () => { + this.loadProperties(); + }); + + document.getElementById('statusFilter').addEventListener('change', () => { + this.loadProperties(); + }); + + document.getElementById('messageStatusFilter').addEventListener('change', () => { + this.loadMessages(); + }); + + // Change password form + document.getElementById('changePasswordForm').addEventListener('submit', (e) => { + e.preventDefault(); + this.handleChangePassword(); + }); + } + + // Authentication Methods + async handleLogin() { + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + const errorDiv = document.getElementById('loginError'); + + try { + this.showLoading(); + + const response = await fetch(`${this.baseURL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + + if (data.success) { + this.token = data.token; + localStorage.setItem('adminToken', this.token); + document.getElementById('adminUsername').textContent = data.user.username; + this.hideLoginModal(); + this.showDashboard(); + this.loadDashboardData(); + this.showToast('Login successful!', 'success'); + } else { + errorDiv.textContent = data.message || 'Login failed'; + errorDiv.classList.add('show'); + } + } catch (error) { + console.error('Login error:', error); + errorDiv.textContent = 'An error occurred during login'; + errorDiv.classList.add('show'); + } finally { + this.hideLoading(); + } + } + + async verifyToken() { + try { + const response = await fetch(`${this.baseURL}/auth/verify`, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const data = await response.json(); + document.getElementById('adminUsername').textContent = data.user.username; + this.showDashboard(); + this.loadDashboardData(); + } else { + this.handleLogout(); + } + } catch (error) { + console.error('Token verification error:', error); + this.handleLogout(); + } + } + + handleLogout() { + localStorage.removeItem('adminToken'); + this.token = null; + this.showLoginModal(); + this.hideDashboard(); + } + + // UI Methods + showLoginModal() { + document.getElementById('loginModal').classList.add('active'); + document.getElementById('loginError').classList.remove('show'); + } + + hideLoginModal() { + document.getElementById('loginModal').classList.remove('active'); + } + + showDashboard() { + document.getElementById('adminDashboard').classList.remove('hidden'); + } + + hideDashboard() { + document.getElementById('adminDashboard').classList.add('hidden'); + } + + showSection(sectionName) { + // Update navigation + document.querySelectorAll('.nav-link').forEach(link => { + link.classList.remove('active'); + }); + document.querySelector(`[data-section="${sectionName}"]`).classList.add('active'); + + // Update sections + document.querySelectorAll('.dashboard-section').forEach(section => { + section.classList.remove('active'); + }); + document.getElementById(`${sectionName}Section`).classList.add('active'); + + this.currentSection = sectionName; + + // Load section data + switch (sectionName) { + case 'dashboard': + this.loadDashboardStats(); + break; + case 'properties': + this.loadProperties(); + break; + case 'messages': + this.loadMessages(); + break; + } + } + + showLoading() { + document.getElementById('loadingSpinner').classList.remove('hidden'); + } + + hideLoading() { + document.getElementById('loadingSpinner').classList.add('hidden'); + } + + showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.textContent = message; + + document.getElementById('toastContainer').appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, 5000); + } + + // Data Loading Methods + async loadDashboardData() { + await Promise.all([ + this.loadDashboardStats(), + this.loadProperties(), + this.loadMessages() + ]); + } + + async loadDashboardStats() { + try { + const response = await fetch(`${this.baseURL}/properties/admin/stats`, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const data = await response.json(); + this.updateDashboardStats(data.data); + } + } catch (error) { + console.error('Error loading dashboard stats:', error); + } + + // Load contact stats + try { + const response = await fetch(`${this.baseURL}/contact/stats`, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const data = await response.json(); + this.updateContactStats(data.data); + } + } catch (error) { + console.error('Error loading contact stats:', error); + } + } + + updateDashboardStats(stats) { + document.getElementById('totalProperties').textContent = stats.totalProperties || 0; + document.getElementById('totalApartments').textContent = stats.propertiesByCategory.apartments || 0; + document.getElementById('totalLands').textContent = stats.propertiesByCategory.lands || 0; + document.getElementById('totalCars').textContent = stats.propertiesByCategory.cars || 0; + document.getElementById('availableProperties').textContent = stats.propertiesByStatus.available || 0; + } + + updateContactStats(stats) { + document.getElementById('totalMessages').textContent = stats.totalMessages || 0; + document.getElementById('unreadCount').textContent = stats.messagesByStatus.unread || 0; + + // Update badge visibility + const badge = document.getElementById('unreadCount'); + if (stats.messagesByStatus.unread > 0) { + badge.style.display = 'inline'; + } else { + badge.style.display = 'none'; + } + } + + async loadProperties() { + try { + this.showLoading(); + + const category = document.getElementById('categoryFilter').value; + const status = document.getElementById('statusFilter').value; + + let url = `${this.baseURL}/properties`; + const params = new URLSearchParams(); + + if (category) params.append('category', category); + if (status) params.append('status', status); + + if (params.toString()) { + url += `?${params.toString()}`; + } + + const response = await fetch(url); + + if (response.ok) { + const data = await response.json(); + this.properties = data.data; + this.renderPropertiesTable(); + } else { + this.showToast('Failed to load properties', 'error'); + } + } catch (error) { + console.error('Error loading properties:', error); + this.showToast('Error loading properties', 'error'); + } finally { + this.hideLoading(); + } + } + + renderPropertiesTable() { + const tbody = document.getElementById('propertiesTableBody'); + tbody.innerHTML = ''; + + if (this.properties.length === 0) { + tbody.innerHTML = 'No properties found'; + return; + } + + this.properties.forEach(property => { + const row = document.createElement('tr'); + row.innerHTML = ` + + ${property.image_url ? + `${property.title}` : + '
' + } + + + ${property.title} +
+ ${property.description ? property.description.substring(0, 50) + '...' : ''} + + + + ${property.category.charAt(0).toUpperCase() + property.category.slice(1)} + + + KSH ${this.formatPrice(property.price)} + ${property.location} + + + ${(property.status || 'available').charAt(0).toUpperCase() + (property.status || 'available').slice(1)} + + + +
+ + +
+ + `; + tbody.appendChild(row); + }); + } + + async loadMessages() { + try { + this.showLoading(); + + const status = document.getElementById('messageStatusFilter').value; + + let url = `${this.baseURL}/contact`; + if (status) { + url += `?status=${status}`; + } + + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const data = await response.json(); + this.messages = data.data; + this.renderMessagesTable(); + } else { + this.showToast('Failed to load messages', 'error'); + } + } catch (error) { + console.error('Error loading messages:', error); + this.showToast('Error loading messages', 'error'); + } finally { + this.hideLoading(); + } + } + + renderMessagesTable() { + const tbody = document.getElementById('messagesTableBody'); + tbody.innerHTML = ''; + + if (this.messages.length === 0) { + tbody.innerHTML = 'No messages found'; + return; + } + + this.messages.forEach(message => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${message.name} + ${message.email} + ${message.phone || 'N/A'} + +
+ ${message.message.substring(0, 100)}${message.message.length > 100 ? '...' : ''} +
+ + + + ${message.status.charAt(0).toUpperCase() + message.status.slice(1)} + + + ${this.formatDate(message.created_at)} + +
+ + + +
+ + `; + tbody.appendChild(row); + }); + } + + // Property Management Methods + showPropertyModal(property = null) { + const modal = document.getElementById('propertyModal'); + const form = document.getElementById('propertyForm'); + const title = document.getElementById('propertyModalTitle'); + + if (property) { + // Edit mode + title.textContent = 'Edit Property'; + this.populatePropertyForm(property); + } else { + // Add mode + title.textContent = 'Add Property'; + form.reset(); + document.getElementById('propertyId').value = ''; + document.getElementById('currentImage').innerHTML = ''; + } + + modal.classList.add('active'); + } + + populatePropertyForm(property) { + document.getElementById('propertyId').value = property.id; + document.getElementById('propertyTitle').value = property.title; + document.getElementById('propertyCategory').value = property.category; + document.getElementById('propertyPrice').value = property.price; + document.getElementById('propertyLocation').value = property.location; + document.getElementById('propertySize').value = property.size || ''; + document.getElementById('propertyStatus').value = property.status || 'available'; + document.getElementById('propertyDescription').value = property.description || ''; + document.getElementById('propertyFeatures').value = property.features || ''; + + // Show current image + const currentImageDiv = document.getElementById('currentImage'); + if (property.image_url) { + currentImageDiv.innerHTML = ` +

Current Image:

+ Current image + `; + } else { + currentImageDiv.innerHTML = ''; + } + } + + async handlePropertySubmit() { + try { + this.showLoading(); + + const form = document.getElementById('propertyForm'); + const formData = new FormData(form); + const propertyId = document.getElementById('propertyId').value; + + const url = propertyId ? + `${this.baseURL}/properties/${propertyId}` : + `${this.baseURL}/properties`; + + const method = propertyId ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method: method, + headers: { + 'Authorization': `Bearer ${this.token}` + }, + body: formData + }); + + const data = await response.json(); + + if (data.success) { + this.showToast( + propertyId ? 'Property updated successfully!' : 'Property added successfully!', + 'success' + ); + this.closePropertyModal(); + this.loadProperties(); + this.loadDashboardStats(); + } else { + this.showToast(data.message || 'Failed to save property', 'error'); + } + } catch (error) { + console.error('Error saving property:', error); + this.showToast('Error saving property', 'error'); + } finally { + this.hideLoading(); + } + } + + async editProperty(id) { + const property = this.properties.find(p => p.id === id); + if (property) { + this.showPropertyModal(property); + } else { + // Fetch property details + try { + const response = await fetch(`${this.baseURL}/properties/${id}`); + if (response.ok) { + const data = await response.json(); + this.showPropertyModal(data.data); + } else { + this.showToast('Failed to load property details', 'error'); + } + } catch (error) { + console.error('Error loading property:', error); + this.showToast('Error loading property', 'error'); + } + } + } + + async deleteProperty(id) { + if (!confirm('Are you sure you want to delete this property?')) { + return; + } + + try { + this.showLoading(); + + const response = await fetch(`${this.baseURL}/properties/${id}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + const data = await response.json(); + + if (data.success) { + this.showToast('Property deleted successfully!', 'success'); + this.loadProperties(); + this.loadDashboardStats(); + } else { + this.showToast(data.message || 'Failed to delete property', 'error'); + } + } catch (error) { + console.error('Error deleting property:', error); + this.showToast('Error deleting property', 'error'); + } finally { + this.hideLoading(); + } + } + + closePropertyModal() { + document.getElementById('propertyModal').classList.remove('active'); + } + + // Message Management Methods + async viewMessage(id) { + try { + const response = await fetch(`${this.baseURL}/contact/${id}`, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const data = await response.json(); + this.showMessageModal(data.data); + + // Mark as read if unread + if (data.data.status === 'unread') { + await this.updateMessageStatus(id, 'read', false); + } + } else { + this.showToast('Failed to load message', 'error'); + } + } catch (error) { + console.error('Error loading message:', error); + this.showToast('Error loading message', 'error'); + } + } + + showMessageModal(message) { + const modal = document.getElementById('messageModal'); + const content = document.getElementById('messageContent'); + + content.innerHTML = ` +
+
+

${message.name}

+ + ${message.status.charAt(0).toUpperCase() + message.status.slice(1)} + +
+
+

Email: ${message.email}

+

Phone: ${message.phone || 'N/A'}

+

Date: ${this.formatDate(message.created_at)}

+
+
+
Message:
+

${message.message}

+
+
+ + +
+
+ `; + + modal.classList.add('active'); + } + + async updateMessageStatus(id, status, showToast = true) { + try { + const response = await fetch(`${this.baseURL}/contact/${id}/status`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ status }) + }); + + const data = await response.json(); + + if (data.success) { + if (showToast) { + this.showToast(`Message marked as ${status}!`, 'success'); + } + this.loadMessages(); + this.loadDashboardStats(); + } else { + if (showToast) { + this.showToast(data.message || 'Failed to update message status', 'error'); + } + } + } catch (error) { + console.error('Error updating message status:', error); + if (showToast) { + this.showToast('Error updating message status', 'error'); + } + } + } + + async deleteMessage(id) { + if (!confirm('Are you sure you want to delete this message?')) { + return; + } + + try { + this.showLoading(); + + const response = await fetch(`${this.baseURL}/contact/${id}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + const data = await response.json(); + + if (data.success) { + this.showToast('Message deleted successfully!', 'success'); + this.loadMessages(); + this.loadDashboardStats(); + } else { + this.showToast(data.message || 'Failed to delete message', 'error'); + } + } catch (error) { + console.error('Error deleting message:', error); + this.showToast('Error deleting message', 'error'); + } finally { + this.hideLoading(); + } + } + + closeMessageModal() { + document.getElementById('messageModal').classList.remove('active'); + } + + // Password Change Method + async handleChangePassword() { + const currentPassword = document.getElementById('currentPassword').value; + const newPassword = document.getElementById('newPassword').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + // Validation + if (newPassword !== confirmPassword) { + this.showToast('New passwords do not match', 'error'); + return; + } + + if (newPassword.length < 6) { + this.showToast('New password must be at least 6 characters long', 'error'); + return; + } + + try { + this.showLoading(); + + const response = await fetch(`${this.baseURL}/auth/change-password`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + currentPassword, + newPassword + }) + }); + + const data = await response.json(); + + if (data.success) { + this.showToast('Password changed successfully!', 'success'); + document.getElementById('changePasswordForm').reset(); + } else { + this.showToast(data.message || 'Failed to change password', 'error'); + } + } catch (error) { + console.error('Error changing password:', error); + this.showToast('Error changing password', 'error'); + } finally { + this.hideLoading(); + } + } + + // Utility Methods + formatPrice(price) { + return new Intl.NumberFormat('en-KE', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(price); + } + + formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('en-KE', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + formatCurrency(amount) { + return `KSH ${this.formatPrice(amount)}`; + } + + truncateText(text, maxLength = 100) { + if (!text) return ''; + return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; + } + + // API Helper Methods + async apiRequest(endpoint, options = {}) { + const defaultOptions = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.token}` + } + }; + + const config = { + ...defaultOptions, + ...options, + headers: { + ...defaultOptions.headers, + ...options.headers + } + }; + + try { + const response = await fetch(`${this.baseURL}${endpoint}`, config); + + if (response.status === 401) { + this.handleLogout(); + throw new Error('Unauthorized'); + } + + return await response.json(); + } catch (error) { + console.error('API Request Error:', error); + throw error; + } + } + + // Data Export Methods + async exportProperties() { + try { + this.showLoading(); + + const response = await fetch(`${this.baseURL}/properties/export`, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `properties_${new Date().toISOString().split('T')[0]}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + this.showToast('Properties exported successfully!', 'success'); + } else { + this.showToast('Failed to export properties', 'error'); + } + } catch (error) { + console.error('Error exporting properties:', error); + this.showToast('Error exporting properties', 'error'); + } finally { + this.hideLoading(); + } + } + + async exportMessages() { + try { + this.showLoading(); + + const response = await fetch(`${this.baseURL}/contact/export`, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `messages_${new Date().toISOString().split('T')[0]}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + this.showToast('Messages exported successfully!', 'success'); + } else { + this.showToast('Failed to export messages', 'error'); + } + } catch (error) { + console.error('Error exporting messages:', error); + this.showToast('Error exporting messages', 'error'); + } finally { + this.hideLoading(); + } + } + + // Search and Filter Methods + searchProperties(query) { + const filteredProperties = this.properties.filter(property => + property.title.toLowerCase().includes(query.toLowerCase()) || + property.description.toLowerCase().includes(query.toLowerCase()) || + property.location.toLowerCase().includes(query.toLowerCase()) + ); + + this.renderFilteredProperties(filteredProperties); + } + + renderFilteredProperties(properties) { + const tbody = document.getElementById('propertiesTableBody'); + tbody.innerHTML = ''; + + if (properties.length === 0) { + tbody.innerHTML = 'No properties found'; + return; + } + + properties.forEach(property => { + const row = document.createElement('tr'); + row.innerHTML = ` + + ${property.image_url ? + `${property.title}` : + '
' + } + + + ${property.title} +
+ ${this.truncateText(property.description, 50)} + + + + ${property.category.charAt(0).toUpperCase() + property.category.slice(1)} + + + ${this.formatCurrency(property.price)} + ${property.location} + + + ${(property.status || 'available').charAt(0).toUpperCase() + (property.status || 'available').slice(1)} + + + +
+ + +
+ + `; + tbody.appendChild(row); + }); + } + + // Bulk Operations + async bulkUpdatePropertyStatus(propertyIds, status) { + try { + this.showLoading(); + + const response = await fetch(`${this.baseURL}/properties/bulk-update`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + propertyIds, + status + }) + }); + + const data = await response.json(); + + if (data.success) { + this.showToast(`${propertyIds.length} properties updated successfully!`, 'success'); + this.loadProperties(); + this.loadDashboardStats(); + } else { + this.showToast(data.message || 'Failed to update properties', 'error'); + } + } catch (error) { + console.error('Error updating properties:', error); + this.showToast('Error updating properties', 'error'); + } finally { + this.hideLoading(); + } + } + + // Real-time Updates + setupWebSocket() { + if (typeof WebSocket !== 'undefined') { + const ws = new WebSocket(`ws://localhost:3000/admin-updates`); + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + + switch (data.type) { + case 'new_message': + this.handleNewMessage(data.message); + break; + case 'property_updated': + this.handlePropertyUpdate(data.property); + break; + } + }; + + ws.onerror = (error) => { + console.error('WebSocket error:', error); + }; + } + } + + handleNewMessage(message) { + // Update unread count + const currentCount = parseInt(document.getElementById('unreadCount').textContent); + document.getElementById('unreadCount').textContent = currentCount + 1; + document.getElementById('unreadCount').style.display = 'inline'; + + // Show notification + this.showToast(`New message from ${message.name}`, 'info'); + + // Refresh messages if on messages section + if (this.currentSection === 'messages') { + this.loadMessages(); + } + } + + handlePropertyUpdate(property) { + // Refresh properties if on properties section + if (this.currentSection === 'properties') { + this.loadProperties(); + } + + // Update dashboard stats + this.loadDashboardStats(); + } +} + +// Global Functions for onclick handlers +function closePropertyModal() { + adminDashboard.closePropertyModal(); +} + +function closeMessageModal() { + adminDashboard.closeMessageModal(); +} + +// Initialize Admin Dashboard +let adminDashboard; + +document.addEventListener('DOMContentLoaded', () => { + adminDashboard = new AdminDashboard(); +}); + +// Handle page visibility change to refresh data +document.addEventListener('visibilitychange', () => { + if (!document.hidden && adminDashboard && adminDashboard.token) { + adminDashboard.loadDashboardStats(); + } +}); \ No newline at end of file diff --git a/assets/goodroads.jpeg b/public/assets/goodroads.jpeg similarity index 100% rename from assets/goodroads.jpeg rename to public/assets/goodroads.jpeg diff --git a/assets/icon.jpeg b/public/assets/icon.jpeg similarity index 100% rename from assets/icon.jpeg rename to public/assets/icon.jpeg diff --git a/assets/land1.jpeg b/public/assets/land1.jpeg similarity index 100% rename from assets/land1.jpeg rename to public/assets/land1.jpeg diff --git a/assets/land2.jpeg b/public/assets/land2.jpeg similarity index 100% rename from assets/land2.jpeg rename to public/assets/land2.jpeg diff --git a/assets/land3.jpeg b/public/assets/land3.jpeg similarity index 100% rename from assets/land3.jpeg rename to public/assets/land3.jpeg diff --git a/assets/land4.jpeg b/public/assets/land4.jpeg similarity index 100% rename from assets/land4.jpeg rename to public/assets/land4.jpeg diff --git a/assets/land5.jpeg b/public/assets/land5.jpeg similarity index 100% rename from assets/land5.jpeg rename to public/assets/land5.jpeg diff --git a/assets/land6.jpeg b/public/assets/land6.jpeg similarity index 100% rename from assets/land6.jpeg rename to public/assets/land6.jpeg diff --git a/assets/location.jpg b/public/assets/location.jpg similarity index 100% rename from assets/location.jpg rename to public/assets/location.jpg diff --git a/assets/office1.jpeg b/public/assets/office1.jpeg similarity index 100% rename from assets/office1.jpeg rename to public/assets/office1.jpeg diff --git a/assets/office2.jpeg b/public/assets/office2.jpeg similarity index 100% rename from assets/office2.jpeg rename to public/assets/office2.jpeg diff --git a/assets/office3.jpeg b/public/assets/office3.jpeg similarity index 100% rename from assets/office3.jpeg rename to public/assets/office3.jpeg diff --git a/assets/phone.svg b/public/assets/phone.svg similarity index 100% rename from assets/phone.svg rename to public/assets/phone.svg diff --git a/assets/staff1.jpeg b/public/assets/staff1.jpeg similarity index 100% rename from assets/staff1.jpeg rename to public/assets/staff1.jpeg diff --git a/assets/staff2.jpeg b/public/assets/staff2.jpeg similarity index 100% rename from assets/staff2.jpeg rename to public/assets/staff2.jpeg diff --git a/assets/staff3.jpeg b/public/assets/staff3.jpeg similarity index 100% rename from assets/staff3.jpeg rename to public/assets/staff3.jpeg diff --git a/assets/whatsapp.svg b/public/assets/whatsapp.svg similarity index 100% rename from assets/whatsapp.svg rename to public/assets/whatsapp.svg diff --git a/contact.html b/public/contact.html similarity index 91% rename from contact.html rename to public/contact.html index 8713214..7f4039c 100644 --- a/contact.html +++ b/public/contact.html @@ -1,10 +1,11 @@ + - + Contact Us - Good Foot Properties - + +
@@ -55,11 +57,9 @@

Contact Us

-
@@ -70,4 +70,5 @@

Contact Us

- + + \ No newline at end of file diff --git a/style.css b/public/css/style.css similarity index 100% rename from style.css rename to public/css/style.css diff --git a/index.html b/public/index.html similarity index 76% rename from index.html rename to public/index.html index 3550658..3e7c78b 100644 --- a/index.html +++ b/public/index.html @@ -1,34 +1,39 @@ + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + Good Foot Properties Limited - + +
@@ -159,10 +164,12 @@

Consultation

-
+

About Good Foot Properties Limited

- Good Foot Properties Limited helps buyers and sellers find quality land opportunities in Kenya including Dagoretti, Kikuyu, Nachu, Kamango, Lusigetti, Gikambura, Baraniki, and Thigio. Our commitment to excellence and transparency makes us a trusted name in the property industry. + Good Foot Properties Limited helps buyers and sellers find quality land opportunities in Kenya including + Dagoretti, Kikuyu, Nachu, Kamango, Lusigetti, Gikambura, Baraniki, and Thigio. Our commitment to excellence and + transparency makes us a trusted name in the property industry.

@@ -181,18 +188,18 @@

Why Choose Good Foot Properties Limited?

-
- - + + + - - + + - - + + - -
+ + @@ -236,5 +243,7 @@

Why Choose Good Foot Properties Limited?

revealOnScroll(); // run on load }); + - + + \ No newline at end of file diff --git a/public/js/script.js b/public/js/script.js new file mode 100644 index 0000000..b342e5e --- /dev/null +++ b/public/js/script.js @@ -0,0 +1,67 @@ +// script.js +document.getElementById('contactForm').addEventListener('submit', function (e) { + e.preventDefault(); + fetch('/api/contact/submit', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + name: new FormData(e.target).get('name'), + email: new FormData(e.target).get('email'), + message: new FormData(e.target).get('message') + }) + }).then(r => r.json()).then(d => { + alert(d.success ? 'Message sent successfully!' : 'Error: ' + d.message); + if(d.success) e.target.reset(); + }); +}); + +// Fade-in effect on scroll +const observer = new IntersectionObserver( + (entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); + }, + { + threshold: 0.1 + } +); + +document.querySelectorAll("section").forEach(section => { + observer.observe(section); +}); + +// API Integration - Load Properties +async function loadProperties() { + try { + console.log('🔄 Loading properties from API...'); + const response = await fetch('/api/properties?limit=6'); + const data = await response.json(); + + if (data.success) { + const propertyList = document.querySelector('.property-list'); + if (propertyList) { + propertyList.innerHTML = data.data.map(p => ` +
+ ${p.title} +

${p.title}

+

Price: Ksh ${p.price.toLocaleString()}

+

Location: ${p.location}

+ View Details +
+ `).join(''); + console.log('✅ Properties loaded from API:', data.data.length); + } + } + } catch (error) { + console.error('❌ Error loading properties:', error); + } +} + +// Load properties when page loads +document.addEventListener('DOMContentLoaded', loadProperties); + +console.log('🔥 Script loaded successfully!'); diff --git a/public/js/script.js.backup b/public/js/script.js.backup new file mode 100644 index 0000000..7ea911d --- /dev/null +++ b/public/js/script.js.backup @@ -0,0 +1,132 @@ +// script.js +document.getElementById('contactForm').addEventListener('submit', function (e) { + e.preventDefault(); + fetch('/api/contact/submit', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name: new FormData(e.target).get('name'), email: new FormData(e.target).get('email'), message: new FormData(e.target).get('message')})}).then(r => r.json()).then(d => {alert(d.success ? 'Message sent successfully!' : 'Error: ' + d.message); if(d.success) e.target.reset();}); +}); + +// Fade-in effect on scroll +const observer = new IntersectionObserver( + (entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); + }, + { + threshold: 0.1 + } +); + +document.querySelectorAll("section").forEach(section => { + observer.observe(section); +}); + +const form = document.getElementById("propertyForm"); + +if (form) { + form.addEventListener("submit", async (e) => { + e.preventDefault(); + + const formData = new FormData(form); + + const response = await fetch("http://localhost/goodfoot-admin/add_property.php", { + method: "POST", + body: formData, + }); + + const result = await response.text(); + alert(result); // Show PHP response + }); +} +const contactForm = document.getElementById("contactForm"); + +if (contactForm) { + contactForm.addEventListener("submit", async (e) => { + e.preventDefault(); + + const formData = new FormData(contactForm); + + const response = await fetch("http://localhost/goodfoot-admin/send_contact.php", { + method: "POST", + body: formData, + }); + + const result = await response.text(); + alert(result); + contactForm.reset(); + }); +} + + +// API Integration +async function loadProperties() { + try { + const response = await fetch('/api/properties?limit=6'); + const data = await response.json(); + if (data.success) { + document.querySelector('.property-list').innerHTML = data.data.map(p => ` +
+ ${p.title} +

${p.title}

+

Price: Ksh ${p.price.toLocaleString()}

+

Location: ${p.location}

+ View Details +
+ `).join(''); + } + } catch (error) { + console.error('Error:', error); + } +} +document.addEventListener('DOMContentLoaded', loadProperties); + +// Force load properties from API +setTimeout(() => { + fetch('/api/properties?limit=6') + .then(r => r.json()) + .then(data => { + if (data.success) { + const propertyList = document.querySelector('.property-list'); + if (propertyList) { + propertyList.innerHTML = data.data.map(p => ` +
+ ${p.title} +

${p.title}

+

Price: Ksh ${p.price.toLocaleString()}

+

Location: ${p.location}

+ View Details +
+ `).join(''); + console.log('✅ Properties loaded from API:', data.data.length); + } + } + }) + .catch(err => console.error('❌ Error loading properties:', err)); +}, 1000); + +// Force load properties from API +setTimeout(() => { + fetch('/api/properties?limit=6') + .then(r => r.json()) + .then(data => { + if (data.success) { + const propertyList = document.querySelector('.property-list'); + if (propertyList) { + propertyList.innerHTML = data.data.map(p => ` +
+ ${p.title} +

${p.title}

+

Price: Ksh ${p.price.toLocaleString()}

+

Location: ${p.location}

+ View Details +
+ `).join(''); + console.log('✅ Properties loaded from API:', data.data.length); + } + } + }) + .catch(err => console.error('❌ Error loading properties:', err)); +}, 1000); +console.log('🔥 Script loaded successfully!'); diff --git a/public/js/script.js.temp b/public/js/script.js.temp new file mode 100644 index 0000000..7ea911d --- /dev/null +++ b/public/js/script.js.temp @@ -0,0 +1,132 @@ +// script.js +document.getElementById('contactForm').addEventListener('submit', function (e) { + e.preventDefault(); + fetch('/api/contact/submit', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name: new FormData(e.target).get('name'), email: new FormData(e.target).get('email'), message: new FormData(e.target).get('message')})}).then(r => r.json()).then(d => {alert(d.success ? 'Message sent successfully!' : 'Error: ' + d.message); if(d.success) e.target.reset();}); +}); + +// Fade-in effect on scroll +const observer = new IntersectionObserver( + (entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); + }, + { + threshold: 0.1 + } +); + +document.querySelectorAll("section").forEach(section => { + observer.observe(section); +}); + +const form = document.getElementById("propertyForm"); + +if (form) { + form.addEventListener("submit", async (e) => { + e.preventDefault(); + + const formData = new FormData(form); + + const response = await fetch("http://localhost/goodfoot-admin/add_property.php", { + method: "POST", + body: formData, + }); + + const result = await response.text(); + alert(result); // Show PHP response + }); +} +const contactForm = document.getElementById("contactForm"); + +if (contactForm) { + contactForm.addEventListener("submit", async (e) => { + e.preventDefault(); + + const formData = new FormData(contactForm); + + const response = await fetch("http://localhost/goodfoot-admin/send_contact.php", { + method: "POST", + body: formData, + }); + + const result = await response.text(); + alert(result); + contactForm.reset(); + }); +} + + +// API Integration +async function loadProperties() { + try { + const response = await fetch('/api/properties?limit=6'); + const data = await response.json(); + if (data.success) { + document.querySelector('.property-list').innerHTML = data.data.map(p => ` +
+ ${p.title} +

${p.title}

+

Price: Ksh ${p.price.toLocaleString()}

+

Location: ${p.location}

+ View Details +
+ `).join(''); + } + } catch (error) { + console.error('Error:', error); + } +} +document.addEventListener('DOMContentLoaded', loadProperties); + +// Force load properties from API +setTimeout(() => { + fetch('/api/properties?limit=6') + .then(r => r.json()) + .then(data => { + if (data.success) { + const propertyList = document.querySelector('.property-list'); + if (propertyList) { + propertyList.innerHTML = data.data.map(p => ` +
+ ${p.title} +

${p.title}

+

Price: Ksh ${p.price.toLocaleString()}

+

Location: ${p.location}

+ View Details +
+ `).join(''); + console.log('✅ Properties loaded from API:', data.data.length); + } + } + }) + .catch(err => console.error('❌ Error loading properties:', err)); +}, 1000); + +// Force load properties from API +setTimeout(() => { + fetch('/api/properties?limit=6') + .then(r => r.json()) + .then(data => { + if (data.success) { + const propertyList = document.querySelector('.property-list'); + if (propertyList) { + propertyList.innerHTML = data.data.map(p => ` +
+ ${p.title} +

${p.title}

+

Price: Ksh ${p.price.toLocaleString()}

+

Location: ${p.location}

+ View Details +
+ `).join(''); + console.log('✅ Properties loaded from API:', data.data.length); + } + } + }) + .catch(err => console.error('❌ Error loading properties:', err)); +}, 1000); +console.log('🔥 Script loaded successfully!'); diff --git a/public/js/temp_stats_fix.js b/public/js/temp_stats_fix.js new file mode 100644 index 0000000..d56a401 --- /dev/null +++ b/public/js/temp_stats_fix.js @@ -0,0 +1,41 @@ +const getDashboardStats = async (req, res) => { + try { + const [ + totalProperties, + landsCount, + carsCount, + apartmentsCount, + availableCount, + soldCount, + unreadMessages + ] = await Promise.all([ + database.get('SELECT COUNT(*) as count FROM properties'), + database.get('SELECT COUNT(*) as count FROM properties WHERE category = "lands"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE category = "cars"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE category = "apartments"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE status = "available"'), + database.get('SELECT COUNT(*) as count FROM properties WHERE status = "sold"'), + database.get('SELECT COUNT(*) as count FROM contact_messages WHERE status = "unread"') + ]); + + res.json({ + success: true, + data: { + total: totalProperties.count, + propertiesByCategory: { + lands: landsCount.count, + cars: carsCount.count, + apartments: apartmentsCount.count + }, + propertiesByStatus: { + available: availableCount.count, + sold: soldCount.count + }, + messages: unreadMessages.count + } + }); + } catch (error) { + console.error('Dashboard stats error:', error); + res.status(500).json({ success: false, message: 'Failed to get dashboard stats' }); + } +}; diff --git a/script.js b/script.js deleted file mode 100644 index c5f90fc..0000000 --- a/script.js +++ /dev/null @@ -1,61 +0,0 @@ -// script.js -document.getElementById('contactForm').addEventListener('submit', function (e) { - e.preventDefault(); - alert('Thank you! We will contact you soon.'); -}); - -// Fade-in effect on scroll -const observer = new IntersectionObserver( - (entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - entry.target.classList.add('visible'); - observer.unobserve(entry.target); - } - }); - }, - { - threshold: 0.1 - } -); - -document.querySelectorAll("section").forEach(section => { - observer.observe(section); -}); - -const form = document.getElementById("propertyForm"); - -if (form) { - form.addEventListener("submit", async (e) => { - e.preventDefault(); - - const formData = new FormData(form); - - const response = await fetch("http://localhost/goodfoot-admin/add_property.php", { - method: "POST", - body: formData, - }); - - const result = await response.text(); - alert(result); // Show PHP response - }); -} -const contactForm = document.getElementById("contactForm"); - -if (contactForm) { - contactForm.addEventListener("submit", async (e) => { - e.preventDefault(); - - const formData = new FormData(contactForm); - - const response = await fetch("http://localhost/goodfoot-admin/send_contact.php", { - method: "POST", - body: formData, - }); - - const result = await response.text(); - alert(result); - contactForm.reset(); - }); -} - From 2fd850f850c1daca8c06c2e3dbd991117e2f6fd1 Mon Sep 17 00:00:00 2001 From: q-pie Date: Mon, 14 Jul 2025 06:48:24 +0300 Subject: [PATCH 2/4] Add comprehensive setup instructions and environment template - Added detailed SETUP_INSTRUCTIONS.md for colleague setup - Created .env.example template file - Documented all installation steps and dependencies - Included troubleshooting guide and project structure - Added API endpoint documentation - Provided production deployment guidelines Amp-Thread: https://ampcode.com/threads/T-1f1e5d13-066a-49eb-aea3-845a79b68df5 Co-authored-by: Amp --- .env.example | 11 +++ SETUP_INSTRUCTIONS.md | 209 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 .env.example create mode 100644 SETUP_INSTRUCTIONS.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9c4032c --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +PORT=3000 +NODE_ENV=development +JWT_SECRET=your-super-secret-jwt-key-here-change-this-in-production +FRONTEND_URL=http://localhost:3000 + +# Database (SQLite file will be created automatically) +DB_PATH=./database/properties.db + +# Admin credentials (will be created automatically) +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin123 diff --git a/SETUP_INSTRUCTIONS.md b/SETUP_INSTRUCTIONS.md new file mode 100644 index 0000000..2363758 --- /dev/null +++ b/SETUP_INSTRUCTIONS.md @@ -0,0 +1,209 @@ +# Good Foot Properties - Setup Instructions + +## For Your Colleague to Set Up the Project + +### Prerequisites +Make sure you have the following installed: +- **Node.js** (version 14 or higher) - [Download here](https://nodejs.org/) +- **Git** - [Download here](https://git-scm.com/) + +### Step 1: Clone and Switch to the Feature Branch + +```bash +# If cloning fresh (recommended) +git clone https://github.com/GMurira/goodfootproperties.git +cd goodfootproperties + +# Switch to the feature branch with all the new backend work +git checkout feature/backend-property-system + +# OR if already have the repo cloned: +cd goodfootproperties +git fetch origin +git checkout feature/backend-property-system +git pull origin feature/backend-property-system +``` + +### Step 2: Install Dependencies + +```bash +# Install all required npm packages +npm install +``` + +This will install: +- **express** - Web framework +- **cors** - Cross-origin resource sharing +- **dotenv** - Environment variables +- **sqlite3** - Database +- **multer** - File uploads +- **bcryptjs** - Password hashing +- **jsonwebtoken** - Authentication +- **nodemon** - Development auto-restart + +### Step 3: Environment Setup + +Create a `.env` file in the root directory: + +```bash +# Create environment file +touch .env +``` + +Add the following content to `.env`: + +```env +PORT=3000 +NODE_ENV=development +JWT_SECRET=your-super-secret-jwt-key-here-change-this-in-production +FRONTEND_URL=http://localhost:3000 + +# Database (SQLite file will be created automatically) +DB_PATH=./database/properties.db + +# Admin credentials (will be created automatically) +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin123 +``` + +### Step 4: Create Required Directories + +```bash +# Create database directory +mkdir -p database + +# Create upload directories (if not already created) +mkdir -p backend/uploads/lands +mkdir -p backend/uploads/cars +mkdir -p backend/uploads/apartments +``` + +### Step 5: Start the Development Server + +```bash +# Start in development mode (auto-restarts on file changes) +npm run dev + +# OR start in production mode +npm start +``` + +You should see: +``` +🚀 Good Foot Properties API Server Started! +📍 Server running on: http://localhost:3000 +🌐 Environment: development +📡 API Documentation: http://localhost:3000/api +🏠 Frontend: http://localhost:3000 +📧 Contact: http://localhost:3000/contact +🔐 Admin: http://localhost:3000/admin + +Connected to SQLite database +Properties table ready +Admin users table ready +Contact messages table ready +``` + +### Step 6: Access the Application + +- **Frontend**: http://localhost:3000 +- **Admin Dashboard**: http://localhost:3000/admin +- **Contact Page**: http://localhost:3000/contact +- **API Documentation**: http://localhost:3000/api + +### Default Admin Login +- **Username**: admin +- **Password**: admin123 + +⚠️ **Important**: Change the admin password immediately after first login! + +## Project Structure + +``` +goodfootproperties/ +├── backend/ +│ ├── controllers/ # Business logic +│ ├── middleware/ # Authentication middleware +│ ├── models/ # Database models +│ ├── routes/ # API endpoints +│ ├── uploads/ # Uploaded images +│ ├── utils/ # Utility functions +│ └── server.js # Main server file +├── public/ +│ ├── admin/ # Admin dashboard +│ ├── assets/ # Images and static files +│ ├── css/ # Stylesheets +│ ├── js/ # Frontend JavaScript +│ ├── index.html # Main homepage +│ └── contact.html # Contact page +├── database/ # SQLite database files +├── package.json # Dependencies and scripts +└── .env # Environment variables +``` + +## API Endpoints + +### Properties +- `GET /api/properties` - Get all properties +- `GET /api/properties/category/:category` - Get by category (lands/cars/apartments) +- `POST /api/properties` - Add property (admin only) +- `PUT /api/properties/:id` - Update property (admin only) +- `DELETE /api/properties/:id` - Delete property (admin only) + +### Contact +- `POST /api/contact/submit` - Submit contact message +- `GET /api/contact` - Get all messages (admin only) + +### Authentication +- `POST /api/auth/login` - Admin login +- `GET /api/auth/verify` - Verify token + +## Troubleshooting + +### Common Issues: + +1. **Port already in use**: + ```bash + # Kill any process using port 3000 + npx kill-port 3000 + # Or change PORT in .env file + ``` + +2. **Database permission issues**: + ```bash + # Make sure database directory exists and is writable + chmod 755 database/ + ``` + +3. **Module not found errors**: + ```bash + # Delete node_modules and reinstall + rm -rf node_modules package-lock.json + npm install + ``` + +4. **Images not loading**: + - Check that upload directories exist + - Verify file permissions on backend/uploads/ + +## For Production Deployment + +1. Set `NODE_ENV=production` in .env +2. Change JWT_SECRET to a strong random string +3. Update admin credentials +4. Set proper CORS origins +5. Use a process manager like PM2: + ```bash + npm install -g pm2 + pm2 start backend/server.js --name "goodfoot-api" + ``` + +## Need Help? + +If you encounter any issues: +1. Check the server logs in the terminal +2. Verify all dependencies are installed with `npm list` +3. Ensure the `.env` file is properly configured +4. Check that all required directories exist + +The database and admin user will be created automatically on first run. From 433bb47902741b88af44f12089b7022a99aa19f4 Mon Sep 17 00:00:00 2001 From: GMurira Date: Tue, 15 Jul 2025 08:03:54 +0300 Subject: [PATCH 3/4] Fix: Deployment issue and DB path --- .env.example | 11 ------- public/contact.html | 42 +++++++++++++++------------ public/script.js | 63 +++++++++++++++++++++++++++++++++++++++++ robot.txt => robots.txt | 0 4 files changed, 87 insertions(+), 29 deletions(-) delete mode 100644 .env.example create mode 100644 public/script.js rename robot.txt => robots.txt (100%) diff --git a/.env.example b/.env.example deleted file mode 100644 index 9c4032c..0000000 --- a/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -PORT=3000 -NODE_ENV=development -JWT_SECRET=your-super-secret-jwt-key-here-change-this-in-production -FRONTEND_URL=http://localhost:3000 - -# Database (SQLite file will be created automatically) -DB_PATH=./database/properties.db - -# Admin credentials (will be created automatically) -ADMIN_USERNAME=admin -ADMIN_PASSWORD=admin123 diff --git a/public/contact.html b/public/contact.html index 7f4039c..b91420c 100644 --- a/public/contact.html +++ b/public/contact.html @@ -1,11 +1,10 @@ - - + Contact Us - Good Foot Properties - + -
@@ -44,22 +42,31 @@

🏠 Good Foot Properties

- -
-

Contact Us

-
- - - - -
-
+ + +
+ + + + + + + + + + +
+ + +
-
@@ -70,5 +77,4 @@

Contact Us

- - \ No newline at end of file + diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..7c7c3e9 --- /dev/null +++ b/public/script.js @@ -0,0 +1,63 @@ +// script.js + +// Old basic alert — replaced below with real server response +// document.getElementById('contactForm').addEventListener('submit', function (e) { +// e.preventDefault(); +// alert('Thank you! We will contact you soon.'); +// }); + +// Fade-in effect on scroll +const observer = new IntersectionObserver( + (entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); + }, + { + threshold: 0.1 + } +); + +document.querySelectorAll("section").forEach(section => { + observer.observe(section); +}); + +const form = document.getElementById("propertyForm"); + +if (form) { + form.addEventListener("submit", async (e) => { + e.preventDefault(); + + const formData = new FormData(form); + + const response = await fetch("http://localhost/goodfoot-admin/add_property.php", { + method: "POST", + body: formData, + }); + + const result = await response.text(); + alert(result); // ✅ Alert from PHP (property submission) + }); +} + +const contactForm = document.getElementById("contactForm"); + +if (contactForm) { + contactForm.addEventListener("submit", async (e) => { + e.preventDefault(); + + const formData = new FormData(contactForm); + + const response = await fetch("http://localhost/goodfoot-admin/send_contact.php", { + method: "POST", + body: formData, + }); + + const result = await response.text(); + alert(result); // ✅ Alert from PHP (contact message) + contactForm.reset(); + }); +} diff --git a/robot.txt b/robots.txt similarity index 100% rename from robot.txt rename to robots.txt From 165d5f086edadaacf7ff0e7e21ad76d7ea12ebc4 Mon Sep 17 00:00:00 2001 From: GMurira Date: Tue, 15 Jul 2025 08:18:26 +0300 Subject: [PATCH 4/4] Working on responsiveness: --- public/contact.html | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/public/contact.html b/public/contact.html index b91420c..7a09c92 100644 --- a/public/contact.html +++ b/public/contact.html @@ -3,7 +3,7 @@ - Contact Us - Good Foot Properties + Contact Us - Good Foot Properties Limited