implemented anonymous user authentification with browser identification number from cookies

This commit is contained in:
2026-04-25 12:48:24 +02:00
parent 601c13012c
commit 360eb3744a
4 changed files with 86 additions and 33 deletions

View File

@@ -19,7 +19,7 @@ CREATE INDEX idx_votes_browser ON votes(browser_id);
-- Drops old Constraint voter_name based -- Drops old Constraint voter_name based
ALTER TABLE votes 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 -- Creates new Constraint browser_id based
ALTER TABLE votes ALTER TABLE votes

View File

@@ -126,6 +126,23 @@ function handle_read($input) {
'features' => $features '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); json_response($featureCollection);
} }
@@ -162,10 +179,10 @@ function handle_create($input) {
try { try {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO contributions 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 VALUES
(:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type, (:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type,
:category, :title, :description, :author_name) :category, :title, :description, :author_name, :browser_id)
"); ");
$stmt->execute([ $stmt->execute([
@@ -175,7 +192,8 @@ function handle_create($input) {
':category' => $input['category'], ':category' => $input['category'],
':title' => $input['title'], ':title' => $input['title'],
':description' => $input['description'] ?? '', ':description' => $input['description'] ?? '',
':author_name' => $input['author_name'] ':author_name' => $input['author_name'],
':browser_id' => $input['browser_id'] ?? null
]); ]);
json_response([ json_response([
@@ -320,11 +338,16 @@ function handle_vote($input) {
// Prepared SQL Statement // Prepared SQL Statement
try { try {
// Checks if Voter already voted on this Contribution // 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(" $stmt = $pdo->prepare("
SELECT vote_id, vote_type FROM votes 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(); $existing = $stmt->fetch();
if ($existing) { if ($existing) {
@@ -339,27 +362,29 @@ function handle_vote($input) {
$stmt->execute([':vid' => $existing['vote_id']]); $stmt->execute([':vid' => $existing['vote_id']]);
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO votes (contribution_id, voter_name, vote_type) INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id)
VALUES (:cid, :voter, :vtype) VALUES (:cid, :voter, :vtype, :bid)
"); ");
$stmt->execute([ $stmt->execute([
':cid' => $input['contribution_id'], ':cid' => $input['contribution_id'],
':voter' => $input['voter_name'], ':voter' => $input['voter_name'],
':vtype' => $input['vote_type'] ':vtype' => $input['vote_type'],
':bid' => $browser_id
]); ]);
json_response(['message' => 'Vote changed.', 'action' => 'changed'], 200); json_response(['message' => 'Vote changed.', 'action' => 'changed'], 200);
} }
} else { } else {
// No existing Vote — Inserts Vote // No existing Vote — Inserts Vote
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO votes (contribution_id, voter_name, vote_type) INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id)
VALUES (:cid, :voter, :vtype) VALUES (:cid, :voter, :vtype, :bid)
"); ");
$stmt->execute([ $stmt->execute([
':cid' => $input['contribution_id'], ':cid' => $input['contribution_id'],
':voter' => $input['voter_name'], ':voter' => $input['voter_name'],
':vtype' => $input['vote_type'] ':vtype' => $input['vote_type'],
]); ':bid' => $browser_id
]);
json_response(['message' => 'Vote recorded.', 'action' => 'created'], 201); json_response(['message' => 'Vote recorded.', 'action' => 'created'], 201);
} }

View File

@@ -5,18 +5,8 @@
// Renders Leaflet Map Interface including Leaflet Plugins // 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/db.php';
require_once __DIR__ . '/api/auth.php';
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// Loads Municipality Configuration // Loads Municipality Configuration
@@ -122,6 +112,8 @@ $news_items = $stmt->fetchAll();
<!-- Mobile Hamburger Menu --> <!-- Mobile Hamburger Menu -->
<button class="header-menu-toggle" onclick="toggleMobileNav()"> <button class="header-menu-toggle" onclick="toggleMobileNav()">
<i class="fa-solid fa-bars"></i>
</button>
</header> </header>
@@ -370,6 +362,9 @@ $news_items = $stmt->fetchAll();
// Category Definitions from Database // Category Definitions from Database
const CATEGORIES = <?= json_encode(get_categories(), JSON_UNESCAPED_UNICODE) ?>; const CATEGORIES = <?= json_encode(get_categories(), JSON_UNESCAPED_UNICODE) ?>;
// Admin Status from PHP Session
const IS_ADMIN = <?= (function_exists('is_admin') && is_admin()) ? 'true' : 'false' ?>;
</script> </script>
<!-- Application Logic --> <!-- Application Logic -->

View File

@@ -16,9 +16,26 @@
// API Endpoint as relative Path // API Endpoint as relative Path
const API_URL = 'api/contributions.php'; 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') || ''; 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 // Application State
let map; // Leaflet Map Instance let map; // Leaflet Map Instance
let sidebar; // Sidebar Instance let sidebar; // Sidebar Instance
@@ -290,9 +307,15 @@ function apiCall(data, callback) {
}); });
} }
// Loads all Contributions from API and displays Contributions on Map // Loads all Contributions from API and displays Contributions on Map
function loadContributions() { 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) { if (data.error) {
console.error('Load Error:', data.error); console.error('Load Error:', data.error);
return; return;
@@ -300,6 +323,14 @@ function loadContributions() {
contributionsData = data.features || []; 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 // Removes existing Layer if present
if (contributionsLayer) { if (contributionsLayer) {
map.removeLayer(contributionsLayer); map.removeLayer(contributionsLayer);
@@ -384,7 +415,7 @@ function buildPopupHtml(feature) {
'<i class="fa-solid fa-thumbs-down"></i> <span id="dislikes-' + props.contribution_id + '">' + props.dislikes_count + '</span>' + '<i class="fa-solid fa-thumbs-down"></i> <span id="dislikes-' + props.contribution_id + '">' + props.dislikes_count + '</span>' +
'</button>' + '</button>' +
'</div>' + '</div>' +
(currentUser === props.author_name ? (props.browser_id === browserId || (typeof IS_ADMIN !== 'undefined' && IS_ADMIN) ?
'<div class="popup-detail-actions">' + '<div class="popup-detail-actions">' +
'<button class="btn btn-primary" onclick="editContribution(' + props.contribution_id + ')"><i class="fa-solid fa-pen"></i> Bearbeiten</button>' + '<button class="btn btn-primary" onclick="editContribution(' + props.contribution_id + ')"><i class="fa-solid fa-pen"></i> Bearbeiten</button>' +
'<button class="btn btn-danger" onclick="deleteContribution(' + props.contribution_id + ')"><i class="fa-solid fa-trash"></i> Löschen</button>' + '<button class="btn btn-danger" onclick="deleteContribution(' + props.contribution_id + ')"><i class="fa-solid fa-trash"></i> Löschen</button>' +
@@ -441,7 +472,8 @@ function submitCreate() {
description: description, description: description,
geom: geom, geom: geom,
geom_type: geomType, geom_type: geomType,
author_name: currentUser author_name: currentUser,
browser_id: browserId
}, function (response) { }, function (response) {
if (response.error) { if (response.error) {
Swal.fire('Fehler', response.error, 'error'); Swal.fire('Fehler', response.error, 'error');
@@ -570,7 +602,8 @@ function voteContribution(contributionId, voteType) {
action: 'vote', action: 'vote',
contribution_id: contributionId, contribution_id: contributionId,
voter_name: currentUser, voter_name: currentUser,
vote_type: voteType vote_type: voteType,
browser_id: browserId
}, function (response) { }, function (response) {
if (response.error) { if (response.error) {
return; return;