Initial project push.
This commit is contained in:
83
src/Controller/ApiWebhookController.php
Normal file
83
src/Controller/ApiWebhookController.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Controller;
|
||||
|
||||
use Origo\Controller\ControllerBase;
|
||||
use Origo\Controller\ControllerInterface;
|
||||
use Origo\Services\Database;
|
||||
|
||||
class ApiWebhookController extends ControllerBase implements ControllerInterface {
|
||||
|
||||
/**
|
||||
* The database service.
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
protected Database $database;
|
||||
|
||||
/**
|
||||
* Add common services.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->database = new Database();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response.
|
||||
*
|
||||
* @return string
|
||||
* The response.
|
||||
*/
|
||||
public function getResponse(): string {
|
||||
header('Access-Control-Allow-Origin: *'); // Allow all origins.
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
// Handle preflight OPTIONS request
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit(); // Stop here - don't process the request
|
||||
}
|
||||
// Make sure we have a path and account before proceeding.
|
||||
$path = $this->request->post('path');
|
||||
$account_id = $this->request->post('account');
|
||||
if (
|
||||
!$path ||
|
||||
!$account_id
|
||||
) {
|
||||
header("HTTP/1.1 422 Unprocessable Entity");
|
||||
die();
|
||||
}
|
||||
// @todo: validate the account id.
|
||||
$entry = [
|
||||
'page' => $path,
|
||||
'account_id' => $account_id,
|
||||
'user_agent' => $this->request->post('user_agent'),
|
||||
'referrer' => $this->request->post('referrer'),
|
||||
'ip' => $this->request->ip(),
|
||||
];
|
||||
$clean_entry = array_filter($entry);
|
||||
$keys = array_keys($clean_entry);
|
||||
$key_string = implode(',', $keys);
|
||||
$placeholders = ':' . implode(', :', $keys);
|
||||
$query = $this->database->prepare("INSERT INTO pageviews ($key_string) VALUES ($placeholders)");
|
||||
foreach ($clean_entry as $key => $value) {
|
||||
$query->bindValue(":$key", $value);
|
||||
}
|
||||
$success = $query->execute();
|
||||
|
||||
if ($success) {
|
||||
$response = [
|
||||
"SUCCESS" => "Entry has been stored.",
|
||||
];
|
||||
header('Content-Type: application/json');
|
||||
}
|
||||
else {
|
||||
header("HTTP/1.1 422 Unprocessable Entity");
|
||||
die();
|
||||
}
|
||||
|
||||
return json_encode($response);
|
||||
}
|
||||
|
||||
}
|
||||
40
src/Controller/ControllerBase.php
Normal file
40
src/Controller/ControllerBase.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Controller;
|
||||
|
||||
use Origo\Services\Database;;
|
||||
use Origo\Services\Renderer;
|
||||
use Origo\Services\Request;
|
||||
|
||||
abstract class ControllerBase {
|
||||
|
||||
/**
|
||||
* The request service.
|
||||
*
|
||||
* @var Request
|
||||
*/
|
||||
protected Request $request;
|
||||
|
||||
/**
|
||||
* The renderer service.
|
||||
*
|
||||
* @var Renderer
|
||||
*/
|
||||
protected Renderer $renderer;
|
||||
|
||||
/**
|
||||
* The database service.
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
protected Database $database;
|
||||
|
||||
/**
|
||||
* Add common services.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->request = new Request();
|
||||
$this->renderer = new Renderer();
|
||||
$this->database = new Database();
|
||||
}
|
||||
}
|
||||
9
src/Controller/ControllerInterface.php
Normal file
9
src/Controller/ControllerInterface.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Controller;
|
||||
|
||||
use Origo\Services\Request;
|
||||
|
||||
interface ControllerInterface {
|
||||
public function getResponse(): string;
|
||||
}
|
||||
29
src/Controller/MainController.php
Normal file
29
src/Controller/MainController.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Controller;
|
||||
|
||||
use Origo\Controller\ControllerBase;
|
||||
use Origo\Controller\ControllerInterface;
|
||||
use Origo\Services\Template;
|
||||
|
||||
class MainController extends ControllerBase implements ControllerInterface {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response.
|
||||
*/
|
||||
public function getResponse(): string {
|
||||
$session_id = $this->request->getCookie('ORIGOSESS');
|
||||
if($session_id) {
|
||||
header("Location: /dashboard");
|
||||
die();
|
||||
}
|
||||
$template = new Template('main.html');
|
||||
|
||||
return $template->render();
|
||||
}
|
||||
|
||||
}
|
||||
64
src/Controller/PlannerController.php
Normal file
64
src/Controller/PlannerController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Controller;
|
||||
|
||||
use Origo\Entity\User;
|
||||
use Origo\Controller\ControllerInterface;
|
||||
use Origo\Services\Template;
|
||||
use Origo\Services\Request;
|
||||
use Origo\Services\Renderer;
|
||||
|
||||
class PlannerController extends ControllerBase implements ControllerInterface {
|
||||
|
||||
public function getResponse(): string {
|
||||
|
||||
// Okie dokie. so here i need to get the users id,
|
||||
// then load all the tasks for that user.
|
||||
// I guess sorting by their creation date would be good.
|
||||
$user = new User();
|
||||
$user->loadUserFromSession($this->request->getCookie('ORIGOSESS'));
|
||||
$query = $this->database->query("SELECT * FROM tasks WHERE user_id = (:user_id) ORDER BY created_at DESC");
|
||||
$user_id = $user->get('id');
|
||||
$query->bindParam(':user_id', $user_id);
|
||||
$query->execute();
|
||||
$results = $query->fetchAll();
|
||||
|
||||
$tasks = '<div class="task-container">';
|
||||
|
||||
if ($results) {
|
||||
foreach ($results as $result) {
|
||||
$title = htmlspecialchars($result['task']);
|
||||
$status = htmlspecialchars($result['status']);
|
||||
$description = htmlspecialchars($result['description']);
|
||||
$url = htmlspecialchars($result['external_url']);
|
||||
|
||||
$statusClass = (strtolower($status) === 'completed') ? 'status-completed' : 'status-pending';
|
||||
|
||||
$tasks .= '
|
||||
<div class="task-card">
|
||||
<div class="task-info">
|
||||
<h3 class="task-title">' . $title . '</h3>
|
||||
<div class="task-description">' . $description . '</div>';
|
||||
|
||||
if ($url) {
|
||||
$tasks .= '<a href="' . $url . '" class="btn btn-soft-clay" target="_blank">View Details →</a>';
|
||||
}
|
||||
|
||||
$tasks .= '
|
||||
</div>
|
||||
<div class="task-status ' . $statusClass . '">' . $status . '</div>
|
||||
</div>';
|
||||
}
|
||||
} else {
|
||||
$tasks .= '<p style="text-align:center; color:var(--color-medium-gray);">No tasks yet.</p>';
|
||||
}
|
||||
|
||||
$tasks .= '</div>';
|
||||
|
||||
$template = new Template('task-list.html');
|
||||
return $template->render([
|
||||
'tasks' => $tasks,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
22
src/Controller/UserDashboardController.php
Normal file
22
src/Controller/UserDashboardController.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Controller;
|
||||
|
||||
use Origo\Entity\User;
|
||||
use Origo\Controller\ControllerInterface;
|
||||
use Origo\Services\Template;
|
||||
use Origo\Services\Request;
|
||||
use Origo\Services\Renderer;
|
||||
|
||||
class UserDashboardController implements ControllerInterface {
|
||||
|
||||
/**
|
||||
* Get the response for the dashboard.
|
||||
*/
|
||||
public function getResponse(): string {
|
||||
$template = new Template('dashboard.html');
|
||||
|
||||
return $template->render();
|
||||
}
|
||||
|
||||
}
|
||||
37
src/Controller/UserLoginController.php
Normal file
37
src/Controller/UserLoginController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Controller;
|
||||
|
||||
use Origo\Controller\ControllerInterface;
|
||||
use Origo\Services\Request;
|
||||
use Origo\Services\Renderer;
|
||||
use Origo\Services\Template;
|
||||
use Origo\Entity\User;
|
||||
|
||||
class UserLoginController implements ControllerInterface {
|
||||
|
||||
private $request;
|
||||
private Renderer $renderer;
|
||||
|
||||
public function __construct() {
|
||||
$this->request = new Request();
|
||||
$this->renderer = new Renderer();
|
||||
}
|
||||
|
||||
public function getResponse(): string {
|
||||
$username = $this->request->post('username') ?? FALSE;
|
||||
$password = $this->request->post('password') ?? FALSE;
|
||||
$pw_hash = hash('sha256', $password);
|
||||
// Check if user can login.
|
||||
$user = new User();
|
||||
$session_id = $user->login($username, $pw_hash);
|
||||
|
||||
if (!$session_id) {
|
||||
return $this->renderer->render403();
|
||||
}
|
||||
else {
|
||||
header("Location: /dashboard");
|
||||
die();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Entity/Task.php
Normal file
22
src/Entity/Task.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Origo\Entity;
|
||||
|
||||
final class Task {
|
||||
|
||||
protected int $id;
|
||||
protected int $user_id;
|
||||
protected string $task;
|
||||
protected string $status;
|
||||
protected string $external_url;
|
||||
protected string $created_at;
|
||||
|
||||
public function load(int $task_id) {
|
||||
// TODO: Implement load() method.
|
||||
// Load the task from the database to the object.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
83
src/Entity/User.php
Normal file
83
src/Entity/User.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Entity;
|
||||
|
||||
use Origo\Services\Database;
|
||||
|
||||
class User {
|
||||
|
||||
protected $id;
|
||||
protected $username;
|
||||
protected $email;
|
||||
protected $permissions = [];
|
||||
protected $database;
|
||||
|
||||
public function __construct() {
|
||||
$this->database = new Database();
|
||||
}
|
||||
|
||||
public function get($property) {
|
||||
return $this->{$property};
|
||||
}
|
||||
|
||||
public function set($property, $value) {
|
||||
$this->{$property} = $value;
|
||||
}
|
||||
|
||||
public function getPermissions() {
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
public function hasPermission($permission) {
|
||||
return in_array($permission, $this->permissions);
|
||||
}
|
||||
|
||||
public function login($username, $password) {
|
||||
$statement = $this->database->prepare("SELECT username, id from users where username = ? AND password = ?");
|
||||
$statement->execute([
|
||||
$username,
|
||||
$password
|
||||
]);
|
||||
$result = $statement->fetch();
|
||||
if (!$result) {
|
||||
return FALSE;
|
||||
}
|
||||
return $this->createSession($result['id']);
|
||||
}
|
||||
|
||||
private function createSession(int $user_id = 0) {
|
||||
// Bail out early if id is less than 1.
|
||||
if ($user_id < 1) {
|
||||
return FALSE;
|
||||
}
|
||||
$session_id = 'ORIGO' . bin2hex(random_bytes(16));
|
||||
$stmt = $this->database->prepare("INSERT INTO sessions (user_id, session_id) VALUES (:user_id, :session_id)");
|
||||
$stmt->bindParam(':user_id', $user_id);
|
||||
$stmt->bindParam(':session_id', $session_id);
|
||||
$success = $stmt->execute();
|
||||
if ($success) {
|
||||
setcookie("ORIGOSESS", $session_id, time() + 3600);
|
||||
return $session_id;
|
||||
}
|
||||
}
|
||||
|
||||
public function loadUserFromSession(string $session_id) {
|
||||
$statement = $this->database->prepare("SELECT user_id FROM sessions WHERE session_id = ?");
|
||||
$statement->execute([$session_id]);
|
||||
$result = $statement->fetch();
|
||||
$this->id = $result['user_id'] ?? '';
|
||||
$this->loadPermissions();
|
||||
}
|
||||
|
||||
private function loadPermissions(): void {
|
||||
$statement = $this->database->prepare("SELECT permission from permissions WHERE user_id = ?");
|
||||
$statement->execute([$this->id]);
|
||||
$result = $statement->fetch();
|
||||
if ($result) {
|
||||
foreach ($result as $permission) {
|
||||
$this->permissions[] = $permission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
17
src/Services/Database.php
Normal file
17
src/Services/Database.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace Origo\Services;
|
||||
|
||||
use PDO;
|
||||
|
||||
class Database extends PDO {
|
||||
|
||||
public $connection;
|
||||
|
||||
public function __construct() {
|
||||
$database = '/app/database/origo.db';
|
||||
parent::__construct("sqlite:{$database}");
|
||||
$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
}
|
||||
17
src/Services/Renderer.php
Normal file
17
src/Services/Renderer.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Services;
|
||||
|
||||
use Origo\Services\Template;
|
||||
|
||||
final class Renderer {
|
||||
|
||||
/**
|
||||
* Render a 403 page.
|
||||
*/
|
||||
public function render403() {
|
||||
$template = new Template('403.html');
|
||||
return $template->render();
|
||||
}
|
||||
|
||||
}
|
||||
89
src/Services/Request.php
Normal file
89
src/Services/Request.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Services;
|
||||
|
||||
class Request {
|
||||
|
||||
/**
|
||||
* Get a POST parameter.
|
||||
*
|
||||
* @param string $key
|
||||
* The key.
|
||||
* @param mixed $default
|
||||
* The default value.
|
||||
* @return mixed
|
||||
* The value.
|
||||
*/
|
||||
public function post(string $key, $default = NULL) {
|
||||
$input_stream = file_get_contents('php://input');
|
||||
$input = [];
|
||||
if ($input_stream) {
|
||||
$parsed = parse_str($input_stream, $input);
|
||||
}
|
||||
else {
|
||||
$input = $_POST;
|
||||
}
|
||||
return $input[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a GET parameter.
|
||||
*
|
||||
* @param string $key
|
||||
* The key.
|
||||
* @param mixed $default
|
||||
* The default value.
|
||||
* @return mixed
|
||||
* The value.
|
||||
*/
|
||||
public function get(string $key, $default = NULL) {
|
||||
return $_GET[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request method.
|
||||
*
|
||||
* @return string
|
||||
* The method.
|
||||
*/
|
||||
public function method(): string {
|
||||
return $_SERVER['REQUEST_METHOD'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request IP.
|
||||
*
|
||||
* @return string
|
||||
* The IP.
|
||||
*/
|
||||
public function ip(): string {
|
||||
// Check common proxy headers first
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($ips[0]);
|
||||
}
|
||||
|
||||
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
return $_SERVER['HTTP_X_REAL_IP'];
|
||||
}
|
||||
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
return $_SERVER['HTTP_CLIENT_IP'];
|
||||
}
|
||||
|
||||
// Fallback to REMOTE_ADDR
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cookie.
|
||||
*
|
||||
* @param string $key
|
||||
* The key.
|
||||
* @return string
|
||||
* The value.
|
||||
*/
|
||||
public function getCookie(string $key): string {
|
||||
return $_COOKIE[$key] ?? '';
|
||||
}
|
||||
}
|
||||
55
src/Services/Router.php
Normal file
55
src/Services/Router.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Services;
|
||||
|
||||
class Router {
|
||||
|
||||
/**
|
||||
* The routes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $routes;
|
||||
|
||||
/**
|
||||
* Add common services.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->routes = [
|
||||
'/' => \Origo\Controller\MainController::class,
|
||||
'/planner' => \Origo\Controller\PlannerController::class,
|
||||
'/dashboard' => \Origo\Controller\UserDashboardController::class,
|
||||
'/user-login' => \Origo\Controller\UserLoginController::class,
|
||||
'/api/v1/webhook' => \Origo\Controller\ApiWebhookController::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route controller.
|
||||
*
|
||||
* @param string $path
|
||||
* The path.
|
||||
* @return string
|
||||
* The controller.
|
||||
*/
|
||||
public function getRouteController(string $path): string {
|
||||
return $this->routes[$path];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the route exists.
|
||||
*
|
||||
* @param string $path
|
||||
* The path.
|
||||
* @return bool
|
||||
*/
|
||||
public function routeExists(string $path): bool {
|
||||
$route_exists = FALSE;
|
||||
if (array_key_exists($path, $this->routes)) {
|
||||
$route_exists = TRUE;
|
||||
}
|
||||
|
||||
return $route_exists;
|
||||
}
|
||||
|
||||
}
|
||||
65
src/Services/Template.php
Normal file
65
src/Services/Template.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Origo\Services;
|
||||
|
||||
class Template {
|
||||
|
||||
/**
|
||||
* The template to render.
|
||||
*/
|
||||
protected $template;
|
||||
|
||||
/**
|
||||
* Creates a new Template object.
|
||||
*/
|
||||
public function __construct($template_name) {
|
||||
$this->template = file_get_contents('templates/' . $template_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the template and returns the result.
|
||||
*/
|
||||
public function render(array $replacements = []) {
|
||||
$this->swapHtml();
|
||||
|
||||
if ($replacements) {
|
||||
$this->replaceSections($replacements);
|
||||
}
|
||||
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace dynamic sections.
|
||||
*
|
||||
* @param array $swaps
|
||||
* An array of placeholders and their replacements.
|
||||
*/
|
||||
public function replaceSections(array $swaps) {
|
||||
foreach ($swaps as $placeholder => $contents) {
|
||||
$placeholder = "{{ " . $placeholder . " }}";
|
||||
$this->template = str_replace($placeholder, $contents, $this->template);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all sections in the template with the contents of the included file.
|
||||
*/
|
||||
public function swapHtml() {
|
||||
preg_match_all('/\[\[\s*(.+?)\s*\]\]/', $this->template, $matches);
|
||||
|
||||
foreach ($matches[0] as $i => $placeholder) {
|
||||
$filename = $matches[1][$i];
|
||||
$template_path = rtrim('templates', '/') . '/' . $filename;
|
||||
|
||||
if (file_exists($template_path)) {
|
||||
$contents = file_get_contents($template_path);
|
||||
}
|
||||
else {
|
||||
$contents = "<!-- Missing include: {$filename} -->";
|
||||
}
|
||||
$this->template = str_replace($placeholder, $contents, $this->template);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user