From: Slightly Cyberpunk Date: Thu, 13 Feb 2025 03:29:40 +0000 (-0500) Subject: User Management X-Git-Url: http://git.slightlycyberpunk.com%2C%20git.slightlycyberpunk.com/git/?a=commitdiff_plain;h=2685fa3f356b53e43ea6449cbeb838985ed02746;p=CCCP.git User Management --- diff --git a/api.php b/api.php index 056f8be..4ed7fd7 100644 --- a/api.php +++ b/api.php @@ -592,6 +592,155 @@ if( isset($_GET['get']) && echo '{"status": "OK", "id": '.$id.'}'; +// URL: ?set=saveUser +// update user details +// GET: uid (required), name (optional), username (optional), password (optional), lead (optional), access (optional) +} else if( isset($_GET['set']) && $_GET['set'] == "saveUser") { + // Validate permissions + if( $_SESSION['permissions'] > CCCP_PERM_LEAD ) { + return; + } + + // TODO: If a lead, confirm we are this user's lead + // Better error handling...maybe check lastInsertId is valid on the insert ones + // UPDATE rather than just delete/insert on lead/canvass so it doesn't lose one updating the other + $status = 1; + if( isset($_GET['uid']) ) { + $status = 0; + // Update a name + if( isset($_GET['name']) ) { + $query = "UPDATE users SET realname = ? WHERE id = ?"; + $params= [$_GET['name'], $_GET['uid'] ]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + if( $dbh->errorInfo()[1] != 0 ) { + $status++; + } + } + + // Update a username + if( isset($_GET['username']) ) { + $query = "SELECT * FROM users WHERE username = ? AND id != ?"; + $stmt = $dbh->prepare($query); + $params= [$_GET['username'], $_GET['uid'] ]; + $stmt->execute($params); + $rows = $stmt->fetchAll(); + if(sizeof($rows) > 0) { + $status++; + } + + $query = "UPDATE users SET username = ? WHERE id = ?"; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + if( $dbh->errorInfo()[1] != 0 ) { + $status++; + } + } + + // Update a password + if( isset($_GET['password']) ) { + $query = "UPDATE users SET passhash = ? WHERE id = ?"; + $params= [password_hash($_GET['password'], PASSWORD_DEFAULT), $_GET['uid'] ]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + if( $dbh->errorInfo()[1] != 0 ) { + $status++; + } + } + + // Update a lead + if( isset($_GET['lead']) ) { + $query = "UPDATE canvassGroups SET leadId=? WHERE userId=?;"; + $params= [$_GET['lead'], $_GET['uid'] ]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + + if( $stmt->rowCount() == 0 ) { + $query = "INSERT INTO canvassGroups(canvassId, userId, leadId) VALUES(null,CAST(? AS INTEGER),CAST(? AS INTEGER));"; + $params= [ $_GET['uid'], $_GET['lead'] ]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + } + if( $dbh->errorInfo()[1] != 0 ) { + $status++; + } + } + + // Update access + if( isset($_GET['permissions']) ) { + $query = "UPDATE users SET permissions=? WHERE id=?;"; + $params= [ $_GET['permissions'], $_GET['uid'] ]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + + if( $dbh->errorInfo()[1] != 0 ) { + $status++; + } + } + + // Update a canvass + if( isset($_GET['canvass']) ) { + $query = "INSERT INTO canvassGroups(userId, canvassId, leadId) VALUES(CAST(? AS INTEGER), CAST(? AS INTEGER), null);"; + $params= [ $_GET['uid'], $_GET['canvass'] ]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + + if( $dbh->errorInfo()[1] != 0 ) { + $status++; + } + } + + if( isset($_GET['uncanvass']) ) { + $query = "DELETE FROM canvassGroups WHERE userId = ? AND canvassId = ?;"; + $params= [ $_GET['uid'], $_GET['uncanvass'] ]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + + if( $dbh->errorInfo()[1] != 0 ) { + $status++; + } + } + + + + } + + // TODO: Return success or error + if($status == 0) { + echo "{}"; + } + + +// URL: ?get=canvassFeed +// get the current status of a canvass +// POST: id (required), duration (required), userId (optional) +} else if( isset($_GET['get']) && $_GET['get'] == "canvassFeed") { + if( $_SESSION['permissions'] > CCCP_PERM_LEAD ) { + return; + } + + // Fetch the canvass results from the given duration + $query = "SELECT u.username, v.firstName, v.lastName, cr.timestamp, cr.notes, cr.json, cr.estSupportPct ". + "FROM canvassResults cr, users u, voters v ". + "WHERE u.id = cr.userId AND cr.voterId = v.id ". + "AND canvassId = CAST(? AS INTEGER) ". + "AND timestamp > FROM_UNIXTIME(UNIX_TIMESTAMP()-CAST(? AS INTEGER)) ". + (isset($_GET['userId']) ? "AND cr.userId = ? " : ""). + "ORDER BY timestamp ASC LIMIT 10;"; + if( isset($_GET['id']) && isset($_GET['duration']) ) { + $params = [$_GET['id'], $_GET['duration']]; + } + if( isset($_GET['userId']) ) { + array_push($params, $_GET['userId']); + } + + $stmt = $dbh->prepare($query); + $stmt->execute($params); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode($rows); + + // URL: ?get=canvassMonitor // get the current status of a canvass // POST: id (required) @@ -605,20 +754,9 @@ if( isset($_GET['get']) && "sum((SELECT count(distinct voterId) FROM canvassResults r WHERE r.canvassId = c.id)) madeContacts, ". "(SELECT count(distinct userId) FROM canvassResults r WHERE r.canvassId = c.id) volunteers ". "FROM canvasses c WHERE turfId = CAST(? AS INTEGER)"; -/* $query = "SELECT c.totalContacts total, ". - "count(distinct cr.voterId) madeContacts, ". - "count(distinct u.id) activeVolunteers ". - "FROM canvasses c, users u, canvassResults cr ". - "WHERE c.turfId = CAST(? AS INTEGER) ". - "AND u.id = cr.userId ". - "AND cr.canvassId = c.id ". - "AND cr.timestamp > TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 day));";*/ - -// "GROUP BY u.id"; + if( isset($_GET['id']) ) { $params = [$_GET['id']]; - } else { - $params = ["10"]; } $stmt = $dbh->prepare($query); diff --git a/common.php b/common.php index 38609d0..f5e368e 100644 --- a/common.php +++ b/common.php @@ -1,9 +1,13 @@ Canvassing Phonebank + User Management + + Settings diff --git a/invite.php b/invite.php new file mode 100644 index 0000000..cc5604e --- /dev/null +++ b/invite.php @@ -0,0 +1,161 @@ + + + + + +$2y$10$GBn6IBJDct2zRTsQwnGehumQWZo9MLM8//8SiKw9HLZtk6a9k5t7u + +*/ + +// Connect to MariaDB +$options = [ + PDO::ATTR_EMULATE_PREPARES => false, // Disable emulation mode for "real" prepared statements + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // Make the default fetch be an associative array +]; +$dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options); + + +// If an admin, display selected invite token link and QR -- token should be unique to a lead and/or turf? +if( $_SESSION['permissions'] <= CCCP_PERM_LEAD ) { + $query = "DELETE FROM invites WHERE expiry < CURRENT_TIMESTAMP()"; + $stmt = $dbh->prepare($query); + $stmt->execute(); + + if( isset($_GET['canvassId']) ) { + $canvassId = $_GET['canvassId']; + $leadId = $SESSION['userId']; + $query = "SELECT token FROM invites WHERE canvassId = ? AND userId = ? AND expiry < TIMESTAMPADD(HOUR,1,CURRENT_TIMESTAMP)"; + $params= Array($canvassId,$leadId); + $stmt = $dbh->prepare($query); + $stmt->execute($params); + $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if($res[0]['token']) { + $token = $res[0]['token']; + } else { + $token = base64_encode(random_bytes(64)); + $query = "INSERT INTO invites(token, canvassId, userId) VALUES(?,?,?)"; + $params= Array($token,$canvassId,$leadId); + $stmt = $dbh->prepare($query); + $stmt->execute($params); + $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + $url = CCCP_BASEURL."invite.php?token=".$token; + $qrl = "https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=".urlencode($url); + echo "".$url."
"; + echo ""; + die(); + } +} + +// Register with a token +if( isset($_GET['token']) && isset($_SESSION['userId']) ) { + // TODO: Add the user to the canvass + +} else if( isset($_GET['token']) ) { + $token = $_GET['token']; + echo < + + + + + + + +
+
+ + + + +
+ + + + + + + + + + +
+
+ + +EOF + die(); +} + +// Register from a request +if( isset($_POST['token']) ) { + // Validate the token and get the canvass details + $query = "SELECT * FROM invites WHERE expiry < CURRENT_TIMESTAMP AND token = ?;"; + $params= [$_POST['token']]; + $stmt = $dbh->prepare($query); + $stmt->execute($params); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if(sizeof($rows) == 0) { + echo "Error: Invalid token."; + die(); + } + + $username = "NONE"; + if( isset($_POST['username']) ) { + $username = $_POST['username']; + } + $realname = "NONE"; + if( isset($_POST['realname']) ) { + $realname = $_POST['realname']; + } + $email = "NONE"; + if( isset($_POST['email']) ) { + $email = $_POST['email']; + } + $phone = "NONE"; + if( isset($_POST['phone']) ) { + $phone = $_POST['phone']; + } + $passhash = "NONE"; + if( isset($_POST['password']) ) { + $passhash = password_hash($_POST['password'], PASSWORD_DEFAULT); + } + + + $lead = $rows['userId']; + $canvass = $rows['canvassId']; + + // Add the user with INVITE permissions + $query = "INSERT INTO users(username, realName, passhash, email, phone, permissions) VALUES(?, ?, ?, ?, ?, CCCP_PERM_INVITE);"; + $params= [$username, $realname, $passhash, $email, $phone]; + $stmt = $dbh->prepare($query) + $stmt->execute($params); + $user = $dbh->lastInsertId(); + + $query = "INSERT INTO canvassGroups(canvassId, userId, leadId) VALUES(?,?,?);"; + $params= [$canvass, $user, $lead]; +} diff --git a/js/canvassing.js b/js/canvassing.js index 45c1642..ae74f4c 100644 --- a/js/canvassing.js +++ b/js/canvassing.js @@ -500,7 +500,7 @@ function updateLocation(position) { currentMarker = null; } var markerIcon = L.icon({ - iconUrl: "images/marker-icon-misc-yellow.png", + iconUrl: "images/marker-icon-red-star.png", iconSize: [24, 36], iconAnchor: [12,36] }); @@ -990,97 +990,251 @@ function reduceCacheBug() { function monitorCanvass(canvass, turf) { console.log(canvass); -// fetch("api.php?get=canvassMonitor&id="+canvass).then(data => data.json()) -// .then(output => { - - // similar to startCanvass - // open the map, give buttons to return to menu and to view stats list - // Show pins for last contact from each team - // Dim contacts if no updates in...10m? - // Pop-up on updates - debug("monitorCanvass(..., ...)"); - debug(canvass); - debug(turf); - setLoading(1); - map.on("moveend", loadCanvassers.bind(null, false, canvass)); -// loadTurfs(); - var json = turf.json; - var turfPoints = JSON.parse(json).geometry.coordinates[0]; - var latTot = 0; - var lonTot = 0; - var i = 0; - for(i = 0; i < turfPoints.length; i++) { - latTot += turfPoints[i][1]; - lonTot += turfPoints[i][0]; + // Similar to startCanvass + // open the map, give buttons to return to menu and to view stats list + // Show pins for last contact from each team + // Dim contacts if no updates in...10m? + // Pop-up on updates + debug("monitorCanvass(..., ...)"); + debug(canvass); + debug(turf); + setLoading(1); + map.on("moveend", loadCanvassers.bind(null, false, canvass)); + + var json = turf.json; + var turfPoints = JSON.parse(json).geometry.coordinates[0]; + var latTot = 0; + var lonTot = 0; + var i = 0; + for(i = 0; i < turfPoints.length; i++) { + latTot += turfPoints[i][1]; + lonTot += turfPoints[i][0]; + } + + map.setZoom(16); + map.setView([latTot/i, lonTot/i]); + enableCanvassersControls(canvass); + currentCanvass = canvass; + turfLayer = turf; + setTimeout(toggleTurf.bind(null, turf, true), 1000); + + loadCanvassers(false, canvass); + + toggleControls(); + setLoading(-1); + debug("End monitorCanvass"); +} + +var clogMode = 0; +function enableCanvassersControls(canvass) { + debug("enableCanvassersControls()"); + if( document.getElementById("canvassToolbar") != null) { + debug("End enableCanvassControls (Early)"); + return; + } + + var mapElem = document.getElementById("map"); + mapElem.setAttribute("class", "full"); + + document.getElementById("controls").style.display = "none"; + + var coder = L.Control.geocoder({defaultMarkGeocode: false}) + .on('markgeocode', function(e) { + var position = {}; + position.coords = {}; + position.coords.latitude = e.geocode.center.lat; + position.coords.longitude = e.geocode.center.lng; + updateLocation(position); + + if(this.manual) { + clearTimeout(addrErr); + document.getElementById("canvassToolbar").classList.remove("addrInput"); + } + }) + .addTo(map); + var geoctl = document.getElementsByClassName("leaflet-control-geocoder")[0]; + geoctl.parentElement.removeChild(geoctl); + + var toolbar = document.createElement("div"); + toolbar.setAttribute("id", "canvassToolbar"); + + var gpsBtn = document.createElement("button"); + gpsBtn.onclick = function() { + if(geoWatcher == null) { + geoWatcher = navigator.geolocation.watchPosition(updateLocation, locationFailure, {maximumAge: 30000}); + this.classList.add("watching"); + } else { + navigator.geolocation.clearWatch(geoWatcher); + this.classList.remove("watching"); + geoWatcher = null; + if(currentMarker != null) { + map.removeLayer(currentMarker); + currentMarker = null; + } } + } + var gpsImg = document.createElement("img"); + gpsImg.src = "images/GPSLocate.png"; + gpsBtn.appendChild(gpsImg); + gpsBtn.setAttribute("id", "geocoderGPSBtn"); + toolbar.appendChild(gpsBtn); - map.setZoom(16); - map.setView([latTot/i, lonTot/i]); - enableCanvassControls(); - currentCanvass = canvass; - turfLayer = turf; - setTimeout(toggleTurf.bind(null, turf, true), 1000); -// viewCanvass(canvass); + var refreshBtn = document.createElement("button"); + refreshBtn.onclick = function() { + loadCanvassers(false, canvass); + } + var refreshImg = document.createElement("img"); + refreshImg.src = "images/RefreshCanvassers.png"; + refreshBtn.appendChild(refreshImg); + refreshBtn.setAttribute("id", "geocoderRefreshBtn"); + toolbar.appendChild(refreshBtn); -/* var options = { - method: "POST", - headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}, - body: "id="+canvass.id; + var clogBtn = document.createElement("button"); + clogBtn.onclick = function() { + if( clogMode == 0 ) { + document.getElementById("clogImg").src = "images/CanvassLog-Full.png"; + clogMode = 1; + canvassFeed(true, canvass.id); + } else if( clogMode == 1) { + document.getElementById("clogImg").src = "images/CanvassLog-Off.png"; + clogMode = 2; + canvassFeed(true, canvass.id); + } else { + clearTimeout(clogTimeout); + document.getElementById("clogImg").src = "images/CanvassLog-Sm.png"; + clogMode = 0; + canvassFeed(true); + } } - fetch("api.php?get=canvassMonitor", options).then(data => data.json()) - .then(resp => { - loadCanvassers(false); - debug("End Async monitorCanvass"); - });*/ - loadCanvassers(false, canvass); + var clogImg = document.createElement("img"); + clogImg.src = "images/CanvassLog-Sm.png"; + clogImg.setAttribute("id", "clogImg"); + clogBtn.appendChild(clogImg); + clogBtn.setAttribute("id", "clogBtn"); + toolbar.appendChild(clogBtn); - toggleControls(); - setLoading(-1); - debug("End Sync monitorCanvass"); -// }); + var menuBtn = document.createElement("button"); + menuBtn.onclick = function() { + loadCanvassers(false, canvass); + document.getElementById("controls").style.display = "block"; + document.getElementById("controls").classList.remove("min"); + var mapElem = document.getElementById("map").classList.remove("full"); + map.invalidateSize(); + var toolbar = document.getElementById("canvassToolbar"); + toolbar.parentElement.removeChild(toolbar); + } + menuBtn.setAttribute("id", "geocoderMenuBtn"); + var menuImg = document.createElement("img"); + menuImg.src = "images/Home.png"; + menuBtn.appendChild(menuImg); + toolbar.appendChild(menuBtn); + + map.invalidateSize(); + document.getElementById("map").appendChild(toolbar); + debug("End enableCanvassersControls"); } +var canvassersTimeout = null; function loadCanvassers(force = false, canvass) { debug("loadCanvassers"); - var options = { - method: "POST", - headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}, - body: "id="+canvass.id - }; - fetch("api.php?get=canvassMonitor", options).then(data => data.json()) + fetch("api.php?get=canvassMonitor&id="+canvass.id).then(data => data.json()) .then(resp => { - console.log(resp); + debug(resp); for(var i = 0; i < resp.length; i++) { - iconImg = "marker-icon-"+iconColors[i]+".png"; - - var markerIcon = L.icon({ - iconUrl: "images/"+iconImg, - iconSize: [48, 72], - iconAnchor: [24,72], - className: 'leaflet-marker' - }); - var markerTitle = "UserID: "+resp[i].userId; - var newMarker = new L.marker([resp[i].latitude, resp[i].longitude], - {icon: markerIcon, title: markerTitle}).addTo(map); -// if( locList[i].count > 1) { - newMarker.bindTooltip(resp[i].contacts+"", - { - permanent: true, - direction: 'right', - className: 'leaflet-voterCount' - } - ); -// } - /*var markerText = new L.Marker([locList[i].latitude, locList[i].longitude], { - icon: new L.DivIcon({ - className: 'my-div-icon', - html: '---'+locList[i].count+'-------' - }) -});*/ - newMarker.on('click', voterList.bind(null, resp[i].latitude, resp[i].longitude)); - + iconImg = "marker-icon-"+iconColors[i]+".png"; + + var markerIcon = L.icon({ + iconUrl: "images/"+iconImg, + iconSize: [48, 72], + iconAnchor: [24,72], + className: 'leaflet-marker' + }); + var markerTitle = "UserID: "+resp[i].userId; + var newMarker = new L.marker([resp[i].latitude, resp[i].longitude], + {icon: markerIcon, title: markerTitle}).addTo(map); + newMarker.bindTooltip(resp[i].contacts+"", { + permanent: true, + direction: 'right', + className: 'leaflet-voterCount' + }); + clogMode = 1; + newMarker.on('click', canvassFeed.bind(null, true, canvass.id, resp[i].userId)); } + clearTimeout(canvassersTimeout); + canvassersTimeout = setTimeout(10000, loadCanvassers.bind(null, false, canvass)); debug("End loadCanvassers"); }); } + +var clogTimeout; +function canvassFeed(start = false, canvassId, userId = -1) { + debug("canvassFeed("+start+","+canvassId+")"); + + // Clear any existing log messages if changing modes + if(start && document.getElementById("toastContainer") != null) { + clearTimeout(clogTimeout); + var toasts = document.getElementById("toastContainer").children; + for(; toasts.length > 0;) { + document.getElementById("toastContainer").removeChild(toasts[0]); + } + } + + // Change modes + if(clogMode == 1 && start) { + toastMessage("Canvass Log Enabled"); + document.getElementById("toastContainer").classList.add("canvassLog"); + } else if(clogMode == 2 && start) { + toastMessage("Canvass Log Expanded"); + document.getElementById("toastContainer").classList.add("canvassLog"); + } else if(start) { + clearTimeout(clogTimeout); + toastMessage("Canvass Log Disabled"); + document.getElementById("toastContainer").classList.remove("canvassLog"); + return; + } + + // Configure the message duration + var duration = 10; + if(clogMode == 1) { + var toastDur = 1.5 * 1000; + } else { + var toastDur = 120 * 1000; + } + // Initial load of full should go back a couple hours + if(start && clogMode == 2) { + var duration = 9999999; + } + + var userQ = ""; + if(userId > 0) { + userQ="&userId="+userId; + } + fetch("api.php?get=canvassFeed&id="+canvassId+"&duration="+duration+userQ).then(data => data.json()) + .then(resp => { + console.log(resp); + + var message = ""; + for(var i = 0; i < resp.length; i++) { + var outcome = "Neutral"; + var outcomeClass = ""; + if(resp[i].estSupportPct > 0) { + outcome = "Positive!"; + outcomeClass = "positive"; + } + message = ""; + message += "
"; + message += "

"+resp[i].username+"

"; + message += ""+resp[i].timestamp+""; + message += ""+resp[i].firstName+" "+resp[i].lastName+"
"; + message += ""+outcome+""; + message += ""+resp[i].notes+""; + message += "
"; + toastMessage(message, toastDur+(150*i)); + } + clogTimeout = setTimeout(canvassFeed.bind(null, false, canvassId, userId), 10000); + debug("End Async canvassFeed"); + }); + + debug("End Sync canvassFeed"); +} diff --git a/js/common.js b/js/common.js index 3027fce..67863f5 100644 --- a/js/common.js +++ b/js/common.js @@ -108,7 +108,7 @@ var displayAreas = []; var colors = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF", "#FF00FF", "#880000", "#008800", "#000088", "#888800", "#008888", "#880088"] -var iconColors = ["red", "blue", "purple", "rose"] +var iconColors = ["red", "blue", "purple", "green", "orange", "yellow"] // INITIALIZATION /////////////////////////////////////////////////////////// @@ -124,7 +124,7 @@ function init() { // Initialize the map -- LeafletJS boilerplate map = L.map('map').setView([41.5, -71.5 ], 10); - L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png' , { maxZoom: 25}).addTo(map); + L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png' , {maxZoom: 25, useCache: true, crossOrigin: true}).addTo(map); if( typeof loadDistrictTypes == "function") { loadDistrictTypes(); @@ -235,20 +235,32 @@ function geoJsonBuilder(jsonStr) { } // Show a "toast" style notification -function toastMessage(message) { +function toastMessage(message, duration = -1) { debug("toastMessage(...)"); debug(message); + if( duration == -1) { + duration = message.length * 60; + } + var toastContainer = document.getElementById("toastContainer"); + if( toastContainer == null ) { + toastContainer = document.createElement("div"); + toastContainer.setAttribute("id", "toastContainer"); + document.body.appendChild(toastContainer); + } + + var tid = Date.now()+""+Math.floor(Math.random()*100); var toastDiv = document.createElement("div"); toastDiv.innerHTML = message; - toastDiv.setAttribute("id", "toastDiv"); - document.body.appendChild(toastDiv); + toastDiv.setAttribute("class", "toastDiv"); + toastDiv.setAttribute("id", "toast-"+tid); + toastContainer.insertBefore(toastDiv, toastContainer.firstChild); setTimeout(function() { - document.getElementById("toastDiv").style.opacity = "0"; + document.getElementById("toast-"+tid).style.opacity = "0"; setTimeout(function() { - document.body.removeChild(document.getElementById("toastDiv")); + document.getElementById("toastContainer").removeChild(document.getElementById("toast-"+tid)); }, 1500); - }, message.length * 60); + }, duration); } // Toggle map display diff --git a/js/users.js b/js/users.js new file mode 100644 index 0000000..207fa03 --- /dev/null +++ b/js/users.js @@ -0,0 +1,91 @@ +function editName(e) { + var btnElem = e.target; + var nameElem = e.target.parentElement.getElementsByClassName('realname')[0]; + nameElem.setAttribute("contenteditable", true); + + btnElem.value = "Save Name"; + btnElem.onclick = saveName; +} + +function saveName(e) { + var btnElem = e.target; + var nameElem = e.target.parentElement.getElementsByClassName('realname')[0]; + var uid = e.target.parentElement.getAttribute("id").split("-")[1]; + + fetch("api.php?set=saveUser&uid="+uid+"&name="+encodeURIComponent(nameElem.innerText)).then(data => data.json()) + .then(resp => { + debug(resp); + + nameElem.removeAttribute("contenteditable"); + btnElem.value = "Edit Name"; + btnElem.onclick = editName; + }); +} + +function editUsername(e) { + var btnElem = e.target; + var nameElem = e.target.parentElement.getElementsByClassName('username')[0]; + nameElem.removeAttribute("disabled"); + + btnElem.value = "Save"; + btnElem.onclick = saveUsername; +} + +function saveUsername(e) { + var btnElem = e.target; + var nameElem = e.target.parentElement.getElementsByClassName('username')[0]; + var uid = e.target.parentElement.getAttribute("id").split("-")[1]; + + fetch("api.php?set=saveUser&uid="+uid+"&username="+encodeURIComponent(nameElem.value)).then(data => data.json()) + .then(resp => { + debug(resp); + + nameElem.setAttribute("disabled", true); + btnElem.value = "Edit"; + btnElem.onclick = editUsername; + }); +} + +function assignLead(e) { + var btnElem = e.target; + var selectElem = e.target.parentElement.getElementsByClassName('leadSelect')[0]; + var uid = e.target.parentElement.parentElement.getAttribute("id").split("-")[1]; + + fetch("api.php?set=saveUser&uid="+uid+"&lead="+encodeURIComponent(selectElem.value)).then(data => data.json()) + .then(resp => { + debug(resp); + }); +} + +function assignAccess(e) { + var btnElem = e.target; + var selectElem = e.target.parentElement.getElementsByClassName('accessSelect')[0]; + var uid = e.target.parentElement.parentElement.getAttribute("id").split("-")[1]; + + fetch("api.php?set=saveUser&uid="+uid+"&permissions="+encodeURIComponent(selectElem.value)).then(data => data.json()) + .then(resp => { + debug(resp); + }); +} + +function assignCanvass(e) { + var btnElem = e.target; + var selectElem = e.target.parentElement.getElementsByClassName('canvassSelect')[0]; + var uid = e.target.parentElement.parentElement.getAttribute("id").split("-")[1]; + var cid = selectElem.value; + + if(cid == "") { + cid = btnElem.parentElement.parentElement.parentElement.parentElement.getAttribute("id").split("-")[1]; + fetch("api.php?set=saveUser&uid="+uid+"&uncanvass="+encodeURIComponent(cid)).then(data => data.json()) + .then(resp => { + debug(resp); + window.location.reload(); + }); + } else { + fetch("api.php?set=saveUser&uid="+uid+"&canvass="+encodeURIComponent(cid)).then(data => data.json()) + .then(resp => { + debug(resp); + window.location.reload(); + }); + } +} diff --git a/login.php b/login.php index f0e54fa..3aafd1c 100644 --- a/login.php +++ b/login.php @@ -36,20 +36,28 @@ $options = [ $dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options); // Check if password is valid -if( isset($_GET['username']) && isset($_GET['password']) ) { +if( isset($_GET['username']) ) { $_POST['username'] = $_GET['username']; +} +if( isset($_GET['password']) ) { $_POST['password'] = $_GET['password']; } +if( isset($_GET['passhash']) ) { + $_POST['passhash'] = $_GET['passhash']; +} + if( isset($_POST) && sizeof($_POST) > 0 ) { $_POST['username']; - $_POST['password']; + $password = $_POST['password']; + $passhash = $_POST['passhash']; $query = "SELECT * FROM users WHERE UPPER(username) = ?;"; $params= Array(strtoupper($_POST['username'])); $stmt = $dbh->prepare($query); $stmt -> execute($params); $row = $stmt->fetchAll(); - if(password_verify($_POST['password'], $row[0]['passhash']) ) { + if( (isset($password) && password_verify($password, $row[0]['passhash'])) || + (isset($passhash) && $passhash == $row[0]['passhash']) ) { $_SESSION['username'] = $_POST['username']; $_SESSION['userId'] = $row[0]['id']; $_SESSION['authtime'] = time(); diff --git a/users.php b/users.php new file mode 100644 index 0000000..6c35992 --- /dev/null +++ b/users.php @@ -0,0 +1,646 @@ + + + + + false, // Disable emulation mode for "real" prepared statements + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // Make the default fetch be an associative array +]; +$dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options); + +// Fetch the workers +if( $_SESSION['permissions'] <= CCCP_PERM_ADMIN ) { + $query = "SELECT distinct c.name, c.id from canvassGroups cg, canvasses c WHERE cg.canvassId = c.id;"; + $params = []; +} else if( $_SESSION['permissions'] <= CCCP_PERM_LEAD ) { + $query = "SELECT distinct c.name, c.id FROM canvassGroups cg, canvasses c WHERE cg.canvassId = c.id AND cg.leadId = ?"; + $params = [$_SESSION['userId']]; +} else { + header("Location: ./login.php"); + die(); +} + +// Fetch group details +$stmt = $dbh->prepare($query); +$stmt->execute($params); +$groups = $stmt->fetchAll(); +?> + + + + + + + + + + + + + + + + + + + + User Management | CCCP: Cargobike Community Canvassing Program + + + +
+
+ Content is loading...please stand by... + (0) +
+ Home | User Management +prepare($query); + $stmt->execute(); + $canvasses = $stmt->fetchAll(PDO::FETCH_ASSOC); + $canvassOptions = ""; + for($i = 0; $i < sizeof($canvasses); $i++) { + $canvassOptions .= '".PHP_EOL; + + } + + // Fetch list of leads + $query = "SELECT * FROM users WHERE permissions <= ".CCCP_PERM_LEAD.";"; + $stmt = $dbh->prepare($query); + $stmt->execute(); + $leads = $stmt->fetchAll(PDO::FETCH_ASSOC); + $leadOptions = ""; + for($i = 0; $i < sizeof($leads); $i++) { + $leadOptions .= '".PHP_EOL; + + } + + // list of auth levels + $authList = Array(CCCP_PERM_ADMIN => 'Admin', + CCCP_PERM_LEAD => 'Lead', + CCCP_PERM_VOLUNTEER => 'Volunteer', + CCCP_PERM_INVITE => "Invite"); + $authKeys = array_keys($authList); + $authOptions = ""; + for($i = 0; $i < sizeof($authList); $i++) { + $authOptions .= '".PHP_EOL; + } + + + // List of canvass groups + for($i = 0; $i < sizeof($groups); $i++) { + $name = $groups[$i]['name']; +// $query = "SELECT u.*, cg.leadId FROM users u, canvassGroups cg ". +// "WHERE u.id = cg.userId AND u.id IN (SELECT DISTINCT userId FROM canvassGroups WHERE canvassId = ?);"; + $query = "SELECT u.*, cg.leadId FROM users u INNER JOIN canvassGroups cg ON u.id=cg.userId ". + "WHERE cg.canvassId = ?"; + $stmt = $dbh->prepare($query); + $stmt->execute([$groups[$i]['id']]); + $users = $stmt->fetchAll(PDO::FETCH_ASSOC); + $cid = $groups[$i]['id']; + + // TODO: parameterize these URLs for the QR codes! + echo '
+'.$name.'
'.PHP_EOL; + echo '
'.PHP_EOL; + echo ' Invite Canvassers'; + for($j = 0; $j < sizeof($users); $j++) { + $id = $users[$j]['id']; + $username = $users[$j]['username']; + $realname = $users[$j]['realName']; + $passhash = $users[$j]['passhash']; + $access = $users[$j]['permissions']; + $lead = $users[$j]['leadId']; + $canvass = $groups[$i]['id']; + + // Select the correct option + $iauthOptions = str_replace('value="'.$access.'"', 'value="'.$access.'" selected', $authOptions); + $ileadOptions = str_replace('value="'.$lead.'"', 'value="'.$lead.'" selected', $leadOptions); + $icanvassOptions = str_replace('value="'.$canvass.'"', 'value="'.$canvass.'" selected', $canvassOptions); + + $loginurl = CCCP_BASEURL."login.php?username=$username&passhash=$passhash"; + $qrsrc = "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=".urlencode($loginurl); + echo "
"; + echo "
$realname
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " "; + echo "
"; + +// $authOptions = str_replace(' selected>', '>', $authOptions); + } + echo '
'.PHP_EOL; + echo '
'.PHP_EOL; + +/* echo<<+$name +
+
+
+EOF;*/ + } + + // List of canvass groups +/* $query = "SELECT * FROM canvasses WHERE id IN (SELECT DISTINCT canvassId FROM canvassGroups);"; + $stmt = $dbh->prepare($query); + $stmt->execute(); + $canvasses = $stmt->fetchAll(PDO::FETCH_ASSOC); + for($i = 0; $i < sizeof($canvasses); $i++) { + $name = $canvasses[$i]['name']; + echo '
+$name
'.PHP_EOL; + echo '
'.PHP_EOL; + }*/ + + + // Admin user group for remaining users + if( $_SESSION['permissions'] <= CCCP_PERM_ADMIN ) { + // Fetch all unassigned users + $query = "SELECT DISTINCT u.*, cg.leadId FROM users u LEFT OUTER JOIN canvassGroups cg ON cg.userId = u.id ". + "WHERE u.id NOT IN (SELECT DISTINCT userId FROM canvassGroups WHERE canvassId IS NOT NULL);"; + $stmt = $dbh->prepare($query); + $stmt->execute(); + $users = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // TODO: parameterize these URLs for the QR codes! + echo '
+Unassigned
'.PHP_EOL; + echo '
'.PHP_EOL; + for($j = 0; $j < sizeof($users); $j++) { + $id = $users[$j]['id']; + $username = $users[$j]['username']; + $realname = $users[$j]['realName']; + $passhash = $users[$j]['passhash']; + $access = $users[$j]['permissions']; + $lead = $users[$j]['leadId']; + + // Select the correct option + $iauthOptions = str_replace('value="'.$access.'"', 'value="'.$access.'" selected', $authOptions); + $ileadOptions = str_replace('value="'.$lead.'"', 'value="'.$lead.'" selected', $leadOptions); + + $loginurl = "https://cccp.slightlycyberpunk.com/CCCP/login.php?username=$username&passhash=$passhash"; + $qrsrc = "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=".urlencode($loginurl); + echo "
"; + echo "
$realname
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " ".PHP_EOL; + echo " ".PHP_EOL; + echo "
".PHP_EOL; + echo "
".PHP_EOL; + echo " "; + echo "
"; + +// $authOptions = str_replace(' selected>', '>', $authOptions); + } + echo '
'.PHP_EOL; + echo '
'.PHP_EOL; + } + +/* +
+Voters +
+ Import Voter File + + + + +
+
+ Use the form below to upload your state voter file. This should be uploaded + as a CSV file with the following columns:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Column NumberColumn NameExample Value
1 First Name Euguene
2 Middle Name Victor
3 Last Name Debs
4 Birth Year 1855
5 Status Inctive
6 Party Unaffiliated
7 Precinct 0101
8 National Congressional District01
9 State Senate District 03
10State House District 04
11Rep Vote 83
12Ward Council
13Ward District
14School District
15Special District
16Phone 826-968-8333
17Email
18State Voter ID 77658288
19Address Line 1 151 N Desplaines St
20Address Line 2
21City Chicago
22State Illinois
23Zip 60661
24Registration Date 1848-02-21
25Voting History 1904;1908-General;1912-Primary;1920
+
+ The first line of this file should be a header containing the field names. + (It will be ignored.)
+ State voter ID is the only unique column -- if you upload multiple files, + records containing the same voter ID will overwrite each other, If + you want to upload multiple files, you should start with the oldest (or least accurate) first.
+ If the state voter ID is left blank, this could result in duplicate entries.
+
+
+
+ +
+
+ + Import Custom List + + + + + +
+
+ Use the form below to upload an additional CSV list of voters to target.
+ This is for lists of members, donors to a particular candidate, or anyone else + you might want to include separately from the main voter file.
+
+ + + + + + + + + + + +
Column NumberColumn NameExample Value
1CCCP Internal Voter ID *708370
2CCCP Internal Address ID *717885
3First Name Richard
4Last Name Stallman
5Address Line 1 51 Franklin St
6Address Line 1
7City Boston
8State MA
9Zip 02110
+

* If you have extracted voters from the CCCP internal database in order to build these lists, + you can include the CCCP voters.id and voterAddresses.id values to ensure the voters are linked correctly. + If you are not sure what this means please leave these columns blank, voters will be matched based + on the name and address instead.

+

In addition to columns listed above, any additional columns will be stored and displayed + on the voter details screen. For example, you could add a tenth column with the amount + contributed to a campaign, and an eleventh column with the date of the contribution.

+

The first row should be a header containing the column names.

+
+

+
+

+
+

+
+ + Manage Custom Lists + +prepare($query); +$stmt->execute(); +$rows = $stmt->fetchAll(); + +for($i = 0; $i < sizeof($rows); $i++) { + echo ''.PHP_EOL; + echo ''.PHP_EOL; + echo ''.PHP_EOL; + echo ''.PHP_EOL; + echo ''.PHP_EOL; +} +?> +
'.$rows[$i]['name'].'Priority '.$rows[$i]['priority'].'
+ + Voter Geocoding +
+ + + + + + + + + + + + + + + + + + + +
+

Geocoding converts voter addresses into a longitude and latitude position. This is needed + in order to locate voters on the map; pins will not appear for voters who are not yet geocoded.

+ Geoapify.com
+ + + +
+ + + +
+ + + +
Last triggered:
+ + + + + + + + + + + + + + + + + + + + + +
Maps.co
+ + + +
+ + + +
+ + + +
Last triggered:
+ + + + + + + + + + + + + + + + + + + + + +
TomTom
+ + + +
+ + + +
+ + + +
Last triggered:
+ + + + + + + + + + + + + + +
Records Remaining: + + +
Records Verified: + / +
+ +
+
+
+ +
+Demographics +
+ Import Census Statistics + + + + +
+
+

Use the form below to upload a census data file for display in the demographics panel. + (To display that data you must also upload a corresponding area file in the section below.)

+

These data files can be downloaded from data.census.gov

+

The file you download (and then upload) should be a .zip archive containing three files:

+
    +
  • [file ID]-Column-Metadata.csv
  • +
  • [file ID]-Data.csv
  • +
  • [file ID]-Table-Notes.txt
  • +
+

You can download these by using the Advanced Search tool. There is a more detailed guide + available on the census website, but the basic process is:

+
    +
  1. Select a geography of either blocks, block groups, or precincts (select all within your state)
  2. +
  3. Select a topic of demographics you want to add
  4. +
  5. Open the Tables tab and find the table with the data you want to import
  6. +
  7. Using the Download Table Data link, download a zip file to upload below:
  8. +
+
+
+ +
+
+ + Import Census Areas + + + + +
+
+

Use the form below to upload a geojson file containing a demographic area or voting district. Be aware of redistricting when choosing + which files to upload. At this time, CCCP cannot handle data from multiple years, so you should only update data that uses current + divisions.

+

Blockgroup and precinct files can be downloaded (as Census Block Groups and Voting Districts) from the following link:
+ US Census Cartographic Files
+ Block files are also available from the census, although a bit harder to find, but you can use the link below for the 2020 Census:
+ Census TIGER TABBLOCK for 2020

+

These files must then be converted into geojson format, using a tool like JOSM

+

Simply open the .shp file from JOSM and then save the same file but change the format to geojson.

+
+
+ +
+
+ + + +
+
+*/ +?> + + + + + +
+ +
+
+ +
+ +
+
+ + +