i finally committed i guess

Signed-off-by: boris <boris@borishub.co.uk>
This commit is contained in:
boris
2025-03-15 01:59:16 +00:00
parent 8de2b7f29e
commit 709596eea2
113 changed files with 25075 additions and 54344 deletions

112
Models/AuthExample.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
/**
* Example controller showing how to use the simplified authentication
*
* This file demonstrates how to use the User::checkAuth() and User::checkAdmin()
* methods to protect routes without using middleware.
*/
require_once('Models/User.php');
/**
* Example of a protected endpoint that requires authentication
*/
function protectedEndpoint() {
// Check if user is authenticated
$auth = User::checkAuth();
if (!$auth) {
// The checkAuth method already sent the error response
return;
}
// User is authenticated, proceed with the endpoint logic
$response = [
'status' => 'success',
'message' => 'You are authenticated',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username']
]
];
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of an admin-only endpoint
*/
function adminEndpoint() {
// Check if user is an admin
$auth = User::checkAdmin();
if (!$auth) {
// The checkAdmin method already sent the error response
return;
}
// User is an admin, proceed with the admin-only logic
$response = [
'status' => 'success',
'message' => 'You have admin access',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username']
]
];
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of a public endpoint that doesn't require authentication
* but can still use authentication data if available
*/
function publicEndpoint() {
// Check if user is authenticated, but don't require it
$auth = User::checkAuth(false);
$response = [
'status' => 'success',
'message' => 'This is a public endpoint'
];
// Add user info if authenticated
if ($auth) {
$response['user'] = [
'id' => $auth['uid'],
'username' => $auth['username']
];
} else {
$response['user'] = 'Guest';
}
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of how to use these functions in a simple router
*/
function handleRequest() {
$route = $_GET['route'] ?? 'public';
switch ($route) {
case 'protected':
protectedEndpoint();
break;
case 'admin':
adminEndpoint();
break;
case 'public':
default:
publicEndpoint();
break;
}
}
// Call the router function
handleRequest();

199
Models/AuthService.php Normal file
View File

@@ -0,0 +1,199 @@
<?php
require_once('UserDataSet.php');
/**
* Authentication service for handling JWT-based authentication
*/
class AuthService {
private string $secretKey;
private int $tokenExpiry;
/**
* Initialises the authentication service
* Loads configuration from environment variables
* @throws Exception if OpenSSL extension is not loaded
*/
public function __construct() {
// Load environment variables from .env file
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Skip comments
if (strpos($line, '#') === 0) continue;
// Parse environment variable
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!empty($name)) {
putenv(sprintf('%s=%s', $name, $value));
}
}
}
// Set configuration from environment variables with defaults
$this->secretKey = getenv('JWT_SECRET_KEY') ?: 'your-256-bit-secret';
$this->tokenExpiry = (int)(getenv('JWT_TOKEN_EXPIRY') ?: 3600);
// Verify OpenSSL extension is available
if (!extension_loaded('openssl')) {
throw new Exception('OpenSSL extension is required for JWT');
}
}
/**
* Generates a JWT token for a user
* @param array $userData User information to include in token
* @return string The generated JWT token
*/
public function generateToken(array $userData): string {
$issuedAt = time();
$expire = $issuedAt + $this->tokenExpiry;
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'uid' => $userData['id'],
'username' => $userData['username'],
'accessLevel' => $userData['userType']
];
return $this->encodeJWT($payload);
}
/**
* Validates a JWT token
* @param string $token The JWT token to validate
* @return array|null The decoded payload if valid, null otherwise
*/
public function validateToken(string $token): ?array {
try {
$payload = $this->decodeJWT($token);
// Check if token is expired
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time()) {
return null;
}
return $payload;
} catch (Exception $e) {
return null;
}
}
/**
* Encodes data into a JWT token
* @param array $payload The data to encode
* @return string The encoded JWT token
*/
private function encodeJWT(array $payload): string {
// Create and encode header
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
$header = $this->base64UrlEncode($header);
// Create and encode payload
$payload = json_encode($payload);
$payload = $this->base64UrlEncode($payload);
// Create and encode signature
$signature = hash_hmac('sha256', "$header.$payload", $this->secretKey, true);
$signature = $this->base64UrlEncode($signature);
return "$header.$payload.$signature";
}
/**
* Decodes a JWT token
* @param string $token The JWT token to decode
* @return array|null The decoded payload if valid, null otherwise
*/
private function decodeJWT(string $token): ?array {
// Split token into components
$parts = explode('.', $token);
if (count($parts) !== 3) {
return null;
}
[$header, $payload, $signature] = $parts;
// Verify signature
$validSignature = $this->base64UrlEncode(
hash_hmac('sha256', "$header.$payload", $this->secretKey, true)
);
if ($signature !== $validSignature) {
return null;
}
// Decode and return payload
return json_decode($this->base64UrlDecode($payload), true);
}
/**
* Encodes data using base64url encoding
* @param string $data The data to encode
* @return string The encoded data
*/
private function base64UrlEncode(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Decodes base64url encoded data
* @param string $data The data to decode
* @return string The decoded data
*/
private function base64UrlDecode(string $data): string {
return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', 3 - (3 + strlen($data)) % 4));
}
/**
* Generates a refresh token for a user
* @param array $userData User information to include in token
* @return string The generated refresh token
*/
public function generateRefreshToken(array $userData): string {
$issuedAt = time();
$expire = $issuedAt + ($this->tokenExpiry * 24); // Refresh token lasts 24 times longer than access token
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'uid' => $userData['id'],
'username' => $userData['username'],
'type' => 'refresh'
];
return $this->encodeJWT($payload);
}
/**
* Refreshes an access token using a refresh token
* @param string $refreshToken The refresh token
* @return string|null The new access token if valid, null otherwise
*/
public function refreshToken(string $refreshToken): ?string {
try {
$payload = $this->decodeJWT($refreshToken);
// Check if token is expired or not a refresh token
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time() ||
!isset($payload['type']) || $payload['type'] !== 'refresh') {
return null;
}
// Generate a new access token
$userData = [
'id' => $payload['uid'],
'username' => $payload['username'],
'userType' => isset($payload['accessLevel']) ? $payload['accessLevel'] : 0
];
return $this->generateToken($userData);
} catch (Exception $e) {
return null;
}
}
}

43
Models/Database.php Normal file → Executable file
View File

@@ -1,18 +1,31 @@
<?php
/**
* Database connection handler using Singleton pattern
*/
class Database {
/**
* @var Database
* @var Database|null The singleton instance
*/
protected static $_dbInstance = null;
/**
* @var PDO
* @var PDO The database connection handle
*/
protected $_dbHandle;
/**
* Gets the database connection handle
* @return PDO The database connection
*/
public function getDbConnection(): PDO
{
return $this->_dbHandle;
}
/**
* Gets the singleton instance of the Database class
* @return Database The database instance
*/
public static function getInstance(): ?Database
{
if(self::$_dbInstance == null) {
@@ -21,17 +34,37 @@ class Database {
return self::$_dbInstance;
}
/**
* Private constructor to prevent direct instantiation
* Initialises the database connection
* @throws PDOException if connection fails
*/
private function __construct() {
try {
$this->_dbHandle = new PDO("sqlite:Databases/ecobuddynew.sqlite");
// Create PDO connection with error handling
$this->_dbHandle = new PDO("sqlite:Databases/ecobuddy.sqlite");
// Configure PDO for better error handling and performance
$this->_dbHandle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$this->_dbHandle->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// SQLite3 sometimes just forgets foreign keys exist i guess (https://stackoverflow.com/questions/15301643/sqlite3-forgets-to-use-foreign-keys)
$this->_dbHandle->exec('PRAGMA foreign_keys = ON;');
// Set transaction timeout to 5 seconds, just stops the app from hanging when the db is busy
$this->_dbHandle->exec('PRAGMA busy_timeout = 5000;');
}
catch (PDOException $e) {
echo $e->getMessage();
// Log the error and rethrow
error_log("Database connection error: " . $e->getMessage());
throw $e;
}
}
/**
* Destructor to clean up database connection
*/
public function __destruct() {
$this->_dbHandle = null; // destroys the PDO handle when no longer needed
$this->_dbHandle = null;
}
}

158
Models/FacilityData.php Normal file → Executable file
View File

@@ -1,7 +1,52 @@
<?php
/**
* Represents a facility in the EcoBuddy system
*
* This class serves as a data model for facilities, encapsulating all
* the properties and behaviours of a single facility. It follows the
* Data Transfer Object (DTO) pattern that I learned about in my
* software architecture module.
*
* Each facility has location data, descriptive information, and metadata
* about who contributed it. This class provides a clean interface for
* accessing this data throughout the application.
*/
class FacilityData {
protected $_id, $_title, $_category, $_status, $_description, $_houseNumber, $_streetName, $_county, $_town, $_postcode, $_lng, $_lat, $_contributor;
/**
* Facility properties
*
* @var int $_id - Unique identifier for the facility
* @var string $_title - Name of the facility
* @var string $_category - Category/type of the facility
* @var string $_status - Current status of the facility
* @var string $_description - Detailed description of the facility
* @var string $_houseNumber - Building number or name
* @var string $_streetName - Street name
* @var string $_county - County
* @var string $_town - Town or city
* @var string $_postcode - Postal code
* @var float $_lng - Longitude coordinate
* @var float $_lat - Latitude coordinate
* @var string $_contributor - Username of the person who added the facility
*/
protected $_id;
protected $_title;
protected $_category;
protected $_status;
protected $_description;
protected $_houseNumber;
protected $_streetName;
protected $_county;
protected $_town;
protected $_postcode;
protected $_lng;
protected $_lat;
protected $_contributor;
/**
* Initialises a new facility with data from the database
* @param array $dbRow Database row containing facility data
*/
public function __construct($dbRow) {
$this->_id = $dbRow['id'];
$this->_title = $dbRow['title'];
@@ -18,43 +63,154 @@ class FacilityData {
$this->_contributor = $dbRow['contributor'];
}
/**
* Gets the facility's unique identifier
*
* This ID is used throughout the application to reference this specific
* facility, particularly in database operations and API requests.
*
* @return int The facility ID
*/
public function getId() {
return $this->_id;
}
/**
* Gets the facility's title
*
* The title is the primary name or label for the facility that
* is displayed to users in the interface.
*
* @return string The facility title
*/
public function getTitle() {
return $this->_title;
}
/**
* Gets the facility's category
*
* The category helps classify facilities by type, such as
* recycling centre, community garden, etc.
*
* @return string The facility category
*/
public function getCategory() {
return $this->_category;
}
/**
* Gets the facility's current status
*
* The status indicates whether the facility is operational,
* under maintenance, closed, etc.
*
* @return string The facility status
*/
public function getStatus() {
return $this->_status;
}
/**
* Gets the facility's description
*
* The description provides detailed information about the facility,
* its purpose, services offered, etc.
*
* @return string The facility description
*/
public function getDescription() {
return $this->_description;
}
/**
* Gets the facility's house/building number
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The house/building number
*/
public function getHouseNumber() {
return $this->_houseNumber;
}
/**
* Gets the facility's street name
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The street name
*/
public function getStreetName() {
return $this->_streetName;
}
/**
* Gets the facility's county
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The county
*/
public function getCounty() {
return $this->_county;
}
/**
* Gets the facility's town or city
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The town or city
*/
public function getTown() {
return $this->_town;
}
/**
* Gets the facility's postcode
*
* This is part of the facility's address and helps locate it physically.
* It's also useful for searching facilities by location.
*
* @return string The postcode
*/
public function getPostcode() {
return $this->_postcode;
}
/**
* Gets the facility's longitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The longitude coordinate
*/
public function getLng() {
return $this->_lng;
}
/**
* Gets the facility's latitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The latitude coordinate
*/
public function getLat() {
return $this->_lat;
}
/**
* Gets the username of the facility's contributor
*
* This tracks who added the facility to the system,
* which is useful for auditing and attribution.
*
* @return string The contributor's username
*/
public function getContributor() {
return $this->_contributor;
}

586
Models/FacilityDataSet.php Normal file → Executable file
View File

@@ -12,76 +12,6 @@ class FacilityDataSet
$this->_dbHandle = $this->_dbInstance->getDbConnection();
}
/**
* @param $data
* @return bool
* Broken last minute, dont have time to fix.
* add / update facility to database from array of columns
*/
public function addFacility($data): bool
{
$userQuery = "
SELECT ecoUser.id FROM ecoUser
WHERE ecoUser.username = :contributor;
";
$catQuery = "
SELECT ecoCategories.id FROM ecoCategories
WHERE ecoCategories.name = :category;
";
$sqlQuery = "
INSERT OR REPLACE INTO ecoFacilities
(id,
title,
category,
description,
houseNumber,
streetName,
county,
town,
postcode,
lng,
lat,
contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, -1, -1, ?)
;";
// gets contributor name
$stmt = $this->_dbHandle->prepare($userQuery);
$stmt->bindParam(':contributor', $data->contributor, PDO::PARAM_STR);
$stmt = $this->_dbHandle->prepare($userQuery);
$stmt->execute();
$data['contributor'] = (int)$stmt->fetch(PDO::FETCH_ASSOC);
// gets category ID
$stmt = $this->_dbHandle->prepare($catQuery);
$stmt->bindParam(':category', $data->category, PDO::PARAM_STR);
$stmt = $this->_dbHandle->prepare($catQuery);
$stmt->execute();
$data['category'] = (int)$stmt->fetch(PDO::FETCH_ASSOC);
// run main query and bind updated parameters
$stmt = $this->_dbHandle->prepare($sqlQuery);
// Ensures only one value is returned per column name
$stmt->setFetchMode(\PDO::FETCH_ASSOC);
if (isset($data['id'])) {
$stmt->bindParam(1, $data['id']);
}
$stmt->bindParam(2, $data['title'], PDO::PARAM_STR);
$stmt->bindParam(3, $data['category'], PDO::PARAM_INT);
$stmt->bindParam(4, $data['description'], PDO::PARAM_STR);
$stmt->bindParam(5, $data['houseNumber'], PDO::PARAM_STR);
$stmt->bindParam(6, $data['streetName'], PDO::PARAM_STR);
$stmt->bindParam(7, $data['county'], PDO::PARAM_STR);
$stmt->bindParam(8, $data['town'], PDO::PARAM_STR);
$stmt->bindParam(9, $data['postcode'], PDO::PARAM_STR);
$stmt->bindParam(10, $data['contributor'], PDO::PARAM_INT);
$stmt->execute();
// var_dump($stmt);
// var_dump($this->_dbHandle->errorInfo());
return !($stmt->rowCount());
}
/**
* @param $id
* @return bool
@@ -89,114 +19,444 @@ class FacilityDataSet
*/
public function deleteFacility($id): bool
{
$sqlQuery = "DELETE FROM ecoFacilities WHERE ecoFacilities.id = :id;";
$stmt = $this->_dbHandle->prepare($sqlQuery);
$stmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$stmt->execute();
var_dump($stmt);
echo $stmt->rowCount();
return !($stmt->rowCount() == 0);
try {
// Start transaction
$this->_dbHandle->beginTransaction();
// Delete related status records first
$statusQuery = "DELETE FROM ecoFacilityStatus WHERE facilityid = :id;";
$statusStmt = $this->_dbHandle->prepare($statusQuery);
$statusStmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$statusStmt->execute();
// Delete the facility
$facilityQuery = "DELETE FROM ecoFacilities WHERE id = :id;";
$facilityStmt = $this->_dbHandle->prepare($facilityQuery);
$facilityStmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$facilityStmt->execute();
// Commit transaction
$this->_dbHandle->commit();
return $facilityStmt->rowCount() > 0;
} catch (PDOException $e) {
// Rollback on error
$this->_dbHandle->rollBack();
error_log("Error deleting facility: " . $e->getMessage());
return false;
}
}
/**
* @return array|false Returns array of facilities or false on error
* Fetch all facility records with related data
*/
public function fetchAll(): array|false
{
try {
error_log('Starting fetchAll...');
$query = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
COALESCE(GROUP_CONCAT(ecoFacilityStatus.statusComment, '; '), '') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
COALESCE(ecoUser.username, 'Unknown') AS contributor
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username
ORDER BY ecoFacilities.id ASC;
";
error_log('Preparing query...');
$dataStmt = $this->_dbHandle->prepare($query);
error_log('Executing query...');
$dataStmt->execute();
error_log('Fetching results...');
$results = $dataStmt->fetchAll(PDO::FETCH_ASSOC);
if ($results === false) {
error_log('Query returned false');
return false;
}
error_log('Query successful. Row count: ' . count($results));
return $results;
} catch (PDOException $e) {
error_log("Database error in fetchAll: " . $e->getMessage());
error_log("SQL State: " . $e->getCode());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
} catch (Exception $e) {
error_log("General error in fetchAll: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* @param $filterArray
* @param $sortArray
* @return array
* Fetch all records depending on filters, and sort by defined column
* Creates a new facility in the database
* @param array $data Facility data
* @return array|false The created facility data or false on failure
*/
public function fetchAll($filterArray, $sortArray): array
public function createFacility($data)
{
// Define columns for filtering and sorting
$filterColumns = [
0 => 'ecoFacilityStatus.statusComment',
1 => 'ecoFacilities.title',
2 => 'ecoCategories.name',
3 => 'ecoFacilities.description',
4 => 'ecoFacilities.streetName',
5 => 'ecoFacilities.county',
6 => 'ecoFacilities.town',
7 => 'ecoFacilities.postcode',
8 => 'ecoUser.username'
];
try {
$this->_dbHandle->beginTransaction();
$sortColumns = [
0 => 'ecoFacilityStatus.statusComment',
1 => 'ecoFacilities.title',
2 => 'ecoCategories.name',
3 => 'ecoFacilities.description',
4 => 'ecoFacilities.streetName',
5 => 'ecoFacilities.county',
6 => 'ecoFacilities.town',
7 => 'ecoFacilities.postcode',
8 => 'ecoUser.username'
];
// Validate coordinates
if (!is_numeric($data['lng']) || !is_numeric($data['lat']) ||
$data['lng'] < -180 || $data['lng'] > 180 ||
$data['lat'] < -90 || $data['lat'] > 90) {
throw new Exception('Invalid coordinates provided');
}
// Validate and select the filter column
$selectedFilterColumn = $filterColumns[$filterArray['category']] ?? 'ecoFacilities.title';
// Validate and select the sort column
$selectedSortColumn = $sortColumns[$sortArray['sort']] ?? 'ecoFacilities.title';
// Validate sort direction
$direction = strtolower($sortArray['dir']) === 'desc' ? 'DESC' : 'ASC';
// Base query for filtering and sorting
$baseQuery = "
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
WHERE {$selectedFilterColumn} LIKE :term
";
// Get contributor ID
$contributorId = $this->getContributorId($data['contributor']);
if (!$contributorId) {
throw new Exception('Invalid contributor name');
}
// Get category ID
$categoryId = $this->getCategoryId($data['category']);
if (!$categoryId) {
// If category doesn't exist, create it
$categoryId = $this->createCategory($data['category']);
if (!$categoryId) {
throw new Exception('Failed to create category: ' . $data['category']);
}
}
// Query to count total results
$countQuery = "SELECT COUNT(DISTINCT ecoFacilities.id) AS total {$baseQuery}";
// Insert facility
$sql = "INSERT INTO ecoFacilities (title, category, description, houseNumber,
streetName, county, town, postcode, lng, lat, contributor)
VALUES (:title, :category, :description, :houseNumber,
:streetName, :county, :town, :postcode, :longitude, :latitude, :contributor)";
$stmt = $this->_dbHandle->prepare($sql);
$params = [
':title' => $data['title'],
':category' => $categoryId,
':description' => $data['description'],
':houseNumber' => $data['houseNumber'],
':streetName' => $data['streetName'],
':county' => $data['county'],
':town' => $data['town'],
':postcode' => $data['postcode'],
':longitude' => $data['lng'],
':latitude' => $data['lat'],
':contributor' => $contributorId
];
// Query to fetch filtered and sorted results
$dataQuery = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
GROUP_CONCAT(ecoFacilityStatus.statusComment, ', ') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
ecoUser.username AS contributor
{$baseQuery}
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username
ORDER BY {$selectedSortColumn} {$direction};
";
// Surround 'term' with % to allow usage with LIKE
$filterArray['term'] = '%' . $filterArray['term'] . '%' ?? '%';
// Prepare and execute the count query
$countStmt = $this->_dbHandle->prepare($countQuery);
$countStmt->bindValue(':term', $filterArray['term'], PDO::PARAM_STR);
$countStmt->execute();
// Set total results to output of count statement
$totalResults = (int)$countStmt->fetchColumn();
error_log("Executing SQL with params: " . print_r($params, true));
if (!$stmt->execute($params)) {
throw new Exception('Failed to insert facility: ' . implode(', ', $stmt->errorInfo()));
}
// Prepare and execute the data query
$dataStmt = $this->_dbHandle->prepare($dataQuery);
$dataStmt->bindValue(':term', $filterArray['term'], PDO::PARAM_STR);
$dataStmt->execute();
$facilityId = $this->_dbHandle->lastInsertId();
$this->_dbHandle->commit();
// Fetch results into FacilityData objects
$dataSet = [];
while ($row = $dataStmt->fetch()) {
$dataSet[] = new FacilityData($row);
// Return the created facility
return $this->getFacilityById($facilityId);
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error in createFacility: " . $e->getMessage());
throw $e;
}
}
return [
'dataset' => $dataSet,
'count' => $totalResults
];
private function createCategory($categoryName)
{
try {
$sql = "INSERT INTO ecoCategories (name) VALUES (:name)";
$stmt = $this->_dbHandle->prepare($sql);
$stmt->execute([':name' => $categoryName]);
return $this->_dbHandle->lastInsertId();
} catch (Exception $e) {
error_log("Error creating category: " . $e->getMessage());
return false;
}
}
/**
* Updates an existing facility in the database
* @param int $id Facility ID
* @param array $data Updated facility data
* @return array|false The updated facility data or false on failure
*/
public function updateFacility($id, $data) {
try {
// Start transaction
$this->_dbHandle->beginTransaction();
// Validate coordinates
if (!is_numeric($data['lng']) || !is_numeric($data['lat']) ||
$data['lng'] < -180 || $data['lng'] > 180 ||
$data['lat'] < -90 || $data['lat'] > 90) {
throw new Exception('Invalid coordinates');
}
// Get Contributor ID
$query = "SELECT ecoUser.id FROM ecoUser WHERE ecoUser.username = :contributor;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':contributor', $data['contributor']);
$stmt->execute();
$contributorResult = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$contributorResult) {
throw new Exception('Invalid contributor username');
}
$contributorId = $contributorResult['id'];
// Get Category ID
$query = "SELECT ecoCategories.id FROM ecoCategories WHERE ecoCategories.name = :category;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':category', $data['category']);
$stmt->execute();
$categoryResult = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$categoryResult) {
throw new Exception('Invalid category name');
}
$categoryId = $categoryResult['id'];
// Update facility
$query = "
UPDATE ecoFacilities
SET title = :title,
category = :category,
description = :description,
houseNumber = :houseNumber,
streetName = :streetName,
county = :county,
town = :town,
postcode = :postcode,
lng = :lng,
lat = :lat,
contributor = :contributor
WHERE id = :id
";
$stmt = $this->_dbHandle->prepare($query);
$params = [
':title' => $data['title'],
':category' => $categoryId,
':description' => $data['description'],
':houseNumber' => $data['houseNumber'],
':streetName' => $data['streetName'],
':county' => $data['county'],
':town' => $data['town'],
':postcode' => $data['postcode'],
':lng' => $data['lng'],
':lat' => $data['lat'],
':contributor' => $contributorId,
':id' => $id
];
error_log("Executing update query with params: " . print_r($params, true));
if (!$stmt->execute($params)) {
throw new Exception('Failed to update facility: ' . implode(', ', $stmt->errorInfo()));
}
if ($stmt->rowCount() > 0) {
$this->_dbHandle->commit();
return $this->getFacilityById($id);
}
$this->_dbHandle->rollBack();
return false;
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error updating facility: " . $e->getMessage());
return false;
}
}
/**
* Gets a facility by its ID
* @param int $id Facility ID
* @return array|false The facility data or false if not found
*/
public function getFacilityById($id) {
try {
$query = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
COALESCE(GROUP_CONCAT(ecoFacilityStatus.statusComment, ';'), '') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
COALESCE(ecoUser.username, 'Unknown') AS contributor
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
WHERE ecoFacilities.id = ?
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username;
";
$stmt = $this->_dbHandle->prepare($query);
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Error getting facility: " . $e->getMessage());
return false;
}
}
private function getContributorId($username)
{
try {
$query = "SELECT ecoUser.id FROM ecoUser WHERE ecoUser.username = :username;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':username', $username);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['id'] : false;
} catch (Exception $e) {
error_log("Error getting contributor ID: " . $e->getMessage());
return false;
}
}
private function getCategoryId($categoryName)
{
try {
$query = "SELECT ecoCategories.id FROM ecoCategories WHERE ecoCategories.name = :name;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':name', $categoryName);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['id'] : false;
} catch (Exception $e) {
error_log("Error getting category ID: " . $e->getMessage());
return false;
}
}
/**
* Adds a new status comment to a facility
* @param int $facilityId The ID of the facility
* @param string $statusComment The status comment to add
* @return bool True if successful, false otherwise
*/
public function addFacilityStatus($facilityId, $statusComment)
{
try {
// Log input parameters
error_log("Adding facility status - Facility ID: " . $facilityId . ", Comment: " . $statusComment);
// Start transaction
$this->_dbHandle->beginTransaction();
// Insert new status comment
$query = "INSERT INTO ecoFacilityStatus (facilityId, statusComment) VALUES (:facilityId, :statusComment)";
$stmt = $this->_dbHandle->prepare($query);
// Log the prepared statement
error_log("Prepared statement: " . $query);
// Bind values and log them
$stmt->bindValue(':facilityId', (int)$facilityId, PDO::PARAM_INT);
$stmt->bindValue(':statusComment', $statusComment);
error_log("Bound values - Facility ID: " . (int)$facilityId . ", Comment: " . $statusComment);
if (!$stmt->execute()) {
$errorInfo = $stmt->errorInfo();
error_log("SQL Error: " . print_r($errorInfo, true));
throw new Exception('Failed to insert status comment: ' . implode(', ', $errorInfo));
}
$this->_dbHandle->commit();
error_log("Successfully added facility status");
return true;
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error adding facility status: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* Gets all status comments for a facility
* @param int $facilityId The ID of the facility
* @return array Array of status comments with their IDs
*/
public function getFacilityStatuses($facilityId)
{
try {
$query = "SELECT id, statusComment FROM ecoFacilityStatus WHERE facilityId = :facilityId ORDER BY id DESC";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':facilityId', (int)$facilityId, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Error getting facility statuses: " . $e->getMessage());
return [];
}
}
/**
* Updates an existing status comment
* @param int $statusId The ID of the status comment
* @param string $statusComment The updated status comment
* @return bool True if successful, false otherwise
*/
public function updateFacilityStatus($statusId, $statusComment)
{
try {
$query = "UPDATE ecoFacilityStatus SET statusComment = :statusComment WHERE id = :statusId";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':statusId', (int)$statusId, PDO::PARAM_INT);
$stmt->bindValue(':statusComment', $statusComment);
return $stmt->execute();
} catch (Exception $e) {
error_log("Error updating facility status: " . $e->getMessage());
return false;
}
}
/**
* Deletes a specific status comment
* @param int $statusId The ID of the status comment to delete
* @return bool True if successful, false otherwise
*/
public function deleteFacilityStatus($statusId)
{
try {
$query = "DELETE FROM ecoFacilityStatus WHERE id = :statusId";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':statusId', (int)$statusId, PDO::PARAM_INT);
return $stmt->execute();
} catch (Exception $e) {
error_log("Error deleting facility status: " . $e->getMessage());
return false;
}
}
}

0
Models/Paginator.php Normal file → Executable file
View File

191
Models/User.php Normal file → Executable file
View File

@@ -1,62 +1,133 @@
<?php
require_once('UserDataSet.php');
class User {
protected $_username, $_loggedIn, $_userId, $_accessLevel;
require_once('AuthService.php');
/**
* User class - Handles user authentication and session management
*
* This class manages user authentication using JWT tokens and provides
* methods for logging in, logging out, and checking user permissions.
* I've implemented this based on JWT authentication
*/
class User {
/**
* Class properties
* @var string $_username - The user's username
* @var bool $_loggedIn - Whether the user is currently logged in
* @var string $_userId - The user's unique ID
* @var int $_accessLevel - The user's access level (admin = 1, regular user = 2)
* @var AuthService $_authService - Service for JWT token handling
*/
protected $_username, $_loggedIn, $_userId, $_accessLevel;
protected $_authService;
/**
* Gets the current user's username
*
* @return string The username of the current user
*/
public function getUsername() {
return $this->_username;
}
/**
* Gets the current user's ID
*
* @return string The ID of the current user
*/
public function getUserId() {
return $this->_userId;
}
/**
* Open session, set field variables
* Constructor - Initialises user from JWT token if available
*
* Checks for a JWT token in the Authorization header and validates it.
* If valid, sets user properties based on the token payload.
* Also starts a session if needed for CAPTCHA verification during registration.
*/
public function __construct() {
session_start();
// Initialise default values
$this->_username = "None";
$this->_loggedIn = false;
$this->_userId = "0";
$this->_accessLevel = null;
// if user logged in, set variables.
if(isset($_SESSION['login'])) {
$this->_username = $_SESSION['login'];
$this->_userId = $_SESSION['uid'];
$this->_loggedIn = true;
$this->_accessLevel = $_SESSION['accessLevel'];
$this->_authService = new AuthService();
// Check for JWT token in Authorization header
$headers = getallheaders();
$token = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : null;
// Validate token if it exists
if ($token) {
$payload = $this->_authService->validateToken($token);
if ($payload) {
$this->_username = $payload['username'];
$this->_userId = $payload['uid'];
$this->_accessLevel = $payload['accessLevel'];
$this->_loggedIn = true;
}
}
// Start session only if needed for CAPTCHA
if (session_status() === PHP_SESSION_NONE && isset($_GET['page']) && $_GET['page'] === 'register') {
session_start();
}
}
/**
* Sets the user's access level
*
* @param int $level The access level to set (admin = 1, regular user = 2)
* @return void
*/
private function setAccessLevel($level) {
$this->_accessLevel = $level;
$_SESSION['accessLevel'] = $level;
}
/**
* Gets the user's access level
*
* @return int|null The user's access level (admin = 1, regular user = 2) or null if not set
*/
public function getAccessLevel() {
return $this->_accessLevel;
}
/**
* @param $username
* @param $password
* @return bool
* Using a username and password, authenticate a user and assign variables from query
* Authenticates a user using username and password
*
* Checks credentials against the database and generates a JWT token if valid.
* Sets user properties if authentication is successful.
*
* @param string $username The username to authenticate
* @param string $password The password to verify
* @return string|bool JWT token if authentication was successful, false otherwise
*/
public function Authenticate($username, $password): bool
public function Authenticate($username, $password)
{
$users = new UserDataSet();
$userDataSet = $users->checkUserCredentials($username, $password);
$accessLevel = $users->checkAccessLevel($username);
if(count($userDataSet) > 0) {
$_SESSION['login'] = $username;
$_SESSION['uid'] = $userDataSet[0]->getId();
$this->setAccessLevel($accessLevel);
$userData = $userDataSet[0];
$accessLevel = $users->checkAccessLevel($username);
// Generate JWT token
$token = $this->_authService->generateToken([
'id' => $userData->getId(),
'username' => $userData->getUsername(),
'userType' => $accessLevel
]);
// Set user properties
$this->_loggedIn = true;
$this->_username = $username;
$this->_userId = $userDataSet[0]->getId();
return true;
$this->_userId = $userData->getId();
$this->_accessLevel = $accessLevel;
return $token;
}
else {
$this->_loggedIn = false;
@@ -65,26 +136,84 @@ class User {
}
/**
* Logs the user out
*
* Resets all user properties to their default values.
* Note: This doesn't invalidate the JWT token - handled client-side
* by removing the token from storage.
*
* @return void
* Unset user variables from session, and set variables to default values - destroying session.
*/
public function logout() {
unset($_SESSION['login']);
unset($_SESSION['uid']);
// Reset user properties
$this->_loggedIn = false;
$this->_username = "None";
$this->_userId = "0";
session_destroy();
$this->_accessLevel = null;
}
/**
* Checks if the user is currently logged in
*
* @return bool True if the user is logged in, false otherwise
*/
public function isLoggedIn(): bool
{
return $this->_loggedIn;
}
public function __destruct()
/**
* Static method to check if a request is authenticated
*
* This method can be called from any controller to check if the request
* has a valid JWT token. It returns the payload if authenticated or
* sends an error response and returns false if not.
*
* @param bool $required Whether authentication is required (defaults to true)
* @return array|false The payload if authenticated, false otherwise
*/
public static function checkAuth(bool $required = true)
{
$authService = new AuthService();
// Get the token from the Authorization header
$headers = getallheaders();
$token = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : null;
// Validate the token
$payload = $token ? $authService->validateToken($token) : null;
// If authentication is required and no valid token, return error
if ($required && !$payload) {
header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['error' => 'Authentication required']);
return false;
}
return $payload;
}
/**
* Static method to check if a request is from an admin
*
* This method can be called from any controller to check if the request
* has a valid JWT token with admin access level. It returns the payload
* if authenticated as admin or sends an error response and returns false if not.
*
* @return array|false The payload if authenticated as admin, false otherwise
*/
public static function checkAdmin()
{
$payload = self::checkAuth(true);
if ($payload && isset($payload['accessLevel']) && $payload['accessLevel'] == 1) {
return $payload;
}
header('Content-Type: application/json');
http_response_code(403);
echo json_encode(['error' => 'Admin access required']);
return false;
}
}

0
Models/UserData.php Normal file → Executable file
View File

0
Models/UserDataSet.php Normal file → Executable file
View File