diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6674ac2 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Kopieer naar .env en pas de waarden aan. .env wordt niet gecommit (zie .gitignore). + +TYPE=docker +QRCODE_GENERATOR=internal-chillerlan.qrcode +BASE_URL=http://localhost + +DATABASE_HOST=php-dynamic-qrcode-db +DATABASE_PORT=3306 +DATABASE_NAME=qrcode +DATABASE_USER=qrcode +DATABASE_PASSWORD=change-me-to-a-strong-password +DATABASE_PREFIX= +DATABASE_CHARSET=utf8 + +MYSQL_ROOT_PASSWORD=change-me-to-a-strong-root-password diff --git a/.gitignore b/.gitignore index e00e331..e56fac6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .settings .project .idea -.DS_Store \ No newline at end of file +.DS_Store +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5668259..3e07eeb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -86,7 +86,10 @@ RUN docker-php-ext-install sockets && docker-php-ext-enable sockets RUN mkdir -p /opt && chmod 777 /opt WORKDIR /opt -RUN git clone https://github.com/chillerlan/php-qrcode.git \ +# Vastgezet op 5.0.5 (laatste 5.x-release): vanaf 6.0.0 vereist de library PHP >= 8.4, +# terwijl deze image op PHP 8.3 draait. Een ongepinde clone van master is bovendien +# een reproduceerbaarheids-/supply-chain-risico (build kan zonder waarschuwing breken). +RUN git clone --branch 5.0.5 --depth 1 https://github.com/chillerlan/php-qrcode.git \ && chmod -R 777 ./php-qrcode RUN cp ./php-qrcode/composer.json /var/www/html/composer.json RUN mkdir -p /var/www/html/test && chmod 777 /var/www/html/test diff --git a/Dockerfile.fpm b/Dockerfile.fpm new file mode 100644 index 0000000..01c9edc --- /dev/null +++ b/Dockerfile.fpm @@ -0,0 +1,59 @@ +FROM php:8.3-fpm + +RUN if [ "$(grep '^VERSION_ID=' /etc/os-release | cut -d '=' -f 2 | tr -d '"')" -eq "9" ]; then \ + sed -i -e 's/deb.debian.org/archive.debian.org/g' \ + -e 's/security.debian.org/archive.debian.org/g' \ + -e '/stretch-updates/d' /etc/apt/sources.list; \ + fi + +ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ + +RUN chmod +x /usr/local/bin/install-php-extensions + +RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \ + && DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \ + curl \ + libzip-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + libfreetype6-dev \ + zip unzip \ + && install-php-extensions \ + gd \ + gettext \ + intl \ + mysqli \ + opcache \ + pdo_mysql \ + sockets \ + zip + +# Install Composer. +ENV PATH=$PATH:/root/composer/vendor/bin \ + COMPOSER_ALLOW_SUPERUSER=1 \ + COMPOSER_HOME=/root/composer +RUN cd /opt \ + && curl -sSL https://getcomposer.org/installer > composer-setup.php \ + && curl -sSL https://composer.github.io/installer.sha384sum > composer-setup.sha384sum \ + && sha384sum --check composer-setup.sha384sum \ + && php composer-setup.php --install-dir=/usr/local/bin --filename=composer --2 \ + && rm /opt/composer-setup.php /opt/composer-setup.sha384sum + +RUN mkdir -p /opt && chmod 777 /opt +WORKDIR /opt +# Zie Dockerfile: vastgezet op 5.0.5, want 6.0.0+ vereist PHP >= 8.4. +RUN git clone --branch 5.0.5 --depth 1 https://github.com/chillerlan/php-qrcode.git \ + && chmod -R 777 ./php-qrcode +RUN cp ./php-qrcode/composer.json /var/www/html/composer.json +RUN cp -R ./php-qrcode/src /var/www/html/ + +WORKDIR /var/www/html +RUN composer update +COPY ./src ./ +RUN chown -R www-data:www-data /var/www/html \ + && find /var/www/html -type f -exec chmod 644 {} \; \ + && find /var/www/html -type d -exec chmod 755 {} \; \ + && chmod -R 775 /var/www/html/saved_qrcode + +EXPOSE 9000 +CMD ["php-fpm"] diff --git a/db/init.sql b/db/init.sql new file mode 100644 index 0000000..4f5d28b --- /dev/null +++ b/db/init.sql @@ -0,0 +1,91 @@ +SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + +CREATE TABLE IF NOT EXISTS `users` ( + `id` int(25) NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL, + `password` varchar(255) NOT NULL, + `series_id` varchar(60) DEFAULT NULL, + `remember_token` varchar(255) DEFAULT NULL, + `expires` datetime DEFAULT NULL, + `type` varchar(10) NOT NULL, + `must_change_password` tinyint(1) NOT NULL DEFAULT 0, + `password_changed_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ; + +-- Default super admin account. Credentials: superadmin / superadmin +-- must_change_password=1 forces a password change on first login (see Fase 1 hardening). +INSERT INTO `users` (`id`, `username`, `password`, `series_id`, `remember_token`, `expires`, `type`, `must_change_password`, `password_changed_at`) VALUES +(1, 'superadmin', '$2y$10$xpZc5KC.aU2XHkcqhuZGFuAnqmtL4Unt8MysOyylceq.19XIyoZpG', NULL, NULL, NULL, 'super', 1, NULL); + +CREATE TABLE IF NOT EXISTS `dynamic_qrcodes` ( + `id` int(10) NOT NULL AUTO_INCREMENT, + `id_owner` int(25) NULL DEFAULT NULL, + `filename` varchar(45) NOT NULL, + `format` varchar(45) DEFAULT NULL, + `identifier` longtext, + `link` varchar(500) DEFAULT NULL, + `qrcode` varchar(60) DEFAULT NULL, + `scan` int(11) NOT NULL DEFAULT '0', + `state` varchar(20) NOT NULL DEFAULT 'enable', + `created_by` int(10) unsigned NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_by` int(10) unsigned NOT NULL DEFAULT '0', + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ; + +CREATE TABLE IF NOT EXISTS `static_qrcodes` ( + `id` int(10) NOT NULL AUTO_INCREMENT, + `id_owner` int(25) NULL DEFAULT NULL, + `filename` varchar(45) CHARACTER SET utf8 NOT NULL, + `format` varchar(45) DEFAULT NULL, + `type` varchar(45) CHARACTER SET utf8 DEFAULT NULL, + `content` mediumtext CHARACTER SET utf8, + `qrcode` varchar(60) CHARACTER SET utf8 DEFAULT NULL, + `state` varchar(20) CHARACTER SET utf8 NOT NULL DEFAULT 'enable', + `created_by` int(10) unsigned NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_by` int(10) unsigned NOT NULL DEFAULT '0', + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ; + +-- Security hardening (Fase 1): rate limiting op login pogingen +CREATE TABLE IF NOT EXISTS `login_attempts` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL, + `ip_address` varchar(45) NOT NULL, + `success` tinyint(1) NOT NULL DEFAULT 0, + `attempted_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `username_attempted_at` (`username`, `attempted_at`), + KEY `ip_attempted_at` (`ip_address`, `attempted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Security hardening (Fase 1): audit log van gevoelige acties +CREATE TABLE IF NOT EXISTS `audit_log` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(25) DEFAULT NULL, + `username` varchar(50) DEFAULT NULL, + `action` varchar(50) NOT NULL, + `target_type` varchar(30) DEFAULT NULL, + `target_id` varchar(50) DEFAULT NULL, + `ip_address` varchar(45) DEFAULT NULL, + `user_agent` varchar(255) DEFAULT NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `created_at` (`created_at`), + KEY `user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/db/migrations/002_security_hardening.sql b/db/migrations/002_security_hardening.sql new file mode 100644 index 0000000..edb5099 --- /dev/null +++ b/db/migrations/002_security_hardening.sql @@ -0,0 +1,57 @@ +-- Fase 1 security hardening migratie. +-- Voer uit tegen een bestaande database (gebruikt de originele +-- giandonatoinverso/php-dynamic-qr-code-db image of een oudere init.sql). +-- Kolommen/tabellen worden alleen toegevoegd als ze nog niet bestaan. + +SET @db := DATABASE(); + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'must_change_password' +); +SET @sql := IF(@col_exists = 0, + 'ALTER TABLE `users` ADD COLUMN `must_change_password` TINYINT(1) NOT NULL DEFAULT 0', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'password_changed_at' +); +SET @sql := IF(@col_exists = 0, + 'ALTER TABLE `users` ADD COLUMN `password_changed_at` DATETIME DEFAULT NULL', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- Bestaand superadmin account met het fabriekswachtwoord (superadmin/superadmin) +-- moet bij eerstvolgende login het wachtwoord wijzigen. +UPDATE `users` +SET `must_change_password` = 1 +WHERE `username` = 'superadmin' + AND `password` = '$2y$10$xpZc5KC.aU2XHkcqhuZGFuAnqmtL4Unt8MysOyylceq.19XIyoZpG'; + +CREATE TABLE IF NOT EXISTS `login_attempts` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL, + `ip_address` varchar(45) NOT NULL, + `success` tinyint(1) NOT NULL DEFAULT 0, + `attempted_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `username_attempted_at` (`username`, `attempted_at`), + KEY `ip_attempted_at` (`ip_address`, `attempted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `audit_log` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(25) DEFAULT NULL, + `username` varchar(50) DEFAULT NULL, + `action` varchar(50) NOT NULL, + `target_type` varchar(30) DEFAULT NULL, + `target_id` varchar(50) DEFAULT NULL, + `ip_address` varchar(45) DEFAULT NULL, + `user_agent` varchar(255) DEFAULT NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `created_at` (`created_at`), + KEY `user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..fec6531 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,66 @@ +services: + nginx: + image: "nginx:1.27-alpine" + restart: "unless-stopped" + ports: + - "80:80" + # 443 pas openzetten zodra SSL-certificaten zijn gemount (bv. via certbot-volume + # of een losse reverse proxy zoals Caddy/Traefik ervoor). Zie infra-fase van het plan. + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + - php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode:ro + depends_on: + - php-dynamic-qrcode + networks: + - php-dynamic-qrcode-network + + php-dynamic-qrcode: + build: + context: . + dockerfile: Dockerfile.fpm + restart: "unless-stopped" + environment: + TYPE: "docker" + QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}" + BASE_URL: "${BASE_URL:?zet BASE_URL in .env, bv. https://qr.ensembia.com}" + DATABASE_HOST: "php-dynamic-qrcode-db" + DATABASE_PORT: "3306" + DATABASE_NAME: "${DATABASE_NAME:-qrcode}" + DATABASE_USER: "${DATABASE_USER:-qrcode}" + DATABASE_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}" + DATABASE_PREFIX: "${DATABASE_PREFIX:-}" + DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}" + depends_on: + php-dynamic-qrcode-db: + condition: service_healthy + volumes: + - php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode + networks: + - php-dynamic-qrcode-network + + php-dynamic-qrcode-db: + image: "mysql:8.0" + restart: "unless-stopped" + volumes: + - php_dynamic_qrcode_db_data:/var/lib/mysql + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + environment: + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?zet MYSQL_ROOT_PASSWORD in .env}" + MYSQL_DATABASE: "${DATABASE_NAME:-qrcode}" + MYSQL_USER: "${DATABASE_USER:-qrcode}" + MYSQL_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] + interval: 5s + timeout: 5s + retries: 10 + networks: + - php-dynamic-qrcode-network + +volumes: + php_dynamic_qrcode_db_data: + php_dynamic_qrcode_saved_qrcode_data: + +networks: + php-dynamic-qrcode-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index ba133c5..fc1cdbd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,48 +1,53 @@ -version: "3.2" services: php-dynamic-qrcode: - image: "giandonatoinverso/php-dynamic-qr-code:latest" + build: + context: . + dockerfile: Dockerfile restart: "unless-stopped" environment: - TYPE: "docker" - QRCODE_GENERATOR: "internal-chillerlan.qrcode" - BASE_URL: "https://mydomain.com" + TYPE: "${TYPE:-docker}" + QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}" + BASE_URL: "${BASE_URL:-http://localhost}" DATABASE_HOST: "php-dynamic-qrcode-db" DATABASE_PORT: "3306" - DATABASE_NAME: "qrcode" - DATABASE_USER: "qrcode" - DATABASE_PASSWORD: "changeme" - DATABASE_PREFIX: "" - DATABASE_CHARSET: "utf8" + DATABASE_NAME: "${DATABASE_NAME:-qrcode}" + DATABASE_USER: "${DATABASE_USER:-qrcode}" + DATABASE_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}" + DATABASE_PREFIX: "${DATABASE_PREFIX:-}" + DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}" ports: - - 80:80 + - "80:80" depends_on: - - php-dynamic-qrcode-db + php-dynamic-qrcode-db: + condition: service_healthy volumes: - php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode networks: - php-dynamic-qrcode-network php-dynamic-qrcode-db: - image: "giandonatoinverso/php-dynamic-qr-code-db:latest" + image: "mysql:8.0" restart: "unless-stopped" volumes: - php_dynamic_qrcode_db_data:/var/lib/mysql - ports: - - '13306:3306' + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro environment: - MYSQL_ROOT_PASSWORD: "changeme" - MYSQL_DATABASE: "qrcode" - MYSQL_USER: "qrcode" - MYSQL_PASSWORD: "changeme" + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?zet MYSQL_ROOT_PASSWORD in .env}" + MYSQL_DATABASE: "${DATABASE_NAME:-qrcode}" + MYSQL_USER: "${DATABASE_USER:-qrcode}" + MYSQL_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] + interval: 5s + timeout: 5s + retries: 10 networks: - php-dynamic-qrcode-network volumes: php_dynamic_qrcode_db_data: - php_dynamic_qrcode_config_data: php_dynamic_qrcode_saved_qrcode_data: networks: php-dynamic-qrcode-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..8c33173 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + root /var/www/html; + index index.php; + + client_max_body_size 20m; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "same-origin" always; + + location / { + try_files $uri $uri/ /index.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_pass php-dynamic-qrcode:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + # Statisch gegenereerde qrcodes mogen gedownload worden, maar niet als PHP uitgevoerd. + location /saved_qrcode/ { + location ~ \.php$ { + deny all; + } + } + + location ~ /\. { + deny all; + } +} diff --git a/src/authenticate.php b/src/authenticate.php index 9a80d63..5e65f78 100644 --- a/src/authenticate.php +++ b/src/authenticate.php @@ -1,66 +1,89 @@ where('username', $username); $row = $db->getOne('users'); - if ($db->count >= 1) + if ($db->count >= 1 && password_verify($password, $row['password'])) { - $db_password = $row['password']; + qr_record_login_attempt($username, true); + + // Voorkom session fixation: nieuwe sessie-id na een geslaagde login. + session_regenerate_id(true); + + $_SESSION['user_logged_in'] = TRUE; + $_SESSION['type'] = $row['type']; + $_SESSION['user_id'] = $row['id']; + $_SESSION['username'] = $row['username']; + $_SESSION['must_change_password'] = !empty($row['must_change_password']); + $_SESSION['last_activity'] = time(); + + audit_log('login_success'); + $user_id = $row['id']; - if (password_verify($password, $db_password)) + if ($remember) { - $_SESSION['user_logged_in'] = TRUE; - $_SESSION['type'] = $row['type']; - $_SESSION['user_id'] = $row['id']; + $series_id = randomString(16); + $remember_token = getSecureRandomToken(20); + $encryted_remember_token = password_hash($remember_token,PASSWORD_DEFAULT); - if ($remember) - { - $series_id = randomString(16); - $remember_token = getSecureRandomToken(20); - $encryted_remember_token = password_hash($remember_token,PASSWORD_DEFAULT); + $expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days')); + $expires = strtotime($expiry_time); + $is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; - $expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days')); - $expires = strtotime($expiry_time); + $cookie_options = [ + 'expires' => $expires, + 'path' => '/', + 'secure' => $is_https, + 'httponly' => true, + 'samesite' => 'Lax', + ]; - setcookie('series_id', $series_id, $expires, '/'); - setcookie('remember_token', $remember_token, $expires, '/'); + setcookie('series_id', $series_id, $cookie_options); + setcookie('remember_token', $remember_token, $cookie_options); - $db = getDbInstance(); - $db->where ('id',$user_id); + $db = getDbInstance(); + $db->where ('id',$user_id); - $update_remember = array( - 'series_id'=> $series_id, - 'remember_token' => $encryted_remember_token, - 'expires' =>$expiry_time - ); - $db->update('users', $update_remember); - } - // Authentication successfull redirect user - header('Location: index.php'); - } - else - { - $_SESSION['login_failure'] = 'Invalid username or password'; - header('Location: login.php'); + $update_remember = array( + 'series_id'=> $series_id, + 'remember_token' => $encryted_remember_token, + 'expires' =>$expiry_time + ); + $db->update('users', $update_remember); } + // Authentication successfull redirect user + header('Location: index.php'); exit; } else { + qr_record_login_attempt($username, false); $_SESSION['login_failure'] = 'Invalid username or password'; header('Location: login.php'); exit; diff --git a/src/bulk_action.php b/src/bulk_action.php index 0f66775..7aa5e3e 100644 --- a/src/bulk_action.php +++ b/src/bulk_action.php @@ -1,9 +1,14 @@ 'Type action field in the request.', @@ -31,9 +36,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } foreach ($params as $param) { - $row = $db->where('id', $param); + $db->where('id', $param); + if ($_SESSION['type'] !== 'super') { + $db->where('id_owner', $_SESSION['user_id']); + $db->orWhere('id_owner', NULL, 'IS'); + } $row = $db->getOne("{$type}_qrcodes"); - @$files[] = SAVED_QRCODE_FOLDER . $row['qrcode']; + if ($row !== NULL) { + $files[] = SAVED_QRCODE_FOLDER . $row['qrcode']; + } } $zip = new ZipArchive(); @@ -50,6 +61,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $zip->close(); + audit_log('bulk_download', $type, implode(',', $params)); + echo json_encode([ 'data' => $url_path, 'status' => 200 @@ -57,10 +70,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit(); } else if($json["action"] == "delete") { $params = $json['params']; - $files = []; - if (isset($json['type'])) { - $type = filter_var($json['type'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); + if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) { + $type = $json['type']; } else { echo json_encode([ 'data' => 'Type action field in the request.', @@ -79,16 +91,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if($type == "dynamic") $instance = new DynamicQrcode(); - else if($type == "static") - $instance = new StaticQrcode(); else - die("Type not allowed"); + $instance = new StaticQrcode(); foreach ($params as $param) { - $a = 0; $instance->deleteQrcode($param, true); } + audit_log('bulk_delete', $type, implode(',', $params)); + echo json_encode([ 'action' => "delete", 'data' => "Qrcode deleted", @@ -96,9 +107,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ]); exit(); - } else - exit("Action not allowed"); + } else { + echo json_encode(['data' => 'Action not allowed', 'status' => 400]); + exit(); + } } else { - exit('Direct access to this script not allowed.'); + http_response_code(405); + echo json_encode(['data' => 'Direct access to this script not allowed.', 'status' => 405]); + exit(); } -?> \ No newline at end of file diff --git a/src/change_password.php b/src/change_password.php new file mode 100644 index 0000000..0117c2d --- /dev/null +++ b/src/change_password.php @@ -0,0 +1,99 @@ +where('id', $_SESSION['user_id']); + $user = $db->getOne('users'); + + if ($user === NULL || !password_verify($current_password, $user['password'])) { + $_SESSION['failure'] = 'Current password is incorrect.'; + } elseif (strlen($new_password) < 10) { + $_SESSION['failure'] = 'New password must be at least 10 characters long.'; + } elseif ($new_password !== $confirm_password) { + $_SESSION['failure'] = 'New password and confirmation do not match.'; + } elseif ($new_password === $current_password) { + $_SESSION['failure'] = 'New password must be different from the current password.'; + } else { + $db = getDbInstance(); + $db->where('id', $_SESSION['user_id']); + $db->update('users', [ + 'password' => password_hash($new_password, PASSWORD_DEFAULT), + 'must_change_password' => 0, + 'password_changed_at' => date('Y-m-d H:i:s'), + ]); + + $_SESSION['must_change_password'] = false; + audit_log('password_changed'); + + $_SESSION['success'] = 'Password updated successfully.'; + header('Location: index.php'); + exit; + } +} +?> + + +
+ + +
+ + + + + + + + +