diff --git a/docker/database/start-scripts/1-seed.sql b/docker/database/start-scripts/1-seed.sql index d18efacf..5cff1847 100644 --- a/docker/database/start-scripts/1-seed.sql +++ b/docker/database/start-scripts/1-seed.sql @@ -3,10 +3,10 @@ INSERT INTO roles (name) VALUES ('Administrador'), ('Gerente'), ('Trabajador'); -INSERT INTO workers (company, name, dni, password, email, role_id, created_at, updated_at, deleted_at) VALUES -('TechCorp', 'Carlos García', '12345678A', 'hashedpassword1', 'carlos.garcia@example.com', 1, NOW(), NOW(), NULL), -('InnovaTech', 'Ana Martínez', '23456789B', 'hashedpassword2', 'ana.martinez@example.com', 2, NOW(), NOW(), NULL), -('DesignWorks', 'José Rodríguez', '34567890C', 'hashedpassword3', 'jose.rodriguez@example.com', 3, NOW(), NOW(), NULL); +INSERT INTO workers (company, name, dni, password, email, role_id) VALUES +('TechCorp', 'Carlos García', '12345678A', 'hashedpassword1', 'carlos.garcia@example.com', 1), +('InnovaTech', 'Ana Martínez', '23456789B', 'hashedpassword2', 'ana.martinez@example.com', 2), +('DesignWorks', 'José Rodríguez', '34567890C', 'hashedpassword3', 'jose.rodriguez@example.com', 3); INSERT INTO contracts (name, start_date, end_date, invoice_proposed, invoice_agreed, invoice_paid) VALUES ('Ayuntamiento de Valencia', '2021-01-01', '2021-12-31', 1000.00, 900.00, 900.00), ('Administración General del Estado', '2021-01-01', '2021-12-31', 2000.00, 1800.00, 1800.00), @@ -50,7 +50,7 @@ INSERT INTO incidences (name, element_id, description) VALUES ('Banco pintado', 2, 'Banco pintado con grafitis'), ('Fuente con fuga', 3, 'Fuente con fuga de agua'); --TODO: tasks, routes and works -INSERT INTO work_orders (contract_id) VALUES +INSERT INTO work_orders (contract_id) VALUES (1), (2), (3); diff --git a/src/app/Controllers/AuthController.php b/src/app/Controllers/AuthController.php new file mode 100644 index 00000000..a4bd1bc8 --- /dev/null +++ b/src/app/Controllers/AuthController.php @@ -0,0 +1,66 @@ + 'Auth/Login', + 'title' => 'Login Page', + 'layout' => 'AuthLayout', + 'data' => [ + 'error' => Session::get('error'), + ], + ]); + + // Clear the error message after displaying it + Session::remove('error'); + } + + public function login($postData) + { + $email = $postData['email'] ?? null; + $password = $postData['password'] ?? null; + + if (! $email || ! $password) { + // Redirect back with error if fields are missing + Session::set('error', 'Email and password are required.'); + header('Location: /auth/login'); + exit; + } + + // Check if the user exists and password matches + $user = User::findBy(['email' => $email, 'password' => $password], true); + + if (! $user || strcmp($user->password, $password) !== 0) { // TODO: Verify hashed password not raw password + echo 'Invalid email or password.'; + // Redirect back with error if authentication fails + Session::set('error', 'Invalid email or password.'); + header('Location: /auth/login'); + exit; + } + + Session::set('user', [ + 'id' => $user->getId(), + 'name' => $user->name, + 'email' => $user->email, + 'role_id' => $user->role_id, + ]); + + header('Location: /'); + exit; + } + + public function logout() + { + Session::destroy(); + header('Location: /auth/login'); + exit; + } +} diff --git a/src/app/Controllers/CAuth.php b/src/app/Controllers/CAuth.php deleted file mode 100644 index 1725657b..00000000 --- a/src/app/Controllers/CAuth.php +++ /dev/null @@ -1,18 +0,0 @@ - "Auth/Login", - "title" => "Login Page", - "layout" => "AuthLayout", - "data" => [] - ]); - } -} diff --git a/src/app/Controllers/CContract.php b/src/app/Controllers/CContract.php deleted file mode 100644 index 3f79c7b1..00000000 --- a/src/app/Controllers/CContract.php +++ /dev/null @@ -1,20 +0,0 @@ - "Contracts", - "title" => "Contracts", - "layout" => "MainLayout", - "data" => ["contracts" => $contracts] - ]); - } -} diff --git a/src/app/Controllers/CElement.php b/src/app/Controllers/CElement.php deleted file mode 100644 index 563d73f3..00000000 --- a/src/app/Controllers/CElement.php +++ /dev/null @@ -1,20 +0,0 @@ - "Element", - "title" => "Element", - "layout" => "MainLayout", - "data" => ["elements" => $elements] - ]); - } -} diff --git a/src/app/Controllers/CHome.php b/src/app/Controllers/CHome.php deleted file mode 100644 index a67a2fc3..00000000 --- a/src/app/Controllers/CHome.php +++ /dev/null @@ -1,20 +0,0 @@ - "Home", - "title" => "Home Page", - "layout" => "MainLayout", - "data" => ["workers" => $workers] - ]); - } -} diff --git a/src/app/Controllers/CIncidence.php b/src/app/Controllers/CIncidence.php deleted file mode 100644 index bb65b3eb..00000000 --- a/src/app/Controllers/CIncidence.php +++ /dev/null @@ -1,45 +0,0 @@ - "Incidence", - "title" => "Incidences", - "layout" => "MainLayout", - ]); - } - - public function findall() - { - $incidences = MIncidence::findAll(); - - View::render([ - "view" => "Incidence/SeeAllIncidences", - "title" => "Incidences", - "layout" => "MainLayout", - "data" => ["incidences" => $incidences] - ]); - } - - public function get() - { - $elements = MElement::findAll(); - - View::render([ - "view" => "Incidence/Create", - "title" => "Create Incidence", - "layout" => "MainLayout", - "data" => [ - "elements" => $elements, - ] - ]); - } -} diff --git a/src/app/Controllers/CPruningType.php b/src/app/Controllers/CPruningType.php deleted file mode 100644 index 854f5721..00000000 --- a/src/app/Controllers/CPruningType.php +++ /dev/null @@ -1,20 +0,0 @@ - "PruningType", - "title" => "Pruning Types", - "layout" => "MainLayout", - "data" => ["pruning_types" => $pruning_types] - ]); - } -} diff --git a/src/app/Controllers/CTaskType.php b/src/app/Controllers/CTaskType.php deleted file mode 100644 index 33ed892b..00000000 --- a/src/app/Controllers/CTaskType.php +++ /dev/null @@ -1,20 +0,0 @@ - "TaskTypes/index", - "title" => "Task Types", - "layout" => "MainLayout", - "data" => ["task_types" => $task_types] - ]); - } -} diff --git a/src/app/Controllers/CTreeType.php b/src/app/Controllers/CTreeType.php deleted file mode 100644 index 78632dc5..00000000 --- a/src/app/Controllers/CTreeType.php +++ /dev/null @@ -1,20 +0,0 @@ - "TreeType", - "title" => "Tree Types", - "layout" => "MainLayout", - "data" => ["tree_types" => $tree_types] - ]); - } -} diff --git a/src/app/Controllers/CWorkOrder.php b/src/app/Controllers/CWorkOrder.php deleted file mode 100644 index 6bf06e08..00000000 --- a/src/app/Controllers/CWorkOrder.php +++ /dev/null @@ -1,60 +0,0 @@ - "order/index", - "title" => "Order", - "layout" => "MainLayout", - "data" => ["orders" => $orders] - ]); - } - - public function find() - { - $id = $_GET['id']; - $order = MWorkOrder::find($id); - View::render([ - "view" => "order", - "title" => "Order", - "layout" => "MainLayout", - "data" => ["order" => $order] - ]); - } - - public function indexCreate() - { - $orders = MWorkOrder::findAll(); - View::render([ - "view" => "order/create", - "title" => "Create Order", - "layout" => "MainLayout", - "data" => ["orders" => $orders] - ]); - } - - public function save() - { - $order = new MWorkOrder(); - - $order->save(); - } - - public function delete() - { - $id = $_GET['id']; - $order = MWorkOrder::find($id); - $order->delete(); - } - - public function update() - { - - } -} \ No newline at end of file diff --git a/src/app/Controllers/CZone.php b/src/app/Controllers/CZone.php deleted file mode 100644 index 07223c97..00000000 --- a/src/app/Controllers/CZone.php +++ /dev/null @@ -1,20 +0,0 @@ - "Zone", - "title" => "Zones", - "layout" => "MainLayout", - "data" => ["zones" => $zones] - ]); - } -} diff --git a/src/app/Controllers/ContractController.php b/src/app/Controllers/ContractController.php new file mode 100644 index 00000000..5bc48649 --- /dev/null +++ b/src/app/Controllers/ContractController.php @@ -0,0 +1,20 @@ + 'Contracts', + 'title' => 'Contracts', + 'layout' => 'MainLayout', + 'data' => ['contracts' => $contracts], + ]); + } +} diff --git a/src/app/Controllers/DashboardController.php b/src/app/Controllers/DashboardController.php new file mode 100644 index 00000000..b1dc8389 --- /dev/null +++ b/src/app/Controllers/DashboardController.php @@ -0,0 +1,18 @@ + 'Dashboard', + 'title' => 'Dashboard', + 'layout' => 'MainLayout', + 'data' => [], + ]); + } +} diff --git a/src/app/Controllers/ElementController.php b/src/app/Controllers/ElementController.php new file mode 100644 index 00000000..d88b8416 --- /dev/null +++ b/src/app/Controllers/ElementController.php @@ -0,0 +1,20 @@ + 'Elements', + 'title' => 'Element', + 'layout' => 'MainLayout', + 'data' => ['elements' => $elements], + ]); + } +} diff --git a/src/app/Controllers/IncidenceController.php b/src/app/Controllers/IncidenceController.php new file mode 100644 index 00000000..79115b81 --- /dev/null +++ b/src/app/Controllers/IncidenceController.php @@ -0,0 +1,44 @@ + 'Incidence', + 'title' => 'Incidences', + 'layout' => 'MainLayout', + ]); + } + + public function findall() + { + $incidences = Incidence::findAll(); + View::render([ + 'view' => 'Incidence/SeeAllIncidences', + 'title' => 'Incidences', + 'layout' => 'MainLayout', + 'data' => ['incidences' => $incidences], + ]); + } + + public function get() + { + $elements = Element::findAll(); + + View::render([ + 'view' => 'Incidence/Create', + 'title' => 'Create Incidence', + 'layout' => 'MainLayout', + 'data' => [ + 'elements' => $elements, + ], + ]); + } +} diff --git a/src/app/Controllers/PruningTypeController.php b/src/app/Controllers/PruningTypeController.php new file mode 100644 index 00000000..24a8ea2e --- /dev/null +++ b/src/app/Controllers/PruningTypeController.php @@ -0,0 +1,20 @@ + 'PruningType', + 'title' => 'Pruning Types', + 'layout' => 'MainLayout', + 'data' => ['pruning_types' => $pruning_types], + ]); + } +} diff --git a/src/app/Controllers/TaskTypeController.php b/src/app/Controllers/TaskTypeController.php new file mode 100644 index 00000000..b0629da2 --- /dev/null +++ b/src/app/Controllers/TaskTypeController.php @@ -0,0 +1,20 @@ + 'TaskTypes/index', + 'title' => 'Task Types', + 'layout' => 'MainLayout', + 'data' => ['task_types' => $task_types], + ]); + } +} diff --git a/src/app/Controllers/TreeTypeController.php b/src/app/Controllers/TreeTypeController.php new file mode 100644 index 00000000..2d13e2f2 --- /dev/null +++ b/src/app/Controllers/TreeTypeController.php @@ -0,0 +1,20 @@ + 'TreeTypes', + 'title' => 'Tree Types', + 'layout' => 'MainLayout', + 'data' => ['tree_types' => $tree_types], + ]); + } +} diff --git a/src/app/Controllers/UserController.php b/src/app/Controllers/UserController.php new file mode 100644 index 00000000..956f45ec --- /dev/null +++ b/src/app/Controllers/UserController.php @@ -0,0 +1,90 @@ + 'Users', + 'title' => 'Manage Users', + 'layout' => 'MainLayout', + 'data' => ['users' => $elements], + ]); + + Session::remove('success'); + } + + public function create() + { + View::render([ + 'view' => 'User/Create', + 'title' => 'Add User', + 'layout' => 'MainLayout', + 'data' => [], + ]); + } + + public function store($postData) + { + $user = new User; + $user->company = $postData['company']; + $user->name = $postData['name']; + $user->dni = $postData['dni']; + $user->email = $postData['email']; + + // Check if password is not empty before updating + if (! empty($postData['password'])) { + $user->password = $postData['password']; + } + + $user->role_id = $postData['role_id']; + $user->save(); + + Session::set('success', 'User created successfully'); + + header('Location: /users'); + } + + public function edit($id) + { + $user = User::find($id); + View::render([ + 'view' => 'User/Edit', + 'title' => 'Edit User', + 'layout' => 'MainLayout', + 'data' => ['user' => $user], + ]); + } + + public function update($id, $postData) + { + $user = User::find($id); + $user->company = $postData['company']; + $user->name = $postData['name']; + $user->dni = $postData['dni']; + $user->email = $postData['email']; + $user->role_id = $postData['role_id']; + $user->save(); + + Session::set('success', 'User updated successfully'); + + header('Location: /users'); + } + + public function destroy($id) + { + $user = User::find($id); + $user->delete(); + + Session::set('success', 'User deleted successfully'); + + header('Location: /users'); + } +} diff --git a/src/app/Controllers/WorkOrderController.php b/src/app/Controllers/WorkOrderController.php new file mode 100644 index 00000000..009d209b --- /dev/null +++ b/src/app/Controllers/WorkOrderController.php @@ -0,0 +1,66 @@ + 'WorkOrders', + 'title' => 'Manage Orders', + 'layout' => 'MainLayout', + 'data' => ['workOrders' => $workOrders], + ]); + Session::remove('success'); + } + + public function create() + { + View::render([ + 'view' => 'Order/Create', + 'title' => 'Add Order', + 'layout' => 'MainLayout', + 'data' => [], + ]); + } + + public function store($postData) {} + + public function edit($id) + { + $order = WorkOrder::find($id); + View::render([ + 'view' => 'Order/Edit', + 'title' => 'Edit Order', + 'layout' => 'MainLayout', + 'data' => ['order' => $order], + ]); + } + + public function update($id, $postData) + { + $order = WorkOrder::find($id); + + $order->save(); + + Session::set('success', 'Order updated successfully'); + + header('Location: /orders'); + } + + public function destroy($id) + { + $order = WorkOrder::find($id); + $order->delete(); + + Session::set('success', 'Order deleted successfully'); + + header('Location: /orders'); + } +} diff --git a/src/app/Controllers/ZoneController.php b/src/app/Controllers/ZoneController.php new file mode 100644 index 00000000..47bef0a9 --- /dev/null +++ b/src/app/Controllers/ZoneController.php @@ -0,0 +1,74 @@ + 'Zones', + 'title' => 'Zones', + 'layout' => 'MainLayout', + 'data' => ['zones' => $zones], + ]); + } + + public function create() + { + View::render([ + 'view' => 'Zone/Create', + 'title' => 'Add Zone', + 'layout' => 'MainLayout', + 'data' => [], + ]); + } + + public function store($postData) + { + $zone = new Zone; + $zone->name = $postData['name']; + $zone->postal_code = $postData['postal_code']; + $zone->point_id = $postData['point_id']; + + $zone->save(); + + header('Location: /zones'); + } + + public function edit($id) + { + $zone = Zone::find($id); + View::render([ + 'view' => 'Zone/Edit', + 'title' => 'Edit Zone', + 'layout' => 'MainLayout', + 'data' => ['zone' => $zone], + ]); + } + + public function update($id, $postData) + { + $zone = Zone::find($id); + + $zone->name = $postData['name']; + $zone->postal_code = $postData['postal_code']; + $zone->point_id = $postData['point_id']; + + $zone->save(); + + header('Location: /zones'); + } + + public function destroy($id) + { + $zone = Zone::find($id); + $zone->delete(); + + header('Location: /zones'); + } +} diff --git a/src/app/Core/Database.php b/src/app/Core/Database.php index b4b96ddf..d4ea9581 100755 --- a/src/app/Core/Database.php +++ b/src/app/Core/Database.php @@ -2,31 +2,33 @@ namespace App\Core; +use Exception; use PDO; use PDOException; class Database { - private static $instance = null; + private static $instance; public static function connect() { - if (!self::$instance) { + if (! self::$instance) { try { // Read the password from the file $db_pass = trim(file_get_contents(getenv('DB_PASS_FILE_PATH'))); self::$instance = new PDO( - "mysql:host=" . getenv('DB_HOST') . ";dbname=" . getenv('DB_NAME'), + 'mysql:host='.getenv('DB_HOST').';dbname='.getenv('DB_NAME'), getenv('DB_USER'), $db_pass ); self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { - Logger::log("Database connection error: " . $e->getMessage()); - throw new \Exception("Database connection failed."); + Logger::log('Database connection error: '.$e->getMessage()); + throw new Exception('Database connection failed.'); } } + return self::$instance; } @@ -36,10 +38,10 @@ public static function prepareAndExecute($query, $params = [], $fetchMode = PDO: try { $stmt = $db->prepare($query); $stmt->execute($params); + return $stmt->fetchAll($fetchMode); } catch (PDOException $e) { - Logger::log("Database query error: " . $e->getMessage()); - throw new \Exception("Query failed."); + return null; } } } diff --git a/src/app/Core/Logger.php b/src/app/Core/Logger.php index f96ed199..4bdebfc2 100755 --- a/src/app/Core/Logger.php +++ b/src/app/Core/Logger.php @@ -6,9 +6,9 @@ class Logger { public static function log($message, $level = 'info') { - $logFile = getenv("LOG_FILE_PATH"); - $logMessage = strtoupper($level) . ' - ' . date('Y-m-d H:i:s') . ' - ' . $message . PHP_EOL; - if (!file_exists($logFile)) { + $logFile = getenv('LOG_FILE_PATH'); + $logMessage = strtoupper($level).' - '.date('Y-m-d H:i:s').' - '.$message.PHP_EOL; + if (! file_exists($logFile)) { touch($logFile); } file_put_contents($logFile, $logMessage, FILE_APPEND); diff --git a/src/app/Core/Router.php b/src/app/Core/Router.php index b3b25c58..cc4a83f5 100755 --- a/src/app/Core/Router.php +++ b/src/app/Core/Router.php @@ -2,53 +2,108 @@ namespace App\Core; +use App\Middlewares\MiddlewareInterface; +use Exception; + class Router { - protected $routes = []; + protected const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; + + protected array $routes = []; - public function load($file) + public function load(string $file): void { - $routes = include $file; - $this->routes = $routes; + $this->routes = include $file; } - public function dispatch($requestMethod, $requestUri) + public function dispatch(string $requestMethod, string $requestUri, array $postData = []): void { - // Check for direct match first + if (! in_array($requestMethod, self::HTTP_METHODS)) { + $this->abort(405, 'Method Not Allowed'); + + return; + } + + // Match exact route if (isset($this->routes[$requestMethod][$requestUri])) { - return $this->callRoute($this->routes[$requestMethod][$requestUri], $requestMethod === 'POST' ? $_POST : []); + $this->callRoute($this->routes[$requestMethod][$requestUri], $requestMethod === 'POST' ? ['postData' => $postData] : []); + + return; } - // If no direct match, check for dynamic routes with parameters + // Match dynamic route with parameters foreach ($this->routes[$requestMethod] as $route => $routeInfo) { - $routePattern = preg_replace('/:\w+/', '(\w+)', $route); - $pattern = '#^' . $routePattern . '$#'; - - if (preg_match($pattern, $requestUri, $matches)) { - array_shift($matches); // Remove the full match + $routePattern = preg_replace('/:\w+/', '([^/]+)', $route); + if (preg_match("#^{$routePattern}$#", $requestUri, $matches)) { + array_shift($matches); $params = $this->extractParams($route, $matches); - return $this->callRoute($routeInfo, $params, $requestMethod === 'POST' ? $_POST : []); + $arguments = $requestMethod === 'POST' ? array_merge($params, ['postData' => $postData]) : $params; + + $this->callRoute($routeInfo, $arguments); + + return; } } - echo "404 Not Found"; + $this->abort(404, 'Not Found'); + } + + public function redirect(string $uri, int $statusCode = 302): void + { + http_response_code($statusCode); + header("Location: {$uri}"); + exit; } - protected function extractParams($route, $matches) + protected function extractParams(string $route, array $matches): array { preg_match_all('/:(\w+)/', $route, $paramNames); + return array_combine($paramNames[1], $matches); } - protected function callRoute($routeInfo, $params = [], $postData = []) + protected function callRoute(array $routeInfo, array $arguments = []): void + { + if (! class_exists($routeInfo['controller'])) { + $this->abort(500, "Controller {$routeInfo['controller']} not found"); + + return; + } + + if (! method_exists($routeInfo['controller'], $routeInfo['method'])) { + $this->abort(500, "Method {$routeInfo['method']} not found in controller {$routeInfo['controller']}"); + + return; + } + + if (isset($routeInfo['middleware'])) { + $this->handleMiddleware($routeInfo['middleware']); + } + + $controller = new $routeInfo['controller']; + $controller->{$routeInfo['method']}(...$arguments); + } + + protected function handleMiddleware(array $middlewares): void { - $controller = new $routeInfo['controller'](); - $method = $routeInfo['method']; + foreach ($middlewares as $middlewareClass) { + if (! class_exists($middlewareClass)) { + throw new Exception("Middleware class {$middlewareClass} not found"); + } - // Combine route parameters and POST data - $arguments = array_merge(array_values($params), [$postData]); + $middleware = new $middlewareClass; + if (! $middleware instanceof MiddlewareInterface) { + throw new Exception("Middleware {$middlewareClass} must implement MiddlewareInterface"); + } - // Unpack the combined array into the method call - $controller->$method(...$arguments); + $middleware->handle($_REQUEST, fn () => null); + } + } + + protected function abort(int $statusCode, string $message): void + { + http_response_code($statusCode); + echo json_encode(['error' => $message]); + exit; } } diff --git a/src/app/Core/Session.php b/src/app/Core/Session.php index 3fd9100b..bc015264 100755 --- a/src/app/Core/Session.php +++ b/src/app/Core/Session.php @@ -6,17 +6,12 @@ class Session { public static function start() { - if (session_status() === PHP_SESSION_NONE) { - session_set_cookie_params([ - 'lifetime' => 0, - 'path' => '/', - 'domain' => $_SERVER['HTTP_HOST'], - 'secure' => isset($_SERVER['HTTPS']), - 'httponly' => true, - 'samesite' => 'Strict' - ]); - session_start(); - } + session_start(); + } + + public static function has($key) + { + return isset($_SESSION[$key]); } public static function set($key, $value) diff --git a/src/app/Core/View.php b/src/app/Core/View.php index 4f4d04bd..0f8ec4d6 100755 --- a/src/app/Core/View.php +++ b/src/app/Core/View.php @@ -6,22 +6,22 @@ class View { public static function render($options = []) { - $title = $options['title'] ?? "Default Title"; - $layout = $options['layout'] ?? "MainLayout"; - $view = $options['view'] ?? "Home"; + $title = $options['title'] ?? 'Default Title'; + $layout = $options['layout'] ?? 'MainLayout'; + $view = $options['view'] ?? 'Home'; $data = $options['data'] ?? []; - if (!file_exists(__DIR__ . "/../Views/{$view}.php")) { + if (! file_exists(__DIR__."/../Views/{$view}.php")) { throw new \Exception("View file not found: {$view}"); } extract($data); ob_start(); - require_once __DIR__ . "/../Views/{$view}.php"; + require_once __DIR__."/../Views/{$view}.php"; $content = ob_get_clean(); - if (file_exists(__DIR__ . "/../Layouts/{$layout}.php")) { - require_once __DIR__ . "/../Layouts/{$layout}.php"; + if (file_exists(__DIR__."/../Layouts/{$layout}.php")) { + require_once __DIR__."/../Layouts/{$layout}.php"; } else { throw new \Exception("Layout file not found: {$layout}"); } diff --git a/src/app/Layouts/AuthLayout.php b/src/app/Layouts/AuthLayout.php index 9ff89ef6..a9fd08bc 100644 --- a/src/app/Layouts/AuthLayout.php +++ b/src/app/Layouts/AuthLayout.php @@ -4,24 +4,28 @@
-