commit dea56a7e80a72f1d1f0da3754f29465dc8d4476a Author: Carlos Gutierrez Date: Sat Feb 21 18:20:41 2026 -0500 Complete mock secure web application with: - User registration and login with CSRF protection - SQL injection prevention and XSS protection - Real-time form validation - Password strength requirements - Show/hide password toggle - Modern dark theme UI - Routes for /login, /register, /home, /logout - API endpoints for CRUD operations - Prettier and ESLint configure diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a1e87d0 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Database Configuration +DB_HOST=localhost +DB_NAME=your_database +DB_USER=your_username +DB_PASSWORD=your_secure_password diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..867f3a7 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "no-undef": "error" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38d7d14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,268 @@ +### Node ### +# Logs +logs +*.log +npm-debug.log* +vendor/ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### PhpStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PhpStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/node,phpstorm + +### PHP ### +# PHP session files +SESS_* +sess_* +# PHP error logs +*.log +# PHP cache +.phpunit.cache/ +.cache/ +# Composer +/vendor/ +/composer.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..04b15c2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "htmlWhitespaceSensitivity": "css" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..755e35d --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Secure Application + +A secure web application with PHP backend and JavaScript frontend featuring authentication, data management, and modern UI. + +## Features + +- User registration and login with secure password handling +- CSRF protection +- SQL injection prevention (PDO prepared statements) +- XSS protection +- Real-time form validation +- Password strength requirements +- Show/hide password toggle +- Responsive dark theme UI + +## Tech Stack + +- **Backend**: PHP 8+ with PostgreSQL +- **Frontend**: Vanilla JavaScript, HTML, CSS +- **Database**: PostgreSQL + +## Requirements + +- PHP 8.0+ +- PostgreSQL +- Node.js (for formatting/linting) + +## Setup + +1. **Install dependencies**: + ```bash + npm install + composer install + ``` + +2. **Configure database**: + Copy `.env.example` to `.env` and update with your database credentials: + ``` + DB_HOST=localhost + DB_NAME=securecode + DB_USER=your_username + DB_PASSWORD=your_password + DB_PORT=5432 + ``` + +3. **Create database**: + ```bash + psql -h localhost -U your_username -d postgres -c "CREATE DATABASE securecode;" + ``` + +4. **Run migrations**: + ```bash + psql -h localhost -U your_username -d securecode -f config/schema.sql + ``` + +5. **Start development server**: + ```bash + php -S localhost:8000 -t public + ``` + +6. **Access the app**: http://localhost:8000 + +## Development + +- **Format code**: `npm run format` +- **Lint code**: `npm run lint` + +## Project Structure + +``` +├── config/ +│ ├── database.php # Database connection & helpers +│ └── schema.sql # Database schema +├── api/ +│ └── index.php # API endpoints +├── public/ +│ ├── index.php # Router +│ ├── views/ # Page templates +│ ├── js/ # JavaScript files +│ └── styles/ # CSS files +└── .env.example # Environment template +``` + +## Password Requirements + +- Minimum 8 characters +- At least one uppercase letter +- At least one lowercase letter +- At least one number +- At least one special character diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000..80867f3 --- /dev/null +++ b/api/index.php @@ -0,0 +1,254 @@ + fn() => print json_encode(['token' => generateToken()]), + 'create' => fn() => handleRegistration($database), + 'login' => fn() => handleLogin($database), + 'submit_data' => fn() => handleDataSubmission($database), + 'get_data' => fn() => handleGetData($database), + 'delete_data' => fn() => handleDeleteData($database), + ]; + + if (isset($actions[$action])) { + $actions[$action](); + } else { + http_response_code(400); + echo json_encode(['error' => 'Invalid action']); + } +} else { + http_response_code(405); + echo json_encode(['error' => 'Method not allowed']); +} + +function handleRegistration($database) { + $token = $_POST['csrf_token'] ?? ''; + + if (!verifyToken($token)) { + http_response_code(403); + echo json_encode(['error' => 'Invalid CSRF token']); + return; + } + + $username = sanitizeInput($_POST['username'] ?? ''); + $email = sanitizeInput($_POST['email'] ?? ''); + $password = $_POST['password'] ?? ''; + + if (empty($username) || empty($email) || empty($password)) { + http_response_code(400); + echo json_encode(['error' => 'All fields are required']); + return; + } + + if (!validateEmail($email)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid email format']); + return; + } + + if (!validateString($username, 50)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid username format']); + return; + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid email format']); + return; + } + + $passwordErrors = validatePassword($password); + if (!empty($passwordErrors)) { + http_response_code(400); + echo json_encode(['error' => $passwordErrors[0]]); + return; + } + + try { + $checkSql = "SELECT id FROM users WHERE email = ? OR username = ?"; + $stmt = $database->query($checkSql, [$email, $username]); + + if ($stmt->fetch()) { + http_response_code(409); + echo json_encode(['error' => 'User already exists']); + return; + } + + $hashedPassword = hashPassword($password); + + $insertSql = "INSERT INTO users (username, email, password, created_at) VALUES (?, ?, ?, NOW())"; + $database->query($insertSql, [sanitizeInput($username), $email, $hashedPassword]); + + echo json_encode(['success' => true, 'message' => 'User created successfully']); + } catch (Exception $e) { + error_log("Registration error: " . $e->getMessage()); + http_response_code(500); + echo json_encode(['error' => 'Failed to create user']); + } +} + +function handleLogin($database) { + $token = $_POST['csrf_token'] ?? ''; + + if (!verifyToken($token)) { + http_response_code(403); + echo json_encode(['error' => 'Invalid CSRF token']); + return; + } + + $email = $_POST['email'] ?? ''; + $password = $_POST['password'] ?? ''; + + if (empty($email) || empty($password)) { + http_response_code(400); + echo json_encode(['error' => 'Email and password are required']); + return; + } + + try { + $sql = "SELECT id, username, email, password FROM users WHERE email = ?"; + $stmt = $database->query($sql, [$email]); + $user = $stmt->fetch(); + + if (!$user || !verifyPassword($password, $user['password'])) { + http_response_code(401); + echo json_encode(['error' => 'Invalid credentials']); + return; + } + + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + + echo json_encode(['success' => true, 'message' => 'Login successful']); + } catch (Exception $e) { + error_log("Login error: " . $e->getMessage()); + http_response_code(500); + echo json_encode(['error' => 'Login failed']); + } +} + +function handleDataSubmission($database) { + if (!isset($_SESSION['user_id'])) { + http_response_code(401); + echo json_encode(['error' => 'Authentication required']); + return; + } + + $token = $_POST['csrf_token'] ?? ''; + + if (!verifyToken($token)) { + http_response_code(403); + echo json_encode(['error' => 'Invalid CSRF token']); + return; + } + + $data = sanitizeInput($_POST['data'] ?? ''); + + if (empty($data)) { + http_response_code(400); + echo json_encode(['error' => 'Data is required']); + return; + } + + if (!validateString($data, 5000)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid data format']); + return; + } + + try { + $sql = "INSERT INTO user_data (user_id, data, created_at) VALUES (?, ?, NOW())"; + $database->query($sql, [$_SESSION['user_id'], $data]); + + echo json_encode(['success' => true, 'message' => 'Data saved successfully']); + } catch (Exception $e) { + error_log("Data submission error: " . $e->getMessage()); + http_response_code(500); + echo json_encode(['error' => 'Failed to save data']); + } +} + +function handleGetData($database) { + if (!isset($_SESSION['user_id'])) { + http_response_code(401); + echo json_encode(['error' => 'Authentication required']); + return; + } + + try { + $sql = "SELECT id, data, created_at FROM user_data WHERE user_id = ? ORDER BY created_at DESC"; + $stmt = $database->query($sql, [$_SESSION['user_id']]); + $data = $stmt->fetchAll(); + + $sanitizedData = array_map(function($item) { + return [ + 'id' => $item['id'], + 'data' => escapeHtml($item['data']), + 'created_at' => $item['created_at'] + ]; + }, $data); + + echo json_encode(['success' => true, 'data' => $sanitizedData]); + } catch (Exception $e) { + error_log("Get data error: " . $e->getMessage()); + http_response_code(500); + echo json_encode(['error' => 'Failed to retrieve data']); + } +} + +function handleDeleteData($database) { + if (!isset($_SESSION['user_id'])) { + http_response_code(401); + echo json_encode(['error' => 'Authentication required']); + return; + } + + $token = $_POST['csrf_token'] ?? ''; + + if (!verifyToken($token)) { + http_response_code(403); + echo json_encode(['error' => 'Invalid CSRF token']); + return; + } + + $id = filter_var($_POST['id'] ?? '', FILTER_VALIDATE_INT); + + if (empty($id)) { + http_response_code(400); + echo json_encode(['error' => 'ID is required']); + return; + } + + try { + $sql = "DELETE FROM user_data WHERE id = ? AND user_id = ?"; + $database->query($sql, [$id, $_SESSION['user_id']]); + + echo json_encode(['success' => true, 'message' => 'Data deleted successfully']); + } catch (Exception $e) { + error_log("Delete data error: " . $e->getMessage()); + http_response_code(500); + echo json_encode(['error' => 'Failed to delete data']); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..aeb3464 --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "user/secure-web-app", + "description": "Secure PHP/JavaScript web application", + "require-dev": { + "squizlabs/php_codesniffer": "^3.9" + }, + "config": { + "php_codesniffer": { + "standard": "PSR12", + "encoding": "utf-8" + } + } +} diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..7ee94e7 --- /dev/null +++ b/config/database.php @@ -0,0 +1,136 @@ +host = getenv('DB_HOST') ?: 'localhost'; + $this->db = getenv('DB_NAME') ?: 'securecode'; + $this->user = getenv('DB_USER') ?: 'carlos'; + $this->pass = getenv('DB_PASSWORD') ?: ''; + $this->port = getenv('DB_PORT') ?: '5432'; + $this->charset = 'utf8'; + } + + public function getConnection() { + if ($this->pdo !== null) { + return $this->pdo; + } + + $dsn = "pgsql:host={$this->host};port={$this->port};dbname={$this->db};options='--client_encoding={$this->charset}'"; + + $options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + $this->pdo = new PDO($dsn, $this->user, $this->pass, $options); + } catch (\PDOException $e) { + error_log("Database connection error: " . $e->getMessage()); + throw new \Exception("Database connection failed"); + } + + return $this->pdo; + } + + public function query($sql, $params = []) { + $pdo = $this->getConnection(); + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + return $stmt; + } +} + +function sanitizeInput($data) { + if (is_array($data)) { + return array_map('sanitizeInput', $data); + } + return htmlspecialchars(trim($data), ENT_QUOTES, 'UTF-8'); +} + +function escapeHtml($data) { + if (is_array($data)) { + return array_map('escapeHtml', $data); + } + return htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); +} + +function validateEmail($email) { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; +} + +function validateString($data, $maxLength = 1000) { + if (is_array($data)) { + return array_map(fn($item) => validateString($item, $maxLength), $data); + } + $data = trim($data); + if (strlen($data) > $maxLength) { + return false; + } + return preg_match('/^[\p{L}\p{N}\s\-_.,!?@]+$/u', $data) === 1 || empty($data); +} + +function sanitizeForDatabase($data) { + if (is_array($data)) { + return array_map('sanitizeForDatabase', $data); + } + return preg_replace('/[^\p{L}\p{N}\s\-_.,!?@]/u', '', $data); +} + +function validatePassword($password) { + $errors = []; + + if (strlen($password) < 8) { + $errors[] = 'Password must be at least 8 characters'; + } + + if (strlen($password) > 128) { + $errors[] = 'Password must be less than 128 characters'; + } + + if (!preg_match('/[a-z]/', $password)) { + $errors[] = 'Password must contain at least one lowercase letter'; + } + + if (!preg_match('/[A-Z]/', $password)) { + $errors[] = 'Password must contain at least one uppercase letter'; + } + + if (!preg_match('/[0-9]/', $password)) { + $errors[] = 'Password must contain at least one number'; + } + + if (!preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) { + $errors[] = 'Password must contain at least one special character'; + } + + return $errors; +} + +function generateToken() { + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + return $_SESSION['csrf_token']; +} + +function verifyToken($token) { + return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); +} + +function hashPassword($password) { + return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); +} + +function verifyPassword($password, $hash) { + return password_verify($password, $hash); +} diff --git a/config/schema.sql b/config/schema.sql new file mode 100644 index 0000000..39470f6 --- /dev/null +++ b/config/schema.sql @@ -0,0 +1,22 @@ +-- PostgreSQL schema for secure application + +-- Create users table +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(100) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + +-- Create user_data table +CREATE TABLE IF NOT EXISTS user_data ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + data TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_user_data_user_id ON user_data(user_id); diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..8265d89 --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +localhost FALSE / FALSE 0 PHPSESSID 5dde60a9b8dd9c20d2ff97d021f326b3 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8a73959 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1259 @@ +{ + "name": "secure-web-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "secure-web-app", + "version": "1.0.0", + "devDependencies": { + "eslint": "^8.57.0", + "prettier": "^3.2.5" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "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": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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", + "dev": true, + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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.", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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==", + "dev": true, + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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/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-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c73e6d6 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "secure-web-app", + "version": "1.0.0", + "description": "Secure PHP/JavaScript web application", + "author": "Carlos Gutierrez ", + "repository": { + "type": "git", + "url": "https://github.com/CarGDev/MSCS535_Assignment14" + }, + "scripts": { + "lint": "eslint public/js/ && ./vendor/bin/phpcs --standard=PSR12 api/ config/", + "lint:fix": "eslint public/js/ --fix", + "format": "prettier --write \"public/**/*.{html,js,css}\"", + "format:check": "prettier --check \"public/**/*.{html,js,css}\"", + "format:all": "prettier --write \"public/**/*.{html,js,css}\" && ./vendor/bin/phpcbf api/ config/" + }, + "devDependencies": { + "eslint": "^8.57.0", + "prettier": "^3.2.5" + } +} diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..50d4c95 --- /dev/null +++ b/public/index.php @@ -0,0 +1,35 @@ + { + await api.getCSRFToken(); +}); diff --git a/public/js/home.js b/public/js/home.js new file mode 100644 index 0000000..86c072a --- /dev/null +++ b/public/js/home.js @@ -0,0 +1,76 @@ +async function loadUserData() { + const dataList = document.getElementById('dataList'); + if (!dataList) return; + + try { + const result = await api.getData(); + if (result.success) { + if (result.data.length === 0) { + dataList.innerHTML = '

No data submitted yet

'; + } else { + dataList.innerHTML = result.data + .map( + (item) => ` +
+
+ ${api.sanitizeHTML(item.data)} + ${new Date(item.created_at).toLocaleString()} +
+ +
` + ) + .join(''); + + document.querySelectorAll('.btn-delete').forEach((btn) => { + btn.addEventListener('click', async (e) => { + const id = e.currentTarget.dataset.id; + try { + await api.deleteData(id); + loadUserData(); + } catch (error) { + console.error('Failed to delete:', error); + } + }); + }); + } + } + } catch (error) { + console.error('Failed to load data:', error); + } +} + +function initDataForm() { + const dataForm = document.getElementById('dataForm'); + const messageDiv = document.getElementById('message'); + + if (dataForm) { + dataForm.addEventListener('submit', async (e) => { + e.preventDefault(); + messageDiv.textContent = 'Processing...'; + messageDiv.className = 'message info'; + + const data = dataForm.data.value.trim(); + + try { + const result = await api.submitData(data); + messageDiv.textContent = result.message; + messageDiv.className = 'message success'; + dataForm.reset(); + loadUserData(); + } catch (error) { + messageDiv.textContent = error.message; + messageDiv.className = 'message error'; + } + }); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + await api.getCSRFToken(); + initDataForm(); + loadUserData(); +}); diff --git a/public/js/login.js b/public/js/login.js new file mode 100644 index 0000000..d839dcd --- /dev/null +++ b/public/js/login.js @@ -0,0 +1,97 @@ +function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +function showError(elementId, message) { + const errorEl = document.getElementById(elementId); + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = message ? 'block' : 'none'; + } +} + +function clearErrors() { + document.querySelectorAll('.error-message').forEach((el) => { + el.textContent = ''; + el.style.display = 'none'; + }); +} + +function initPasswordToggle() { + document.querySelectorAll('.toggle-password').forEach((btn) => { + btn.addEventListener('click', () => { + const targetId = btn.dataset.target; + const input = document.getElementById(targetId); + if (input) { + const isPassword = input.type === 'password'; + input.type = isPassword ? 'text' : 'password'; + btn.querySelector('.eye-open').style.display = isPassword ? 'none' : 'block'; + btn.querySelector('.eye-closed').style.display = isPassword ? 'block' : 'none'; + } + }); + }); +} + +function initLoginForm() { + const loginForm = document.getElementById('loginForm'); + const emailInput = document.getElementById('loginEmail'); + const passwordInput = document.getElementById('loginPassword'); + + if (emailInput) { + emailInput.addEventListener('input', () => { + const email = emailInput.value.trim(); + const error = + !validateEmail(email) && email.length > 0 ? 'Please enter a valid email address' : ''; + showError('loginEmailError', error); + }); + } + + if (passwordInput) { + passwordInput.addEventListener('input', () => { + const password = passwordInput.value; + const error = password.length > 0 && password.length < 1 ? 'Password is required' : ''; + showError('loginPasswordError', error); + }); + } + + if (loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + clearErrors(); + + const email = loginForm.email.value.trim(); + const password = loginForm.password.value; + + if (!validateEmail(email)) { + showError('loginEmailError', 'Please enter a valid email address'); + return; + } + + if (!password) { + showError('loginPasswordError', 'Password is required'); + return; + } + + showError('loginFormError', 'Processing...'); + + try { + const result = await api.login(email, password); + showError('loginFormError', result.message); + loginForm.classList.add('success'); + loginForm.reset(); + setTimeout(() => { + window.location.href = '/home'; + }, 1000); + } catch (error) { + showError('loginFormError', error.message); + } + }); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + await api.getCSRFToken(); + initPasswordToggle(); + initLoginForm(); +}); diff --git a/public/js/register.js b/public/js/register.js new file mode 100644 index 0000000..54d5894 --- /dev/null +++ b/public/js/register.js @@ -0,0 +1,132 @@ +function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +function validatePassword(password) { + if (password.length === 0) return ''; + if (password.length < 8) return 'Password must be at least 8 characters'; + if (password.length > 128) return 'Password must be less than 128 characters'; + if (!/[a-z]/.test(password)) return 'Password must contain at least one lowercase letter'; + if (!/[A-Z]/.test(password)) return 'Password must contain at least one uppercase letter'; + if (!/[0-9]/.test(password)) return 'Password must contain at least one number'; + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) + return 'Password must contain at least one special character'; + return ''; +} + +function validateUsername(username) { + if (username.length === 0) return ''; + if (username.length < 3) return 'Username must be at least 3 characters'; + return ''; +} + +function showError(elementId, message) { + const errorEl = document.getElementById(elementId); + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = message ? 'block' : 'none'; + } +} + +function clearErrors() { + document.querySelectorAll('.error-message').forEach((el) => { + el.textContent = ''; + el.style.display = 'none'; + }); +} + +function initPasswordToggle() { + document.querySelectorAll('.toggle-password').forEach((btn) => { + btn.addEventListener('click', () => { + const targetId = btn.dataset.target; + const input = document.getElementById(targetId); + if (input) { + const isPassword = input.type === 'password'; + input.type = isPassword ? 'text' : 'password'; + btn.querySelector('.eye-open').style.display = isPassword ? 'none' : 'block'; + btn.querySelector('.eye-closed').style.display = isPassword ? 'block' : 'none'; + } + }); + }); +} + +function initRegisterForm() { + const registerForm = document.getElementById('registerForm'); + const usernameInput = document.getElementById('regUsername'); + const emailInput = document.getElementById('regEmail'); + const passwordInput = document.getElementById('regPassword'); + + if (usernameInput) { + usernameInput.addEventListener('input', () => { + const username = usernameInput.value.trim(); + const error = validateUsername(username); + showError('regUsernameError', error); + }); + } + + if (emailInput) { + emailInput.addEventListener('input', () => { + const email = emailInput.value.trim(); + const error = + !validateEmail(email) && email.length > 0 ? 'Please enter a valid email address' : ''; + showError('regEmailError', error); + }); + } + + if (passwordInput) { + passwordInput.addEventListener('input', () => { + const password = passwordInput.value; + const error = validatePassword(password); + showError('regPasswordError', error); + }); + } + + if (registerForm) { + registerForm.addEventListener('submit', async (e) => { + e.preventDefault(); + clearErrors(); + + const username = registerForm.username.value.trim(); + const email = registerForm.email.value.trim(); + const password = registerForm.password.value; + + const usernameError = validateUsername(username); + if (usernameError) { + showError('regUsernameError', usernameError); + return; + } + + if (!validateEmail(email)) { + showError('regEmailError', 'Please enter a valid email address'); + return; + } + + const passwordError = validatePassword(password); + if (passwordError) { + showError('regPasswordError', passwordError); + return; + } + + showError('registerFormError', 'Processing...'); + + try { + const result = await api.create(username, email, password); + showError('registerFormError', result.message); + registerForm.classList.add('success'); + registerForm.reset(); + setTimeout(() => { + window.location.href = '/login'; + }, 1500); + } catch (error) { + showError('registerFormError', error.message); + } + }); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + await api.getCSRFToken(); + initPasswordToggle(); + initRegisterForm(); +}); diff --git a/public/js/request.js b/public/js/request.js new file mode 100644 index 0000000..d1042e5 --- /dev/null +++ b/public/js/request.js @@ -0,0 +1,123 @@ +const API_BASE = 'api/index.php'; + +class ApiRequest { + constructor() { + this.csrfToken = null; + } + + async getCSRFToken() { + try { + const formData = new FormData(); + formData.append('action', 'csrf_token'); + const response = await fetch(API_BASE, { + method: 'POST', + body: formData, + credentials: 'same-origin', + }); + const data = await response.json(); + this.csrfToken = data.token; + return this.csrfToken; + } catch (error) { + console.error('Failed to get CSRF token:', error); + throw error; + } + } + + sanitizeHTML(str) { + const temp = document.createElement('div'); + temp.textContent = str; + return temp.innerHTML; + } + + async request(action, formData) { + if (!this.csrfToken) { + await this.getCSRFToken(); + } + + formData.append('action', action); + formData.append('csrf_token', this.csrfToken); + + try { + const response = await fetch(API_BASE, { + method: 'POST', + body: formData, + credentials: 'same-origin', + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Request failed'); + } + + if (data.error) { + throw new Error(data.error); + } + + return data; + } catch (error) { + if (error.message === 'Invalid CSRF token') { + await this.getCSRFToken(); + formData.set('csrf_token', this.csrfToken); + + const response = await fetch(API_BASE, { + method: 'POST', + body: formData, + credentials: 'same-origin', + }); + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error); + } + return data; + } + throw error; + } + } + + async create(username, email, password) { + const formData = new FormData(); + formData.append('username', username); + formData.append('email', email); + formData.append('password', password); + return this.request('create', formData); + } + + async login(email, password) { + const formData = new FormData(); + formData.append('email', email); + formData.append('password', password); + return this.request('login', formData); + } + + async submitData(data) { + const formData = new FormData(); + formData.append('data', data); + return this.request('submit_data', formData); + } + + async getData() { + const formData = new FormData(); + formData.append('action', 'get_data'); + try { + const response = await fetch(API_BASE, { + method: 'POST', + body: formData, + credentials: 'same-origin', + }); + return await response.json(); + } catch (error) { + console.error('Failed to get data:', error); + throw error; + } + } + + async deleteData(id) { + const formData = new FormData(); + formData.append('id', id); + return this.request('delete_data', formData); + } +} + +const api = new ApiRequest(); diff --git a/public/styles/styles.css b/public/styles/styles.css new file mode 100644 index 0000000..197a122 --- /dev/null +++ b/public/styles/styles.css @@ -0,0 +1,607 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --bg-dark: #0a0a0f; + --bg-card: #12121a; + --bg-card-hover: #1a1a25; + --primary: #6366f1; + --primary-hover: #818cf8; + --primary-glow: rgba(99, 102, 241, 0.4); + --danger: #ef4444; + --danger-hover: #f87171; + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --text-muted: #64748b; + --border: #1e293b; + --gradient-1: #6366f1; + --gradient-2: #8b5cf6; + --gradient-3: #ec4899; +} + +body { + font-family: + 'Inter', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + sans-serif; + background-color: var(--bg-dark); + color: var(--text-primary); + min-height: 100vh; + position: relative; + overflow-x: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.bg-gradient { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(ellipse 80% 50% at 50% -20%, var(--primary-glow), transparent), + radial-gradient(ellipse 60% 40% at 80% 100%, rgba(236, 72, 153, 0.15), transparent); + pointer-events: none; + z-index: 0; +} + +.bg-grid { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px); + background-size: 60px 60px; + pointer-events: none; + z-index: 0; +} + +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 40px; + z-index: 100; + backdrop-filter: blur(10px); + background: rgba(10, 10, 15, 0.7); + border-bottom: 1px solid var(--border); +} + +.logo { + display: flex; + align-items: center; + gap: 10px; + font-size: 20px; + font-weight: 700; +} + +.logo-icon { + font-size: 24px; +} + +.logo-text { + background: linear-gradient(135deg, var(--gradient-1), var(--gradient-3)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.btn-logout { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 12px; + color: var(--danger); + text-decoration: none; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-logout:hover { + background: var(--danger); + color: white; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3); +} + +.btn-logout svg { + transition: transform 0.3s ease; +} + +.btn-logout:hover svg { + transform: translateX(4px); +} + +.main { + position: relative; + z-index: 1; + max-width: 800px; + margin: 0 auto; + padding: 140px 24px 60px; +} + +.hero { + text-align: center; + margin-bottom: 50px; +} + +.hero-badge { + display: inline-block; + padding: 8px 16px; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2)); + border: 1px solid rgba(99, 102, 241, 0.3); + border-radius: 50px; + font-size: 13px; + font-weight: 600; + color: var(--primary-hover); + margin-bottom: 20px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.hero-title { + font-size: 48px; + font-weight: 800; + margin-bottom: 16px; + line-height: 1.2; +} + +.gradient-text { + background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2), var(--gradient-3)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtitle { + font-size: 18px; + color: var(--text-secondary); + max-width: 500px; + margin: 0 auto; +} + +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 24px; + padding: 32px; + margin-bottom: 24px; + transition: all 0.3s ease; +} + +.card:hover { + background: var(--bg-card-hover); + border-color: rgba(99, 102, 241, 0.3); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.card-header { + margin-bottom: 24px; +} + +.card-title { + font-size: 24px; + font-weight: 700; + margin-bottom: 8px; +} + +.card-description { + color: var(--text-secondary); + font-size: 14px; +} + +.data-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.input-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.input-group label { + font-size: 14px; + font-weight: 600; + color: var(--text-secondary); +} + +.input-group textarea { + width: 100%; + padding: 16px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--border); + border-radius: 12px; + color: var(--text-primary); + font-size: 15px; + font-family: inherit; + resize: vertical; + transition: all 0.3s ease; +} + +.input-group textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 4px var(--primary-glow); +} + +.input-group textarea::placeholder { + color: var(--text-muted); +} + +.btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 16px 32px; + background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2)); + border: none; + border-radius: 14px; + color: white; + font-size: 16px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; + align-self: flex-start; +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 15px 35px var(--primary-glow); +} + +.btn-primary svg { + transition: transform 0.3s ease; +} + +.btn-primary:hover svg { + transform: translate(4px, -4px); +} + +.data-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.data-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--border); + border-radius: 12px; + transition: all 0.2s ease; +} + +.data-item:hover { + border-color: rgba(99, 102, 241, 0.3); + background: rgba(0, 0, 0, 0.3); +} + +.data-item-content { + font-size: 15px; + color: var(--text-primary); +} + +.data-item-time { + font-size: 12px; + color: var(--text-muted); +} + +.empty-state { + text-align: center; + color: var(--text-muted); + padding: 40px; + font-size: 14px; +} + +.message { + position: fixed; + bottom: 24px; + right: 24px; + padding: 16px 24px; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + z-index: 200; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.message.error { + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.3); + color: var(--danger-hover); +} + +.message.success { + background: rgba(34, 197, 94, 0.15); + border: 1px solid rgba(34, 197, 94, 0.3); + color: #4ade80; +} + +.message.info { + background: rgba(99, 102, 241, 0.15); + border: 1px solid rgba(99, 102, 241, 0.3); + color: var(--primary-hover); +} + +.register-link { + text-align: center; + margin-top: 15px; +} + +.register-link a { + color: var(--primary-hover); + text-decoration: none; + font-weight: 600; +} + +.register-link a:hover { + text-decoration: underline; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 600; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.error-message { + display: none; + color: var(--danger); + font-size: 13px; + margin-top: 6px; +} + +.form-error { + text-align: center; + margin-bottom: 15px; +} + +form.success .form-error { + color: #4ade80; +} + +button { + background: #007bff; + color: white; + padding: 12px 24px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} + +button:hover { + background: #0056b3; +} + +.section { + display: none; + margin-top: 30px; +} + +#loginSection { + display: block; +} + +h2 { + margin-top: 0; +} + +ul { + padding-left: 20px; +} + +.hidden { + display: none !important; +} + +.container { + width: 100%; + max-width: 450px; + padding: 40px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 24px; +} + +.container h1 { + text-align: center; + margin-bottom: 32px; + background: linear-gradient(135deg, var(--gradient-1), var(--gradient-3)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.container h2 { + margin-bottom: 24px; + color: var(--text-primary); +} + +.container .form-group input { + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--border); + color: var(--text-primary); +} + +.container .form-group input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 4px var(--primary-glow); +} + +.container .form-group input::placeholder { + color: var(--text-muted); +} + +.password-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.password-input-wrapper input { + padding-right: 48px; +} + +.toggle-password { + position: absolute; + right: 12px; + background: none; + border: none; + cursor: pointer; + color: var(--text-muted); + padding: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.toggle-password:hover { + color: var(--text-secondary); + background: none; +} + +.toggle-password svg { + transition: all 0.2s ease; +} + +.container button[type='submit'] { + width: 100%; + padding: 16px; + background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2)); + border: none; + border-radius: 12px; + color: white; + font-size: 16px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; +} + +.container button[type='submit']:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px var(--primary-glow); +} + +.container .register-link { + margin-top: 24px; + color: var(--text-secondary); +} + +.link-btn { + background: none; + border: none; + color: #007bff; + cursor: pointer; + font-size: inherit; + text-decoration: underline; + padding: 0; +} + +.link-btn:hover { + background: none; + color: #0056b3; +} + +@media (max-width: 640px) { + .header { + padding: 16px 20px; + } + + .logo-text { + display: none; + } + + .main { + padding: 100px 16px 40px; + } + + .hero-title { + font-size: 32px; + } + + .card { + padding: 24px; + border-radius: 16px; + } + + .btn-primary { + width: 100%; + } +} + +.data-item-left { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} + +.btn-delete { + display: flex; + align-items: center; + justify-content: center; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 8px; + color: var(--danger); + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-delete:hover { + background: var(--danger); + color: white; +} diff --git a/public/views/home.php b/public/views/home.php new file mode 100644 index 0000000..db87d1a --- /dev/null +++ b/public/views/home.php @@ -0,0 +1,82 @@ + + + + + + + + + Home - Secure App + + +
+
+ +
+ + + Logout + + + + + + +
+ +
+
+
Welcome Back
+

+ Hello, +

+

Your secure space awaits. Manage your data with confidence.

+
+ +
+
+

Submit Data

+

Store your information securely

+
+
+
+ + +
+ +
+
+ +
+
+

Your Data

+

All your stored information

+
+
+

No data submitted yet

+
+
+
+ +
+ + + + + diff --git a/public/views/login.php b/public/views/login.php new file mode 100644 index 0000000..98e7b12 --- /dev/null +++ b/public/views/login.php @@ -0,0 +1,58 @@ + + + + + + + + + Login - Secure App + + +
+
+
+

Secure Application

+ +

Login

+
+
+ + + +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ + + + + diff --git a/public/views/register.php b/public/views/register.php new file mode 100644 index 0000000..15a3978 --- /dev/null +++ b/public/views/register.php @@ -0,0 +1,57 @@ + + + + + + + + Register - Secure App + + +
+
+
+

Secure Application

+ +

Register

+
+
+ + + +
+
+ + + +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ + + + +