This commit is contained in:
Frederico Falcao 2025-05-30 10:46:17 +01:00
commit f2a6525224
51 changed files with 1870 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
vendor/
business/_databaseCredentials.php
tech/backlog.json
tech/tabs/03-third-parties/passwords.json.enc

7
_authenticate.php Normal file
View File

@ -0,0 +1,7 @@
<?php
session_start();
if (!isset($_SESSION["authenticated"]) || $_SESSION["authenticated"] !== true ) {
header("Location: /login.php");
exit;
}

37
business/_database.php Normal file
View File

@ -0,0 +1,37 @@
<?php
require_once __DIR__."/databaseCredentials.php";
// DSN (Data Source Name)
$dsn = "pgsql:host=$db_host;port=$db_port;dbname=$db_name";
try {
// Create PDO instance
$pdo = new PDO($dsn, $db_user, $db_password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
echo 'Database error: ' . $e->getMessage();
}
function db_select($tblName, $cols = [], $filter = []) {
global $pdo;
// Defaults for fitler
if (is_array($filter) && empty($filter)) $filter = ["true"];
if (is_string($filter)) $filter = [$filter];
// Execute query
try {
$sql_query = 'SELECT '.implode(",",$cols).' FROM "'.$tblName.'" WHERE '.implode(" AND ",$filter).';';
$stmt = $pdo->query($sql_query);
} catch (PDOException $e) {
echo "DB QUERY ERROR: ".$e->getMessage(); die("\n\nwhile trying: $sql_query");
}
// Fetch and display results
$response = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $response;
}

30
business/_navbar.php Normal file
View File

@ -0,0 +1,30 @@
<?php require_once __DIR__."/../_authenticate.php"; ?>
<!-- TOP NAVBAR (Layer 1) -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard/sales.php">📊 Admin Panel</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#topNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="topNavbar">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link active" href="dashboard/sales.php">dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="dashboard/reports.php">Reports</a></li>
<li class="nav-item"><a class="nav-link" href="dashboard/settings.php">Settings</a></li>
</ul>
<span class="navbar-text text-light">Logged in as Admin</span>
</div>
</div>
</nav>
<!-- SUBNAV -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link active" href="dashboard/sales.php">Sales</a></li>
<li class="nav-item"><a class="nav-link" href="dashboard/customers.php">Customers</a></li>
<li class="nav-item"><a class="nav-link" href="dashboard/subscriptions.php">Subscriptions</a></li>
<li class="nav-item"><a class="nav-link" href="dashboard/traffic.php">Traffic</a></li>
</ul>
</div>
</nav>

View File

@ -0,0 +1,55 @@
<?php
ob_start(); require_once "../_navbar.php"; $navbar_html = ob_get_clean();
// Example customer data — in real life, you'd fetch this from Supabase/Postgres
$customers = [
['name' => 'John Doe', 'email' => 'john@example.com', 'joined' => '2024-12-01', 'status' => 'Active'],
['name' => 'Jane Smith', 'email' => 'jane@example.com', 'joined' => '2025-01-15', 'status' => 'Active'],
['name' => 'Carlos Perez', 'email' => 'carlos@example.com', 'joined' => '2025-03-10', 'status' => 'Inactive'],
];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Customers Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<?= $navbar_html ?>
<div class="container mt-4">
<h1 class="mb-4">👥 Customers</h1>
<table class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Email</th>
<th>Joined Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer): ?>
<tr>
<td><?= htmlspecialchars($customer['name']) ?></td>
<td><?= htmlspecialchars($customer['email']) ?></td>
<td><?= htmlspecialchars($customer['joined']) ?></td>
<td>
<?php if ($customer['status'] === 'Active'): ?>
<span class="badge bg-success">Active</span>
<?php else: ?>
<span class="badge bg-secondary">Inactive</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,82 @@
<?php
// Example data — you can later pull this from Supabase or Postgres!
$salesToday = "$1,250";
$salesThisMonth = "$18,500";
$topProduct = "Keto Meal Plan";
$newOrders = 42;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sales Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.metric-card {
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
text-align: center;
}
.metric-value {
font-size: 2rem;
font-weight: bold;
}
.metric-label {
font-size: 1.1rem;
color: #555;
}
</style>
</head>
<body class="bg-light">
<?php require "../_navbar.php"; ?>
<div class="container mt-4">
<h1 class="mb-4">📈 Sales Overview</h1>
<div class="row">
<div class="col-md-3">
<div class="metric-card">
<div class="metric-value"><?= htmlspecialchars($salesToday) ?></div>
<div class="metric-label">Today's Sales</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div class="metric-value"><?= htmlspecialchars($salesThisMonth) ?></div>
<div class="metric-label">This Month</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div class="metric-value"><?= htmlspecialchars($newOrders) ?></div>
<div class="metric-label">New Orders</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div class="metric-value"><?= htmlspecialchars($topProduct) ?></div>
<div class="metric-label">Top Product</div>
</div>
</div>
</div>
<h2 class="mt-5">Recent Orders</h2>
<table class="table table-striped">
<thead>
<tr><th>Order ID</th><th>Customer</th><th>Amount</th><th>Status</th></tr>
</thead>
<tbody>
<tr><td>#1001</td><td>John Doe</td><td>$125.00</td><td><span class="badge bg-success">Completed</span></td></tr>
<tr><td>#1002</td><td>Jane Smith</td><td>$79.00</td><td><span class="badge bg-warning">Pending</span></td></tr>
<tr><td>#1003</td><td>Bob Johnson</td><td>$59.99</td><td><span class="badge bg-danger">Cancelled</span></td></tr>
</tbody>
</table>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

106
business/index.php Normal file
View File

@ -0,0 +1,106 @@
<?php
// 0.1 Include external libs
require_once __DIR__."/../vendor/autoload.php";
// 0.2 Require authentication
require_once __DIR__."/../_authenticate.php";
// 0.3 Enable SQL database connection
require_once __DIR__."/_database.php";
$metrics = [
'Total Sales' => '$25,300',
'New Customers' => '120',
'Active Subscriptions' => '350',
'Churn Rate' => '2.5%',
'Monthly Revenue' => '$7,400',
'Site Visitors Today' => '1,540',
];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Business Metrics Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.metric-card {
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
text-align: center;
height: 100%;
}
.metric-value {
font-size: 1.8rem;
font-weight: bold;
}
.metric-label {
font-size: 1rem;
color: #666;
}
</style>
</head>
<body>
<?php require __DIR__."/_navbar.php"; ?>
<!-- MAIN CONTENT -->
<div class="container mt-4">
<h1 class="mb-4">Key Metrics</h1>
<div class="row">
<?php foreach ($metrics as $label => $value): ?>
<div class="col-md-4">
<div class="metric-card">
<div class="metric-value"><?= htmlspecialchars($value) ?></div>
<div class="metric-label"><?= htmlspecialchars($label) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
<br/><br/>
<h1 class="mb-4">Cohort Analysis</h1>
<div class="row">
<?php
// Variables
$tblName = "Customers"; $cols = ["name", "email", "locale"];
// Fetch data
$rows = db_select($tblName, $cols);
?>
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead class="table-light">
<tr>
<?php foreach ($cols as $col): ?>
<th><?= htmlspecialchars(ucfirst($col)) ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<?php foreach ($cols as $col): ?>
<td><?= htmlspecialchars($row[$col]) ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<footer class="text-center mt-5 mb-3 text-muted">
&copy; <?= date("Y") ?> GrowFit Oy
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

29
index.php Normal file
View File

@ -0,0 +1,29 @@
<?php require_once __DIR__."/_authenticate.php"; ?>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-5">
<h2 class="mb-4 text-center"><i class="bi bi-check"></i> Logged in</h2>
<div class="d-grid gap-3">
<a href="tech/" class="btn btn-primary btn-lg"><i class="bi bi-laptop"></i> Tech Admin</a>
<a href="business/" class="btn btn-success btn-lg"><i class="bi bi-briefcase"></i> Business Admin</a>
<a href="logout.php" class="btn btn-outline-secondary">Logout</a>
</div>
</div>
</div>
</div>
</body>
</html>

51
login.php Normal file
View File

@ -0,0 +1,51 @@
<?php
session_start();
// Set your desired password here
$correctPassword = 'chillinglittlebit'.date("d");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$inputPassword = $_POST['password'] ?? '';
if ($inputPassword === $correctPassword) {
$_SESSION['authenticated'] = true;
header('Location: index.php'); // Reload to show the two buttons
exit;
} else {
$error = "❌ Incorrect password.";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-5">
<h2 class="mb-4 text-center">🔐 Please Login</h2>
<?php if (!empty($error)): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST" class="card p-4 shadow-sm bg-white">
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</body>
</html>

6
logout.php Normal file
View File

@ -0,0 +1,6 @@
<?php
session_start();
session_destroy();
header('Location: index.php');
exit;
?>

View File

@ -0,0 +1,16 @@
<?php
$apiToken = '2fce';
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
if (!preg_match('/Bearer\s(\S+)/', $authHeader, $matches) || $apiToken !== $matches[1]) {
// ❌ Unauthorized
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => 'Unauthorized']);
exit;
}

View File

@ -0,0 +1,7 @@
<?php
define('DB_HOST', 'aws-0-eu-north-1.pooler.supabase.com');
define('DB_PORT', '5432');
define('DB_NAME', 'postgres');
define('DB_USER', 'postgres.gaeuzfjsnfttifmfuvol');
define('DB_PASSWORD', 'b=Nh8YQPa3LP3nH<3');

101
tech/api/_dbFuncs.php Normal file
View File

@ -0,0 +1,101 @@
<?php
function connectToDb() {
static $db = null;
if ($db === null) {
$dsn = "pgsql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME;
try {
$db = new PDO($dsn, DB_USER, DB_PASSWORD);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("❌ Connection failed: " . $e->getMessage());
}
}
return $db;
}
function insertInto($table, $data) {
$db = connectToDb();
$columns = '"' . implode('","', array_keys($data)) . '"';
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO \"$table\" ($columns) VALUES ($placeholders)";
$stmt = $db->prepare($sql);
$stmt->execute($data);
return $db->lastInsertId();
}
function selectFrom($table, $conditions = []) {
$db = connectToDb();
$whereClause = '';
if (!empty($conditions)) {
$clauses = [];
foreach ($conditions as $key => $value) {
$clauses[] = "\"$key\" = :$key";
}
$whereClause = 'WHERE ' . implode(' AND ', $clauses);
}
$sql = "SELECT * FROM \"$table\" $whereClause";
$stmt = $db->prepare($sql);
$stmt->execute($conditions);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function update($table, $data, $conditions) {
$db = connectToDb();
$setParts = [];
foreach ($data as $key => $value) {
$setParts[] = "\"$key\" = :set_$key";
}
$setClause = implode(', ', $setParts);
$whereParts = [];
foreach ($conditions as $key => $value) {
$whereParts[] = "\"$key\" = :where_$key";
}
$whereClause = implode(' AND ', $whereParts);
$sql = "UPDATE \"$table\" SET $setClause WHERE $whereClause";
$params = [];
foreach ($data as $key => $value) {
$params['set_' . $key] = $value;
}
foreach ($conditions as $key => $value) {
$params['where_' . $key] = $value;
}
$stmt = $db->prepare($sql);
return $stmt->execute($params);
}
function upsert($table, $data, $conflictColumns) {
$db = connectToDb();
$columns = '"' . implode('","', array_keys($data)) . '"';
$placeholders = ':' . implode(', :', array_keys($data));
$updateParts = [];
foreach ($data as $key => $value) {
$updateParts[] = "\"$key\" = EXCLUDED.\"$key\"";
}
$updateClause = implode(', ', $updateParts);
$conflicts = '"' . implode('","', (array)$conflictColumns) . '"';
$sql = "INSERT INTO \"$table\" ($columns) VALUES ($placeholders)
ON CONFLICT ($conflicts) DO UPDATE SET $updateClause";
$stmt = $db->prepare($sql);
return $stmt->execute($data);
}

View File

@ -0,0 +1,3 @@
<?php
define("TELEGRAM_BOT_TOKEN","8095944700:AAGjybT203bk1RteZSlXCtQ8ndmNMrpsNAw");
define("TELEGRAM_CHAT_ID",'-4618727626'); // GrowFit eCommerce

View File

@ -0,0 +1,36 @@
<?php
/**
* Sends a message to a Telegram chat.
*
* @param string $message The message text.
* @return bool True on success, false on failure.
*/
function telegramSendMessage($message) {
$url = "https://api.telegram.org/bot" . TELEGRAM_BOT_TOKEN . "/sendMessage";
$postData = [
'chat_id' => TELEGRAM_CHAT_ID,
'text' => $message
];
// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
error_log("Telegram cURL error: " . curl_error($ch));
curl_close($ch);
return false;
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 200;
}

View File

@ -0,0 +1,16 @@
<?php
require_once __DIR__."/_authenticateApiRequest.php";
// ✅ Authenticated
header('Content-Type: application/json');
//file_put_contents(date("Y-m-d_H-i-s").".json", file_get_contents("php://input"));
echo json_encode(['message' => '✅ Authenticated request']);
/*
*
* HERE SHOULD BE THE CODE TO RUN: NPM BUILD, etc...
* (it's associated to deploy button in (upload.php)
*
*/
exit;

View File

@ -0,0 +1,96 @@
<?php
require_once __DIR__. "/_authenticateApiRequest.php"; // ✅ Authenticated
require_once __DIR__."/_dbCredentials.php";
require_once __DIR__."/_dbFuncs.php";
require_once __DIR__."/_telegramCredentials.php";
require_once __DIR__."/_telegramFunctions.php";
header('Content-Type: application/json');
// Define only the suffixes, not the full "api::..." strings
$reactToContentTypes = [
"api::meal-prep.meal-prep" =>
[
"sqlTableName" => "MealPrep",
"columns" => [
"name"
],
"keyColumn" => "strapiDocumentId"
],
"api::meal-plan.meal-plan" =>
[
"sqlTableName" => "MealPlan",
"columns" => [
"name",
"numberOfItems",
"price",
],
"keyColumn" => "strapiDocumentId"
],
"api::single-meal.single-meal" =>
[
"sqlTableName" => "SingleMeal",
"columns" => [
"name"
],
"keyColumn" => "strapiDocumentId"
],
];
// Get raw POST body once
$rawInput = file_get_contents("php://input");
$event = json_decode($rawInput, true);
// Handle malformed JSON
if (!is_array($event) || !isset($event["uid"])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON or missing uid']);
exit;
}
telegramSendMessage("✏️ New content updated at STRAPI (".$event["uid"].")");
try {
// 🔎 Check if the CONTENT-TYPE (uid) is in the list we care about
if (in_array($event["uid"], array_keys($reactToContentTypes))) {
// 🎯 Get content type settings (SQL table & columns mapping)
$contentType = $reactToContentTypes[$event["uid"]];
$sqlTblName = $contentType["sqlTableName"];
// 🏗 Build the data array for upsert
$cols = [];
foreach ($contentType["columns"] as $c) {
if (isset($event["entry"][$c])) {
$cols[$c] = $event["entry"][$c];
} else {
// You might want to handle missing fields here
$cols[$c] = null;
}
}
// 🔑 Get the unique key column for upsert
$keyColumn = $contentType["keyColumn"];
// 🚀 Run the UPSERT (insert or update)
$success = upsert($sqlTblName, $cols, $keyColumn);
if ($success) {
telegramSendMessage("🔄 Data successfully propagated to SUPABASE");
} else {
telegramSendMessage("❌ Upsert failed");
}
} else {
// ⚠️ uid not in the list of content types we're watching
telegramSendMessage(' No action taken for uid: ' . $event["uid"]);
}
} catch (Exception $e) {
// 🚨 Catch and report any exceptions
http_response_code(500);
echo json_encode(['error' => 'Server Error','details' => $e->getMessage()]);
telegramSendMessage("500 Server Error: (".__FILE__.") ".$e->getMessage());
}
exit;

66
tech/index.php Normal file
View File

@ -0,0 +1,66 @@
<?php
// 1. INCLUDE External Libraries
require_once __DIR__."/../vendor/autoload.php";
// 2. REQUIRE Authentication (login)
require_once __DIR__."/../_authenticate.php";
$tabsDir = __DIR__ . '/tabs';
$directories = array_filter(glob($tabsDir . '/*'), 'is_dir');
//
// TABS (for all tabs)
//
//
// 1. Include constants
foreach (glob($tabsDir . '/*/01_constants.php') as $file) require_once $file;
// 2. Include support functions
foreach (glob($tabsDir . '/*/02_supportFuncs.php') as $file) require_once $file;
// 3. Include handlers
foreach (glob($tabsDir . '/*/03_handler.php') as $file) require_once $file;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GROWFIT ADMIN</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
</head>
<body>
<div class="container py-5">
<a href="/" class="btn btn-sm btn-outline-secondary" > back to main</a>
<br/>
<br/>
<br/>
<h1><i class="bi bi-laptop"></i> Tech Admin Portal</h1>
<br/>
<ul class="nav nav-tabs" role="tablist">
<?php foreach (glob($tabsDir . '/*/04_nav-item.html.php') as $file) require_once $file; ?>
</ul>
<div class="tab-content">
<br/>
<?php foreach (glob($tabsDir . '/*/05_content.html.php') as $file) require_once $file; ?>
</div>
</div>
<?php foreach (glob($tabsDir . '/*/06_modals.html.php') as $file) require_once $file; ?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
<?php foreach (glob($tabsDir . '/*/07_javascript.js') as $file) require_once $file; ?>
</script>
<script>
<?php if (isset($_GET["tab"])) : ?>
(new bootstrap.Tab('[data-bs-target="#<?=$_GET["tab"]?>"]')).show();
<?php endif; ?>
</script>
</body>
</html>

View File

@ -0,0 +1,56 @@
# 01-deploy — Deploy Tab Documentation
This tab provides a web interface for source code deployment and management via Git.
## Features
- **Deploy a specific commit**: Checkout a chosen commit and run build scripts with one click.
- **Pull and merge ZIP from GitHub/GitLab**: Download a repo archive at a specific commit and merge it into the current working tree.
- **Direct ZIP upload**: Upload a ZIP file from your computer to merge and commit to the repository.
- **Browse commit history**: View recent commits, examine their details in a modal, and deploy any commit to a chosen target.
- **Multiple deploy targets**: Supports easy deployment to `www` (production), `beta`, or `beta-2` via dropdown actions.
## File Overview
- `01_constants.php` — Defines `$repoDir`, the local path to your code repository. Used everywhere the repo is referenced.
- `02_supportFuncs.php` — Helper functions for:
- Running Git commands
- Listing commits (`listGitCommits()`)
- Storing and loading last deploy info (`getLastDeployData()`, `setLastDeployData()`)
- Executing shell command sequences with formatted output (`runShellSteps()`)
- Sanitizing inputs for hashes/user paths (`sanitizeRepoInput()`)
- `03_handler.php` — Main backend logic:
- Handles POST/GET for deploys, ZIP pulls, uploads, and commit info retrieval.
- Echoes info/error/success blocks for each operation.
- All sensitive input is sanitized before use in shell commands.
- `04_nav-item.html.php` — Contains the navigation tab button HTML for activating the Deploy tab.
- `05_content.html.php` — Main UI HTML:
- Forms for ZIP pull/upload.
- Table of recent commits, each with deploy dropdown for all targets.
- Commit history linked to the modal for details.
- `06_modals.html.php` — Bootstrap modal for commit details, loaded on demand by JavaScript.
- `07_javascript.js`
- Handles dynamic loading of commit info into the modal via AJAX/fetch.
- Live URL preview while typing GitHub repo/hash for ZIP pulls.
## Usage
1. **Deploy a commit**: Click the dropdown next to a commit, select your target, and the backend will checkout, install, build, and deploy that revision.
2. **Merge from GitHub**: Fill in username/repo/hash and click to pull & merge that remote ZIP into your codebase.
3. **Upload ZIP**: Upload a ZIP file (from external export or other CI pipeline) and have its content merged and committed to the repo.
4. **View commit details**: Click any commit in the table to view the full diff, author, date, and message in a modal.
## Security & Safety
- Inputs are sanitized before being passed to shell or git commands.
- Only users with web access to the admin panel can trigger deploy steps.
- All output from shell commands is displayed to the user for transparency.
## Customization
- To update what happens on deployment, edit the `$steps` arrays in `03_handler.php`.
- For new deploy targets, adjust forms and handler logic as needed.
- The build sequence (npm, asto, rsync, etc.) can be tailored for your tech stack.
---
This tab is a minimal self-service deployment panel fitting teams who need to pull, merge, and deploy new code quickly, review git history, and provide a safe admin pathway for ops and releases.

View File

@ -0,0 +1,10 @@
<?php
/*
* FILE: 01_contants.php
* Description: Support functions that can be used across the app
*/
$gitDir = "/ExtraSpace/GROWFIT_WEBSITE_GIT_DIR";
?>

View File

@ -0,0 +1,57 @@
<?php
// FILE: 02_supportFuncs.php
// Description: Support and utility functions
function listGitCommits($repoDir, $limit = 20) {
global $gitDir;
$cmd = "GIT_DIR=$gitDir git log --pretty=format:\"%H|%ad|%an|%s\" --date=short -n " . intval($limit);
exec($cmd, $output);
$commits = [];
foreach ($output as $line) {
list($hash, $date, $author, $subject) = explode('|', $line, 4);
$commits[] = compact('hash', 'date', 'author', 'subject');
}
return $commits;
}
function getLastDeployData($repoDir) {
$jsonFile = $repoDir . '/deploy_last.json';
if (file_exists($jsonFile)) {
$data = json_decode(file_get_contents($jsonFile), true);
if (is_array($data)) return $data;
}
return ['username'=>'', 'repo'=>'', 'hash'=>''];
}
function setLastDeployData($repoDir, $data) {
$jsonFile = $repoDir . '/deploy_last.json';
file_put_contents($jsonFile, json_encode($data));
}
function runShellSteps($steps, $cwd = null) {
// Runs an array of ['desc'=>..., 'cmd'=>...] steps, prints output and returns overall success
$allOk = true;
echo "<pre class='bg-light p-3 border'><strong>Shell Output:</strong>\n";
foreach ($steps as $step) {
$desc = $step['desc'] ?? '';
$cmd = $step['cmd'];
echo "\n# $desc\n$ $cmd\n";
passthru(($cwd ? "cd ".escapeshellarg($cwd)." && " : "") . "$cmd 2>&1", $ret);
if ($ret !== 0 && stripos($desc, 'commit') === false) {
echo "\n❌ Error in step: $desc\n";
$allOk = false;
break;
}
}
echo "</pre>";
return $allOk;
}
function sanitizeRepoInput($input, $type = 'alphanum') {
// Usage: sanitizeRepoInput($_POST['repo']), etc
if ($type === 'hash') return preg_replace('/[^a-fA-F0-9]/', '', $input);
if ($type === 'repo') return preg_replace('/[^a-zA-Z0-9_-]/', '', $input);
return trim($input);
}
?>

View File

@ -0,0 +1,126 @@
<?php
/*
* FILE: 03_handler.php
* Description: Handles the input provided from the browser/user
*/
// 1. Handle deploy of specific commit
if (isset($_POST['deploy_commit'])) {
global $gitDir;
$commit = sanitizeRepoInput($_POST['deploy_commit'], 'hash');
$target = $_POST['deploy_target'] ?? 'default';
$folders = [
"www" => "/ExtraSpace/www.growfit.fi",
"beta" => "/ExtraSpace/beta.growfit.fi",
"alpha" => "/ExtraSpace/alpha.growfit.fi",
];
if (!isset($folders[$target])) {
echo "<div class='alert alert-danger'>❌ INTERNAL ERROR: Deployment folder doesn't exist.</div>";
return;
}
$gitWorkTree = $folders[$target];
if ($commit) {
echo "<div class='alert alert-info'>🚀 Deploying commit: <strong>$commit</strong> to <strong>$target</strong></div>";
$steps = [
['desc'=>"Clear the target folder", 'cmd'=>"rm -rf $gitWorkTree; mkdir $gitWorkTree"],
['desc'=>"Checkout commit", 'cmd'=>"GIT_DIR=$gitDir git archive $commit | tar -x -C $gitWorkTree"],
['desc'=>"Install NPM packages", 'cmd'=>"cd $gitWorkTree && npm install"],
['desc'=>"Set Astro prerender", 'cmd'=>"cd $gitWorkTree && find src/ -type f -name \"*.astro\" -exec sed -i 's/prerender = false/prerender = true/g' {} +"],
['desc'=>"Build site", 'cmd'=>"cd $gitWorkTree && npm run build"]
];
$allOk = runShellSteps($steps);
if ($allOk) {
echo "<div class='alert alert-success'>✅ Commit $commit deployed to $target.</div>";
} else {
echo "<div class='alert alert-danger'>❌ One or more commands failed.</div>";
}
}
}
// 3. GET COMMIT INFO
if (isset($_GET['get_commit_info'])) {
$hash = sanitizeRepoInput($_GET['get_commit_info'], 'hash');
if (!$hash) {
echo '<div class="alert alert-danger">Invalid commit hash.</div>'; exit;
}
$cmd = gitCmd($repoDir, 'show --stat --pretty=format:"Commit: %H%nAuthor: %an <%ae>%nDate: %ad%n%n%s%n%b" --date=iso ' . escapeshellarg($hash) . ' -m');
exec($cmd . ' 2>&1', $output, $retval);
if ($retval !== 0) {
echo '<div class="alert alert-danger">Error fetching commit info.</div>'; exit;
}
echo '<pre style="white-space: pre-wrap;">'.htmlspecialchars(implode("\n", $output)).'</pre>'; exit;
}
// 4. PULL MERGE ZIP
if (isset($_POST['pull_merge_zip'])) {
$username = sanitizeRepoInput($_POST['username'], 'repo');
$repo = sanitizeRepoInput($_POST['repo'], 'repo');
$hash = sanitizeRepoInput($_POST['hash'], 'hash');
if ($username && $repo && $hash) {
setLastDeployData($repoDir, ['username'=>$username,'repo'=>$repo,'hash'=>$hash]);
$zipUrl = "https://github.com/$username/$repo/archive/$hash.zip";
$tmpZip = "/tmp/{$repo}_$hash.zip";
$tmpExtract = "/tmp/{$repo}_$hash";
$extractedSubdir = "$tmpExtract/{$repo}-$hash";
$commitMsg = "Pulled ZIP from $username/$repo@$hash";
echo "<div class='alert alert-info'>📥 Downloading ZIP from: <code>$zipUrl</code></div>";
$steps = [
['desc'=>"Download ZIP", 'cmd'=>"wget -q -O ".escapeshellarg($tmpZip)." ".escapeshellarg($zipUrl)],
['desc'=>"Clean old extraction", 'cmd'=>"rm -rf ".escapeshellarg($tmpExtract)],
['desc'=>"Extract ZIP", 'cmd'=>"unzip -q ".escapeshellarg($tmpZip)." -d ".escapeshellarg($tmpExtract)],
['desc'=>"Rsync to repo", 'cmd'=>"rsync -a --delete --exclude='.git' $extractedSubdir/ $repoDir/"],
['desc'=>"Git add", 'cmd'=>gitCmd($repoDir, "add -A")],
['desc'=>"Git commit", 'cmd'=>gitCmd($repoDir, "commit -m " . escapeshellarg($commitMsg))]
];
$allOk = runShellSteps($steps);
if ($allOk) {
echo "<div class='alert alert-success'>✅ ZIP merged and committed as: <code>$commitMsg</code></div>";
} else {
echo "<div class='alert alert-danger'>❌ One or more commands failed.</div>";
}
} else {
echo "<div class='alert alert-danger'>❌ Invalid input for username/repo/hash.</div>";
}
}
// 5. Handle direct ZIP upload & merge
if (isset($_POST['upload_zip_btn']) && isset($_FILES['upload_zip']) && $_FILES['upload_zip']['error'] === UPLOAD_ERR_OK) {
$uploadedFile = $_FILES['upload_zip'];
$commitMsg = trim($_POST['commit_msg'] ?? '');
if (!$commitMsg) $commitMsg = "Uploaded ZIP: " . $uploadedFile['name'] . " on " . date('Y-m-d H:i');
$tmpZip = $uploadedFile['tmp_name'];
$zipName = basename($uploadedFile['name']);
$tmpExtract = "/tmp/upload_zip_" . uniqid();
mkdir($tmpExtract, 0777, true);
echo "<div class='alert alert-info'>📤 Uploading & extracting: <code>$zipName</code></div>";
$extractDir = $tmpExtract . ((strpos($uploadedFile['name'], "project-bolt-") === 0) ? "/project" : "");
$steps = [
['desc'=>"Extract ZIP", 'cmd'=>"unzip -q " . escapeshellarg($tmpZip) . " -d " . escapeshellarg($tmpExtract)],
['desc'=>"Git add", 'cmd'=>"GIT_DIR=$gitDir GIT_WORK_TREE=".escapeshellarg($extractDir)." git add -A"],
['desc'=>"Git commit", 'cmd'=>"GIT_DIR=$gitDir GIT_WORK_TREE=".escapeshellarg($extractDir)." git commit -m ".escapeshellarg($commitMsg)],
['desc'=>"Pushing to GITHUB", 'cmd'=>"GIT_DIR=$gitDir GIT_WORK_TREE=".escapeshellarg($extractDir)." git push"],
['desc'=>"Cleaning Up", 'cmd'=>"rm -rf " . escapeshellarg($tmpExtract)],
];
$allOk = runShellSteps($steps);
if ($allOk) {
echo "<div class='alert alert-success'>✅ ZIP merged and committed: <code>" . htmlspecialchars($commitMsg) . "</code></div>";
} else {
echo "<div class='alert alert-warning'> No changes to commit, or an error occurred.</div>";
}
}
?>

View File

@ -0,0 +1,10 @@
<?php /*
FILE: 04_nav-item.html.php
Description: generates the tab link/button, in the navbar
*/ ?>
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#deploy">
<i class="bi bi-rocket-takeoff"></i>
Deploy
</button>
</li>

View File

@ -0,0 +1,95 @@
<?php /*
FILE: 05_content.html.php
Description: the main HTML/PHP content of this tab
*/
?>
<!-- Deploy Tab -->
<div class="tab-pane fade show active" id="deploy">
<!-- Form to pull/merge from public repo -->
<form method="POST" action="?tab=deploy" class="mb-4">
<label class="form-label">Pull an archive ZIP of the entire Codebase from GitHub/GitLab</label>
<p id="github_url_zip"><a href="#" target="_blank"><code>https://github.com/USERNAME/REPO/archive/HASH.zip</code></a></p>
<div class="input-group">
<?php $last = getLastDeployData($repoDir); ?>
<input class="form-control" type="text" name="username" placeholder="USERNAME" value="<?= htmlspecialchars($last['username']) ?>">
<input class="form-control" type="text" name="repo" placeholder="REPO" value="<?= htmlspecialchars($last['repo']) ?>">
<input class="form-control" type="text" name="hash" placeholder="HASH" value="<?= htmlspecialchars($last['hash']) ?>">
<button class="btn btn-primary" type="submit" name="pull_merge_zip">Pull Hash</button>
</div>
</form>
<hr/>
<!-- Direct Upload ZIP Form -->
<form method="POST" action="?tab=deploy" enctype="multipart/form-data" class="mb-4">
<label class="form-label">Directly Upload ZIP</label>
<div class="input-group">
<input class="form-control" type="file" name="upload_zip" accept=".zip" >
<input class="form-control" type="text" name="commit_msg" placeholder="Commit message (optional)">
<button class="btn btn-primary" type="submit" name="upload_zip_btn">Upload & Merge ZIP</button>
</div>
<div class="form-text">The ZIP will be extracted and committed to the repository. Bolt.new /project auto-unwrapped.</div>
</form>
<table class="table table-bordered table-hover mt-4">
<thead>
<tr>
<th>Commit</th>
<th>Date</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
require_once "02_supportFuncs.php";
foreach (listGitCommits($repoDir) as $commit):
?>
<tr>
<td style="font-family:monospace">
<a href="#"
data-bs-toggle="modal"
data-bs-target="#commitInfoModal"
data-commit-hash="<?= htmlspecialchars($commit['hash']) ?>"
style="text-decoration:underline; cursor:pointer"
title="Show commit details">
<?= htmlspecialchars($commit['hash']) ?>
</a>
</td>
<td><?= htmlspecialchars($commit['date']) ?></td>
<td><?= nl2br(htmlspecialchars($commit['subject'])) ?></td>
<td>
<div class="btn-group">
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
🚀 Deploy
</button>
<ul class="dropdown-menu">
<li>
<form method="POST" action="?tab=deploy" class="d-inline">
<input type="hidden" name="deploy_commit" value="<?= htmlspecialchars($commit['hash']) ?>">
<input type="hidden" name="deploy_target" value="www">
<button type="submit" class="dropdown-item">Deploy to www (production)</button>
</form>
</li>
<li>
<form method="POST" action="?tab=deploy" class="d-inline">
<input type="hidden" name="deploy_commit" value="<?= htmlspecialchars($commit['hash']) ?>">
<input type="hidden" name="deploy_target" value="beta">
<button type="submit" class="dropdown-item">Deploy to beta</button>
</form>
</li>
<li>
<form method="POST" action="?tab=deploy" class="d-inline">
<input type="hidden" name="deploy_commit" value="<?= htmlspecialchars($commit['hash']) ?>">
<input type="hidden" name="deploy_target" value="beta-2">
<button type="submit" class="dropdown-item">Deploy to beta-2</button>
</form>
</li>
</ul>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,23 @@
<?php /*
FILE: 06_modals.html.php
Description: any extra modal, hidden windows that get displayed when user clicks something
*/ ?>
<!-- Commit Info Modal -->
<div class="modal fade" id="commitInfoModal" tabindex="-1" aria-labelledby="commitInfoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="commitInfoModalLabel">Commit Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="commitModalBody">
<div class="text-center text-muted p-5">
<div class="spinner-border"></div>
<div>Loading…</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,46 @@
//
// FILE: 07_javascript.js
// Description: the javascript code that will be added to the website
document.addEventListener('DOMContentLoaded', function() {
var commitInfoModal = document.getElementById('commitInfoModal');
if (commitInfoModal) {
commitInfoModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var hash = button.getAttribute('data-commit-hash');
var modalBody = document.getElementById('commitModalBody');
modalBody.innerHTML = `<div class="text-center text-muted p-5">
<div class="spinner-border"></div>
<div>Loading</div>
</div>`;
fetch('get_commit_info.php?hash=' + encodeURIComponent(hash))
.then(resp => resp.text())
.then(html => { modalBody.innerHTML = html; })
.catch(() => {
modalBody.innerHTML = '<div class="alert alert-danger">Failed to load commit info.</div>';
});
});
}
});
document.addEventListener('DOMContentLoaded', function () {
const usernameInput = document.querySelector('input[name="username"]');
const repoInput = document.querySelector('input[name="repo"]');
const hashInput = document.querySelector('input[name="hash"]');
const urlPreview = document.getElementById('github_url_zip');
function updateUrl() {
const username = usernameInput.value.trim() || 'USERNAME';
const repo = repoInput.value.trim() || 'REPO';
const hash = hashInput.value.trim() || 'HASH';
const url = `https://github.com/${username}/${repo}/archive/${hash}.zip`;
urlPreview.innerHTML = `<a href="${url}" target="_blank"><code>${url}</code></a>`;
}
[usernameInput, repoInput, hashInput].forEach(input => {
input.addEventListener('input', updateUrl);
});
updateUrl(); // Run once on load
});

View File

@ -0,0 +1,5 @@
<?php
$backlogFile = $uploadDir . 'backlog.json';
$valid_statuses = ['in progress', 'done', 'cancelled'];
$valid_types = ['bug', 'improvement', 'nice-to-have'];

View File

@ -0,0 +1,9 @@
<?php
function loadBacklog($backlogFile) {
return file_exists($backlogFile)
? json_decode(file_get_contents($backlogFile), true) ?: []
: [];
}
function saveBacklog($backlogFile, $items) {
file_put_contents($backlogFile, json_encode($items, JSON_PRETTY_PRINT));
}

View File

@ -0,0 +1,51 @@
<?php
// Add
if (isset($_POST['add_backlog'])) {
$status = in_array($_POST['backlog_status'] ?? '', $valid_statuses) ? $_POST['backlog_status'] : 'in progress';
$type = in_array($_POST['backlog_type'] ?? '', $valid_types) ? $_POST['backlog_type'] : 'bug';
$items = loadBacklog($backlogFile);
$items[] = [
'id' => uniqid('b_'),
'title' => trim($_POST['backlog_title'] ?? ''),
'desc' => trim($_POST['backlog_desc'] ?? ''),
'status' => $status,
'type' => $type,
'created' => date('Y-m-d H:i:s'),
];
saveBacklog($backlogFile, $items);
echo "<div class='alert alert-success'>✅ Backlog item added.</div>";
}
// Edit
if (
isset(
$_POST['edit_backlog_id'],
$_POST['edit_backlog_title'],
$_POST['edit_backlog_desc'],
$_POST['edit_backlog_status'],
$_POST['edit_backlog_type']
)
) {
$status = in_array($_POST['edit_backlog_status'], $valid_statuses) ? $_POST['edit_backlog_status'] : 'in progress';
$type = in_array($_POST['edit_backlog_type'], $valid_types) ? $_POST['edit_backlog_type'] : 'bug';
$items = loadBacklog($backlogFile);
foreach ($items as &$item) {
if ($item['id'] === $_POST['edit_backlog_id']) {
$item['title'] = trim($_POST['edit_backlog_title']);
$item['desc'] = trim($_POST['edit_backlog_desc']);
$item['status'] = $status;
$item['type'] = $type;
}
}
saveBacklog($backlogFile, $items);
echo "<div class='alert alert-success'>✏️ Backlog item updated.</div>";
}
// Delete
if (isset($_POST['delete_backlog_id'])) {
$items = loadBacklog($backlogFile);
$items = array_filter($items, fn($item) => $item['id'] !== $_POST['delete_backlog_id']);
saveBacklog($backlogFile, $items);
echo "<div class='alert alert-success'>🗑 Backlog item deleted.</div>";
}

View File

@ -0,0 +1,6 @@
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#backlog">
<i class="bi bi-list-check"></i>
Backlog
</button>
</li>

View File

@ -0,0 +1,119 @@
<!-- Backlog Tab -->
<div class="tab-pane fade" id="backlog">
<!-- Add new backlog item -->
<form method="POST" action="?tab=backlog" class="mb-4">
<input type="hidden" name="add_backlog" value="1">
<div class="row g-2 align-items-end">
<div class="col-sm-3">
<label class="form-label">Title</label>
<input class="form-control" name="backlog_title" required>
</div>
<div class="col-sm-3">
<label class="form-label">Description</label>
<input class="form-control" name="backlog_desc">
</div>
<div class="col-sm-2">
<label class="form-label">Type</label>
<select class="form-select" name="backlog_type">
<option value="bug">Bug</option>
<option value="improvement">Improvement</option>
<option value="nice-to-have">Nice-to-have</option>
</select>
</div>
<div class="col-sm-2">
<label class="form-label">Status</label>
<select class="form-select" name="backlog_status">
<option value="in progress">In Progress</option>
<option value="done">Done</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div class="col-sm-2">
<button class="btn btn-primary w-100">Add new</button>
</div>
</div>
</form>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>T</th>
<th>Title</th>
<th>Description</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
$backlog = loadBacklog($backlogFile);
// Custom sorting function
usort($backlog, function($a, $b) {
$status_priority = ['in progress' => 1, 'done' => 2, 'cancelled' => 3];
$type_priority = ['bug' => 1, 'improvement' => 2, 'nice-to-have' => 3];
// Compare by status first
if ($status_priority[$a['status']] != $status_priority[$b['status']]) {
return $status_priority[$a['status']] - $status_priority[$b['status']];
}
// Status is the same, compare by type next
return $type_priority[$a['type']] - $type_priority[$b['type']];
});
foreach ($backlog as $item): ?>
<tr>
<td>
<?php
$type = $item['type'] ?? '';
$iconClass = [
'bug' => 'bi-bug-fill text-danger',
'improvement' => 'bi-tools text-success',
'nice-to-have' => 'bi-star text-secondary'
][$type] ?? '';
?>
<?php if ($iconClass): ?>
<i class="bi <?= $iconClass ?>" title="<?= ucfirst($type) ?>"></i>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($item['title']) ?></td>
<td><?= nl2br(htmlspecialchars($item['desc'])) ?></td>
<td>
<?php
$status = $item['status'] ?? 'in progress';
$badge = [
'in progress' => 'warning',
'done' => 'success',
'cancelled' => 'secondary'
][$status] ?? 'light';
?>
<span class="badge bg-<?= $badge ?>">
<?= ucfirst($status) ?>
</span>
</td>
<td><?= htmlspecialchars($item['created']) ?></td>
<td>
<button class="btn btn-outline-info" onclick="viewBacklog(
'<?= htmlspecialchars($item['id']) ?>',
'<?= htmlspecialchars(addslashes($item['title'])) ?>',
'<?= htmlspecialchars(addslashes($item['desc'])) ?>',
'<?= htmlspecialchars($status) ?>',
'<?= htmlspecialchars($item['type'] ?? '') ?>',
'<?= htmlspecialchars($item['created']) ?>'
)" title="View Details"><i class="bi bi-info-circle"></i></button>
<button class="btn btn-outline-secondary" onclick="editBacklog(
'<?= htmlspecialchars($item['id']) ?>',
'<?= htmlspecialchars(addslashes($item['title'])) ?>',
'<?= htmlspecialchars(addslashes($item['desc'])) ?>',
'<?= htmlspecialchars($status) ?>',
'<?= htmlspecialchars($item['type'] ?? '') ?>'
)" title="Edit"><i class="bi bi-pencil"></i></button>
<form method="POST" class="d-inline" onsubmit="return confirm('Delete this backlog item?');" action="?tab=backlog">
<input type="hidden" name="delete_backlog_id" value="<?= htmlspecialchars($item['id']) ?>">
<button class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
</form>
&nbsp;&nbsp;&nbsp;&nbsp;
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,71 @@
<div class="modal fade" id="editBacklogModal" tabindex="-1" aria-labelledby="editBacklogModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="editBacklogForm" method="POST" action="?tab=backlog">
<div class="modal-header">
<h5 class="modal-title" id="editBacklogModalLabel">Edit Backlog Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="edit_backlog_id" id="editBacklogId">
<div class="mb-3">
<label for="editBacklogTitle" class="form-label">Title</label>
<input type="text" class="form-control" name="edit_backlog_title" id="editBacklogTitle" required>
</div>
<div class="mb-3">
<label for="editBacklogDesc" class="form-label">Description</label>
<textarea class="form-control" name="edit_backlog_desc" id="editBacklogDesc" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="editBacklogStatus" class="form-label">Status</label>
<select class="form-select" name="edit_backlog_status" id="editBacklogStatus">
<option value="in progress">In Progress</option>
<option value="done">Done</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div class="mb-3">
<label for="editBacklogType" class="form-label">Type</label>
<select class="form-select" name="edit_backlog_type" id="editBacklogType">
<option value="bug">Bug</option>
<option value="improvement">Improvement</option>
<option value="nice-to-have">Nice-to-have</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="viewBacklogModal" tabindex="-1" aria-labelledby="viewBacklogModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewBacklogModalLabel">Backlog Item Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<dl class="row mb-0">
<dt class="col-sm-3">Title</dt>
<dd class="col-sm-9" id="viewBacklogTitle"></dd>
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9" id="viewBacklogDesc"></dd>
<dt class="col-sm-3">Status</dt>
<dd class="col-sm-9"><span class="badge" id="viewBacklogStatusBadge"></span></dd>
<dt class="col-sm-3">Type</dt>
<dd class="col-sm-9" id="viewBacklogType"></dd>
<dt class="col-sm-3">Created</dt>
<dd class="col-sm-9" id="viewBacklogCreated"></dd>
</dl>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
function editBacklog(id, title, desc, status, type) {
document.getElementById('editBacklogId').value = id;
document.getElementById('editBacklogTitle').value = title;
document.getElementById('editBacklogDesc').value = desc;
document.getElementById('editBacklogStatus').value = status;
document.getElementById('editBacklogType').value = type;
const modalEl = document.getElementById('editBacklogModal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
}
/**
* Escape HTML special characters in a string.
*/
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* Show modal with backlog item details.
*/
function viewBacklog(id, title, desc, status, type, created) {
document.getElementById('viewBacklogTitle').textContent = title;
const descEl = document.getElementById('viewBacklogDesc');
const escapedDesc = escapeHtml(desc);
descEl.innerHTML = escapedDesc.replace(/\n/g, '<br>');
const statusBadge = document.getElementById('viewBacklogStatusBadge');
const statusMap = { 'in progress': 'warning', 'done': 'success', 'cancelled': 'secondary' };
statusBadge.textContent = status.charAt(0).toUpperCase() + status.slice(1);
statusBadge.className = 'badge bg-' + (statusMap[status] || 'light');
document.getElementById('viewBacklogCreated').textContent = created;
const typeMap = {
'bug': 'bi-bug-fill text-danger',
'improvement': 'bi-tools text-success',
'nice-to-have': 'bi-star text-secondary'
};
const typeIcon = typeMap[type] || '';
const typeEl = document.getElementById('viewBacklogType');
if (typeIcon) {
typeEl.innerHTML = `<i class="bi ${typeIcon}"></i> ${type.charAt(0).toUpperCase() + type.slice(1)}`;
} else {
typeEl.textContent = '';
}
const modalEl = document.getElementById('viewBacklogModal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
}

View File

@ -0,0 +1,46 @@
# 03-third-parties — Password Manager Tab
This tab implements a web-based lightweight password manager for teams.
It allows you to save, view, and manage passwords and related info for third-party accounts/services in a secure file.
## Features
- **Store**: Save site/service, username, password, OTP secret, and card details for each entry.
- **Password Generation**: Generate strong random passwords.
- **Copy to Clipboard**: Instantly copy any password to clipboard with one click (no password is exposed/revealed by default).
- **OTP Code Preview**: If an OTP secret is stored, the current code is shown next to the entry.
- **Edit Protections**: All form submissions use POST and require confirmation before deleting an entry.
- **Encrypted Storage**: All credentials are saved in an encrypted JSON file (`passwords.json.enc`).
## File Overview
- `01_constants.php` — Defines file paths and any constants for this tab.
- `02_supportFuncs.php` — Helper functions for managing and encrypting credentials, generating OTPs, etc.
- `03_handler.php` — Logic for processing form submissions and updating the password list.
- `04_nav-item.html.php` — Generates the navigation item for this password manager tab.
- `05_content.html.php` — Main UI and form rendering for password management, entry listing, and actions.
- `06_modals.html.php` — (Not used / empty or for modal dialogs).
- `07_javascript.js` — Handles client-side UX, e.g. copying passwords to clipboard.
- `passwords.json.enc` — The encrypted vault file storing all team credentials.
## Usage
1. Navigate to the "Password Manager" tab in the interface.
2. Add new credentials with their site, username, password, OTP, card details, etc.
3. Use the 📋 button next to any password to copy it to your clipboard.
4. To delete an entry, click the red ✖ button and confirm.
## Security Notes
- All credentials are stored only on the server, encrypted at rest.
- Passwords are shown as dots by default; they can only be copied (not revealed) for safety.
- Use secure team practices with this tool and limit exposure of your admin interface.
## Customizing
- To change storage or encryption, update `02_supportFuncs.php` and references to `passwords.json.enc`.
- UI layout can be modified in `05_content.html.php`.
- Client logic (like clipboard copying) is in `07_javascript.js`.
---
This tab is intended for lightweight, shared team secrets/password management. For high-security requirements, use a dedicated enterprise password vault solution.

View File

@ -0,0 +1,16 @@
<?php
/*
* FILE: 01_contants.php
* Description: Support functions that can be used across the app
*/
$repoDir = "/ExtraSpace/admin.growfit.fi/tech";
$passwordFile = __DIR__ . '/passwords.json.enc'; // Encrypted JSON file
/*
| Encryption key (32+ random chars is best).
| - Leave empty ("") if youre happy with **plain-text** storage.
| - Change it to rotate keys; old file becomes unreadable.
*/
$passwordKey = "";
?>

View File

@ -0,0 +1,62 @@
<?php
// FILE: 02_supportFuncs.php
// Description: Support and utility functions
function getPasswordList(string $passwordFile, string $key): array {
if (!file_exists($passwordFile)) return [];
$raw = file_get_contents($passwordFile);
// Plain text mode
if ($key === "") return json_decode($raw, true) ?: [];
// Encrypted mode
$json = decryptData($raw, $key);
return $json === false ? [] : (json_decode($json, true) ?: []);
}
function savePasswordList(string $passwordFile, array $data, string $key): void {
$json = json_encode($data, JSON_PRETTY_PRINT);
$out = $key === "" ? $json : encryptData($json, $key);
file_put_contents($passwordFile, $out);
}
/* Encryption helpers unchanged except key now fixed */
function encryptData(string $plain, string $key): string {
$m = "aes-256-cbc";
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($m));
$enc= openssl_encrypt($plain, $m, $key, 0, $iv);
return base64_encode($iv . $enc);
}
function decryptData(string $cipherB64, string $key): string|false {
$m = "aes-256-cbc";
$bin= base64_decode($cipherB64, true);
if ($bin === false) return false;
$ivLen = openssl_cipher_iv_length($m);
$iv = substr($bin, 0, $ivLen);
$enc = substr($bin, $ivLen);
return openssl_decrypt($enc, $m, $key, 0, $iv);
}
function generatePassword(int $len = 14): string {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_-=+';
return substr(str_shuffle(str_repeat($chars, (int)ceil($len / strlen($chars)))), 0, $len);
}
/**
* Return the current 6-digit TOTP for a given base32 secret.
* On error (empty or invalid secret) returns ''.
*/
function currentOtpCode(string $secret): string
{
if ($secret === '') return '';
try {
$totp = OTPHP\TOTP::create($secret);
return $totp->now(); // 6-digit string
} catch (\Throwable $e) {
return '';
}
}
?>

View File

@ -0,0 +1,45 @@
<?php
/*
* FILE: 03_handler.php
* Description: Handles the input provided from the browser/user
*/
/* Bring the key/file into local scope */
$pwFile = $passwordFile; // from constants
$key = $passwordKey; // static, can be ""
/* ---------- DELETE PASSWORD ---------- */
if (isset($_POST['delete_idx'])) {
$idx = (int)$_POST['delete_idx'];
$pwList = getPasswordList($pwFile, $key);
if (isset($pwList[$idx])) {
array_splice($pwList, $idx, 1);
savePasswordList($pwFile, $pwList, $key);
}
header("Location: ".$_SERVER['REQUEST_URI']); exit;
}
/* ---------- GENERATE PASSWORD BUTTON ---------- */
if (isset($_POST['generate_password'])) {
$_POST['password'] = generatePassword(16);
}
/* ---------- ADD / UPDATE PASSWORD ---------- */
if (isset($_POST['add_password'])) {
$pwList = getPasswordList($pwFile, $key);
$pwList[] = [
'site' => trim($_POST['site']),
'username' => trim($_POST['username']),
'password' => $_POST['password'],
'otp_secret' => trim($_POST['otp_secret'] ?? ''),
'cc_number' => trim($_POST['cc_number'] ?? ''),
'cc_exp' => trim($_POST['cc_exp'] ?? ''),
'ccv' => trim($_POST['ccv'] ?? ''),
'monthly' => trim($_POST['monthly'] ?? ''),
];
savePasswordList($pwFile, $pwList, $key);
header("Location: ".$_SERVER['REQUEST_URI']); exit;
}
?>

View File

@ -0,0 +1,10 @@
<?php /*
FILE: 04_nav-item.html.php
Description: generates the tab link/button, in the navbar
*/ ?>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#third-parties">
<i class="bi bi-link-45deg"></i>
3rd Parties
</button>
</li>

View File

@ -0,0 +1,113 @@
<?php /*
FILE: 05_content.html.php
Description: the main HTML/PHP content of this tab
*/
?>
<!-- 3rd parties Tab -->
<div class="tab-pane fade" id="third-parties">
<h5 class="mb-3">Password Manager</h5>
<!-- Add Password Form -->
<form method="post" action="?tab=third-parties" class="mb-4">
<div class="row g-2">
<div class="col-md-3"><input class="form-control" name="site" placeholder="Site / Service" required></div>
<div class="col-md-2"><input class="form-control" name="username" placeholder="Username / Email" required></div>
<div class="col-md-2"><input class="form-control" name="password" placeholder="Password" value="<?= htmlspecialchars($_POST['password'] ?? '') ?>" required></div>
<div class="col-md-2"><input class="form-control" name="otp_secret" placeholder="OTP secret (base32)"></div>
<div class="col-md-2"><input class="form-control" name="cc_number" placeholder="Card number"></div>
<div class="col-md-1"><input class="form-control" name="cc_exp" placeholder="MM/YY"></div>
<div class="col-md-1"><input class="form-control" name="ccv" placeholder="CCV"></div>
<div class="col-md-1"><input class="form-control" name="monthly" placeholder="€/mo"></div>
</div>
<div class="mt-2">
<button class="btn btn-secondary" name="generate_password">Generate password</button>
<button class="btn btn-primary" name="add_password">Save entry</button>
</div>
</form>
<!-- Passwords Table -->
<table id="pwTable" class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>Site</th>
<th>User</th>
<th>Password</th>
<th>OTP</th>
<th>Card #</th>
<th>Exp</th>
<th>CCV</th>
<th>/mo</th>
<th></th>
</tr>
</thead>
<tbody>
<?php
$pwList = getPasswordList($passwordFile, $passwordKey);
$itemsPerPage = 8;
foreach ($pwList as $i => $row):
?>
<tr>
<td><?= htmlspecialchars($row['site']) ?></td>
<td><?= htmlspecialchars($row['username']) ?></td>
<!-- password -->
<td class="position-relative">
<div class="input-group input-group-sm">
<input type="password" readonly class="form-control form-control-sm pw-field"
value="<?= htmlspecialchars($row['password']) ?>"
onclick="this.type='text'">
<button type="button" class="btn btn-outline-secondary btn-sm copy-pw-btn" title="Copy to clipboard">
<i class="bi bi-clipboard"></i>
</button>
</div>
</td>
<!-- OTP -->
<td class="text-nowrap">
<?= $row['otp_secret']? currentOtpCode($row['otp_secret']):'' ?>
</td>
<!-- card -->
<td>
<?= htmlspecialchars($row['cc_number']) ?>
</td>
<td><?= htmlspecialchars($row['cc_exp']) ?></td>
<!-- CCV -->
<td>
<?php if ($row['ccv']): ?>
<input type="password" readonly class="form-control form-control-sm"
value="<?= htmlspecialchars($row['ccv']) ?>"
onclick="this.type='text'">
<?php endif; ?>
</td>
<td><?= htmlspecialchars($row['monthly']) ?></td>
<!-- delete -->
<td class="text-center">
<form method="post" onsubmit="return confirm('Delete this entry?')">
<input type="hidden" name="delete_idx" value="<?= $i ?>">
<button class="btn btn-outline-danger "><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
$totalItems = count($pwList);
$totalPages = (int)ceil($totalItems / $itemsPerPage);
if ($totalPages > 1): ?>
<nav aria-label="Password pagination">
<ul class="pagination justify-content-center" id="pwPagination" data-items-per-page="<?= $itemsPerPage ?>">
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
<li class="page-item<?= $p === 1 ? ' active' : '' ?>">
<a class="page-link" href="#" data-page="<?= $p ?>"><?= $p ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
</div>

View File

@ -0,0 +1,7 @@
<?php /*
FILE: 06_modals.html.php
Description: any extra modal, hidden windows that get displayed when user clicks something
*/ ?>

View File

@ -0,0 +1,17 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.copy-pw-btn').forEach(function(btn) {
btn.addEventListener('click', function () {
var input = btn.closest('.input-group').querySelector('.pw-field');
if (input) {
navigator.clipboard.writeText(input.value).then(function() {
btn.innerHTML = '✅';
});
}
});
});
});
//
// FILE: 07_javascript.js
// Description: the javascript code that will be added to the website

View File

@ -0,0 +1,2 @@
<?php
$schemaFile = $uploadDir.'schema.sql';

View File

@ -0,0 +1,5 @@
<?php
if (isset($_POST['sql_schema'])) {
file_put_contents($schemaFile, $_POST['sql_schema']);
echo "<div class='alert alert-success'>✅ SQL schema saved.</div>";
}

View File

@ -0,0 +1,6 @@
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#schema">
<i class="bi bi-database"></i>
SQL Schema
</button>
</li>

View File

@ -0,0 +1,10 @@
<!-- SQL Schema Tab -->
<div class="tab-pane fade" id="schema">
<form method="POST" action="?tab=schema">
<div class="mb-3">
<label class="form-label">SQL Schema Editor</label>
<textarea id="sqlSchemaEditor" class="form-control" name="sql_schema" rows="60"><?= htmlspecialchars($schemaContent) ?></textarea>
</div>
<button class="btn btn-primary">💾 Save Schema</button>
</form>
</div>

View File

@ -0,0 +1,14 @@
document.addEventListener('DOMContentLoaded', () => {
const editor = CodeMirror.fromTextArea(document.getElementById('sqlSchemaEditor'), {
mode: 'text/x-sql',
theme: 'material',
lineNumbers: false,
indentUnit: 2,
tabSize: 2,
});
// Force CodeMirror to refresh when the tab is shown
const schemaTab = document.querySelector('button[data-bs-target="#schema"]');
schemaTab.addEventListener('shown.bs.tab', function () {
editor.refresh();
});
});

View File

@ -0,0 +1,5 @@
<?php
if (isset($_POST['resync'])) {
require __DIR__ . "/resyncProductList.php";
echo "<div class='alert alert-success'>🔄 ReSync executed.</div>";
}

View File

@ -0,0 +1,6 @@
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#action">
<i class="bi bi-lightning"></i>
Actions
</button>
</li>

View File

@ -0,0 +1,6 @@
<!-- Actions Tab -->
<div class="tab-pane fade" id="actions">
<form method="POST" action="?tab=actions">
<button name="resync" value="1" class="btn btn-warning">🔄 ReSync Strapi with Supabase</button>
</form>
</div>

View File

@ -0,0 +1,6 @@
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#backup">
<i class="bi bi-floppy"></i>
Backup
</button>
</li>

View File

@ -0,0 +1,4 @@
<!-- Backup Tab -->
<div class="tab-pane fade" id="backup">
<p class="text-muted">⚙️ Coming soon...</p>
</div>