From 360eb3744ad302f304d40d26cbe39fb583e4dff0 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Sat, 25 Apr 2026 12:48:24 +0200 Subject: [PATCH] implemented anonymous user authentification with browser identification number from cookies --- migrations/005_browser_id.sql | 2 +- public/api/contributions.php | 57 +++++++++++++++++++++++++---------- public/index.php | 17 ++++------- public/js/app.js | 43 +++++++++++++++++++++++--- 4 files changed, 86 insertions(+), 33 deletions(-) diff --git a/migrations/005_browser_id.sql b/migrations/005_browser_id.sql index 7338902..d4fdad1 100644 --- a/migrations/005_browser_id.sql +++ b/migrations/005_browser_id.sql @@ -19,7 +19,7 @@ CREATE INDEX idx_votes_browser ON votes(browser_id); -- Drops old Constraint voter_name based ALTER TABLE votes - DROP CONSTRAINT IF EXISTS votes_contribution_id_voter_name_key; + DROP CONSTRAINT IF EXISTS votes_unique_per_voter; -- Creates new Constraint browser_id based ALTER TABLE votes diff --git a/public/api/contributions.php b/public/api/contributions.php index 66bc512..80aadb3 100644 --- a/public/api/contributions.php +++ b/public/api/contributions.php @@ -126,6 +126,23 @@ function handle_read($input) { 'features' => $features ]; + // Includes User's Votes for persistent Vote Display + // Returns which Contributions the current Browser has voted on + $browser_id = $input['browser_id'] ?? ''; + if ($browser_id !== '') { + $stmt = $pdo->prepare(" + SELECT contribution_id, vote_type + FROM votes + WHERE browser_id = :bid + "); + $stmt->execute([':bid' => $browser_id]); + $user_votes = []; + foreach ($stmt->fetchAll() as $v) { + $user_votes[$v['contribution_id']] = $v['vote_type']; + } + $featureCollection['user_votes'] = $user_votes; + } + json_response($featureCollection); } @@ -162,10 +179,10 @@ function handle_create($input) { try { $stmt = $pdo->prepare(" INSERT INTO contributions - (municipality_id, geom, geom_type, category, title, description, author_name) + (municipality_id, geom, geom_type, category, title, description, author_name, browser_id) VALUES (:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type, - :category, :title, :description, :author_name) + :category, :title, :description, :author_name, :browser_id) "); $stmt->execute([ @@ -175,7 +192,8 @@ function handle_create($input) { ':category' => $input['category'], ':title' => $input['title'], ':description' => $input['description'] ?? '', - ':author_name' => $input['author_name'] + ':author_name' => $input['author_name'], + ':browser_id' => $input['browser_id'] ?? null ]); json_response([ @@ -320,11 +338,16 @@ function handle_vote($input) { // Prepared SQL Statement try { // Checks if Voter already voted on this Contribution + $browser_id = $input['browser_id'] ?? ''; + if (empty($browser_id)) { + error_response('Browser ID required for Voting.'); + } + $stmt = $pdo->prepare(" SELECT vote_id, vote_type FROM votes - WHERE contribution_id = :cid AND voter_name = :voter + WHERE contribution_id = :cid AND browser_id = :bid "); - $stmt->execute([':cid' => $input['contribution_id'], ':voter' => $input['voter_name']]); + $stmt->execute([':cid' => $input['contribution_id'], ':bid' => $browser_id]); $existing = $stmt->fetch(); if ($existing) { @@ -339,27 +362,29 @@ function handle_vote($input) { $stmt->execute([':vid' => $existing['vote_id']]); $stmt = $pdo->prepare(" - INSERT INTO votes (contribution_id, voter_name, vote_type) - VALUES (:cid, :voter, :vtype) + INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id) + VALUES (:cid, :voter, :vtype, :bid) "); $stmt->execute([ ':cid' => $input['contribution_id'], ':voter' => $input['voter_name'], - ':vtype' => $input['vote_type'] + ':vtype' => $input['vote_type'], + ':bid' => $browser_id ]); json_response(['message' => 'Vote changed.', 'action' => 'changed'], 200); } } else { // No existing Vote — Inserts Vote $stmt = $pdo->prepare(" - INSERT INTO votes (contribution_id, voter_name, vote_type) - VALUES (:cid, :voter, :vtype) - "); - $stmt->execute([ - ':cid' => $input['contribution_id'], - ':voter' => $input['voter_name'], - ':vtype' => $input['vote_type'] - ]); + INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id) + VALUES (:cid, :voter, :vtype, :bid) + "); + $stmt->execute([ + ':cid' => $input['contribution_id'], + ':voter' => $input['voter_name'], + ':vtype' => $input['vote_type'], + ':bid' => $browser_id + ]); json_response(['message' => 'Vote recorded.', 'action' => 'created'], 201); } diff --git a/public/index.php b/public/index.php index c95a371..a380de0 100644 --- a/public/index.php +++ b/public/index.php @@ -5,18 +5,8 @@ // Renders Leaflet Map Interface including Leaflet Plugins // ===================================================================== -// Reads Environment Configfile -$envFile = __DIR__ . '/../../.env'; -if (file_exists($envFile)) { - $lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - if (strpos(trim($line), '#') === 0) continue; - list($key, $value) = array_map('trim', explode('=', $line, 2)); - putenv("$key=$value"); - } -} - require_once __DIR__ . '/api/db.php'; +require_once __DIR__ . '/api/auth.php'; // ----------------------------------------------------------------- // Loads Municipality Configuration @@ -122,6 +112,8 @@ $news_items = $stmt->fetchAll(); @@ -370,6 +362,9 @@ $news_items = $stmt->fetchAll(); // Category Definitions from Database const CATEGORIES = ; + + // Admin Status from PHP Session + const IS_ADMIN = ; diff --git a/public/js/app.js b/public/js/app.js index eb2278d..be8a33e 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -16,9 +16,26 @@ // API Endpoint as relative Path const API_URL = 'api/contributions.php'; -// Current User Name, set via Login Modal, stored in sessionStorage +// Username set via Login Modal stored in sessionStorage let currentUser = sessionStorage.getItem('webgis_user') || ''; +// Browser Identification Number for anonymous User Identification stored as Cookie +let browserId = getBrowserId(); + +function getBrowserId() { + let id = document.cookie.replace(/(?:(?:^|.*;\s*)webgis_browser_id\s*=\s*([^;]*).*$)|^.*$/, '$1'); + if (!id) { + id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0; + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + // Cookie Expiration in one Year + document.cookie = 'webgis_browser_id=' + id + ';path=/;max-age=31536000;SameSite=Lax'; + } + return id; +} + + // Application State let map; // Leaflet Map Instance let sidebar; // Sidebar Instance @@ -290,9 +307,15 @@ function apiCall(data, callback) { }); } + // Loads all Contributions from API and displays Contributions on Map function loadContributions() { - apiCall({ action: 'read', municipality_id: MUNICIPALITY.id }, function (data) { + const readParams = { action: 'read', municipality_id: MUNICIPALITY.id }; + + // Sends Browser ID for persistent Vote Display + readParams.browser_id = browserId; + + apiCall(readParams, function (data) { if (data.error) { console.error('Load Error:', data.error); return; @@ -300,6 +323,14 @@ function loadContributions() { contributionsData = data.features || []; + // Restores Vote Highlights from API Response + if (data.user_votes) { + userVotes = {}; + for (const key in data.user_votes) { + userVotes[key] = data.user_votes[key]; + } + } + // Removes existing Layer if present if (contributionsLayer) { map.removeLayer(contributionsLayer); @@ -384,7 +415,7 @@ function buildPopupHtml(feature) { ' ' + props.dislikes_count + '' + '' + '' + - (currentUser === props.author_name ? + (props.browser_id === browserId || (typeof IS_ADMIN !== 'undefined' && IS_ADMIN) ? '