moved api folder to public
This commit is contained in:
326
public/api/contributions.php
Normal file
326
public/api/contributions.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
// =====================================================================
|
||||
// Contributions API Endpoint
|
||||
// Handles CRUD Operations for Contributions (Points, Lines, Polygons)
|
||||
// and Voting. Actions are determined by the 'action' Parameter in
|
||||
// the Request.
|
||||
//
|
||||
// Supported Actions:
|
||||
// read — Load approved Contributions
|
||||
// create — Insert Contributions
|
||||
// update — Update Contributions
|
||||
// delete — Delete Contributions
|
||||
// vote — Like or Dislike Contributions
|
||||
// =====================================================================
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Read Action Parameter and Route to correct Handler
|
||||
// ---------------------------------------------------------------------
|
||||
$input = get_input();
|
||||
$action = $input['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
case 'read':
|
||||
handle_read($input);
|
||||
break;
|
||||
case 'create':
|
||||
handle_create($input);
|
||||
break;
|
||||
case 'update':
|
||||
handle_update($input);
|
||||
break;
|
||||
case 'delete':
|
||||
handle_delete($input);
|
||||
break;
|
||||
case 'vote':
|
||||
handle_vote($input);
|
||||
break;
|
||||
default:
|
||||
error_response('Unknown Action. Supported Actions are read, create, update, delete, vote.');
|
||||
}
|
||||
|
||||
|
||||
// =====================================================================
|
||||
// Action Handlers
|
||||
// =====================================================================
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// READ: Loads approved Contributions as GeoJSON FeatureCollection
|
||||
// Required: municipality_id
|
||||
// Optional: category
|
||||
// ---------------------------------------------------------------------
|
||||
function handle_read($input) {
|
||||
$pdo = get_db();
|
||||
|
||||
// Validate Input
|
||||
$missing = validate_required($input, ['municipality_id']);
|
||||
if (!empty($missing)) {
|
||||
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||
}
|
||||
|
||||
$municipality_id = $input['municipality_id'];
|
||||
|
||||
// Builds SQL Query with Placeholders for prepared Statement
|
||||
$sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson
|
||||
FROM contributions
|
||||
WHERE municipality_id = :mid AND status = 'approved'";
|
||||
$params = [':mid' => $municipality_id];
|
||||
|
||||
// Optional: Filters by Category
|
||||
if (!empty($input['category'])) {
|
||||
$sql .= " AND category = :cat";
|
||||
$params[':cat'] = $input['category'];
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY created_at DESC";
|
||||
|
||||
try {
|
||||
// Prepared Statement to prevent SQL Injection
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
// Fetches Results as PHP-Array
|
||||
$rows = $stmt->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
// Builds GeoJSON FeatureCollection
|
||||
$features = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$geometry = json_decode($row['geojson']);
|
||||
|
||||
// Removes raw Geometry Columns from Properties
|
||||
unset($row['geom']);
|
||||
unset($row['geojson']);
|
||||
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => $geometry,
|
||||
'properties' => $row
|
||||
];
|
||||
}
|
||||
|
||||
$featureCollection = [
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => $features
|
||||
];
|
||||
|
||||
json_response($featureCollection);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// CREATE: Inserts new Contributions
|
||||
// Required: municipality_id, geom, geom_type, category, title, author_name
|
||||
// Optional: description
|
||||
// ---------------------------------------------------------------------
|
||||
function handle_create($input) {
|
||||
$pdo = get_db();
|
||||
|
||||
// Validates Input
|
||||
$missing = validate_required($input, [
|
||||
'municipality_id', 'geom', 'geom_type', 'category', 'title', 'author_name'
|
||||
]);
|
||||
if (!empty($missing)) {
|
||||
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||
}
|
||||
|
||||
// Validates Geometry Type
|
||||
$valid_geom_types = ['point', 'line', 'polygon'];
|
||||
if (!in_array($input['geom_type'], $valid_geom_types)) {
|
||||
error_response('Invalid Geometry Type. Must be: ' . implode(', ', $valid_geom_types));
|
||||
}
|
||||
|
||||
// Validates GeoJSON
|
||||
$geojson = json_decode($input['geom']);
|
||||
if (!$geojson || !isset($geojson->type)) {
|
||||
error_response('Invalid GeoJSON in Geometry Field.');
|
||||
}
|
||||
|
||||
// Prepared SQL Statement
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO contributions
|
||||
(municipality_id, geom, geom_type, category, title, description, author_name)
|
||||
VALUES
|
||||
(:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type,
|
||||
:category, :title, :description, :author_name)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':mid' => $input['municipality_id'],
|
||||
':geom' => $input['geom'],
|
||||
':geom_type' => $input['geom_type'],
|
||||
':category' => $input['category'],
|
||||
':title' => $input['title'],
|
||||
':description' => $input['description'] ?? '',
|
||||
':author_name' => $input['author_name']
|
||||
]);
|
||||
|
||||
json_response([
|
||||
'message' => 'Contribution created successfully.',
|
||||
'contribution_id' => (int) $pdo->lastInsertId()
|
||||
], 201);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// UPDATE: Updates existing Contributions
|
||||
// Required: contribution_id
|
||||
// Optional: category, title, description, status
|
||||
// Provided Fields are updated. Others remain unchanged.
|
||||
// ---------------------------------------------------------------------
|
||||
function handle_update($input) {
|
||||
$pdo = get_db();
|
||||
|
||||
// Validates Input
|
||||
$missing = validate_required($input, ['contribution_id']);
|
||||
if (!empty($missing)) {
|
||||
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||
}
|
||||
|
||||
$contribution_id = $input['contribution_id'];
|
||||
|
||||
// Checks if Contribution exists
|
||||
$stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id");
|
||||
$stmt->execute([':id' => $contribution_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
error_response('Contribution not found.', 404);
|
||||
}
|
||||
|
||||
// Builds dynamic SQL Query to only update sent Fields
|
||||
$updatable_fields = ['category', 'title', 'description', 'status'];
|
||||
$set_clauses = [];
|
||||
$params = [':id' => $contribution_id];
|
||||
|
||||
foreach ($updatable_fields as $field) {
|
||||
if (isset($input[$field]) && $input[$field] !== '') {
|
||||
$set_clauses[] = "$field = :$field";
|
||||
$params[":$field"] = $input[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($set_clauses)) {
|
||||
error_response('No Fields to update. Provide at least one of: ' . implode(', ', $updatable_fields));
|
||||
}
|
||||
|
||||
// Validates Status
|
||||
if (isset($params[':status'])) {
|
||||
$valid_statuses = ['pending', 'approved', 'rejected', 'in_progress', 'done'];
|
||||
if (!in_array($params[':status'], $valid_statuses)) {
|
||||
error_response('Invalid Status. Must be: ' . implode(', ', $valid_statuses));
|
||||
}
|
||||
}
|
||||
|
||||
// Builds SQL Statement
|
||||
$sql = "UPDATE contributions SET " . implode(', ', $set_clauses) . " WHERE contribution_id = :id";
|
||||
|
||||
// Prepared SQL Statement
|
||||
try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
json_response(['message' => 'Contribution updated successfully.']);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// DELETE: Deletes existing Contributions
|
||||
// Required: contribution_id
|
||||
// Associated Votes are deleted automatically
|
||||
// ---------------------------------------------------------------------
|
||||
function handle_delete($input) {
|
||||
$pdo = get_db();
|
||||
|
||||
// Validates Input
|
||||
$missing = validate_required($input, ['contribution_id']);
|
||||
if (!empty($missing)) {
|
||||
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||
}
|
||||
|
||||
$contribution_id = $input['contribution_id'];
|
||||
|
||||
// Checks if Contribution exists
|
||||
$stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id");
|
||||
$stmt->execute([':id' => $contribution_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
error_response('Contribution not found.', 404);
|
||||
}
|
||||
|
||||
// Prepared SQL Statement
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM contributions WHERE contribution_id = :id");
|
||||
$stmt->execute([':id' => $contribution_id]);
|
||||
|
||||
json_response(['message' => 'Contribution deleted successfully.']);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// VOTE: Likes or Dislikes a Contribution
|
||||
// Required: contribution_id, voter_name, vote_type
|
||||
// Database Trigger automatically updates Likes and Dislikes Count
|
||||
// UNIQUE Constraint prevents duplicate Votes per Voter.
|
||||
// ---------------------------------------------------------------------
|
||||
function handle_vote($input) {
|
||||
$pdo = get_db();
|
||||
|
||||
// Validates Input
|
||||
$missing = validate_required($input, ['contribution_id', 'voter_name', 'vote_type']);
|
||||
if (!empty($missing)) {
|
||||
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||
}
|
||||
|
||||
// Validates Vote Type
|
||||
$valid_vote_types = ['like', 'dislike'];
|
||||
if (!in_array($input['vote_type'], $valid_vote_types)) {
|
||||
error_response('Invalid vote_type. Must be: ' . implode(', ', $valid_vote_types));
|
||||
}
|
||||
|
||||
// Checks if Contribution exists
|
||||
$stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id");
|
||||
$stmt->execute([':id' => $input['contribution_id']]);
|
||||
if (!$stmt->fetch()) {
|
||||
error_response('Contribution not found.', 404);
|
||||
}
|
||||
|
||||
// Prepared SQL Statement
|
||||
try {
|
||||
$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']
|
||||
]);
|
||||
|
||||
json_response(['message' => 'Vote recorded successfully.'], 201);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// UNIQUE Constraint Violation - Voter already voted on this Contribution
|
||||
if ($e->getCode() == '23505') {
|
||||
error_response('You have already voted on this Contribution.', 409);
|
||||
}
|
||||
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
94
public/api/db.php
Normal file
94
public/api/db.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
// =====================================================================
|
||||
// Database Helper
|
||||
// Provides PDO Connection to Database and shared miscellaneous
|
||||
// Functions for all API Endpoints.
|
||||
// =====================================================================
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// JSON Response
|
||||
// Creates JSON Response including HTTP Status Code and HTTP Header
|
||||
// for every API Endpoint and terminates the Script.
|
||||
// ---------------------------------------------------------------------
|
||||
function json_response($data, $status_code = 200) {
|
||||
// Defines HTTP Status Code and HTTP Header
|
||||
// 1XX Informational, 2XX Successful, 3XX Redirection,
|
||||
// 4XX Client Error, 5XX Server Error
|
||||
http_response_code($status_code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
// Converts PHP-Array to JSON-String
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Error Response
|
||||
// Creates standardized Error Responses with Error Message and HTTP Status
|
||||
// Code. Uses json_response() for consistent Formatting.
|
||||
// ---------------------------------------------------------------------
|
||||
function error_response($message, $status_code = 400) {
|
||||
json_response(['error' => $message], $status_code);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Validate Required Fields
|
||||
// Checks if specified Fields exist in the given Data Array and are
|
||||
// non-empty. Returns an Array of missing Field Names, or an empty
|
||||
// Array if all Fields are present.
|
||||
// ---------------------------------------------------------------------
|
||||
function validate_required($data, $fields) {
|
||||
$missing = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
// Checks if Fields exists in Data Array and are not empty
|
||||
if (!isset($data[$field]) || trim($data[$field]) === '') {
|
||||
$missing[] = $field;
|
||||
}
|
||||
}
|
||||
// Returns Array of missing Fields or emty Array
|
||||
return $missing;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Get POST Input
|
||||
// Reads POST Parameters. Returns an associative Array.
|
||||
// Fallback to JSON Request Body if no POST Data is present.
|
||||
// ---------------------------------------------------------------------
|
||||
function get_input() {
|
||||
// Checks for standard POST Requests
|
||||
if (!empty($_POST)) {
|
||||
return array_map('trim', $_POST);
|
||||
}
|
||||
|
||||
// Fall back for JSON POST Requests
|
||||
$json = file_get_contents('php://input');
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (is_array($data)) {
|
||||
return array_map('trim', $data);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Get PDO Connection
|
||||
// Returns PDO Instance wrapped in a Function to prevent global
|
||||
// Variable Dependencies in Endpoint Files.
|
||||
// ---------------------------------------------------------------------
|
||||
function get_db() {
|
||||
global $pdo;
|
||||
|
||||
if (!$pdo) {
|
||||
error_response('Database Connection failed.', 500);
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
51
public/api/init.php
Normal file
51
public/api/init.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
// =====================================================================
|
||||
// Database Connection
|
||||
// =====================================================================
|
||||
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
// Defines Environment Variables
|
||||
$host = getenv('POSTGRES_HOSTNAME');
|
||||
$port = getenv('POSTGRES_PORT');
|
||||
$db = getenv('POSTGRES_DB');
|
||||
$user = getenv('POSTGRES_USER');
|
||||
$pass = getenv('POSTGRES_PASSWORD');
|
||||
|
||||
// Output Buffering and Session Start
|
||||
ob_start();
|
||||
session_start();
|
||||
|
||||
// Initializes Database Connection
|
||||
try {
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false
|
||||
];
|
||||
$dsn = "pgsql:host=$host;dbname=$db;port=$port";
|
||||
$pdo = new PDO($dsn, $user, $pass, $opt);
|
||||
|
||||
|
||||
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Creates Error Message
|
||||
} catch(PDOException $e) {
|
||||
echo "Error: ".$e->getMessage();
|
||||
}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user