diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..767467d --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +NODE_ENV=development +PORT=8765 +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_NAME=my_app +DB_USER=my_app +DB_PASSWORD= diff --git a/.gitignore b/.gitignore index 3c07115..b9fbc02 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ nbproject/* .sass-cache/ # node /node_modules/* +.env +.env.* +!.env.example # yarn yarn-debug.log yarn-error.log diff --git a/.sequelizerc b/.sequelizerc new file mode 100644 index 0000000..6a106b7 --- /dev/null +++ b/.sequelizerc @@ -0,0 +1,6 @@ +const path = require('node:path'); + +module.exports = { + config: path.resolve('node/config/database.cjs'), + 'migrations-path': path.resolve('node/migrations'), +}; diff --git a/README.md b/README.md index 13805e4..161efbd 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,68 @@ -# Test REST API (CakePHP 5) +# REST API: Express, Sequelize i MySQL -Testowy serwis REST API: klienci (`customers`), ich auta (`cars`) i użytkownicy (`users`), -uwierzytelnianie tokenem JWT, dokumentacja Swagger UI. +Serwis REST dla użytkowników, klientów i samochodów. Aplikacja Node.js korzysta z istniejących tabel `users`, `customers` i `cars`; aplikacja nie tworzy ani nie synchronizuje ich automatycznie przy uruchomieniu. -## Uruchomienie +## Wymagania i uruchomienie + +- Node.js 20 lub nowszy +- MySQL z tabelami używanymi przez dotychczasową aplikację ```bash -composer install -bin/cake migrations migrate # tworzy tabele users, customers, cars -bin/cake seeds run DatabaseSeed # 2 konta, 100 klientów, 1-5 aut na klienta (czyści tabele!) -bin/cake server -p 8765 +npm install +cp .env.example .env ``` -Dokumentacja i konsola testowa: **http://localhost:8765/docs** (specyfikacja: `webroot/openapi.yaml`). +Ustaw w `.env` połączenie do tej samej bazy MySQL co dotychczasowa aplikacja. Następnie wykonaj migrację dodającą unikalne pole `login` do tabeli `users` (dotychczasowym użytkownikom ustawi `login = email`): -Konfiguracja bazy i sekretu JWT: `config/app_local.php` (`Datasources.default`, `Jwt.secret`, `Jwt.ttl`). -Sekret można też podać zmienną środowiskową `JWT_SECRET`. +```bash +npm run db:migrate +npm start +``` -## Konta testowe +API działa domyślnie na `http://localhost:8765`, dokumentacja Swagger UI na `http://localhost:8765/docs`, a specyfikacja OpenAPI na `http://localhost:8765/openapi.yaml`. Port można zmienić przez `PORT`. -| Rola | E-mail | Hasło | -|-------|-------------------|-----------| -| admin | admin@example.com | Admin123! | -| user | user@example.com | User1234! | +Migrację można cofnąć przez `npx sequelize-cli db:migrate:undo`. Start serwera nigdy nie uruchamia migracji ani `sequelize.sync()`. -* **admin** – wszystko, w tym CRUD użytkowników. -* **user** – logowanie, własny profil, CRUD klientów i aut. +## Uwierzytelnianie i uprawnienia + +API używa HTTP Basic Auth: podaj `login` i hasło w nagłówku `Authorization: Basic ...` albo skorzystaj z przycisku **Authorize** w Swagger UI. Loginem jest pole `users.login`; użytkownicy istniejący przed migracją zachowają dotychczasowe dane logowania, bo ich login zostanie ustawiony na adres e-mail. + +HTTP Basic przesyła hasło w każdym żądaniu; w środowisku produkcyjnym wystawiaj API wyłącznie przez HTTPS. + +Konta dodawane przez dotychczasowy seeder, o ile istnieją w bazie: + +| Rola | Login po migracji | Hasło | +|---|---|---| +| admin | `admin@example.com` | `Admin123!` | +| user | `user@example.com` | `User1234!` | + +Hasła nadal są przechowywane jako bcrypt. Nieaktywne konta nie mogą się uwierzytelniać. Dostęp do `/api/users` wymaga roli `admin`; klienci i samochody są dostępne dla każdego uwierzytelnionego użytkownika. + +```bash +curl -u 'admin@example.com:Admin123!' http://localhost:8765/api/auth/me +curl -u 'user@example.com:User1234!' 'http://localhost:8765/api/customers?q=nowak&with_cars=1' +``` ## Endpointy -| Metoda | Ścieżka | Opis | -|----------------------|--------------------------------------|---------------------------------------| -| POST | `/api/auth/login` | logowanie, zwraca token JWT | -| GET / PUT / PATCH | `/api/auth/me` | własny profil | -| GET / POST | `/api/customers` | lista (filtry, paginacja) / dodanie | -| GET / PUT / PATCH / DELETE | `/api/customers/{id}` | klient (GET zwraca też auta) | -| GET / POST | `/api/customers/{id}/cars` | auta klienta / dodanie auta klientowi | -| GET / POST | `/api/cars` | lista aut / dodanie | -| GET / PUT / PATCH / DELETE | `/api/cars/{id}` | auto (GET zwraca też właściciela) | -| GET / POST | `/api/users` | użytkownicy (tylko admin) | -| GET / PUT / PATCH / DELETE | `/api/users/{id}` | użytkownik (tylko admin) | +| Metody | Ścieżka | Opis | +|---|---|---| +| POST | `/api/auth/login` | Potwierdzenie logowania Basic Auth i aktualizacja `last_login` | +| GET / PUT / PATCH | `/api/auth/me` | Odczyt i edycja własnego profilu | +| GET / POST | `/api/users` | Lista i dodawanie użytkowników (admin) | +| GET / PUT / PATCH / DELETE | `/api/users/{id}` | Odczyt, edycja i usuwanie użytkownika (admin) | +| GET / POST | `/api/customers` | Lista klientów i dodawanie | +| GET / PUT / PATCH / DELETE | `/api/customers/{id}` | Szczegóły klienta z samochodami, edycja i usuwanie | +| GET / POST | `/api/customers/{customer_id}/cars` | Samochody klienta i dodawanie samochodu | +| GET / POST | `/api/cars` | Lista samochodów i dodawanie | +| GET / PUT / PATCH / DELETE | `/api/cars/{id}` | Szczegóły samochodu z właścicielem, edycja i usuwanie | -Przykład: - -```bash -TOKEN=$(curl -s -X POST http://localhost:8765/api/auth/login \ - -H 'Content-Type: application/json' \ - -d '{"email":"user@example.com","password":"User1234!"}' | jq -r .data.token) - -curl -s "http://localhost:8765/api/customers?q=nowak&with_cars=1" -H "Authorization: Bearer $TOKEN" -``` - -## Pola - -**Klient** – wymagane: `first_name`, `last_name`, `email` (unikalny), `phone`; -opcjonalne: `type` (`individual`/`company`, dla `company` wymagane `company_name`), `company_name`, -`tax_id` (NIP, 10 cyfr), `street`, `city`, `postal_code` (`00-000`), `country` (ISO, domyślnie `PL`), -`birth_date` (`YYYY-MM-DD`), `notes`. - -**Auto** – wymagane: `customer_id`, `make`, `model`, `production_year`, `vin` (17 znaków, unikalny), -`registration_number` (unikalny), `fuel_type` (`petrol`/`diesel`/`lpg`/`hybrid`/`electric`); -opcjonalne: `color`, `engine_capacity` (cm³), `power_hp`, `transmission` (`manual`/`automatic`), -`mileage` (km), `notes`. - -**Użytkownik** – wymagane: `email` (unikalny), `password` (min. 8 znaków), `first_name`, `last_name`; -opcjonalne: `role` (`admin`/`user`, domyślnie `user`), `active` (nieaktywny nie może się zalogować). +Listy obsługują `page`, `limit` (domyślnie 20, maksymalnie 100), `sort` i `direction` (`asc`/`desc`). Endpointy obsługują wyszukiwanie i filtry opisane w Swagger UI; liczba wyników jest zwracana w `X-Total-Count`. ## Format odpowiedzi -* sukces: `{"success": true, "data": ..., "meta": {...paginacja}}` -* błąd: `{"success": false, "error": {"code": 422, "message": "Validation failed.", "fields": {...}}}` -* kody: `200`, `201` (+ nagłówek `Location`), `204` (DELETE), `400`, `401`, `403`, `404`, `405`, `422` +- Sukces: `{"success": true, "data": ...}`; odpowiedź z listą zawiera także `meta`. +- Błąd walidacji: `{"success": false, "error": {"code": 422, "message": "Validation failed.", "fields": {...}}}`. +- Usunięcie: `204 No Content`. + +Migracja nie modyfikuje plików PHP; można je usunąć osobno po przełączeniu i sprawdzeniu wdrożenia Node.js. diff --git a/node/app.js b/node/app.js new file mode 100644 index 0000000..c123161 --- /dev/null +++ b/node/app.js @@ -0,0 +1,35 @@ +require('dotenv').config(); + +const fs = require('node:fs'); +const path = require('node:path'); +const express = require('express'); +const swaggerUi = require('swagger-ui-express'); +const YAML = require('yamljs'); +const apiRouter = require('./routes/api'); + +const app = express(); +const openApiPath = path.resolve(__dirname, '../webroot/openapi.yaml'); +const openApiDocument = YAML.parse(fs.readFileSync(openApiPath, 'utf8')); + +app.disable('x-powered-by'); +app.use(express.json({ limit: '1mb' })); +app.get('/openapi.yaml', (req, res) => res.sendFile(openApiPath)); +app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument)); +app.get('/', (req, res) => res.redirect('/docs')); +app.use('/api', apiRouter); + +app.use((req, res) => res.status(404).json({ + success: false, + error: { code: 404, message: 'Not found.' }, +})); + +app.use((error, req, res, next) => { + if (res.headersSent) return next(error); + if (error instanceof SyntaxError && error.status === 400 && 'body' in error) { + return res.status(400).json({ success: false, error: { code: 400, message: 'Invalid JSON body.' } }); + } + console.error(error); + return res.status(500).json({ success: false, error: { code: 500, message: 'Internal server error.' } }); +}); + +module.exports = app; diff --git a/node/config/database.cjs b/node/config/database.cjs new file mode 100644 index 0000000..9242220 --- /dev/null +++ b/node/config/database.cjs @@ -0,0 +1,17 @@ +require('dotenv').config(); + +const config = { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + host: process.env.DB_HOST || '127.0.0.1', + port: Number(process.env.DB_PORT || 3306), + dialect: 'mysql', + logging: false, +}; + +module.exports = { + development: config, + test: config, + production: config, +}; diff --git a/node/config/sequelize.js b/node/config/sequelize.js new file mode 100644 index 0000000..20af4b9 --- /dev/null +++ b/node/config/sequelize.js @@ -0,0 +1,16 @@ +const { Sequelize } = require('sequelize'); + +const sequelize = new Sequelize( + process.env.DB_NAME, + process.env.DB_USER, + process.env.DB_PASSWORD, + { + host: process.env.DB_HOST || '127.0.0.1', + port: Number(process.env.DB_PORT || 3306), + dialect: 'mysql', + logging: false, + timezone: '+00:00', + }, +); + +module.exports = sequelize; diff --git a/node/lib/api.js b/node/lib/api.js new file mode 100644 index 0000000..1ea47d4 --- /dev/null +++ b/node/lib/api.js @@ -0,0 +1,172 @@ +const { Op, ValidationError, UniqueConstraintError, ForeignKeyConstraintError } = require('sequelize'); + +const USER_FIELDS = ['login', 'email', 'password', 'first_name', 'last_name', 'role', 'active']; +const PROFILE_FIELDS = ['email', 'password', 'first_name', 'last_name']; +const CUSTOMER_FIELDS = [ + 'type', 'first_name', 'last_name', 'email', 'phone', 'company_name', 'tax_id', + 'street', 'city', 'postal_code', 'country', 'birth_date', 'notes', +]; +const CAR_FIELDS = [ + 'customer_id', 'make', 'model', 'production_year', 'vin', 'registration_number', + 'fuel_type', 'color', 'engine_capacity', 'power_hp', 'transmission', 'mileage', 'notes', +]; + +function pick(source, fields) { + source ||= {}; + return Object.fromEntries(fields.filter((field) => source[field] !== undefined) + .map((field) => [field, source[field]])); +} + +function cleanUser(user) { + const data = user?.toJSON ? user.toJSON() : { ...user }; + delete data.password; + return data; +} + +function response(res, data, status = 200, extra = {}) { + if (status === 204) return res.status(status).end(); + return res.status(status).json({ success: status < 400, data, ...extra }); +} + +function validationResponse(res, fields) { + return res.status(422).json({ + success: false, + error: { code: 422, message: 'Validation failed.', fields }, + }); +} + +function validationFields(error) { + if (error instanceof ValidationError || error instanceof UniqueConstraintError) { + return Object.fromEntries(error.errors.map((item) => [item.path, item.message])); + } + if (error instanceof ForeignKeyConstraintError) { + return { customer_id: 'The referenced record does not exist.' }; + } + return null; +} + +function fail(res, error) { + const fields = validationFields(error); + if (fields) return validationResponse(res, fields); + throw error; +} + +function requireFields(data, names) { + const errors = {}; + for (const name of names) { + if (data[name] === undefined || data[name] === null || data[name] === '') { + errors[name] = 'This field is required.'; + } + } + return errors; +} + +function validateUser(data, creating) { + const errors = requireFields(data, creating ? ['login', 'email', 'password', 'first_name', 'last_name'] : []); + if (data.login !== undefined && (typeof data.login !== 'string' || !data.login.trim() || data.login.length > 255)) errors.login = 'Login is required and must be at most 255 characters.'; + if (data.email !== undefined && (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email) || data.email.length > 255)) errors.email = 'Enter a valid email address.'; + if (data.password !== undefined && (String(data.password).length < 8 || String(data.password).length > 72)) errors.password = 'Password must be between 8 and 72 characters.'; + for (const field of ['first_name', 'last_name']) { + if (data[field] !== undefined && (typeof data[field] !== 'string' || !data[field].trim() || data[field].length > 100)) errors[field] = 'This field is required and must be at most 100 characters.'; + } + if (data.role !== undefined && !['admin', 'user'].includes(data.role)) errors.role = 'Role must be admin or user.'; + if (data.active !== undefined && typeof data.active !== 'boolean') errors.active = 'Active must be a boolean.'; + return errors; +} + +function validateCustomer(data, creating) { + const errors = requireFields(data, creating ? ['first_name', 'last_name', 'email', 'phone'] : []); + if (data.type !== undefined && !['individual', 'company'].includes(data.type)) errors.type = 'Type must be individual or company.'; + if (data.email !== undefined && (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email) || data.email.length > 255)) errors.email = 'Enter a valid email address.'; + for (const [field, max] of Object.entries({ first_name: 100, last_name: 100, phone: 30, company_name: 255, street: 255, city: 100, notes: 5000 })) { + const required = ['first_name', 'last_name', 'phone'].includes(field); + if (data[field] !== undefined && ((data[field] === null && required) || (data[field] !== null && (typeof data[field] !== 'string' || data[field].length > max)))) { + errors[field] = `Must be a string of at most ${max} characters.`; + } + } + if (data.phone !== undefined && !/^\+?[0-9 ()-]{6,30}$/.test(data.phone)) errors.phone = 'Invalid phone number.'; + if (data.type === 'company' && (creating || data.company_name !== undefined) + && (typeof data.company_name !== 'string' || !data.company_name.trim())) { + errors.company_name = 'Company name is required for company customers.'; + } + if (data.tax_id != null && (typeof data.tax_id !== 'string' || !/^[0-9]{10}$/.test(data.tax_id))) errors.tax_id = 'Tax ID (NIP) must consist of 10 digits.'; + if (data.postal_code != null && !/^[0-9]{2}-[0-9]{3}$/.test(data.postal_code)) errors.postal_code = 'Postal code must be in format 00-000.'; + if (data.country !== undefined && !/^[A-Z]{2}$/.test(data.country)) errors.country = 'Country must be a two-letter uppercase code.'; + if (data.birth_date != null) { + const date = typeof data.birth_date === 'string' && /^(\d{4})-(\d{2})-(\d{2})$/.exec(data.birth_date); + const parsedDate = date + ? new Date(Date.UTC(Number(date[1]), Number(date[2]) - 1, Number(date[3]))) + : null; + if (!parsedDate || parsedDate.toISOString().slice(0, 10) !== data.birth_date) { + errors.birth_date = 'Birth date must be a valid date in YYYY-MM-DD format.'; + } + } + return errors; +} + +function validateCar(data, creating) { + const errors = requireFields(data, creating + ? ['customer_id', 'make', 'model', 'production_year', 'vin', 'registration_number', 'fuel_type'] + : []); + for (const field of ['make', 'model']) { + if (data[field] !== undefined && (typeof data[field] !== 'string' || !data[field].trim() || data[field].length > 50)) errors[field] = 'This field is required and must be at most 50 characters.'; + } + for (const field of ['customer_id', 'production_year', 'engine_capacity', 'power_hp', 'mileage']) { + if (data[field] !== undefined && data[field] !== null && !Number.isInteger(Number(data[field]))) errors[field] = 'Must be an integer.'; + } + if (data.production_year !== undefined && (Number(data.production_year) < 1900 || Number(data.production_year) > new Date().getFullYear() + 1)) errors.production_year = 'Production year is out of range.'; + if (data.customer_id !== undefined && Number(data.customer_id) < 1) errors.customer_id = 'Customer ID must be positive.'; + if (data.vin !== undefined && !/^[A-HJ-NPR-Z0-9]{17}$/i.test(data.vin)) errors.vin = 'VIN must have 17 characters (letters I, O, Q are not allowed).'; + if (data.registration_number !== undefined && !/^[A-Z0-9 ]{4,10}$/i.test(data.registration_number)) errors.registration_number = 'Registration number must have 4-10 letters/digits.'; + if (data.fuel_type !== undefined && !['petrol', 'diesel', 'lpg', 'hybrid', 'electric'].includes(data.fuel_type)) errors.fuel_type = 'Invalid fuel type.'; + if (data.transmission != null && !['manual', 'automatic'].includes(data.transmission)) errors.transmission = 'Transmission must be manual or automatic.'; + if (data.engine_capacity != null && (Number(data.engine_capacity) < 0 || Number(data.engine_capacity) > 10000)) errors.engine_capacity = 'Engine capacity must be between 0 and 10000.'; + if (data.power_hp != null && (Number(data.power_hp) < 1 || Number(data.power_hp) > 2000)) errors.power_hp = 'Power must be between 1 and 2000.'; + if (data.mileage != null && Number(data.mileage) < 0) errors.mileage = 'Mileage must not be negative.'; + if (data.color != null && (typeof data.color !== 'string' || data.color.length > 30)) errors.color = 'Color must be at most 30 characters.'; + if (data.notes != null && (typeof data.notes !== 'string' || data.notes.length > 5000)) errors.notes = 'Notes must be at most 5000 characters.'; + if (typeof data.vin === 'string') data.vin = data.vin.toUpperCase().trim(); + if (typeof data.registration_number === 'string') data.registration_number = data.registration_number.toUpperCase().replaceAll(' ', ''); + return errors; +} + +function pageOptions(query, sortable, defaultSort = 'id') { + const page = Math.max(1, Number.parseInt(query.page, 10) || 1); + const limit = Math.min(100, Math.max(1, Number.parseInt(query.limit, 10) || 20)); + const sort = sortable.includes(query.sort) ? query.sort : defaultSort; + const direction = String(query.direction).toLowerCase() === 'desc' ? 'DESC' : 'ASC'; + return { page, limit, sort, direction, offset: (page - 1) * limit }; +} + +async function list(res, model, options, include) { + const { page, limit, sort, direction, offset, ...query } = options; + const result = await model.findAndCountAll({ + ...query, include, distinct: true, limit, offset, order: [[sort, direction]], + }); + const total = result.count; + res.set('X-Total-Count', String(total)); + return response(res, result.rows, 200, { + meta: { + page, limit, total, pages: Math.ceil(total / limit), + has_next_page: page * limit < total, has_prev_page: page > 1, + sort, direction: direction.toLowerCase(), + }, + }); +} + +function notFound(res, resource) { + return res.status(404).json({ + success: false, + error: { code: 404, message: `${resource} not found.` }, + }); +} + +function asyncRoute(handler) { + return (req, res, next) => Promise.resolve(handler(req, res, next)).catch(next); +} + +module.exports = { + USER_FIELDS, PROFILE_FIELDS, CUSTOMER_FIELDS, CAR_FIELDS, pick, cleanUser, + response, validationResponse, validationFields, fail, requireFields, + validateUser, validateCustomer, validateCar, pageOptions, list, notFound, asyncRoute, Op, +}; diff --git a/node/middleware/auth.js b/node/middleware/auth.js new file mode 100644 index 0000000..20c58e5 --- /dev/null +++ b/node/middleware/auth.js @@ -0,0 +1,46 @@ +const bcrypt = require('bcryptjs'); +const { User } = require('../models'); + +function unauthorized(res) { + res.set('WWW-Authenticate', 'Basic realm="REST API", charset="UTF-8"'); + return res.status(401).json({ + success: false, + error: { code: 401, message: 'Invalid login or password.' }, + }); +} + +async function authenticate(req, res, next) { + const header = req.get('authorization') || ''; + const match = /^Basic\s+([A-Za-z0-9+/]+={0,2})$/i.exec(header); + if (!match) return unauthorized(res); + + const decoded = Buffer.from(match[1], 'base64').toString('utf8'); + const separator = decoded.indexOf(':'); + if (separator < 1) return unauthorized(res); + + const login = decoded.slice(0, separator); + const password = decoded.slice(separator + 1); + const user = await User.unscoped().findOne({ + attributes: ['id', 'login', 'password', 'role', 'active', 'email', 'first_name', 'last_name', 'last_login', 'created', 'modified'], + where: { login }, + }); + if (!user || !user.active) return unauthorized(res); + + const compatibleHash = user.password.replace(/^\$2y\$/, '$2b$'); + if (!(await bcrypt.compare(password, compatibleHash))) return unauthorized(res); + + req.user = user; + next(); +} + +function requireAdmin(req, res, next) { + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + error: { code: 403, message: 'This action requires the admin role.' }, + }); + } + next(); +} + +module.exports = { authenticate, requireAdmin }; diff --git a/node/migrations/20261027000000-add-login-to-users.js b/node/migrations/20261027000000-add-login-to-users.js new file mode 100644 index 0000000..ea28e6c --- /dev/null +++ b/node/migrations/20261027000000-add-login-to-users.js @@ -0,0 +1,38 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + const columns = await queryInterface.describeTable('users'); + if (!columns.login) { + await queryInterface.addColumn('users', 'login', { + type: Sequelize.STRING(255), + allowNull: true, + }); + } + + await queryInterface.sequelize.query( + "UPDATE `users` SET `login` = `email` WHERE `login` IS NULL OR `login` = ''", + ); + await queryInterface.changeColumn('users', 'login', { + type: Sequelize.STRING(255), + allowNull: false, + }); + + const indexes = await queryInterface.showIndex('users'); + if (!indexes.some((index) => index.name === 'users_login_unique')) { + await queryInterface.addIndex('users', ['login'], { + name: 'users_login_unique', + unique: true, + }); + } + }, + + async down(queryInterface) { + const indexes = await queryInterface.showIndex('users'); + if (indexes.some((index) => index.name === 'users_login_unique')) { + await queryInterface.removeIndex('users', 'users_login_unique'); + } + const columns = await queryInterface.describeTable('users'); + if (columns.login) await queryInterface.removeColumn('users', 'login'); + }, +}; diff --git a/node/models/index.js b/node/models/index.js new file mode 100644 index 0000000..3a0c7e6 --- /dev/null +++ b/node/models/index.js @@ -0,0 +1,75 @@ +const bcrypt = require('bcryptjs'); +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/sequelize'); + +const timestamps = { + timestamps: true, + createdAt: 'created', + updatedAt: 'modified', +}; + +const User = sequelize.define('User', { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + login: { type: DataTypes.STRING(255), allowNull: false, unique: true }, + email: { type: DataTypes.STRING(255), allowNull: false, unique: true, validate: { isEmail: true } }, + password: { type: DataTypes.STRING(255), allowNull: false }, + first_name: { type: DataTypes.STRING(100), allowNull: false }, + last_name: { type: DataTypes.STRING(100), allowNull: false }, + role: { type: DataTypes.STRING(20), allowNull: false, defaultValue: 'user', validate: { isIn: [['admin', 'user']] } }, + active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true }, + last_login: { type: DataTypes.DATE, allowNull: true }, +}, { + ...timestamps, + tableName: 'users', + defaultScope: { attributes: { exclude: ['password'] } }, + scopes: { withPassword: { attributes: { include: ['password'] } } }, + hooks: { + beforeSave: async (user) => { + if (user.changed('password')) { + user.password = await bcrypt.hash(user.password, 10); + } + }, + }, +}); + +const Customer = sequelize.define('Customer', { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + type: { type: DataTypes.STRING(20), allowNull: false, defaultValue: 'individual', validate: { isIn: [['individual', 'company']] } }, + first_name: { type: DataTypes.STRING(100), allowNull: false }, + last_name: { type: DataTypes.STRING(100), allowNull: false }, + email: { type: DataTypes.STRING(255), allowNull: false, unique: true, validate: { isEmail: true } }, + phone: { type: DataTypes.STRING(30), allowNull: false }, + company_name: { type: DataTypes.STRING(255), allowNull: true }, + tax_id: { type: DataTypes.STRING(20), allowNull: true }, + street: { type: DataTypes.STRING(255), allowNull: true }, + city: { type: DataTypes.STRING(100), allowNull: true }, + postal_code: { type: DataTypes.STRING(10), allowNull: true }, + country: { type: DataTypes.STRING(2), allowNull: false, defaultValue: 'PL' }, + birth_date: { type: DataTypes.DATEONLY, allowNull: true }, + notes: { type: DataTypes.TEXT, allowNull: true }, + created_by: { type: DataTypes.INTEGER, allowNull: true }, +}, { ...timestamps, tableName: 'customers' }); + +const Car = sequelize.define('Car', { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + customer_id: { type: DataTypes.INTEGER, allowNull: false }, + make: { type: DataTypes.STRING(50), allowNull: false }, + model: { type: DataTypes.STRING(50), allowNull: false }, + production_year: { type: DataTypes.INTEGER, allowNull: false }, + vin: { type: DataTypes.STRING(17), allowNull: false, unique: true }, + registration_number: { type: DataTypes.STRING(10), allowNull: false, unique: true }, + fuel_type: { type: DataTypes.STRING(20), allowNull: false, validate: { isIn: [['petrol', 'diesel', 'lpg', 'hybrid', 'electric']] } }, + color: { type: DataTypes.STRING(30), allowNull: true }, + engine_capacity: { type: DataTypes.INTEGER, allowNull: true }, + power_hp: { type: DataTypes.INTEGER, allowNull: true }, + transmission: { type: DataTypes.STRING(20), allowNull: true, validate: { isIn: [['manual', 'automatic']] } }, + mileage: { type: DataTypes.INTEGER, allowNull: true }, + notes: { type: DataTypes.TEXT, allowNull: true }, +}, { ...timestamps, tableName: 'cars' }); + +User.hasMany(Customer, { foreignKey: 'created_by', as: 'createdCustomers' }); +Customer.belongsTo(User, { foreignKey: 'created_by', as: 'creator' }); +Customer.hasMany(Car, { foreignKey: 'customer_id', as: 'cars', onDelete: 'CASCADE' }); +Car.belongsTo(Customer, { foreignKey: 'customer_id', as: 'customer' }); + +module.exports = { sequelize, User, Customer, Car }; diff --git a/node/routes/api.js b/node/routes/api.js new file mode 100644 index 0000000..5acbd4b --- /dev/null +++ b/node/routes/api.js @@ -0,0 +1,202 @@ +const express = require('express'); +const { Op } = require('sequelize'); +const { User, Customer, Car } = require('../models'); +const { authenticate, requireAdmin } = require('../middleware/auth'); +const api = require('../lib/api'); + +const router = express.Router(); +const route = api.asyncRoute; +router.use(route(authenticate)); + +router.post('/auth/login', route(async (req, res) => { + await req.user.update({ last_login: new Date() }); + return api.response(res, api.cleanUser(req.user)); +})); + +router.route('/auth/me') + .get((req, res) => api.response(res, api.cleanUser(req.user))) + .put(route((req, res) => updateProfile(req, res))) + .patch(route((req, res) => updateProfile(req, res))); + +async function updateProfile(req, res) { + const data = api.pick(req.body, api.PROFILE_FIELDS); + const errors = api.validateUser(data, false); + if (Object.keys(errors).length) return api.validationResponse(res, errors); + await req.user.update(data); + return api.response(res, api.cleanUser(req.user)); +} + +router.route('/users') + .all(requireAdmin) + .get(route(async (req, res) => { + const options = api.pageOptions(req.query, ['id', ...api.USER_FIELDS.filter((field) => field !== 'password'), 'last_login', 'created', 'modified']); + const where = {}; + if (req.query.q) where[Op.or] = ['login', 'email', 'first_name', 'last_name'].map((field) => ({ [field]: { [Op.like]: `%${req.query.q}%` } })); + if (req.query.role) where.role = req.query.role; + if (req.query.active !== undefined) where.active = req.query.active === 'true' || req.query.active === '1'; + const result = await User.findAndCountAll({ where, limit: options.limit, offset: options.offset, order: [[options.sort, options.direction]] }); + const total = result.count; + res.set('X-Total-Count', String(total)); + return api.response(res, result.rows.map(api.cleanUser), 200, { + meta: { page: options.page, limit: options.limit, total, pages: Math.ceil(total / options.limit), has_next_page: options.page * options.limit < total, has_prev_page: options.page > 1, sort: options.sort, direction: options.direction.toLowerCase() }, + }); + })) + .post(route(async (req, res) => { + const data = api.pick(req.body, api.USER_FIELDS); + if (data.active === undefined) data.active = true; + if (data.role === undefined) data.role = 'user'; + const errors = api.validateUser(data, true); + if (Object.keys(errors).length) return api.validationResponse(res, errors); + try { + const user = await User.create(data); + res.location(`/api/users/${user.id}`); + return api.response(res, api.cleanUser(user), 201); + } catch (error) { return api.fail(res, error); } + })); + +router.route('/users/:id') + .all(requireAdmin) + .get(route(async (req, res) => { + const user = await User.findByPk(req.params.id); + return user ? api.response(res, api.cleanUser(user)) : api.notFound(res, 'User'); + })) + .put(route((req, res) => updateUser(req, res))) + .patch(route((req, res) => updateUser(req, res))) + .delete(route(async (req, res) => { + const user = await User.findByPk(req.params.id); + if (!user) return api.notFound(res, 'User'); + if (user.id === req.user.id) return res.status(400).json({ success: false, error: { code: 400, message: 'You cannot delete your own account.' } }); + await user.destroy(); + return api.response(res, null, 204); + })); + +async function updateUser(req, res) { + const user = await User.findByPk(req.params.id); + if (!user) return api.notFound(res, 'User'); + const data = api.pick(req.body, api.USER_FIELDS); + if (user.id === req.user.id) { + if (data.role !== undefined && data.role !== user.role) return res.status(400).json({ success: false, error: { code: 400, message: 'You cannot change your own role.' } }); + if (data.active === false) return res.status(400).json({ success: false, error: { code: 400, message: 'You cannot deactivate your own account.' } }); + } + const errors = api.validateUser(data, false); + if (Object.keys(errors).length) return api.validationResponse(res, errors); + try { + await user.update(data); + return api.response(res, api.cleanUser(user)); + } catch (error) { return api.fail(res, error); } +} + +router.route('/customers') + .get(route(async (req, res) => { + const options = api.pageOptions(req.query, ['id', 'type', 'first_name', 'last_name', 'email', 'company_name', 'city', 'country', 'created', 'modified']); + const where = {}; + if (req.query.q) where[Op.or] = ['first_name', 'last_name', 'email', 'phone', 'company_name', 'city', 'tax_id'].map((field) => ({ [field]: { [Op.like]: `%${req.query.q}%` } })); + for (const field of ['type', 'city', 'country']) if (req.query[field]) where[field] = req.query[field]; + const include = String(req.query.with_cars).toLowerCase() === 'true' || req.query.with_cars === '1' ? [{ model: Car, as: 'cars' }] : undefined; + return api.list(res, Customer, { ...options, where }, include); + })) + .post(route(async (req, res) => { + const data = api.pick(req.body, api.CUSTOMER_FIELDS); + if (data.type === undefined) data.type = 'individual'; + if (data.country === undefined) data.country = 'PL'; + const errors = api.validateCustomer(data, true); + if (Object.keys(errors).length) return api.validationResponse(res, errors); + try { + const customer = await Customer.create({ ...data, created_by: req.user.id }); + res.location(`/api/customers/${customer.id}`); + return api.response(res, customer, 201); + } catch (error) { return api.fail(res, error); } + })); + +router.route('/customers/:id') + .get(route(async (req, res) => { + const customer = await Customer.findByPk(req.params.id, { include: [{ model: Car, as: 'cars' }] }); + return customer ? api.response(res, customer) : api.notFound(res, 'Customer'); + })) + .put(route((req, res) => updateCustomer(req, res))) + .patch(route((req, res) => updateCustomer(req, res))) + .delete(route(async (req, res) => { + const customer = await Customer.findByPk(req.params.id); + if (!customer) return api.notFound(res, 'Customer'); + await customer.destroy(); + return api.response(res, null, 204); + })); + +async function updateCustomer(req, res) { + const customer = await Customer.findByPk(req.params.id); + if (!customer) return api.notFound(res, 'Customer'); + const data = api.pick(req.body, api.CUSTOMER_FIELDS); + const errors = api.validateCustomer(data, false); + if (Object.keys(errors).length) return api.validationResponse(res, errors); + try { + await customer.update(data); + return api.response(res, customer); + } catch (error) { return api.fail(res, error); } +} + +router.route('/customers/:customer_id/cars') + .get(route(async (req, res) => { + const customer = await Customer.findByPk(req.params.customer_id); + if (!customer) return api.notFound(res, 'Customer'); + const options = api.pageOptions(req.query, ['id', 'customer_id', 'make', 'model', 'production_year', 'registration_number', 'fuel_type', 'mileage', 'power_hp', 'created', 'modified']); + return api.list(res, Car, { ...options, where: { customer_id: customer.id } }); + })) + .post(route(async (req, res) => { + const customer = await Customer.findByPk(req.params.customer_id); + if (!customer) return api.notFound(res, 'Customer'); + return createCar(req, res, customer.id); + })); + +router.route('/cars') + .get(route(async (req, res) => { + const options = api.pageOptions(req.query, ['id', 'customer_id', 'make', 'model', 'production_year', 'registration_number', 'fuel_type', 'mileage', 'power_hp', 'created', 'modified']); + const where = {}; + if (req.query.customer_id) where.customer_id = req.query.customer_id; + if (req.query.q) where[Op.or] = ['make', 'model', 'vin', 'registration_number'].map((field) => ({ [field]: { [Op.like]: `%${req.query.q}%` } })); + for (const field of ['make', 'fuel_type', 'transmission']) if (req.query[field]) where[field] = req.query[field]; + if (req.query.year_from) where.production_year = { ...(where.production_year || {}), [Op.gte]: Number(req.query.year_from) }; + if (req.query.year_to) where.production_year = { ...(where.production_year || {}), [Op.lte]: Number(req.query.year_to) }; + const include = String(req.query.with_customer).toLowerCase() === 'true' || req.query.with_customer === '1' ? [{ model: Customer, as: 'customer' }] : undefined; + return api.list(res, Car, { ...options, where }, include); + })) + .post(route((req, res) => createCar(req, res))); + +async function createCar(req, res, customerId) { + const data = api.pick(req.body, api.CAR_FIELDS); + if (customerId !== undefined) data.customer_id = customerId; + const errors = api.validateCar(data, true); + if (Object.keys(errors).length) return api.validationResponse(res, errors); + try { + const car = await Car.create(data); + res.location(`/api/cars/${car.id}`); + return api.response(res, car, 201); + } catch (error) { return api.fail(res, error); } +} + +router.route('/cars/:id') + .get(route(async (req, res) => { + const car = await Car.findByPk(req.params.id, { include: [{ model: Customer, as: 'customer' }] }); + return car ? api.response(res, car) : api.notFound(res, 'Car'); + })) + .put(route((req, res) => updateCar(req, res))) + .patch(route((req, res) => updateCar(req, res))) + .delete(route(async (req, res) => { + const car = await Car.findByPk(req.params.id); + if (!car) return api.notFound(res, 'Car'); + await car.destroy(); + return api.response(res, null, 204); + })); + +async function updateCar(req, res) { + const car = await Car.findByPk(req.params.id); + if (!car) return api.notFound(res, 'Car'); + const data = api.pick(req.body, api.CAR_FIELDS); + const errors = api.validateCar(data, false); + if (Object.keys(errors).length) return api.validationResponse(res, errors); + try { + await car.update(data); + return api.response(res, car); + } catch (error) { return api.fail(res, error); } +} + +module.exports = router; diff --git a/node/server.js b/node/server.js new file mode 100644 index 0000000..0718e8a --- /dev/null +++ b/node/server.js @@ -0,0 +1,27 @@ +require('dotenv').config(); + +const app = require('./app'); +const { sequelize } = require('./models'); + +const port = Number(process.env.PORT || 8765); + +async function start() { + await sequelize.authenticate(); + const server = app.listen(port, () => { + console.log(`REST API listening on http://localhost:${port}`); + }); + + const shutdown = async () => { + server.close(async () => { + await sequelize.close(); + process.exit(0); + }); + }; + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +start().catch((error) => { + console.error('Unable to connect to the database:', error.message); + process.exitCode = 1; +}); diff --git a/node/test/api.test.js b/node/test/api.test.js new file mode 100644 index 0000000..3eaacd8 --- /dev/null +++ b/node/test/api.test.js @@ -0,0 +1,91 @@ +const assert = require('node:assert/strict'); +const http = require('node:http'); +const test = require('node:test'); +const loginMigration = require('../migrations/20261027000000-add-login-to-users'); +const { + validateUser, + validateCustomer, + validateCar, +} = require('../lib/api'); +const app = require('../app'); + +test('user validation requires a login and an eight-character password', () => { + assert.deepEqual(validateUser({ + login: 'jan', + email: 'jan@example.com', + password: 'Haslo123!', + first_name: 'Jan', + last_name: 'Kowalski', + }, true), {}); + assert.equal(validateUser({ password: 'short' }, false).password, 'Password must be between 8 and 72 characters.'); +}); + +test('customer validation enforces company name, phone and Polish postal code formats', () => { + const errors = validateCustomer({ + type: 'company', + first_name: 'Jan', + last_name: 'Kowalski', + email: 'jan@example.com', + phone: '123', + postal_code: '12345', + birth_date: '2024-99-99', + }, true); + assert.ok(errors.company_name); + assert.ok(errors.phone); + assert.ok(errors.postal_code); + assert.ok(errors.birth_date); + assert.equal(validateCustomer({ first_name: null }, false).first_name, 'Must be a string of at most 100 characters.'); +}); + +test('car validation normalizes identifiers and rejects invalid VINs', () => { + const data = { + customer_id: '1', + make: 'Toyota', + model: 'Corolla', + production_year: 2020, + vin: '1HGCM82633A004352', + registration_number: 'wa 12345', + fuel_type: 'petrol', + }; + assert.deepEqual(validateCar(data, true), {}); + assert.equal(data.vin, '1HGCM82633A004352'); + assert.equal(data.registration_number, 'WA12345'); + assert.ok(validateCar({ vin: 'INVALID' }, false).vin); +}); + +test('login migration backfills existing users and adds a unique login index', async () => { + const calls = []; + const queryInterface = { + describeTable: async () => ({ email: {} }), + addColumn: async (...args) => calls.push(['addColumn', ...args]), + sequelize: { query: async (...args) => calls.push(['query', ...args]) }, + changeColumn: async (...args) => calls.push(['changeColumn', ...args]), + showIndex: async () => [], + addIndex: async (...args) => calls.push(['addIndex', ...args]), + }; + await loginMigration.up(queryInterface, { STRING: (length) => `STRING(${length})` }); + assert.deepEqual(calls.map(([name]) => name), ['addColumn', 'query', 'changeColumn', 'addIndex']); + assert.match(calls[1][1], /SET `login` = `email`/); + assert.equal(calls[3][3].unique, true); +}); + +test('HTTP app exposes OpenAPI docs and challenges unauthenticated API requests', async (t) => { + const server = http.createServer(app); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + t.after(() => new Promise((resolve, reject) => { + server.close((error) => error ? reject(error) : resolve()); + })); + const baseUrl = `http://127.0.0.1:${server.address().port}`; + + const docs = await fetch(`${baseUrl}/docs`); + assert.equal(docs.status, 200); + assert.match(await docs.text(), /swagger-ui/); + + const spec = await fetch(`${baseUrl}/openapi.yaml`); + assert.equal(spec.status, 200); + assert.match(await spec.text(), /scheme: basic/); + + const protectedResponse = await fetch(`${baseUrl}/api/auth/me`); + assert.equal(protectedResponse.status, 401); + assert.match(protectedResponse.headers.get('www-authenticate'), /^Basic/); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..289cd9a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2354 @@ +{ + "name": "test-rest-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test-rest-api", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "mysql2": "^3.14.0", + "sequelize": "^6.37.6", + "swagger-ui-express": "^5.0.1", + "yamljs": "^0.3.0" + }, + "devDependencies": { + "sequelize-cli": "^6.6.2" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.9", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.9.tgz", + "integrity": "sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "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": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "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.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "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" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lru.min": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz", + "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/moment": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.6.tgz", + "integrity": "sha512-enu73KhbAua2L3dIxjc6qg6ZJ7H3pt1BImZASWNWY5xSsmJ1ZuUPULTDGXgmJbI61vb+nJAG19IZclIq19UidQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.3", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz", + "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "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/sequelize-cli": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.5.tgz", + "integrity": "sha512-DqyISCULOaEbTM+rRQH4YvcUWeOC1XDiSKcjsC6TfAnT7W837mNkChJhtB/Z4FdCFHRCojmiP7zsrA4pARmacA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "js-beautify": "1.15.4", + "lodash": "^4.17.21", + "picocolors": "^1.1.1", + "resolve": "^1.22.1", + "umzug": "^2.3.0", + "yargs": "^16.2.0" + }, + "bin": { + "sequelize": "lib/sequelize", + "sequelize-cli": "lib/sequelize" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/sql-escaper": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.2.tgz", + "integrity": "sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.34.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.34.0.tgz", + "integrity": "sha512-H6tAIjr3XqE31PbkZl0OFya1ziRLDfTwh3J6OHdumPtPCLfjTnyLeagl/55kFNZcTOz9a9QI9WP0FXldwiGJag==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "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", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "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" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/yamljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/yamljs/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f0f59ce --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "test-rest-api", + "version": "1.0.0", + "private": true, + "description": "REST API for users, customers and cars", + "main": "node/server.js", + "scripts": { + "start": "node node/server.js", + "dev": "node --watch node/server.js", + "db:migrate": "sequelize-cli db:migrate --config node/config/database.cjs --migrations-path node/migrations", + "test": "node --test" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "mysql2": "^3.14.0", + "sequelize": "^6.37.6", + "swagger-ui-express": "^5.0.1", + "yamljs": "^0.3.0" + }, + "devDependencies": { + "sequelize-cli": "^6.6.2" + }, + "overrides": { + "uuid": "^11.1.1" + } +} diff --git a/webroot/openapi.yaml b/webroot/openapi.yaml index 8a0fa4a..b650bc5 100644 --- a/webroot/openapi.yaml +++ b/webroot/openapi.yaml @@ -1,98 +1,42 @@ openapi: 3.0.3 info: - title: Test REST API + title: REST API version: 1.0.0 - description: | - Testowy serwis REST API (CakePHP 5): klienci (`customers`), ich auta (`cars`) i użytkownicy (`users`). - - ## Uwierzytelnianie - 1. Wywołaj `POST /api/auth/login` z e-mailem i hasłem. - 2. Skopiuj `data.token` i kliknij **Authorize** (u góry strony) – wklej sam token (bez `Bearer`). - 3. Token wysyłany jest w nagłówku `Authorization: Bearer ` i jest ważny 8 godzin. - - Konta testowe: - - | Rola | E-mail | Hasło | - |-------|---------------------|------------| - | admin | admin@example.com | Admin123! | - | user | user@example.com | User1234! | - - ## Uprawnienia - * **admin** – pełny dostęp, w tym CRUD użytkowników (`/api/users`). - * **user** – logowanie, własny profil (`/api/auth/me`), CRUD klientów i aut. - - ## Format odpowiedzi - * Sukces: `{"success": true, "data": ...}` (listy dodatkowo mają `meta` z paginacją). - * Błąd: `{"success": false, "error": {"code": 422, "message": "...", "fields": {...}}}`. - * `DELETE` zwraca `204 No Content`. - - ## Listy - Wszystkie listy obsługują `page`, `limit` (domyślnie 20, max 100), `sort` i `direction` (`asc`/`desc`). - Łączna liczba rekordów jest również w nagłówku `X-Total-Count`. + description: API użytkowników, klientów i samochodów. Wszystkie endpointy wymagają HTTP Basic Auth; w Swagger UI wybierz Authorize i podaj login oraz hasło. servers: - url: / - description: Ten serwer security: - - bearerAuth: [] + - basicAuth: [] tags: - name: Auth - description: Logowanie i profil zalogowanego użytkownika - - name: Customers - description: Klienci (wszyscy zalogowani) - - name: Cars - description: Auta klientów (wszyscy zalogowani) - name: Users - description: Użytkownicy systemu (tylko admin) - + - name: Customers + - name: Cars paths: /api/auth/login: post: tags: [Auth] - summary: Logowanie – zwraca token JWT - security: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [email, password] - properties: - email: { type: string, format: email, example: admin@example.com } - password: { type: string, format: password, example: Admin123! } + summary: Potwierdź logowanie i zaktualizuj ostatnie logowanie responses: '200': - description: Zalogowano + description: Uwierzytelniony użytkownik content: application/json: - schema: - type: object - properties: - success: { type: boolean, example: true } - data: - type: object - properties: - token: { type: string, example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... } - token_type: { type: string, example: Bearer } - expires_in: { type: integer, description: Czas ważności w sekundach, example: 28800 } - user: { $ref: '#/components/schemas/User' } + schema: { $ref: '#/components/schemas/UserResponse' } '401': { $ref: '#/components/responses/Unauthorized' } - /api/auth/me: get: tags: [Auth] - summary: Profil zalogowanego użytkownika + summary: Pobierz własny profil responses: '200': - description: OK + description: Profil użytkownika content: application/json: schema: { $ref: '#/components/schemas/UserResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - patch: + put: tags: [Auth] - summary: Edycja własnego profilu (imię, nazwisko, e-mail, hasło) - description: Pola `role` i `active` są ignorowane – może je zmienić tylko admin przez `/api/users/{id}`. + summary: Zaktualizuj własny profil requestBody: required: true content: @@ -104,11 +48,10 @@ paths: content: application/json: schema: { $ref: '#/components/schemas/UserResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } '422': { $ref: '#/components/responses/ValidationError' } - put: + patch: tags: [Auth] - summary: Edycja własnego profilu (alias PATCH) + summary: Częściowo zaktualizuj własny profil requestBody: required: true content: @@ -120,283 +63,7 @@ paths: content: application/json: schema: { $ref: '#/components/schemas/UserResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } '422': { $ref: '#/components/responses/ValidationError' } - - /api/customers: - get: - tags: [Customers] - summary: Lista klientów - parameters: - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/limit' - - name: sort - in: query - schema: - type: string - enum: [id, type, first_name, last_name, email, company_name, city, country, created, modified] - - $ref: '#/components/parameters/direction' - - name: q - in: query - description: Szukaj w imieniu, nazwisku, e-mailu, telefonie, nazwie firmy, mieście i NIP - schema: { type: string } - - name: type - in: query - schema: { type: string, enum: [individual, company] } - - name: city - in: query - schema: { type: string } - - name: country - in: query - schema: { type: string, example: PL } - - name: with_cars - in: query - description: Dołącz listę aut każdego klienta - schema: { type: boolean } - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - success: { type: boolean, example: true } - data: - type: array - items: { $ref: '#/components/schemas/Customer' } - meta: { $ref: '#/components/schemas/PaginationMeta' } - '401': { $ref: '#/components/responses/Unauthorized' } - post: - tags: [Customers] - summary: Dodaj klienta - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/CustomerCreate' } - responses: - '201': - description: Utworzono - content: - application/json: - schema: { $ref: '#/components/schemas/CustomerResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '422': { $ref: '#/components/responses/ValidationError' } - - /api/customers/{id}: - parameters: - - $ref: '#/components/parameters/id' - get: - tags: [Customers] - summary: Szczegóły klienta (z autami) - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - success: { type: boolean, example: true } - data: { $ref: '#/components/schemas/CustomerWithCars' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - patch: - tags: [Customers] - summary: Częściowa edycja klienta - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/CustomerInput' } - responses: - '200': - description: Zaktualizowano - content: - application/json: - schema: { $ref: '#/components/schemas/CustomerResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - '422': { $ref: '#/components/responses/ValidationError' } - put: - tags: [Customers] - summary: Edycja klienta - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/CustomerInput' } - responses: - '200': - description: Zaktualizowano - content: - application/json: - schema: { $ref: '#/components/schemas/CustomerResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - '422': { $ref: '#/components/responses/ValidationError' } - delete: - tags: [Customers] - summary: Usuń klienta (razem z jego autami) - responses: - '204': { description: Usunięto } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - - /api/customers/{customer_id}/cars: - parameters: - - name: customer_id - in: path - required: true - schema: { type: integer, example: 1 } - get: - tags: [Cars] - summary: Auta danego klienta - parameters: - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/limit' - - $ref: '#/components/parameters/carSort' - - $ref: '#/components/parameters/direction' - responses: - '200': { $ref: '#/components/responses/CarList' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - post: - tags: [Cars] - summary: Dodaj auto do klienta - description: '`customer_id` jest brany ze ścieżki.' - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/CarCreateNested' } - responses: - '201': - description: Utworzono - content: - application/json: - schema: { $ref: '#/components/schemas/CarResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - '422': { $ref: '#/components/responses/ValidationError' } - - /api/cars: - get: - tags: [Cars] - summary: Lista aut - parameters: - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/limit' - - $ref: '#/components/parameters/carSort' - - $ref: '#/components/parameters/direction' - - name: q - in: query - description: Szukaj w marce, modelu, VIN i numerze rejestracyjnym - schema: { type: string } - - name: customer_id - in: query - schema: { type: integer } - - name: make - in: query - schema: { type: string, example: Toyota } - - name: fuel_type - in: query - schema: { $ref: '#/components/schemas/FuelType' } - - name: transmission - in: query - schema: { type: string, enum: [manual, automatic] } - - name: year_from - in: query - schema: { type: integer, example: 2015 } - - name: year_to - in: query - schema: { type: integer, example: 2022 } - - name: with_customer - in: query - description: Dołącz dane właściciela - schema: { type: boolean } - responses: - '200': { $ref: '#/components/responses/CarList' } - '401': { $ref: '#/components/responses/Unauthorized' } - post: - tags: [Cars] - summary: Dodaj auto - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/CarCreate' } - responses: - '201': - description: Utworzono - content: - application/json: - schema: { $ref: '#/components/schemas/CarResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '422': { $ref: '#/components/responses/ValidationError' } - - /api/cars/{id}: - parameters: - - $ref: '#/components/parameters/id' - get: - tags: [Cars] - summary: Szczegóły auta (z właścicielem) - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - success: { type: boolean, example: true } - data: { $ref: '#/components/schemas/CarWithCustomer' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - patch: - tags: [Cars] - summary: Częściowa edycja auta - description: Zmiana `customer_id` przenosi auto do innego klienta. - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/CarInput' } - responses: - '200': - description: Zaktualizowano - content: - application/json: - schema: { $ref: '#/components/schemas/CarResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - '422': { $ref: '#/components/responses/ValidationError' } - put: - tags: [Cars] - summary: Edycja auta - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/CarInput' } - responses: - '200': - description: Zaktualizowano - content: - application/json: - schema: { $ref: '#/components/schemas/CarResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - '422': { $ref: '#/components/responses/ValidationError' } - delete: - tags: [Cars] - summary: Usuń auto - responses: - '204': { description: Usunięto } - '401': { $ref: '#/components/responses/Unauthorized' } - '404': { $ref: '#/components/responses/NotFound' } - /api/users: get: tags: [Users] @@ -404,36 +71,21 @@ paths: parameters: - $ref: '#/components/parameters/page' - $ref: '#/components/parameters/limit' - - name: sort - in: query - schema: - type: string - enum: [id, email, first_name, last_name, role, active, last_login, created, modified] - - $ref: '#/components/parameters/direction' - name: q in: query - description: Szukaj w e-mailu, imieniu i nazwisku schema: { type: string } - name: role in: query - schema: { $ref: '#/components/schemas/Role' } + schema: { type: string, enum: [admin, user] } - name: active in: query schema: { type: boolean } responses: '200': - description: OK + description: Lista użytkowników content: application/json: - schema: - type: object - properties: - success: { type: boolean, example: true } - data: - type: array - items: { $ref: '#/components/schemas/User' } - meta: { $ref: '#/components/schemas/PaginationMeta' } - '401': { $ref: '#/components/responses/Unauthorized' } + schema: { $ref: '#/components/schemas/UserList' } '403': { $ref: '#/components/responses/Forbidden' } post: tags: [Users] @@ -449,48 +101,38 @@ paths: content: application/json: schema: { $ref: '#/components/schemas/UserResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '403': { $ref: '#/components/responses/Forbidden' } '422': { $ref: '#/components/responses/ValidationError' } - /api/users/{id}: parameters: - $ref: '#/components/parameters/id' get: tags: [Users] - summary: Szczegóły użytkownika (admin) + summary: Pobierz użytkownika (admin) responses: '200': - description: OK + description: Użytkownik content: application/json: schema: { $ref: '#/components/schemas/UserResponse' } - '401': { $ref: '#/components/responses/Unauthorized' } - '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } - patch: - tags: [Users] - summary: Częściowa edycja użytkownika (admin) - description: Admin nie może zmienić własnej roli ani dezaktywować własnego konta. - requestBody: - required: true - content: - application/json: - schema: { $ref: '#/components/schemas/UserInput' } - responses: - '200': - description: Zaktualizowano - content: - application/json: - schema: { $ref: '#/components/schemas/UserResponse' } - '400': { $ref: '#/components/responses/BadRequest' } - '401': { $ref: '#/components/responses/Unauthorized' } - '403': { $ref: '#/components/responses/Forbidden' } - '404': { $ref: '#/components/responses/NotFound' } - '422': { $ref: '#/components/responses/ValidationError' } put: tags: [Users] - summary: Edycja użytkownika (admin) + summary: Zaktualizuj użytkownika (admin) + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/UserInput' } + responses: + '200': + description: Zaktualizowano + content: + application/json: + schema: { $ref: '#/components/schemas/UserResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + patch: + tags: [Users] + summary: Częściowo zaktualizuj użytkownika (admin) requestBody: required: true content: @@ -502,35 +144,250 @@ paths: content: application/json: schema: { $ref: '#/components/schemas/UserResponse' } - '400': { $ref: '#/components/responses/BadRequest' } - '401': { $ref: '#/components/responses/Unauthorized' } - '403': { $ref: '#/components/responses/Forbidden' } - '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/ValidationError' } delete: tags: [Users] summary: Usuń użytkownika (admin) - description: Nie można usunąć własnego konta. responses: '204': { description: Usunięto } '400': { $ref: '#/components/responses/BadRequest' } - '401': { $ref: '#/components/responses/Unauthorized' } - '403': { $ref: '#/components/responses/Forbidden' } + /api/customers: + get: + tags: [Customers] + summary: Lista klientów + parameters: + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/limit' + - name: q + in: query + schema: { type: string, description: Szukanie po danych klienta } + - name: type + in: query + schema: { type: string, enum: [individual, company] } + - name: city + in: query + schema: { type: string } + - name: country + in: query + schema: { type: string } + - name: with_cars + in: query + schema: { type: boolean } + responses: + '200': + description: Lista klientów + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerList' } + post: + tags: [Customers] + summary: Dodaj klienta + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerCreate' } + responses: + '201': + description: Utworzono + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + /api/customers/{id}: + parameters: + - $ref: '#/components/parameters/id' + get: + tags: [Customers] + summary: Szczegóły klienta wraz z samochodami + responses: + '200': + description: Klient + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerResponse' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [Customers] + summary: Zaktualizuj klienta + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerInput' } + responses: + '200': + description: Zaktualizowano + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + patch: + tags: [Customers] + summary: Częściowo zaktualizuj klienta + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerInput' } + responses: + '200': + description: Zaktualizowano + content: + application/json: + schema: { $ref: '#/components/schemas/CustomerResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [Customers] + summary: Usuń klienta i jego samochody + responses: + '204': { description: Usunięto } + '404': { $ref: '#/components/responses/NotFound' } + /api/customers/{customer_id}/cars: + parameters: + - name: customer_id + in: path + required: true + schema: { type: integer } + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/limit' + get: + tags: [Cars] + summary: Lista samochodów klienta + responses: + '200': + description: Samochody klienta + content: + application/json: + schema: { $ref: '#/components/schemas/CarList' } + '404': { $ref: '#/components/responses/NotFound' } + post: + tags: [Cars] + summary: Dodaj samochód klientowi + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NestedCarInput' } + responses: + '201': + description: Utworzono + content: + application/json: + schema: { $ref: '#/components/schemas/CarResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + /api/cars: + get: + tags: [Cars] + summary: Lista samochodów + parameters: + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/limit' + - name: q + in: query + schema: { type: string, description: Szukanie po marce, modelu i numerach auta } + - name: customer_id + in: query + schema: { type: integer } + - name: make + in: query + schema: { type: string } + - name: fuel_type + in: query + schema: { type: string, enum: [petrol, diesel, lpg, hybrid, electric] } + - name: transmission + in: query + schema: { type: string, enum: [manual, automatic] } + - name: year_from + in: query + schema: { type: integer } + - name: year_to + in: query + schema: { type: integer } + - name: with_customer + in: query + schema: { type: boolean } + responses: + '200': + description: Lista samochodów + content: + application/json: + schema: { $ref: '#/components/schemas/CarList' } + post: + tags: [Cars] + summary: Dodaj samochód + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CarCreate' } + responses: + '201': + description: Utworzono + content: + application/json: + schema: { $ref: '#/components/schemas/CarResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + /api/cars/{id}: + parameters: + - $ref: '#/components/parameters/id' + get: + tags: [Cars] + summary: Szczegóły samochodu z właścicielem + responses: + '200': + description: Samochód + content: + application/json: + schema: { $ref: '#/components/schemas/CarResponse' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [Cars] + summary: Zaktualizuj samochód + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CarInput' } + responses: + '200': + description: Zaktualizowano + content: + application/json: + schema: { $ref: '#/components/schemas/CarResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + patch: + tags: [Cars] + summary: Częściowo zaktualizuj samochód + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CarInput' } + responses: + '200': + description: Zaktualizowano + content: + application/json: + schema: { $ref: '#/components/schemas/CarResponse' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [Cars] + summary: Usuń samochód + responses: + '204': { description: Usunięto } '404': { $ref: '#/components/responses/NotFound' } - components: securitySchemes: - bearerAuth: + basicAuth: type: http - scheme: bearer - bearerFormat: JWT - + scheme: basic parameters: id: name: id in: path required: true - schema: { type: integer, example: 1 } + schema: { type: integer } page: name: page in: query @@ -539,67 +396,32 @@ components: name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 20 } - direction: - name: direction - in: query - schema: { type: string, enum: [asc, desc], default: asc } - carSort: - name: sort - in: query - schema: - type: string - enum: [id, customer_id, make, model, production_year, registration_number, fuel_type, mileage, power_hp, created, modified] - responses: Unauthorized: - description: Brak lub nieprawidłowy token / błędne dane logowania + description: Brak lub nieprawidłowe dane HTTP Basic Auth content: application/json: schema: { $ref: '#/components/schemas/Error' } - example: { success: false, error: { code: 401, message: Authentication is required to continue } } Forbidden: - description: Brak uprawnień (wymagana rola admin) + description: Wymagana rola admin content: application/json: schema: { $ref: '#/components/schemas/Error' } - example: { success: false, error: { code: 403, message: This action requires the admin role. } } NotFound: description: Nie znaleziono rekordu content: application/json: schema: { $ref: '#/components/schemas/Error' } - example: { success: false, error: { code: 404, message: Record not found in table `customers`. } } BadRequest: description: Niedozwolona operacja content: application/json: schema: { $ref: '#/components/schemas/Error' } ValidationError: - description: Błędy walidacji (klucz = pole, wartość = reguła => komunikat) + description: Błędy walidacji content: application/json: schema: { $ref: '#/components/schemas/Error' } - example: - success: false - error: - code: 422 - message: Validation failed. - fields: - email: { _isUnique: This email is already in use. } - phone: { _required: This field is required } - CarList: - description: OK - content: - application/json: - schema: - type: object - properties: - success: { type: boolean, example: true } - data: - type: array - items: { $ref: '#/components/schemas/Car' } - meta: { $ref: '#/components/schemas/PaginationMeta' } - schemas: Error: type: object @@ -612,64 +434,46 @@ components: message: { type: string } fields: type: object - additionalProperties: - type: object - additionalProperties: { type: string } - - PaginationMeta: + additionalProperties: true + Pagination: type: object properties: - page: { type: integer, example: 1 } - limit: { type: integer, example: 20 } - total: { type: integer, example: 100 } - pages: { type: integer, example: 5 } - has_next_page: { type: boolean, example: true } - has_prev_page: { type: boolean, example: false } - sort: { type: string, nullable: true, example: id } - direction: { type: string, nullable: true, example: asc } - - Role: - type: string - enum: [admin, user] - - FuelType: - type: string - enum: [petrol, diesel, lpg, hybrid, electric] - - # ---------- Users ---------- + page: { type: integer } + limit: { type: integer } + total: { type: integer } + pages: { type: integer } + has_next_page: { type: boolean } + has_prev_page: { type: boolean } + sort: { type: string } + direction: { type: string } User: type: object properties: - id: { type: integer, readOnly: true, example: 1 } - email: { type: string, format: email, example: admin@example.com } - first_name: { type: string, example: Adam } - last_name: { type: string, example: Administrator } - role: { $ref: '#/components/schemas/Role' } - active: { type: boolean, example: true } - last_login: { type: string, format: date-time, nullable: true, readOnly: true } - created: { type: string, format: date-time, readOnly: true } - modified: { type: string, format: date-time, readOnly: true } + id: { type: integer } + login: { type: string } + email: { type: string, format: email } + first_name: { type: string } + last_name: { type: string } + role: { type: string, enum: [admin, user] } + active: { type: boolean } + last_login: { type: string, format: date-time, nullable: true } + created: { type: string, format: date-time } + modified: { type: string, format: date-time } UserInput: type: object properties: - email: { type: string, format: email, maxLength: 255 } + login: { type: string, maxLength: 255 } + email: { type: string, format: email } password: { type: string, format: password, minLength: 8, maxLength: 72 } first_name: { type: string, maxLength: 100 } last_name: { type: string, maxLength: 100 } - role: { $ref: '#/components/schemas/Role' } + role: { type: string, enum: [admin, user] } active: { type: boolean } UserCreate: allOf: - $ref: '#/components/schemas/UserInput' - type: object - required: [email, password, first_name, last_name] - example: - email: jan.kowalski@example.com - password: Haslo123! - first_name: Jan - last_name: Kowalski - role: user - active: true + required: [login, email, password, first_name, last_name] ProfileInput: type: object properties: @@ -677,36 +481,35 @@ components: password: { type: string, format: password, minLength: 8 } first_name: { type: string } last_name: { type: string } - example: - first_name: Jan - password: NoweHaslo123! UserResponse: type: object properties: - success: { type: boolean, example: true } + success: { type: boolean } data: { $ref: '#/components/schemas/User' } - - # ---------- Customers ---------- + UserList: + type: object + properties: + success: { type: boolean } + data: + type: array + items: { $ref: '#/components/schemas/User' } + meta: { $ref: '#/components/schemas/Pagination' } CustomerInput: type: object properties: - type: - type: string - enum: [individual, company] - default: individual - description: Dla `company` wymagane jest `company_name` - first_name: { type: string, maxLength: 100, example: Anna } - last_name: { type: string, maxLength: 100, example: Nowak } - email: { type: string, format: email, maxLength: 255, description: Unikalny, example: anna.nowak@example.com } - phone: { type: string, pattern: '^\+?[0-9 ()-]{6,30}$', example: '+48 600 100 200' } - company_name: { type: string, nullable: true, maxLength: 255, example: null } - tax_id: { type: string, nullable: true, pattern: '^[0-9]{10}$', description: NIP (10 cyfr), example: null } - street: { type: string, nullable: true, maxLength: 255, example: ul. Długa 5/12 } - city: { type: string, nullable: true, maxLength: 100, example: Kraków } - postal_code: { type: string, nullable: true, pattern: '^[0-9]{2}-[0-9]{3}$', example: 30-001 } - country: { type: string, pattern: '^[A-Z]{2}$', default: PL, description: ISO 3166-1 alpha-2, example: PL } - birth_date: { type: string, format: date, nullable: true, example: '1985-04-12' } - notes: { type: string, nullable: true, maxLength: 5000, example: Preferuje kontakt telefoniczny. } + type: { type: string, enum: [individual, company], default: individual } + first_name: { type: string, maxLength: 100 } + last_name: { type: string, maxLength: 100 } + email: { type: string, format: email } + phone: { type: string, maxLength: 30 } + company_name: { type: string, nullable: true } + tax_id: { type: string, nullable: true } + street: { type: string, nullable: true } + city: { type: string, nullable: true } + postal_code: { type: string, nullable: true } + country: { type: string, default: PL } + birth_date: { type: string, format: date, nullable: true } + notes: { type: string, nullable: true } CustomerCreate: allOf: - $ref: '#/components/schemas/CustomerInput' @@ -714,86 +517,93 @@ components: required: [first_name, last_name, email, phone] Customer: allOf: - - type: object - properties: - id: { type: integer, readOnly: true, example: 1 } - $ref: '#/components/schemas/CustomerInput' - type: object properties: - created_by: { type: integer, nullable: true, readOnly: true, description: ID użytkownika, który dodał klienta } - created: { type: string, format: date-time, readOnly: true } - modified: { type: string, format: date-time, readOnly: true } - CustomerWithCars: - allOf: - - $ref: '#/components/schemas/Customer' - - type: object - properties: - cars: - type: array - items: { $ref: '#/components/schemas/Car' } + id: { type: integer } + created_by: { type: integer, nullable: true } + created: { type: string, format: date-time } + modified: { type: string, format: date-time } CustomerResponse: type: object properties: - success: { type: boolean, example: true } - data: { $ref: '#/components/schemas/Customer' } - - # ---------- Cars ---------- - CarFields: + success: { type: boolean } + data: + allOf: + - $ref: '#/components/schemas/Customer' + - type: object + properties: + cars: + type: array + items: { $ref: '#/components/schemas/Car' } + CustomerList: type: object properties: - make: { type: string, maxLength: 50, example: Toyota } - model: { type: string, maxLength: 50, example: Corolla } - production_year: { type: integer, minimum: 1900, example: 2020 } - vin: - type: string - pattern: '^[A-HJ-NPR-Z0-9]{17}$' - description: 17 znaków, bez liter I, O, Q. Unikalny. Zapisywany wielkimi literami. - example: JTDBR32E720123456 - registration_number: - type: string - description: 4-10 liter/cyfr. Unikalny. Spacje są usuwane, litery zamieniane na wielkie. - example: KR12345 - fuel_type: { $ref: '#/components/schemas/FuelType' } - color: { type: string, nullable: true, maxLength: 30, example: srebrny } - engine_capacity: { type: integer, nullable: true, minimum: 0, maximum: 10000, description: Pojemność w cm³, example: 1798 } - power_hp: { type: integer, nullable: true, minimum: 1, maximum: 2000, description: Moc w KM, example: 122 } - transmission: { type: string, nullable: true, enum: [manual, automatic], example: automatic } - mileage: { type: integer, nullable: true, minimum: 0, description: Przebieg w km, example: 45000 } - notes: { type: string, nullable: true, maxLength: 5000, example: null } + success: { type: boolean } + data: + type: array + items: { $ref: '#/components/schemas/Customer' } + meta: { $ref: '#/components/schemas/Pagination' } CarInput: - allOf: - - type: object - properties: - customer_id: { type: integer, example: 1 } - - $ref: '#/components/schemas/CarFields' + type: object + properties: + customer_id: { type: integer } + make: { type: string } + model: { type: string } + production_year: { type: integer } + vin: { type: string, minLength: 17, maxLength: 17 } + registration_number: { type: string } + fuel_type: { type: string, enum: [petrol, diesel, lpg, hybrid, electric] } + color: { type: string, nullable: true } + engine_capacity: { type: integer, nullable: true } + power_hp: { type: integer, nullable: true } + transmission: { type: string, enum: [manual, automatic], nullable: true } + mileage: { type: integer, nullable: true } + notes: { type: string, nullable: true } CarCreate: allOf: - $ref: '#/components/schemas/CarInput' - type: object required: [customer_id, make, model, production_year, vin, registration_number, fuel_type] - CarCreateNested: - allOf: - - $ref: '#/components/schemas/CarFields' - - type: object - required: [make, model, production_year, vin, registration_number, fuel_type] Car: allOf: - - type: object - properties: - id: { type: integer, readOnly: true, example: 1 } - $ref: '#/components/schemas/CarInput' - type: object properties: - created: { type: string, format: date-time, readOnly: true } - modified: { type: string, format: date-time, readOnly: true } - CarWithCustomer: - allOf: - - $ref: '#/components/schemas/Car' - - type: object - properties: - customer: { $ref: '#/components/schemas/Customer' } + id: { type: integer } + created: { type: string, format: date-time } + modified: { type: string, format: date-time } + NestedCarInput: + type: object + required: [make, model, production_year, vin, registration_number, fuel_type] + properties: + make: { type: string } + model: { type: string } + production_year: { type: integer } + vin: { type: string, minLength: 17, maxLength: 17 } + registration_number: { type: string } + fuel_type: { type: string, enum: [petrol, diesel, lpg, hybrid, electric] } + color: { type: string, nullable: true } + engine_capacity: { type: integer, nullable: true } + power_hp: { type: integer, nullable: true } + transmission: { type: string, enum: [manual, automatic], nullable: true } + mileage: { type: integer, nullable: true } + notes: { type: string, nullable: true } CarResponse: type: object properties: - success: { type: boolean, example: true } - data: { $ref: '#/components/schemas/Car' } + success: { type: boolean } + data: + allOf: + - $ref: '#/components/schemas/Car' + - type: object + properties: + customer: { $ref: '#/components/schemas/Customer' } + CarList: + type: object + properties: + success: { type: boolean } + data: + type: array + items: { $ref: '#/components/schemas/Car' } + meta: { $ref: '#/components/schemas/Pagination' }