Initial project push.

This commit is contained in:
dan612
2026-01-15 09:50:55 -05:00
commit c4312feb95
226 changed files with 32233 additions and 0 deletions

View 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);
}
}

View 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();
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Origo\Controller;
use Origo\Services\Request;
interface ControllerInterface {
public function getResponse(): string;
}

View 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();
}
}

View 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,
]);
}
}

View 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();
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}
}
}