i finally committed i guess
Signed-off-by: boris <boris@borishub.co.uk>
This commit is contained in:
112
Models/AuthExample.php
Normal file
112
Models/AuthExample.php
Normal 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
199
Models/AuthService.php
Normal 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
43
Models/Database.php
Normal file → Executable 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
158
Models/FacilityData.php
Normal file → Executable 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
586
Models/FacilityDataSet.php
Normal file → Executable 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
0
Models/Paginator.php
Normal file → Executable file
191
Models/User.php
Normal file → Executable file
191
Models/User.php
Normal file → Executable 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
0
Models/UserData.php
Normal file → Executable file
0
Models/UserDataSet.php
Normal file → Executable file
0
Models/UserDataSet.php
Normal file → Executable file
Reference in New Issue
Block a user