diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4d5756e..c96ab07 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -112,7 +112,7 @@ jobs: # Copy Backend Files to VM using scp - name: Copy Backend to VM run: | - scp -v -o StrictHostKeyChecking=no -r ./backend/* ${{ secrets.VM_USER }}@${{ secrets.VM_IP }}:~/smetovi/backend + scp -o StrictHostKeyChecking=no -r ./backend/* ${{ secrets.VM_USER }}@${{ secrets.VM_IP }}:~/smetovi/backend # Restart Node.js Backend on VM - name: Restart Backend on VM diff --git a/.vscode/launch.json b/.vscode/launch.json index 80cec60..221c7b7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,11 +9,17 @@ "request": "launch", "name": "Launch Program", "env": { - "DB_HOST": "localhost", - "DB_USER": "root", - "DB_PASSWORD": "", - "DB_NAME": "smetovi", - "PORT": "4001" + "DATABASE_URL":"postgresql://neondb_owner:nekYVhH8Tzo6@ep-holy-moon-a5i4sdfw-pooler.us-east-2.aws.neon.tech/smetovi?sslmode=require", + "JWT_SECRET":"mysueprsecret", + "JWT_REFRESH_SECRET":"myrefreshsupersecret", + "PORT":"4000", + "SMTP_HOST":"mail.smetovi.ba", + "SMTP_PORT":"465", + "SMTP_USER":"support@smetovi.ba", + "SMTP_PASS":"P8es3G[W=7m&", + "DEFAULT_SENDER_EMAIL":"support@smetovi.ba", + "DEFAULT_RECEIVER_EMAIL":"support@smetovi.ba", + "DEFAULT_RECEIVER_NAME":"Smetovi" }, "skipFiles": [ "/**" diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..2a69c3c --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,113 @@ +# Operating System Files +.DS_Store +Thumbs.db + +# Editor and IDE Files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Build and Dependency Directories +node_modules/ +build/ +dist/ +target/ +out/ +.next/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment Variables +.env +.env.local +.env.*.local + +# Compiled Files +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Package Files +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Database Files +*.sqlite +*.db + +# Temporary Files +*.tmp +*.temp +*.bak + +# Python +__pycache__/ +*.py[cod] +*.pyo +*.egg-info/ +.Python +venv/ +env/ + +# Java +*.class +*.jar +*.war +*.ear +*.logs +.gradle/ + +# JavaScript / Node.js +npm-debug.log +yarn-error.log +.pnp/ +.pnp.js + +# Ruby +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +# Miscellaneous +*.bak +*.swp +*.swo +*.swn +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Ignore Angular/Webpack cache and build files +.cache/ +*.cache +*.log +node_modules/ +dist/ +build/ +webpack* \ No newline at end of file diff --git a/backend/api_test/locations_enpoints.http b/backend/api_test/locations_enpoints.http deleted file mode 100644 index a74cd84..0000000 --- a/backend/api_test/locations_enpoints.http +++ /dev/null @@ -1,95 +0,0 @@ -### Location API Tests - -@baseUrl = http://localhost:4000 - -### Create a new location -# POST {{baseUrl}}/api/locations -# Content-Type: application/json - -# { -# "name": "Wall Street", -# "address": "New York, NY 10022", -# "description": "Famous urban park in Manhattan", -# "longitude": -73.9665, -# "latitude": 40.7812, -# "CategoryId": 1 -# } - -### Get all locations -GET {{baseUrl}}/locations - -# ### Get a specific location by ID -# GET {{baseUrl}}/locations/1 - -# ### Update a location -# PUT {{baseUrl}}/locations/1 -# Content-Type: application/json - -# { -# "name": "Central Park Updated", -# "description": "Updated description for Central Park" -# } - -# ### Delete a location -# DELETE {{baseUrl}}/locations/1 - -# ### Create another location -# POST {{baseUrl}}/locations -# Content-Type: application/json - -# { -# "name": "Eiffel Tower", -# "address": "Champ de Mars, 5 Avenue Anatole France, 75007 Paris, France", -# "description": "Iconic iron lattice tower on the Champ de Mars in Paris", -# "longitude": 2.2945, -# "latitude": 48.8584, -# "categoryId": 1, -# "userId": 1, -# } - -# ### Get all locations (after creating new ones) -# GET {{baseUrl}}/locations - -# ### Get a non-existent location -# GET {{baseUrl}}/locations/999 - -# ### Update a non-existent location -# PUT {{baseUrl}}/locations/999 -# Content-Type: application/json - -# { -# "name": "Non-existent Location" -# } - -# ### Delete a non-existent location -# DELETE {{baseUrl}}/locations/999 - -# ### Create a location with missing required fields -# POST {{baseUrl}}/locations -# Content-Type: application/json - -# { -# "name": "Incomplete Location" -# } - -# ### Create a location with invalid category ID -# POST {{baseUrl}}/locations -# Content-Type: application/json - -# { -# "name": "Invalid Category Location", -# "address": "Some Address", -# "categoryId": 9999 -# } - -# ### Get locations with category details -# GET {{baseUrl}}/locations?include=category - -# ### Get locations with user details -# GET {{baseUrl}}/locations?include=users - -# ### Get locations within a specific area (if implemented) -# GET {{baseUrl}}/locations?minLat=40&maxLat=41&minLong=-74&maxLong=-73 - -# ### Search locations by name (if implemented) -# GET {{baseUrl}}/locations?search=park \ No newline at end of file diff --git a/backend/config/db.js b/backend/config/db.js index 9d40df9..38f0589 100644 --- a/backend/config/db.js +++ b/backend/config/db.js @@ -1,14 +1,20 @@ -const mysql = require("mysql2"); -require("dotenv").config(); - -const pool = mysql.createPool({ - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - waitForConnections: true, - connectionLimit: 10, - queueLimit: 0, +const { Sequelize } = require('sequelize'); +const dotenv = require('dotenv'); + +dotenv.config(); + +const sequelize = new Sequelize(process.env.DATABASE_URL, { + dialect: 'postgres', + logging: false, }); -module.exports = pool.promise(); +const connectDB = async () => { + try { + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); + } catch (error) { + console.error('Unable to connect to the database:', error); + } +}; + +module.exports = { sequelize, connectDB }; \ No newline at end of file diff --git a/backend/config/dbInit.js b/backend/config/dbInit.js deleted file mode 100644 index 5ae064e..0000000 --- a/backend/config/dbInit.js +++ /dev/null @@ -1,107 +0,0 @@ -const mysql = require("mysql2"); -require("dotenv").config(); - -const connection = mysql.createConnection({ - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, -}); - -// Initialize database and tables -const initDB = () => { - connection.connect((err) => { - if (err) throw err; - console.log("Connected to MySQL!"); - - // Create database if it doesn't exist - connection.query( - `CREATE DATABASE IF NOT EXISTS \`${process.env.DB_NAME}\`;`, - (err) => { - if (err) throw err; - console.log(`Database ${process.env.DB_NAME} checked/created`); - - // Use the created or existing database - connection.changeUser({ database: process.env.DB_NAME }, (err) => { - if (err) throw err; - - // Create tables - createTables(); - }); - } - ); - }); -}; - -const createTables = () => { - const userTable = ` - CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - ); - `; - - const categoryTable = ` - CREATE TABLE IF NOT EXISTS categories ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE, - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - ); - `; - - const locationTable = ` - CREATE TABLE IF NOT EXISTS locations ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - address VARCHAR(255), - description TEXT, - longitude DECIMAL(9,6), - latitude DECIMAL(9,6), - user_id INT, - category_id INT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL - ); - `; - - const contactTable = ` - CREATE TABLE IF NOT EXISTS contacts ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(63) NOT NULL, - contactInfo VARCHAR(63), -- Email is optional - subject VARCHAR(127) NOT NULL, - message VARCHAR(255) NOT NULL, -- Message length is now 255 characters - createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `; - - connection.query(userTable, (err) => { - if (err) throw err; - console.log("User table checked/created"); - }); - - connection.query(categoryTable, (err) => { - if (err) throw err; - console.log("Category table checked/created"); - }); - - connection.query(locationTable, (err) => { - if (err) throw err; - console.log("Location table checked/created"); - }); - - // Create contact table if needed - // connection.query(contactTable, (err) => { - // if (err) throw err; - // console.log("Contact table checked/created"); - // }); -}; - -module.exports = initDB; diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js new file mode 100644 index 0000000..07dd540 --- /dev/null +++ b/backend/controllers/authController.js @@ -0,0 +1,69 @@ +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const { User, RefreshToken } = require('../models'); +const dotenv = require('dotenv'); + +dotenv.config(); + +const register = async (req, res) => { + const { username, email, password } = req.body; + try { + const hashedPassword = await bcrypt.hash(password, 10); + const user = await User.create({ username, email, password: hashedPassword }); + res.status(201).json(user); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}; + +const users = async (req, res) => { + try { + const allUsers = await User.findAll({ + attributes: ['id', 'username', 'email'], + }); + res.status(200).json(allUsers); + } catch (error) { + console.error('Error fetching users:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +const login = async (req, res) => { + const { email, password } = req.body; + try { + const user = await User.findOne({ where: { email } }); + if (!user) return res.status(404).json({ error: 'User not found' }); + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) return res.status(400).json({ error: 'Invalid credentials' }); + + const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + const refreshToken = jwt.sign({ id: user.id }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' }); + + // Store refresh token in the database + await RefreshToken.create({ token: refreshToken, userId: user.id }); + + res.json({ token, refreshToken }); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}; + +const refreshToken = async (req, res) => { + const { token } = req.body; + if (!token) return res.status(401).json({ error: 'Refresh token is required' }); + + try { + const storedToken = await RefreshToken.findOne({ where: { token } }); + if (!storedToken) return res.status(403).json({ error: 'Invalid refresh token' }); + + const decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET); + const newToken = jwt.sign({ id: decoded.id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + + res.json({ token: newToken }); + } catch (error) { + res.status(403).json({ error: 'Invalid refresh token' }); + } +}; + +module.exports = { register, login, refreshToken, users }; \ No newline at end of file diff --git a/backend/controllers/categoryController.js b/backend/controllers/categoryController.js index c7c9af0..c7ad1d3 100644 --- a/backend/controllers/categoryController.js +++ b/backend/controllers/categoryController.js @@ -1,46 +1,75 @@ -const CategoryService = require("../services/categoryService"); +const { Category } = require('../models'); -exports.getAllCategories = async (req, res, next) => { +// Get all categories +const getAllCategories = async (req, res) => { try { - const categories = await CategoryService.getAllCategories(); + const categories = await Category.findAll(); res.status(200).json(categories); } catch (error) { - next(error); + res.status(500).json({ error: error.message }); } }; -exports.getCategoryById = async (req, res, next) => { +// Get category by ID +const getCategoryById = async (req, res) => { + const { id } = req.params; try { - const category = await CategoryService.getCategoryById(req.params.id); + const category = await Category.findByPk(id); + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } res.status(200).json(category); } catch (error) { - next(error); + res.status(500).json({ error: error.message }); } }; -exports.createCategory = async (req, res, next) => { +// Create a new category +const createCategory = async (req, res) => { + const { name } = req.body; try { - const category = await CategoryService.createCategory(req.body); - res.status(201).json({ message: "Category created", category }); + const category = await Category.create({ name }); + res.status(201).json(category); } catch (error) { - next(error); + res.status(400).json({ error: error.message }); } }; -exports.updateCategory = async (req, res, next) => { +// Update a category +const updateCategory = async (req, res) => { + const { id } = req.params; + const { name } = req.body; try { - await CategoryService.updateCategory(req.params.id, req.body); - res.status(200).json({ message: "Category updated" }); + const category = await Category.findByPk(id); + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + await category.update({ name }); + res.status(200).json(category); } catch (error) { - next(error); + res.status(400).json({ error: error.message }); } }; -exports.deleteCategory = async (req, res, next) => { +// Delete a category +const deleteCategory = async (req, res) => { + const { id } = req.params; try { - await CategoryService.deleteCategory(req.params.id); - res.status(200).json({ message: "Category deleted" }); + const category = await Category.findByPk(id); + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + await category.destroy(); + res.status(200).json({ message: 'Category deleted successfully' }); } catch (error) { - next(error); + res.status(500).json({ error: error.message }); } }; + +module.exports = { + getAllCategories, + getCategoryById, + createCategory, + updateCategory, + deleteCategory, +}; \ No newline at end of file diff --git a/backend/controllers/contactController.js b/backend/controllers/contactController.js index 94a38dd..527add8 100644 --- a/backend/controllers/contactController.js +++ b/backend/controllers/contactController.js @@ -1,29 +1,63 @@ -const ContactService = require("../services/contactService"); +// contactController.js + +const nodemailer = require("nodemailer"); +const Contact = require("../models/contact"); + +// Function to handle sending contact emails +const sendContactEmail = async (req, res) => { + const { name, contactInfo, subject, message } = req.body; -exports.sendEmail = async (req, res, next) => { try { - const emailData = req.body; - await ContactService.sendEmail(emailData); - res.status(200).json({ message: "Email sent successfully" }); + const contact = await Contact.create({ name, contactInfo, subject, message }); + res.status(201).json({message: "Message sent successfully"}); } catch (error) { - res.status(400).json({ message: error.message }); + res.status(400).json({ error: error.message }); } -}; -exports.getAllMessages = async (req, res, next) => { + // Create a transporter object using SMTP transport + let transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + secure: process.env.SMTP_PORT === "465", // true for 465, false for other ports + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + }, + tls: { + rejectUnauthorized: false, // Allow self-signed certificates + }, + }); + + const mailOptions = { + from: `${name} <${contactInfo || process.env.DEFAULT_SENDER_EMAIL}>`, + to: `${process.env.DEFAULT_RECEIVER_NAME} <${process.env.DEFAULT_RECEIVER_EMAIL}>`, + subject: subject, + text: `Message from ${name} (${contactInfo}):\n\n${message}`, + }; + + console.log("Mail options prepared:", mailOptions); + try { - const messages = await ContactService.getAllMessages(); - res.status(200).json(messages); + const info = await transporter.sendMail(mailOptions); + console.log("Email sent successfully:", info.response); } catch (error) { - next(error); + console.error("Error caught while sending email:", error.message); + throw new Error("An error occurred while sending the email."); } }; -exports.getMessageById = async (req, res, next) => { - try { - const message = await ContactService.getMessageById(req.params.id); - res.status(200).json(message); - } catch (error) { - next(error); +const getAllMessages = async () => { + return await ContactRepository.findAllMessages(); +}; + +const getMessageById = async (id) => { + const message = await ContactRepository.findMessageById(id); + if (!message) { + throw new Error("Message not found"); } + return message; +}; + +module.exports = { + sendContactEmail, }; diff --git a/backend/controllers/locationController.js b/backend/controllers/locationController.js new file mode 100644 index 0000000..0d147eb --- /dev/null +++ b/backend/controllers/locationController.js @@ -0,0 +1,164 @@ +const { Location, User, Category } = require('../models'); + +// Get all locations +const getAllLocations = async (req, res) => { + try { + const locations = await Location.findAll({ + include: [ + { + model: User, + attributes: ['id', 'username', 'email'], // Exclude password + }, + { + model: Category, + attributes: ['id', 'name'], + }, + ], + }); + res.status(200).json(locations); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Get location by ID +const getLocationById = async (req, res) => { + const { id } = req.params; + try { + const location = await Location.findByPk(id, { + include: [ + { + model: User, + attributes: ['id', 'username', 'email'], // Exclude password + }, + { + model: Category, + attributes: ['id', 'name'], + }, + ], + }); + if (!location) { + return res.status(404).json({ error: 'Location not found' }); + } + res.status(200).json(location); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Get locations by user +const getLocationsByUser = async (req, res) => { + const { userId } = req.params; + try { + const locations = await Location.findAll({ + where: { UserId: userId }, + include: [ + { + model: User, + attributes: ['id', 'username', 'email'], // Exclude password + }, + { + model: Category, + attributes: ['id', 'name'], + }, + ], + }); + res.status(200).json(locations); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Get locations by category +const getLocationsByCategory = async (req, res) => { + const { categoryId } = req.params; + try { + const locations = await Location.findAll({ + where: { CategoryId: categoryId }, + include: [ + { + model: User, + attributes: ['id', 'username', 'email'], // Exclude password + }, + { + model: Category, + attributes: ['id', 'name'], + }, + ], + }); + res.status(200).json(locations); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Create a new location +const createLocation = async (req, res) => { + const { name, address, description, longitude, latitude, image, CategoryId } = req.body; + const userId = req.user.id; // Assuming the user ID is available in the request object + + try { + // Check if the category exists + const category = await Category.findByPk(CategoryId); + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + + // Create the location + const location = await Location.create({ + name, + address, + description, + longitude, + latitude, + image, + CategoryId, + UserId: userId, + }); + + res.status(201).json(location); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}; + +// Update location +const updateLocation = async (req, res) => { + const { id } = req.params; + const { name, address, description, longitude, latitude, image, CategoryId } = req.body; + try { + const location = await Location.findByPk(id); + if (!location) { + return res.status(404).json({ error: 'Location not found' }); + } + await location.update({ name, address, description, longitude, latitude, image, CategoryId }); + res.status(200).json(location); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Delete location +const deleteLocation = async (req, res) => { + const { id } = req.params; + try { + const location = await Location.findByPk(id); + if (!location) { + return res.status(404).json({ error: 'Location not found' }); + } + await location.destroy(); + res.status(200).json({ message: 'Location deleted successfully' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +module.exports = { + getAllLocations, + getLocationById, + getLocationsByUser, + getLocationsByCategory, + updateLocation, + deleteLocation, + createLocation +}; \ No newline at end of file diff --git a/backend/controllers/locationsController.js b/backend/controllers/locationsController.js deleted file mode 100644 index b9be93c..0000000 --- a/backend/controllers/locationsController.js +++ /dev/null @@ -1,49 +0,0 @@ -const LocationService = require("../services/locationService"); - -exports.createLocation = async (req, res, next) => { - try { - const location = await LocationService.createLocation(req.body); - res.status(201).json({ message: "Location created", location }); - } catch (error) { - res.status(400).json({ message: error.message }); - } -}; - -exports.updateLocation = async (req, res, next) => { - try { - const location = await LocationService.updateLocation( - req.params.id, - req.body - ); - res.status(200).json({ message: "Location updated", location }); - } catch (error) { - res.status(400).json({ message: error.message }); - } -}; - -exports.getAllLocations = async (req, res, next) => { - try { - const [rows] = await LocationService.getAllLocations(); - res.status(200).json(rows); - } catch (error) { - next(error); - } -}; - -exports.getLocationById = async (req, res, next) => { - try { - const [rows] = await LocationService.getLocationById(req.params.id); - res.status(200).json(rows[0]); - } catch (error) { - next(error); - } -}; - -exports.deleteLocation = async (req, res, next) => { - try { - await LocationService.deleteLocation(req.params.id); - res.status(200).json({ message: "Location deleted" }); - } catch (error) { - next(error); - } -}; diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js deleted file mode 100644 index a82cb0c..0000000 --- a/backend/controllers/userController.js +++ /dev/null @@ -1,56 +0,0 @@ -const UserService = require("../services/userService"); - -exports.register = async (req, res, next) => { - try { - const user = await UserService.createUser(req.body); - res.status(201).json({ message: "User created successfully", user }); - } catch (error) { - res.status(400).json({ message: error.message }); - } -}; - -exports.login = async (req, res, next) => { - try { - const { email, password } = req.body; - const user = await UserService.loginUser(email, password); - res.status(200).json({ message: "Login successful", user }); - } catch (error) { - res.status(400).json({ message: error.message }); - } -}; - -exports.getAllUsers = async (req, res, next) => { - try { - const users = await UserService.getAllUsers(); - res.status(200).json(users); - } catch (error) { - next(error); - } -}; - -exports.getUserById = async (req, res, next) => { - try { - const user = await UserService.getUserById(req.params.id); - res.status(200).json(user); - } catch (error) { - next(error); - } -}; - -exports.updateUser = async (req, res, next) => { - try { - await UserService.updateUser(req.params.id, req.body); - res.status(200).json({ message: "User updated" }); - } catch (error) { - next(error); - } -}; - -exports.deleteUser = async (req, res, next) => { - try { - await UserService.deleteUser(req.params.id); - res.status(200).json({ message: "User deleted" }); - } catch (error) { - next(error); - } -}; diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..9bdf567 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + postgres: + image: postgres:latest + container_name: smetovi_postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: smetovi + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + +volumes: + postgres_data: \ No newline at end of file diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 0000000..592dee3 --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,26 @@ +const jwt = require('jsonwebtoken'); +const dotenv = require('dotenv'); + +dotenv.config(); + +const authMiddleware = (req, res, next) => { + const authHeader = req.header('Authorization'); + if (!authHeader) { + return res.status(401).json({ error: 'Access denied. No token provided.' }); + } + + const token = authHeader.replace('Bearer ', ''); + if (!token) { + return res.status(401).json({ error: 'Access denied. Invalid token format.' }); + } + + try { + const verified = jwt.verify(token, process.env.JWT_SECRET); + req.user = verified; + next(); + } catch (error) { + res.status(400).json({ error: 'Invalid token' }); + } +}; + +module.exports = authMiddleware; \ No newline at end of file diff --git a/backend/middleware/errorMiddleware.js b/backend/middleware/errorMiddleware.js new file mode 100644 index 0000000..980fda1 --- /dev/null +++ b/backend/middleware/errorMiddleware.js @@ -0,0 +1,14 @@ +const errorMiddleware = (err, req, res, next) => { + console.error(err.stack); + + const statusCode = err.statusCode || 500; + const message = err.message || 'Internal Server Error'; + + res.status(statusCode).json({ + success: false, + message, + stack: process.env.NODE_ENV === 'production' ? null : err.stack, + }); + }; + + module.exports = errorMiddleware; \ No newline at end of file diff --git a/backend/middlewares/errorHandler.js b/backend/middlewares/errorHandler.js deleted file mode 100644 index 32b100f..0000000 --- a/backend/middlewares/errorHandler.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = (err, req, res, next) => { - console.error(err.message); - res.status(500).json({ message: "Internal Server Error" }); -}; diff --git a/backend/models/category.js b/backend/models/category.js index e85ffab..f58e5f7 100644 --- a/backend/models/category.js +++ b/backend/models/category.js @@ -1,11 +1,12 @@ -class Category { - constructor(id, name, description, createdAt, updatedAt) { - this.id = id; - this.name = name; - this.description = description; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } -} +const { DataTypes } = require("sequelize"); +const { sequelize } = require("../config/db"); + +const Category = sequelize.define( + "Category", + { + name: { type: DataTypes.STRING, allowNull: false, unique: true }, + }, + { timestamps: true } +); module.exports = Category; diff --git a/backend/models/contact.js b/backend/models/contact.js index 90bb2b7..a1fa713 100644 --- a/backend/models/contact.js +++ b/backend/models/contact.js @@ -1,11 +1,15 @@ -class Contact { - constructor(name, contactInfo, subject, message, createdAt) { - this.name = name; - this.contactInfo = contactInfo; - this.subject = subject; - this.message = message; - this.createdAt = createdAt; - } -} +const { DataTypes } = require("sequelize"); +const { sequelize } = require("../config/db"); + +const Contact = sequelize.define( + "Contact", + { + name: { type: DataTypes.STRING, allowNull: false}, + contactInfo: { type: DataTypes.STRING, allowNull: false }, + subject: { type: DataTypes.STRING, allowNull: false }, + message: { type: DataTypes.STRING, allowNull: false }, + }, + { timestamps: true } +); module.exports = Contact; diff --git a/backend/models/index.js b/backend/models/index.js new file mode 100644 index 0000000..20096de --- /dev/null +++ b/backend/models/index.js @@ -0,0 +1,15 @@ +const User = require('./user'); +const Location = require('./location'); +const Category = require('./category'); +const RefreshToken = require('./refreshToken'); +const Contact = require('./contact'); + +const initModels = async () => { + await User.sync(); + await Category.sync(); + await Location.sync(); + await RefreshToken.sync(); + await Contact.sync(); +}; + +module.exports = { User, Location, Category, RefreshToken, Contact, initModels }; \ No newline at end of file diff --git a/backend/models/location.js b/backend/models/location.js index d84d764..36dad98 100644 --- a/backend/models/location.js +++ b/backend/models/location.js @@ -1,27 +1,22 @@ -class Location { - constructor( - id, - name, - address, - description, - longitude, - latitude, - userId, - categoryId, - createdAt, - updatedAt - ) { - this.id = id; - this.name = name; - this.address = address; - this.description = description; - this.longitude = longitude; - this.latitude = latitude; - this.userId = userId; - this.categoryId = categoryId; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } -} +const { DataTypes } = require("sequelize"); +const { sequelize } = require("../config/db"); +const Category = require("./category"); +const User = require("./user"); + +const Location = sequelize.define( + "Location", + { + name: { type: DataTypes.STRING, allowNull: false }, + address: { type: DataTypes.STRING, allowNull: false }, + description: { type: DataTypes.STRING }, + longitude: { type: DataTypes.FLOAT, allowNull: false }, + latitude: { type: DataTypes.FLOAT, allowNull: false }, + image: { type: DataTypes.STRING }, + }, + { timestamps: true } +); + +Location.belongsTo(Category); +Location.belongsTo(User); module.exports = Location; diff --git a/backend/models/refreshToken.js b/backend/models/refreshToken.js new file mode 100644 index 0000000..2292835 --- /dev/null +++ b/backend/models/refreshToken.js @@ -0,0 +1,9 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/db'); + +const RefreshToken = sequelize.define('RefreshToken', { + token: { type: DataTypes.STRING, allowNull: false }, + userId: { type: DataTypes.INTEGER, allowNull: false }, +}, { timestamps: true }); + +module.exports = RefreshToken; \ No newline at end of file diff --git a/backend/models/user.js b/backend/models/user.js index d5029f2..bc3593c 100644 --- a/backend/models/user.js +++ b/backend/models/user.js @@ -1,12 +1,14 @@ -class User { - constructor(id, name, email, password, createdAt, updatedAt) { - this.id = id; - this.name = name; - this.email = email; - this.password = password; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } -} +const { DataTypes } = require("sequelize"); +const { sequelize } = require("../config/db"); + +const User = sequelize.define( + "User", + { + username: { type: DataTypes.STRING, allowNull: false }, + email: { type: DataTypes.STRING, allowNull: false, unique: true }, + password: { type: DataTypes.STRING, allowNull: false }, + }, + { timestamps: true } +); module.exports = User; diff --git a/backend/package-lock.json b/backend/package-lock.json index 14610d1..ae12f27 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,22 +1,24 @@ { - "name": "backend", + "name": "smetovi-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "backend", + "name": "smetovi-backend", "version": "1.0.0", "license": "ISC", "dependencies": { - "backend": "file:", "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.0", - "mysql": "^2.18.1", - "mysql2": "^3.11.3", + "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "nodemailer": "^6.9.15", + "pg": "^8.13.0", + "sequelize": "^6.37.4" + }, + "devDependencies": { "nodemon": "^3.1.7" } }, @@ -40,6 +42,28 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.5", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/validator": { + "version": "13.12.2", + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -48,8 +72,7 @@ }, "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" @@ -70,29 +93,6 @@ "node": ">= 6.0.0" } }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/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/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -106,6 +106,8 @@ "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" @@ -134,42 +136,15 @@ "node": ">=10" } }, - "node_modules/are-we-there-yet/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/array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/backend": { - "resolved": "", - "link": true + "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==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/bcrypt": { "version": "5.1.1", @@ -185,18 +160,12 @@ "node": ">= 10.0.0" } }, - "node_modules/bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", - "engines": { - "node": "*" - } - }, "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" }, @@ -206,8 +175,7 @@ }, "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", @@ -227,10 +195,22 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -240,6 +220,8 @@ "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" }, @@ -247,18 +229,20 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, "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/call-bind": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -277,6 +261,8 @@ "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", @@ -317,7 +303,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/console-control-strings": { "version": "1.1.0", @@ -327,8 +314,7 @@ }, "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" }, @@ -338,34 +324,27 @@ }, "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.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "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==" - }, - "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" @@ -375,17 +354,23 @@ } }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.7", + "license": "MIT", "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -404,27 +389,16 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "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" @@ -443,6 +417,7 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -450,10 +425,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dottie": { + "version": "2.0.6", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "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", @@ -463,16 +448,14 @@ }, "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/es-define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -482,36 +465,34 @@ }, "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/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/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "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.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -542,10 +523,23 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "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" }, @@ -555,8 +549,7 @@ }, "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", @@ -570,18 +563,27 @@ "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "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" } @@ -620,7 +622,9 @@ "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" @@ -631,8 +635,7 @@ }, "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" } @@ -658,19 +661,9 @@ "node": ">=10" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -710,6 +703,8 @@ "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" }, @@ -719,8 +714,7 @@ }, "node_modules/gopd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -732,14 +726,15 @@ "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-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -749,8 +744,7 @@ }, "node_modules/has-proto": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -760,8 +754,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -777,8 +770,7 @@ }, "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" }, @@ -788,8 +780,7 @@ }, "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", @@ -814,33 +805,9 @@ "node": ">= 6" } }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/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/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" }, @@ -851,7 +818,16 @@ "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==" + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflection": { + "version": "1.13.4", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" }, "node_modules/inflight": { "version": "1.0.6", @@ -866,13 +842,11 @@ }, "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/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" } @@ -881,6 +855,8 @@ "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" }, @@ -892,6 +868,8 @@ "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" } @@ -909,6 +887,8 @@ "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" }, @@ -920,51 +900,83 @@ "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/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", + "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" + "node": ">=12", + "npm": ">=6" } }, - "node_modules/lru.min": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", - "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "node_modules/jwa": { + "version": "1.4.1", "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" + "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", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "license": "MIT" + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -991,32 +1003,28 @@ }, "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" }, @@ -1026,16 +1034,14 @@ }, "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" }, @@ -1047,6 +1053,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1100,87 +1107,30 @@ "node": ">=10" } }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/mysql": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", - "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", - "dependencies": { - "bignumber.js": "9.0.0", - "readable-stream": "2.3.7", - "safe-buffer": "5.1.2", - "sqlstring": "2.3.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mysql/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==" - }, - "node_modules/mysql2": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.3.tgz", - "integrity": "sha512-Qpu2ADfbKzyLdwC/5d4W7+5Yz7yBzCU05YWt5npWzACST37wJsB23wgOSo00qi043urkiRwXtEvJc9UnuLX/MQ==", + "node_modules/moment": { + "version": "2.30.1", "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.1", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, "engines": { - "node": ">= 8.0" + "node": "*" } }, - "node_modules/mysql2/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==", + "node_modules/moment-timezone": { + "version": "0.5.46", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "moment": "^2.29.4" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mysql2/node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "node": "*" } }, - "node_modules/named-placeholders": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", - "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", - "license": "MIT", - "dependencies": { - "lru-cache": "^7.14.1" - }, - "engines": { - "node": ">=12.0.0" - } + "node_modules/ms": { + "version": "2.1.3", + "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" } @@ -1215,6 +1165,7 @@ "version": "6.9.15", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } @@ -1223,6 +1174,8 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1246,27 +1199,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/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==" - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -1286,6 +1218,8 @@ "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" } @@ -1314,8 +1248,7 @@ }, "node_modules/object-inspect": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1325,8 +1258,7 @@ }, "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" }, @@ -1345,8 +1277,7 @@ }, "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" } @@ -1362,13 +1293,89 @@ }, "node_modules/path-to-regexp": { "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz", + "integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } }, "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" }, @@ -1376,15 +1383,40 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "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==" + "node_modules/postgres-array": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, "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" @@ -1396,12 +1428,13 @@ "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==" + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" }, "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" }, @@ -1414,16 +1447,14 @@ }, "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", @@ -1435,28 +1466,25 @@ } }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "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": { - "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" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/readable-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==" - }, "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" }, @@ -1464,6 +1492,10 @@ "node": ">=8.10.0" } }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -1482,8 +1514,6 @@ }, "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", @@ -1497,17 +1527,16 @@ "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.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1517,8 +1546,7 @@ }, "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", @@ -1538,28 +1566,96 @@ "node": ">= 0.8.0" } }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "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/send/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==" + "node_modules/sequelize": { + "version": "6.37.4", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.4.tgz", + "integrity": "sha512-+8B0p00EKmxJpwwruDI0drxh4wNSC0YB9pVhOajRzfMI+uIDi5V7rJPC8RTTkLmKUoAIatJZn6lW9gj6bmmYKQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + "node_modules/sequelize-pool": { + "version": "7.1.0", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } }, "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", @@ -1578,8 +1674,7 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -1594,13 +1689,11 @@ }, "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.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -1624,6 +1717,8 @@ "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" }, @@ -1631,35 +1726,29 @@ "node": ">=10" } }, - "node_modules/sqlstring": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "node_modules/split2": { + "version": "4.2.0", + "license": "ISC", "engines": { - "node": ">= 0.6" + "node": ">= 10.x" } }, "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/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==", + "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.1.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/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==" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1690,6 +1779,8 @@ "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" }, @@ -1718,6 +1809,8 @@ "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" }, @@ -1727,16 +1820,21 @@ }, "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/toposort-class": { + "version": "1.0.1", + "license": "MIT" + }, "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" } @@ -1749,8 +1847,7 @@ }, "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" @@ -1762,12 +1859,17 @@ "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "license": "MIT" }, "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" } @@ -1775,20 +1877,33 @@ "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==" + "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/uuid": { + "version": "8.3.2", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "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" } @@ -1818,12 +1933,26 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wkx": { + "version": "0.5.0", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "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", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 091aa8d..f921bef 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,10 +1,10 @@ { - "name": "backend", + "name": "smetovi-backend", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { - "dev": "nodemon server.js", + "dev": "npx nodemon server.js", "start": "node server.js", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -12,14 +12,16 @@ "author": "", "license": "ISC", "dependencies": { - "backend": "file:", "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.0", - "mysql": "^2.18.1", - "mysql2": "^3.11.3", + "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "nodemailer": "^6.9.15", + "pg": "^8.13.0", + "sequelize": "^6.37.4" + }, + "devDependencies": { "nodemon": "^3.1.7" } } diff --git a/backend/repositories/categoryRepository.js b/backend/repositories/categoryRepository.js deleted file mode 100644 index b9f9031..0000000 --- a/backend/repositories/categoryRepository.js +++ /dev/null @@ -1,40 +0,0 @@ -const db = require("../config/db"); - -class CategoryRepository { - async createCategory(data) { - const [result] = await db.execute( - "INSERT INTO categories (name, description) VALUES (?, ?)", - [data.name, data.description] - ); - return result; - } - - async findCategoryById(id) { - const [rows] = await db.execute("SELECT * FROM categories WHERE id = ?", [ - id, - ]); - return rows[0]; - } - - async findAllCategories() { - const [rows] = await db.execute("SELECT * FROM categories"); - return rows; - } - - async updateCategory(id, data) { - const result = await db.execute( - "UPDATE categories SET name = ?, description = ? WHERE id = ?", - [data.name, data.description, id] - ); - return result; - } - - async deleteCategory(id) { - const result = await db.execute("DELETE FROM categories WHERE id = ?", [ - id, - ]); - return result; - } -} - -module.exports = new CategoryRepository(); diff --git a/backend/repositories/contactRepository.js b/backend/repositories/contactRepository.js deleted file mode 100644 index f823e97..0000000 --- a/backend/repositories/contactRepository.js +++ /dev/null @@ -1,25 +0,0 @@ -const db = require("../config/db"); - -class ContactRepository { - async createMessage(data) { - const [result] = await db.execute( - "INSERT INTO contacts (name, contactInfo, subject, message, createdAt) VALUES (?, ?, ?, ?, ?)", - [data.name, data.contactInfo, data.subject, data.message, new Date()] - ); - return result; - } - - async findAllMessages() { - const [rows] = await db.execute("SELECT * FROM contacts"); - return rows; - } - - async findMessageById(id) { - const [rows] = await db.execute("SELECT * FROM contacts WHERE id = ?", [ - id, - ]); - return rows.length > 0 ? rows[0] : null; - } -} - -module.exports = new ContactRepository(); diff --git a/backend/repositories/locationRepository.js b/backend/repositories/locationRepository.js deleted file mode 100644 index fe56b51..0000000 --- a/backend/repositories/locationRepository.js +++ /dev/null @@ -1,80 +0,0 @@ -const db = require("../config/db"); - -class LocationRepository { - async createLocation(data) { - const [result] = await db.execute( - "INSERT INTO locations (name, address, description, longitude, latitude, user_id, category_id) VALUES (?, ?, ?, ?, ?, ?, ?)", - [ - data.name, - data.address, - data.description, - data.longitude, - data.latitude, - data.userId, - data.categoryId, - ] - ); - return result; - } - - async updateLocation(id, data) { - // Check if the Location exists - const location = await LocationRepository.findLocationById(id); - if (!location) { - throw new Error("Location not found"); - } - - // Check if the User exists - if (data.userId) { - const user = await UserRepository.findUserById(data.userId); - if (!user) { - throw new Error("User not found"); - } - } - - // Check if the Category exists - if (data.categoryId) { - const category = await CategoryRepository.findCategoryById( - data.categoryId - ); - if (!category) { - throw new Error("Category not found"); - } - } - - // Proceed to update the location - const result = await db.execute( - "UPDATE locations SET name = ?, address = ?, description = ?, longitude = ?, latitude = ?, user_id = ?, category_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", - [ - data.name, - data.address, - data.description, - data.longitude, - data.latitude, - data.userId, - data.categoryId, - id, - ] - ); - return result; - } - - async findLocationById(id) { - const [rows] = await db.execute("SELECT * FROM locations WHERE id = ?", [ - id, - ]); - return rows.length > 0 ? rows[0] : null; - } - - async findAllLocations() { - const [rows] = await db.execute("SELECT * FROM locations"); - return rows; - } - - async deleteLocation(id) { - const result = await db.execute("DELETE FROM locations WHERE id = ?", [id]); - return result; - } -} - -module.exports = new LocationRepository(); diff --git a/backend/repositories/userRepository.js b/backend/repositories/userRepository.js deleted file mode 100644 index b28a988..0000000 --- a/backend/repositories/userRepository.js +++ /dev/null @@ -1,43 +0,0 @@ -const db = require("../config/db"); - -class UserRepository { - async createUser(data) { - const [result] = await db.execute( - "INSERT INTO users (name, email, password) VALUES (?, ?, ?)", - [data.name, data.email, data.password] - ); - return result; - } - - async findUserById(id) { - const [rows] = await db.execute("SELECT * FROM users WHERE id = ?", [id]); - return rows.length > 0 ? rows[0] : null; - } - - async findUserByEmail(email) { - const [rows] = await db.execute("SELECT * FROM users WHERE email = ?", [ - email, - ]); - return rows.length > 0 ? rows[0] : null; - } - - async findAllUsers() { - const [rows] = await db.execute("SELECT * FROM users"); - return rows; - } - - async updateUser(id, data) { - const result = await db.execute( - "UPDATE users SET name = ?, email = ?, password = ? WHERE id = ?", - [data.name, data.email, data.password, id] - ); - return result; - } - - async deleteUser(id) { - const result = await db.execute("DELETE FROM users WHERE id = ?", [id]); - return result; - } -} - -module.exports = new UserRepository(); diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js new file mode 100644 index 0000000..388154b --- /dev/null +++ b/backend/routes/authRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const { register, login, refreshToken, users } = require('../controllers/authController'); + +const router = express.Router(); + +router.post('/register', register); +router.post('/login', login); +router.post('/refresh-token', refreshToken); +router.get('/users', users); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/categoryRoutes.js b/backend/routes/categoryRoutes.js index 99ea1de..98ce986 100644 --- a/backend/routes/categoryRoutes.js +++ b/backend/routes/categoryRoutes.js @@ -1,14 +1,18 @@ -const express = require("express"); -const categoryController = require("../controllers/categoryController"); +const express = require('express'); +const { + getAllCategories, + getCategoryById, + createCategory, + updateCategory, + deleteCategory, +} = require('../controllers/categoryController'); const router = express.Router(); -router.get("/", categoryController.getAllCategories); -router.get("/:id", categoryController.getCategoryById); -router.post("/", categoryController.createCategory); -router.patch("/:id", categoryController.updateCategory); -router.delete("/:id", categoryController.deleteCategory); +router.get('/', getAllCategories); +router.get('/:id', getCategoryById); +router.post('/', createCategory); +router.put('/:id', updateCategory); +router.delete('/:id', deleteCategory); -// Other category routes - -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/routes/contactRoutes.js b/backend/routes/contactRoutes.js index c310e18..2a1e3db 100644 --- a/backend/routes/contactRoutes.js +++ b/backend/routes/contactRoutes.js @@ -1,7 +1,6 @@ -const express = require("express"); -const contactController = require("../controllers/contactController"); - +const express = require('express'); const router = express.Router(); +const contactController = require('../controllers/contactController'); // GET methods for future use @@ -11,7 +10,7 @@ const router = express.Router(); // Get a specific message by ID (for future use) // router.get("/:id", contactController.getMessageById); -// Route to send an email -router.post("/send", contactController.sendEmail); +// POST route to handle contact form submission +router.post('/send', contactController.sendContactEmail); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/routes/locationRoutes.js b/backend/routes/locationRoutes.js index b0fa914..ab34b23 100644 --- a/backend/routes/locationRoutes.js +++ b/backend/routes/locationRoutes.js @@ -1,12 +1,23 @@ -const express = require("express"); -const locationController = require("../controllers/locationsController"); +const express = require('express'); +const { + getAllLocations, + getLocationById, + getLocationsByUser, + getLocationsByCategory, + updateLocation, + deleteLocation, + createLocation, +} = require('../controllers/locationController'); +const authMiddleware = require('../middleware/authMiddleware'); const router = express.Router(); -router.get("/", locationController.getAllLocations); -router.get("/:id", locationController.getLocationById); -router.post("/", locationController.createLocation); -router.put("/:id", locationController.updateLocation); -router.delete("/:id", locationController.deleteLocation); +router.get('/', getAllLocations); +router.get('/:id', getLocationById); +router.get('/user/:userId', getLocationsByUser); +router.get('/category/:categoryId', getLocationsByCategory); +router.post('/', authMiddleware, createLocation); +router.put('/:id', authMiddleware, updateLocation); +router.delete('/:id', authMiddleware, deleteLocation); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js deleted file mode 100644 index 5526bbd..0000000 --- a/backend/routes/userRoutes.js +++ /dev/null @@ -1,12 +0,0 @@ -const express = require("express"); -const userController = require("../controllers/userController"); - -const router = express.Router(); - -router.get("/", userController.getAllUsers); -router.get("/:id", userController.getUserById); -router.post("/register", userController.register); -router.patch("/:id", userController.updateUser); -router.delete("/:id", userController.deleteUser); - -module.exports = router; diff --git a/backend/server.js b/backend/server.js index b5977a8..286ba45 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,27 +1,37 @@ -const express = require("express"); -const userRoutes = require("./routes/userRoutes"); -const categoryRoutes = require("./routes/categoryRoutes"); -const locationRoutes = require("./routes/locationRoutes"); -const contactRoutes = require("./routes/contactRoutes"); -const initDB = require("./config/dbInit"); -const errorHandler = require("./middlewares/errorHandler"); -const cors = require("cors"); +const express = require('express'); +const dotenv = require('dotenv'); +const { connectDB } = require('./config/db'); +const { initModels } = require('./models'); +const authRoutes = require('./routes/authRoutes'); +const locationRoutes = require('./routes/locationRoutes'); +const categoryRoutes = require('./routes/categoryRoutes'); +const contactRoutes = require('./routes/contactRoutes'); +const errorMiddleware = require('./middleware/errorMiddleware'); +const cors = require('cors'); + +dotenv.config(); const app = express(); -app.use(express.json()); app.use(cors()); -initDB(); +app.use(express.json()); + +app.use('/api/auth', authRoutes); +app.use('/api/locations', locationRoutes); +app.use('/api/categories', categoryRoutes); +app.use('/api/contact', contactRoutes); + +app.use(errorMiddleware); -app.use("/api/users", userRoutes); -app.use("/api/categories", categoryRoutes); -app.use("/api/locations", locationRoutes); -app.use("/api/contact", contactRoutes); +const startServer = async () => { + await connectDB(); + await initModels(); -app.use(errorHandler); + const PORT = process.env.PORT || 5100; + app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); + }); +}; -const PORT = process.env.PORT || 4000; -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); -}); +startServer(); \ No newline at end of file diff --git a/backend/services/categoryService.js b/backend/services/categoryService.js deleted file mode 100644 index e0f7469..0000000 --- a/backend/services/categoryService.js +++ /dev/null @@ -1,35 +0,0 @@ -const CategoryRepository = require("../repositories/categoryRepository"); -const Category = require("../models/category"); - -class CategoryService { - async createCategory(data) { - const result = await CategoryRepository.createCategory(data); - return new Category( - result.insertId, - data.name, - data.description, - new Date(), - new Date() - ); - } - - async getCategoryById(id) { - const category = await CategoryRepository.findCategoryById(id); - if (!category) throw new Error("Category not found"); - return category; - } - - async getAllCategories() { - return CategoryRepository.findAllCategories(); - } - - async updateCategory(id, data) { - return CategoryRepository.updateCategory(id, data); - } - - async deleteCategory(id) { - return CategoryRepository.deleteCategory(id); - } -} - -module.exports = new CategoryService(); diff --git a/backend/services/contactService.js b/backend/services/contactService.js deleted file mode 100644 index 70822cd..0000000 --- a/backend/services/contactService.js +++ /dev/null @@ -1,76 +0,0 @@ -const Contact = require("../models/contact"); -const nodemailer = require("nodemailer"); -require("dotenv").config(); -const ContactRepository = require("../repositories/contactRepository"); - -class ContactService { - async sendEmail(data) { - const { name, contactInfo, subject, message } = data; - - console.log("Preparing to send email with the following data:", { - name, - contactInfo, - subject, - message, - }); - - const contact = new Contact( - name, - contactInfo, - subject, - message, - new Date() - ); - - // Save message to the databaseif needed - // await ContactRepository.createMessage(contact); - - console.log("Contact message saved to the database:", contact); - - const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT, - secure: process.env.SMTP_PORT === "465", // true for 465, false for other ports - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, - tls: { - rejectUnauthorized: false, // Allow self-signed certificates - }, - }); - - const mailOptions = { - from: `${name} <${contactInfo || process.env.DEFAULT_SENDER_EMAIL}>`, - to: `${process.env.DEFAULT_RECEIVER_NAME} <${process.env.DEFAULT_RECEIVER_EMAIL}>`, - subject: subject, - text: `Message from ${name} (${contactInfo}):\n\n${message}`, - }; - - console.log("Mail options prepared:", mailOptions); - - try { - const info = await transporter.sendMail(mailOptions); - console.log("Email sent successfully:", info.response); - } catch (error) { - console.error("Error caught while sending email:", error.message); - throw new Error("An error occurred while sending the email."); - } - - return contact; - } - - async getAllMessages() { - return await ContactRepository.findAllMessages(); - } - - async getMessageById(id) { - const message = await ContactRepository.findMessageById(id); - if (!message) { - throw new Error("Message not found"); - } - return message; - } -} - -module.exports = new ContactService(); diff --git a/backend/services/locationService.js b/backend/services/locationService.js deleted file mode 100644 index 3ab7eeb..0000000 --- a/backend/services/locationService.js +++ /dev/null @@ -1,55 +0,0 @@ -const LocationRepository = require("../repositories/locationRepository"); -const UserRepository = require("../repositories/userRepository"); -const CategoryRepository = require("../repositories/categoryRepository"); -const Location = require("../models/location"); - -class LocationService { - async createLocation(data) { - // Check if the User exists - const user = await UserRepository.findUserById(data.userId); - if (!user) { - throw new Error("User not found"); - } - - // Check if the Category exists - const category = await CategoryRepository.findCategoryById(data.categoryId); - if (!category) { - throw new Error("Category not found"); - } - - // Proceed to create the location - const result = await LocationRepository.createLocation(data); - return new Location( - result.insertId, - data.name, - data.address, - data.description, - data.longitude, - data.latitude, - data.userId, - data.categoryId, - new Date(), - new Date() - ); - } - - async updateLocation(id, data) { - return LocationRepository.updateLocation(id, data); - } - - async getLocationById(id) { - const location = await LocationRepository.findLocationById(id); - if (!location) throw new Error("Location not found"); - return location; - } - - async getAllLocations() { - return LocationRepository.findAllLocations(); - } - - async deleteLocation(id) { - return LocationRepository.deleteLocation(id); - } -} - -module.exports = new LocationService(); diff --git a/backend/services/userService.js b/backend/services/userService.js deleted file mode 100644 index 35537e6..0000000 --- a/backend/services/userService.js +++ /dev/null @@ -1,74 +0,0 @@ -const bcrypt = require("bcrypt"); -const UserRepository = require("../repositories/userRepository"); -const User = require("../models/user"); - -class UserService { - async loginUser(email, password) { - const user = await UserRepository.findUserByEmail(email); - - if (!user) { - throw new Error("User not found"); - } - - // Compare the provided password with the hashed password in the database - const isMatch = await bcrypt.compare(password, user.password); - - if (!isMatch) { - throw new Error("Invalid password"); - } - - return user; // Password is correct, return user - } - - async createUser(data) { - // Hash the password before storing - console.log(data); - const hashedPassword = await bcrypt.hash(data.password, 10); // 10 is the salt rounds - - const result = await UserRepository.createUser({ - ...data, - password: hashedPassword, // Save hashed password - }); - - return new User( - result.insertId, - data.name, - data.email, - hashedPassword, - new Date(), - new Date() - ); - } - - async getUserById(id) { - const user = await UserRepository.findUserById(id); - if (!user) throw new Error("User not found"); - return user; - } - - async getAllUsers() { - return UserRepository.findAllUsers(); - } - - async updateUser(id, data) { - let updateData = { ...data }; - - // Check if a new password is provided - if (data.password) { - // Hash the new password before updating - const hashedPassword = await bcrypt.hash(data.password, 10); - updateData.password = hashedPassword; - } - - // Proceed with updating the user in the database - const result = await UserRepository.updateUser(id, updateData); - - // Fetch the updated user after the update - const updatedUser = await UserRepository.findUserById(id); - - // Return the user DTO, excluding the password - return updatedUser; - } -} - -module.exports = new UserService(); diff --git a/backend/utils/dbInit.js b/backend/utils/dbInit.js deleted file mode 100644 index 5d7f4f8..0000000 --- a/backend/utils/dbInit.js +++ /dev/null @@ -1,40 +0,0 @@ -import mysql from 'mysql2/promise'; -import sequelize from '../config/database.js'; -import { User, Category, Location } from '../models/index.js'; - -export async function initializeDatabase() { - try { - const { host, username: user, password, database } = sequelize.config; - - // Create a connection to MySQL server without specifying a database - const connection = await mysql.createConnection({ host, user, password }); - - // Check if the database exists - const [rows] = await connection.query( - `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '${database}'` - ); - - if (rows.length === 0) { - // Database doesn't exist, create it - await connection.query(`CREATE DATABASE IF NOT EXISTS ${database}`); - console.log('Database created.'); - } else { - console.log('Database already exists.'); - } - - // Close the MySQL connection - await connection.end(); - - // Now connect to the database using Sequelize - await sequelize.authenticate(); - console.log('Sequelize connection has been established successfully.'); - - // Sync all models with the database - await sequelize.sync({ alter: true }); - console.log('Database synchronized successfully.'); - } catch (error) { - console.error('Unable to initialize the database:', error); - throw error; - } -} - diff --git a/frontend/angular.json b/frontend/angular.json index c647885..200324d 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -49,7 +49,13 @@ "maximumError": "4kb" } ], - "outputHashing": "all" + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] }, "development": { "buildOptimizer": false, diff --git a/frontend/src/app/admin/admin-routing.module.ts b/frontend/src/app/admin/admin-routing.module.ts new file mode 100644 index 0000000..155b057 --- /dev/null +++ b/frontend/src/app/admin/admin-routing.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { DashboardLayoutComponent } from './layout/dashboard-layout/dashboard-layout.component'; +import { LocationsComponent } from './pages/locations/locations.component'; +import { DashboardComponent } from './pages/dashboard/dashboard.component'; +import { SettingsComponent } from './pages/settings/settings.component'; +import { authGuard } from '../shared/guards/auth.guard'; + +const routes: Routes = [ + { path: '', component: DashboardLayoutComponent, + canActivate: [authGuard], + children: [ + { path: '', redirectTo: 'home', pathMatch: 'full' }, // Redirect to dashboard + { path: 'home', component: DashboardComponent }, + { path: 'lokacije', component: LocationsComponent }, + { path: 'postavke', component: SettingsComponent }, +]}, +{ path: '**', redirectTo: '/dashboard/home' } // Redirect unknown routes to home +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AdminRoutingModule { } diff --git a/frontend/src/app/admin/admin.module.ts b/frontend/src/app/admin/admin.module.ts new file mode 100644 index 0000000..dee6884 --- /dev/null +++ b/frontend/src/app/admin/admin.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { AdminRoutingModule } from './admin-routing.module'; + + +@NgModule({ + declarations: [ + ], + imports: [ + CommonModule, + AdminRoutingModule + ] +}) +export class AdminModule { } diff --git a/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.html b/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.html new file mode 100644 index 0000000..679c185 --- /dev/null +++ b/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.html @@ -0,0 +1,69 @@ +
+ + + + +
+
+
+ + +
+ + + +
+ + +
+ +
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/pages/contact/contact.component.scss b/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.scss similarity index 100% rename from frontend/src/app/pages/contact/contact.component.scss rename to frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.scss diff --git a/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.spec.ts b/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.spec.ts new file mode 100644 index 0000000..5560bd1 --- /dev/null +++ b/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardLayoutComponent } from './dashboard-layout.component'; + +describe('DashboardLayoutComponent', () => { + let component: DashboardLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardLayoutComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DashboardLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.ts b/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.ts new file mode 100644 index 0000000..84a5368 --- /dev/null +++ b/frontend/src/app/admin/layout/dashboard-layout/dashboard-layout.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { RouterLink, RouterOutlet } from '@angular/router'; +import { Router, NavigationEnd } from '@angular/router'; +import { AuthService } from 'src/app/shared/services/auth.service'; + +@Component({ + selector: 'app-dashboard-layout', + standalone: true, + imports: [RouterOutlet, RouterLink], + templateUrl: './dashboard-layout.component.html', + styleUrls: ['./dashboard-layout.component.scss'], +}) +export class DashboardLayoutComponent implements OnInit { + constructor(private router: Router, private authService: AuthService) {} + + ngOnInit() { + this.router.events.subscribe((event) => { + if (event instanceof NavigationEnd) { + console.log('Navigated to:', event.url); + } + }); + } + + signOut() { + this.authService.logOut(); + } +} diff --git a/frontend/src/app/admin/pages/dashboard/dashboard.component.html b/frontend/src/app/admin/pages/dashboard/dashboard.component.html new file mode 100644 index 0000000..f690d26 --- /dev/null +++ b/frontend/src/app/admin/pages/dashboard/dashboard.component.html @@ -0,0 +1 @@ +Dashboard \ No newline at end of file diff --git a/frontend/src/app/admin/pages/dashboard/dashboard.component.scss b/frontend/src/app/admin/pages/dashboard/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin/pages/dashboard/dashboard.component.spec.ts b/frontend/src/app/admin/pages/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000..ae4f8b6 --- /dev/null +++ b/frontend/src/app/admin/pages/dashboard/dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('AdminComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin/pages/dashboard/dashboard.component.ts b/frontend/src/app/admin/pages/dashboard/dashboard.component.ts new file mode 100644 index 0000000..45292f4 --- /dev/null +++ b/frontend/src/app/admin/pages/dashboard/dashboard.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { SidebarComponent } from 'src/app/components/sidebar/sidebar.component'; + +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [SidebarComponent], + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class DashboardComponent { + +} diff --git a/frontend/src/app/admin/pages/locations/locations.component.html b/frontend/src/app/admin/pages/locations/locations.component.html new file mode 100644 index 0000000..50b6083 --- /dev/null +++ b/frontend/src/app/admin/pages/locations/locations.component.html @@ -0,0 +1 @@ +

Lokacije

\ No newline at end of file diff --git a/frontend/src/app/admin/pages/locations/locations.component.scss b/frontend/src/app/admin/pages/locations/locations.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin/pages/locations/locations.component.spec.ts b/frontend/src/app/admin/pages/locations/locations.component.spec.ts new file mode 100644 index 0000000..3714ff2 --- /dev/null +++ b/frontend/src/app/admin/pages/locations/locations.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LocationsComponent } from './locations.component'; + +describe('LocationsComponent', () => { + let component: LocationsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LocationsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LocationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin/pages/locations/locations.component.ts b/frontend/src/app/admin/pages/locations/locations.component.ts new file mode 100644 index 0000000..68a77ca --- /dev/null +++ b/frontend/src/app/admin/pages/locations/locations.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-locations', + standalone: true, + imports: [RouterLink], + templateUrl: './locations.component.html', + styleUrls: ['./locations.component.scss'] +}) +export class LocationsComponent { + onClick() { + console.log('Link clicked!'); + } +} diff --git a/frontend/src/app/admin/pages/settings/settings.component.html b/frontend/src/app/admin/pages/settings/settings.component.html new file mode 100644 index 0000000..6571391 --- /dev/null +++ b/frontend/src/app/admin/pages/settings/settings.component.html @@ -0,0 +1 @@ +

Postavke

diff --git a/frontend/src/app/admin/pages/settings/settings.component.scss b/frontend/src/app/admin/pages/settings/settings.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin/pages/settings/settings.component.spec.ts b/frontend/src/app/admin/pages/settings/settings.component.spec.ts new file mode 100644 index 0000000..82c748a --- /dev/null +++ b/frontend/src/app/admin/pages/settings/settings.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsComponent } from './settings.component'; + +describe('SettingsComponent', () => { + let component: SettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SettingsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin/pages/settings/settings.component.ts b/frontend/src/app/admin/pages/settings/settings.component.ts new file mode 100644 index 0000000..7f3364e --- /dev/null +++ b/frontend/src/app/admin/pages/settings/settings.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-settings', + standalone: true, + imports: [], + templateUrl: './settings.component.html', + styleUrls: ['./settings.component.scss'] +}) +export class SettingsComponent { + +} diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1737872..fb1fcca 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,13 +1,11 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { HomeComponent } from './pages/home/home.component'; -import { LocationComponent } from './pages/location/location.component'; -import { ContactComponent } from './pages/contact/contact.component'; +import { LoginComponent } from './pages/login/login.component'; export const routes: Routes = [ - { path: '', component: HomeComponent }, - { path: 'lokacije', component: LocationComponent }, - { path: 'kontakt', component: ContactComponent }, + { path: 'prijava', component: LoginComponent }, + { path: '', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) }, + { path: 'dashboard', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }, { path: '**', redirectTo: '' } // Optional: handle unknown routes ]; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index a535ee7..9004993 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -4,8 +4,8 @@ import { FormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { LocationComponent } from './pages/location/location.component'; -import { ContactComponent } from './pages/contact/contact.component'; +import { LocationComponent } from './home/pages/location/location.component'; +import { ContactComponent } from './home/pages/contact/contact.component'; @NgModule({ declarations: [AppComponent, LocationComponent, ContactComponent], diff --git a/frontend/src/app/components/contact/contact-form/contact-form.component.html b/frontend/src/app/components/contact/contact-form/contact-form.component.html index 433b8a2..13a32ec 100644 --- a/frontend/src/app/components/contact/contact-form/contact-form.component.html +++ b/frontend/src/app/components/contact/contact-form/contact-form.component.html @@ -10,7 +10,7 @@ > - Kontaktirajte nas + Kontaktirajte nas
+