From 965ab70d54d98d1c24990f8ede99e52c4e4615b0 Mon Sep 17 00:00:00 2001 From: Lumerel Deploy Date: Mon, 16 Feb 2026 23:27:32 +0000 Subject: [PATCH] Deploy from Lumerel --- .docker/99-migrate.sh | 54 ++++++++ .dockerignore | 3 + Dockerfile | 23 ++++ composer.json | 15 +++ migrate.php | 33 +++++ migrations/001_create_users_table.php | 9 ++ migrations/002_create_projects_table.php | 10 ++ migrations/003_create_tasks_table.php | 13 ++ public/dashboard.html | 142 ++++++++++++++++++++ public/index.html | 132 ++++++++++++++++++ public/index.php | 151 +++++++++++++++++++++ public/project.html | 162 +++++++++++++++++++++++ src/Controllers/AuthController.php | 54 ++++++++ src/Controllers/ProjectController.php | 36 +++++ src/Controllers/TaskController.php | 21 +++ src/Models/Project.php | 31 +++++ src/Models/Task.php | 30 +++++ src/Models/User.php | 37 ++++++ src/db.php | 15 +++ 19 files changed, 971 insertions(+) create mode 100644 .docker/99-migrate.sh create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 composer.json create mode 100644 migrate.php create mode 100644 migrations/001_create_users_table.php create mode 100644 migrations/002_create_projects_table.php create mode 100644 migrations/003_create_tasks_table.php create mode 100644 public/dashboard.html create mode 100644 public/index.html create mode 100644 public/index.php create mode 100644 public/project.html create mode 100644 src/Controllers/AuthController.php create mode 100644 src/Controllers/ProjectController.php create mode 100644 src/Controllers/TaskController.php create mode 100644 src/Models/Project.php create mode 100644 src/Models/Task.php create mode 100644 src/Models/User.php create mode 100644 src/db.php diff --git a/.docker/99-migrate.sh b/.docker/99-migrate.sh new file mode 100644 index 0000000..66b83d4 --- /dev/null +++ b/.docker/99-migrate.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# IMPORTANT: This script is sourced (not executed) by the webdevops entrypoint. +# Never use "exit" — it would kill the parent entrypoint and prevent supervisord from starting. +# Use "return" instead to leave this script without affecting the parent. + +echo ">>> Waiting for database connection..." +echo ">>> DB_HOST=${DB_HOST:-NOT SET}" +echo ">>> DB_PORT=${DB_PORT:-NOT SET}" +echo ">>> DB_DATABASE=${DB_DATABASE:-NOT SET}" +echo ">>> DB_USERNAME=${DB_USERNAME:-NOT SET}" +echo ">>> DB_PASSWORD is $([ -n "$DB_PASSWORD" ] && echo 'set' || echo 'NOT SET')" + +if [ -z "$DB_HOST" ] || [ -z "$DB_USERNAME" ]; then + echo ">>> ERROR: DB_HOST or DB_USERNAME not set. Skipping migrations." + return 0 2>/dev/null || true +fi + +_migrate_max_retries=30 +_migrate_count=0 +_migrate_done=false + +while [ $_migrate_count -lt $_migrate_max_retries ]; do + if php -r ' + try { + $host = getenv("DB_HOST"); + $port = getenv("DB_PORT") ?: "3306"; + $user = getenv("DB_USERNAME"); + $pass = getenv("DB_PASSWORD"); + new PDO("mysql:host=$host;port=$port", $user, $pass, [PDO::ATTR_TIMEOUT => 3]); + exit(0); + } catch (Throwable $e) { + fwrite(STDERR, "PDO error: " . $e->getMessage() . "\n"); + exit(1); + } + ' 2>&1; then + echo ">>> Database is reachable. Running migrations..." + if php /app/migrate.php; then + echo ">>> Migrations completed successfully." + else + echo ">>> WARNING: migrate.php exited with code $?" + fi + _migrate_done=true + break + fi + + _migrate_count=$((_migrate_count + 1)) + echo ">>> Waiting for database... attempt $_migrate_count/$_migrate_max_retries" + sleep 2 +done + +if [ "$_migrate_done" = false ]; then + echo ">>> WARNING: Could not connect to database after $_migrate_max_retries attempts. Skipping migrations." +fi \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17896fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9590b17 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM webdevops/php-nginx:8.3-alpine +ENV WEB_DOCUMENT_ROOT=/app/public +ARG CACHE_BUST=1771284452 +COPY . /app +RUN echo "index index.php index.html index.htm;" > /opt/docker/etc/nginx/vhost.common.d/01-index.conf \ + && echo "add_header Cache-Control 'no-cache, no-store, must-revalidate';" > /opt/docker/etc/nginx/vhost.common.d/02-no-cache.conf +RUN set -e; if [ -f /app/composer.json ]; then \ + echo ">>> composer.json found, installing dependencies..."; \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \ + cd /app && composer install --no-dev --no-interaction --optimize-autoloader; \ + ls -la /app/vendor/autoload.php; \ + else \ + echo ">>> No composer.json found, skipping composer install"; \ + fi +RUN set -e; if [ -f /app/package.json ]; then \ + echo ">>> package.json found, installing node dependencies..."; \ + apk add --no-cache nodejs npm; \ + cd /app && npm install --production; \ + else \ + echo ">>> No package.json found, skipping npm install"; \ + fi +COPY --chmod=755 .docker/99-migrate.sh /opt/docker/provision/entrypoint.d/99-migrate.sh +RUN chown -R application:application /app \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a8bfbd9 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "taskit/project-management", + "description": "Modern Project Management System", + "type": "project", + "require": { + "php": "^8.1", + "ext-pdo": "*", + "ext-json": "*" + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} \ No newline at end of file diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..62aa430 --- /dev/null +++ b/migrate.php @@ -0,0 +1,33 @@ +exec("CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + migration VARCHAR(255) NOT NULL UNIQUE, + ran_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +)"); + +// Get already run migrations +$stmt = $pdo->query("SELECT migration FROM migrations"); +$ranMigrations = $stmt->fetchAll(PDO::FETCH_COLUMN); + +// Scan migrations directory +$migrationFiles = glob('migrations/*.php'); +sort($migrationFiles); + +foreach ($migrationFiles as $migrationFile) { + $migrationName = basename($migrationFile); + + if (!in_array($migrationName, $ranMigrations)) { + require_once $migrationFile; + + // Record migration as run + $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)"); + $stmt->execute([$migrationName]); + + echo "Ran migration: $migrationName\n"; + } +} + +echo "Migrations complete."; \ No newline at end of file diff --git a/migrations/001_create_users_table.php b/migrations/001_create_users_table.php new file mode 100644 index 0000000..0d18825 --- /dev/null +++ b/migrations/001_create_users_table.php @@ -0,0 +1,9 @@ +exec("CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +)"); \ No newline at end of file diff --git a/migrations/002_create_projects_table.php b/migrations/002_create_projects_table.php new file mode 100644 index 0000000..88d1bab --- /dev/null +++ b/migrations/002_create_projects_table.php @@ -0,0 +1,10 @@ +exec("CREATE TABLE IF NOT EXISTS projects ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + user_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +)"); \ No newline at end of file diff --git a/migrations/003_create_tasks_table.php b/migrations/003_create_tasks_table.php new file mode 100644 index 0000000..9dc2321 --- /dev/null +++ b/migrations/003_create_tasks_table.php @@ -0,0 +1,13 @@ +exec("CREATE TABLE IF NOT EXISTS tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + project_id INT NOT NULL, + user_id INT NOT NULL, + status ENUM('created', 'in_progress', 'testing', 'complete', 'approved') DEFAULT 'created', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +)"); \ No newline at end of file diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 0000000..9da3ea5 --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,142 @@ + + + + + + TaskIt! - Dashboard + + + + + + + +
+
+
+
+
+
Create New Project
+
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..980dff1 --- /dev/null +++ b/public/index.html @@ -0,0 +1,132 @@ + + + + + + TaskIt! - Project Management + + + + + +
+ +
+

TaskIt!

+
+
+ +
+
+ +
+ +

Don't have an account? Register

+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..d04d3b9 --- /dev/null +++ b/public/index.php @@ -0,0 +1,151 @@ + 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'svg' => 'image/svg+xml', + ]; + if (isset($mimeTypes[$extension])) { + header('Content-Type: ' . $mimeTypes[$extension]); + } + readfile($filePath); + exit; + } + } +} + +// Headers for JSON responses +header('Content-Type: application/json'); + +try { + switch (true) { + case $requestUri === '/api/register' && $method === 'POST': + $data = json_decode(file_get_contents('php://input'), true); + $authController = new AuthController(); + echo json_encode($authController->register( + $data['name'], + $data['email'], + $data['password'] + )); + break; + + case $requestUri === '/api/login' && $method === 'POST': + $data = json_decode(file_get_contents('php://input'), true); + $authController = new AuthController(); + echo json_encode($authController->login( + $data['email'], + $data['password'] + )); + break; + + case $requestUri === '/api/logout' && $method === 'POST': + $authController = new AuthController(); + echo json_encode($authController->logout()); + break; + + case $requestUri === '/api/projects' && $method === 'GET': + if (!AuthController::isAuthenticated()) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + break; + } + $projectController = new ProjectController(); + echo json_encode($projectController->getUserProjects()); + break; + + case $requestUri === '/api/projects' && $method === 'POST': + if (!AuthController::isAuthenticated()) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + break; + } + $data = json_decode(file_get_contents('php://input'), true); + $projectController = new ProjectController(); + echo json_encode($projectController->createProject( + $data['name'], + $data['description'] + )); + break; + + case preg_match('/^\/api\/projects\/(\d+)$/', $requestUri, $matches) && $method === 'GET': + if (!AuthController::isAuthenticated()) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + break; + } + $projectController = new ProjectController(); + echo json_encode($projectController->getProjectDetails($matches[1])); + break; + + case $requestUri === '/api/tasks' && $method === 'POST': + if (!AuthController::isAuthenticated()) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + break; + } + $data = json_decode(file_get_contents('php://input'), true); + $taskController = new TaskController(); + echo json_encode($taskController->createTask( + $data['name'], + $data['description'], + $data['project_id'], + $data['status'] ?? 'created' + )); + break; + + case preg_match('/^\/api\/tasks\/(\d+)\/status$/', $requestUri, $matches) && $method === 'PUT': + if (!AuthController::isAuthenticated()) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + break; + } + $data = json_decode(file_get_contents('php://input'), true); + $taskController = new TaskController(); + echo json_encode($taskController->updateTaskStatus( + $matches[1], + $data['status'] + )); + break; + + default: + http_response_code(404); + echo json_encode(['error' => 'Not Found']); + break; + } +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} diff --git a/public/project.html b/public/project.html new file mode 100644 index 0000000..c227175 --- /dev/null +++ b/public/project.html @@ -0,0 +1,162 @@ + + + + + + TaskIt! - Project Details + + + + + + + +
+
+
+
+
+
Create New Task
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+

+

+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php new file mode 100644 index 0000000..669176f --- /dev/null +++ b/src/Controllers/AuthController.php @@ -0,0 +1,54 @@ +userModel = new User(); + } + + public function register($name, $email, $password) { + if ($this->userModel->findByEmail($email)) { + return ['error' => 'Email already exists']; + } + + $result = $this->userModel->register($name, $email, $password); + + if ($result) { + $user = $this->userModel->findByEmail($email); + $_SESSION['user_id'] = $user['id']; + return ['success' => true, 'user' => $user]; + } + + return ['error' => 'Registration failed']; + } + + public function login($email, $password) { + $user = $this->userModel->login($email, $password); + + if ($user) { + $_SESSION['user_id'] = $user['id']; + return ['success' => true, 'user' => $user]; + } + + return ['error' => 'Invalid credentials']; + } + + public function logout() { + session_destroy(); + return ['success' => true]; + } + + public static function isAuthenticated() { + return isset($_SESSION['user_id']); + } + + public static function getCurrentUserId() { + return $_SESSION['user_id'] ?? null; + } +} \ No newline at end of file diff --git a/src/Controllers/ProjectController.php b/src/Controllers/ProjectController.php new file mode 100644 index 0000000..39d9cc7 --- /dev/null +++ b/src/Controllers/ProjectController.php @@ -0,0 +1,36 @@ +projectModel = new Project(); + $this->taskModel = new Task(); + } + + public function createProject($name, $description) { + $userId = AuthController::getCurrentUserId(); + return $this->projectModel->create($name, $description, $userId); + } + + public function getUserProjects() { + $userId = AuthController::getCurrentUserId(); + return $this->projectModel->getUserProjects($userId); + } + + public function getProjectDetails($projectId) { + $userId = AuthController::getCurrentUserId(); + $project = $this->projectModel->getProjectById($projectId, $userId); + $tasks = $this->taskModel->getProjectTasks($projectId); + + return [ + 'project' => $project, + 'tasks' => $tasks + ]; + } +} \ No newline at end of file diff --git a/src/Controllers/TaskController.php b/src/Controllers/TaskController.php new file mode 100644 index 0000000..bed52f4 --- /dev/null +++ b/src/Controllers/TaskController.php @@ -0,0 +1,21 @@ +taskModel = new Task(); + } + + public function createTask($name, $description, $projectId, $status = 'created') { + $userId = AuthController::getCurrentUserId(); + return $this->taskModel->create($name, $description, $projectId, $userId, $status); + } + + public function updateTaskStatus($taskId, $status) { + return $this->taskModel->updateTaskStatus($taskId, $status); + } +} \ No newline at end of file diff --git a/src/Models/Project.php b/src/Models/Project.php new file mode 100644 index 0000000..a60755b --- /dev/null +++ b/src/Models/Project.php @@ -0,0 +1,31 @@ +pdo = $pdo; + } + + public function create($name, $description, $userId) { + $stmt = $this->pdo->prepare("INSERT INTO projects (name, description, user_id) VALUES (?, ?, ?)"); + $stmt->execute([$name, $description, $userId]); + return $this->pdo->lastInsertId(); + } + + public function getUserProjects($userId) { + $stmt = $this->pdo->prepare("SELECT * FROM projects WHERE user_id = ? ORDER BY created_at DESC"); + $stmt->execute([$userId]); + return $stmt->fetchAll(); + } + + public function getProjectById($projectId, $userId) { + $stmt = $this->pdo->prepare("SELECT * FROM projects WHERE id = ? AND user_id = ?"); + $stmt->execute([$projectId, $userId]); + return $stmt->fetch(); + } +} \ No newline at end of file diff --git a/src/Models/Task.php b/src/Models/Task.php new file mode 100644 index 0000000..7b4111f --- /dev/null +++ b/src/Models/Task.php @@ -0,0 +1,30 @@ +pdo = $pdo; + } + + public function create($name, $description, $projectId, $userId, $status = 'created') { + $stmt = $this->pdo->prepare("INSERT INTO tasks (name, description, project_id, user_id, status) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$name, $description, $projectId, $userId, $status]); + return $this->pdo->lastInsertId(); + } + + public function getProjectTasks($projectId) { + $stmt = $this->pdo->prepare("SELECT * FROM tasks WHERE project_id = ? ORDER BY created_at DESC"); + $stmt->execute([$projectId]); + return $stmt->fetchAll(); + } + + public function updateTaskStatus($taskId, $status) { + $stmt = $this->pdo->prepare("UPDATE tasks SET status = ? WHERE id = ?"); + return $stmt->execute([$status, $taskId]); + } +} \ No newline at end of file diff --git a/src/Models/User.php b/src/Models/User.php new file mode 100644 index 0000000..57f876d --- /dev/null +++ b/src/Models/User.php @@ -0,0 +1,37 @@ +pdo = $pdo; + } + + public function register($name, $email, $password) { + $hashedPassword = password_hash($password, PASSWORD_BCRYPT); + + $stmt = $this->pdo->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)"); + return $stmt->execute([$name, $email, $hashedPassword]); + } + + public function login($email, $password) { + $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + return $user; + } + return false; + } + + public function findByEmail($email) { + $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + return $stmt->fetch(); + } +} \ No newline at end of file diff --git a/src/db.php b/src/db.php new file mode 100644 index 0000000..c308929 --- /dev/null +++ b/src/db.php @@ -0,0 +1,15 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4' + ] + ); +} catch (PDOException $e) { + die("Database connection failed: " . $e->getMessage()); +} \ No newline at end of file