init
This commit is contained in:
commit
f2a6525224
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal 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
7
_authenticate.php
Normal 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
37
business/_database.php
Normal 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
30
business/_navbar.php
Normal 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>
|
||||||
55
business/dashboard/customers.php
Normal file
55
business/dashboard/customers.php
Normal 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>
|
||||||
82
business/dashboard/sales.php
Normal file
82
business/dashboard/sales.php
Normal 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
106
business/index.php
Normal 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">
|
||||||
|
© <?= 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
29
index.php
Normal 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
51
login.php
Normal 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
6
logout.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_destroy();
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
?>
|
||||||
16
tech/api/_authenticateApiRequest.php
Normal file
16
tech/api/_authenticateApiRequest.php
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
7
tech/api/_dbCredentials.php
Normal file
7
tech/api/_dbCredentials.php
Normal 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
101
tech/api/_dbFuncs.php
Normal 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);
|
||||||
|
}
|
||||||
3
tech/api/_telegramCredentials.php
Normal file
3
tech/api/_telegramCredentials.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
define("TELEGRAM_BOT_TOKEN","8095944700:AAGjybT203bk1RteZSlXCtQ8ndmNMrpsNAw");
|
||||||
|
define("TELEGRAM_CHAT_ID",'-4618727626'); // GrowFit eCommerce
|
||||||
36
tech/api/_telegramFunctions.php
Normal file
36
tech/api/_telegramFunctions.php
Normal 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;
|
||||||
|
}
|
||||||
16
tech/api/rebuildWebsite.php
Normal file
16
tech/api/rebuildWebsite.php
Normal 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;
|
||||||
96
tech/api/updateProductList.php
Normal file
96
tech/api/updateProductList.php
Normal 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
66
tech/index.php
Normal 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>
|
||||||
|
|
||||||
56
tech/tabs/01-deploy/00_README.md
Normal file
56
tech/tabs/01-deploy/00_README.md
Normal 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.
|
||||||
10
tech/tabs/01-deploy/01_constants.php
Normal file
10
tech/tabs/01-deploy/01_constants.php
Normal 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";
|
||||||
|
|
||||||
|
?>
|
||||||
57
tech/tabs/01-deploy/02_supportFuncs.php
Normal file
57
tech/tabs/01-deploy/02_supportFuncs.php
Normal 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);
|
||||||
|
}
|
||||||
|
?>
|
||||||
126
tech/tabs/01-deploy/03_handler.php
Normal file
126
tech/tabs/01-deploy/03_handler.php
Normal 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>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
10
tech/tabs/01-deploy/04_nav-item.html.php
Normal file
10
tech/tabs/01-deploy/04_nav-item.html.php
Normal 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>
|
||||||
95
tech/tabs/01-deploy/05_content.html.php
Normal file
95
tech/tabs/01-deploy/05_content.html.php
Normal 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>
|
||||||
23
tech/tabs/01-deploy/06_modals.html.php
Normal file
23
tech/tabs/01-deploy/06_modals.html.php
Normal 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>
|
||||||
46
tech/tabs/01-deploy/07_javascript.js
Normal file
46
tech/tabs/01-deploy/07_javascript.js
Normal 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
|
||||||
|
});
|
||||||
5
tech/tabs/02-backlog/01_constants.php
Normal file
5
tech/tabs/02-backlog/01_constants.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
$backlogFile = $uploadDir . 'backlog.json';
|
||||||
|
|
||||||
|
$valid_statuses = ['in progress', 'done', 'cancelled'];
|
||||||
|
$valid_types = ['bug', 'improvement', 'nice-to-have'];
|
||||||
9
tech/tabs/02-backlog/02_supportFuncs.php
Normal file
9
tech/tabs/02-backlog/02_supportFuncs.php
Normal 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));
|
||||||
|
}
|
||||||
51
tech/tabs/02-backlog/03_handler.php
Normal file
51
tech/tabs/02-backlog/03_handler.php
Normal 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>";
|
||||||
|
}
|
||||||
6
tech/tabs/02-backlog/04_nav-item.html.php
Normal file
6
tech/tabs/02-backlog/04_nav-item.html.php
Normal 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>
|
||||||
119
tech/tabs/02-backlog/05_content.html.php
Normal file
119
tech/tabs/02-backlog/05_content.html.php
Normal 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>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
71
tech/tabs/02-backlog/06_modals.html.php
Normal file
71
tech/tabs/02-backlog/06_modals.html.php
Normal 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>
|
||||||
56
tech/tabs/02-backlog/07_javascript.js
Normal file
56
tech/tabs/02-backlog/07_javascript.js
Normal 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
46
tech/tabs/03-third-parties/00_README.md
Normal file
46
tech/tabs/03-third-parties/00_README.md
Normal 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.
|
||||||
16
tech/tabs/03-third-parties/01_constants.php
Normal file
16
tech/tabs/03-third-parties/01_constants.php
Normal 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 you’re happy with **plain-text** storage.
|
||||||
|
| - Change it to rotate keys; old file becomes unreadable.
|
||||||
|
*/
|
||||||
|
$passwordKey = "";
|
||||||
|
?>
|
||||||
62
tech/tabs/03-third-parties/02_supportFuncs.php
Normal file
62
tech/tabs/03-third-parties/02_supportFuncs.php
Normal 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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
45
tech/tabs/03-third-parties/03_handler.php
Normal file
45
tech/tabs/03-third-parties/03_handler.php
Normal 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;
|
||||||
|
}
|
||||||
|
?>
|
||||||
10
tech/tabs/03-third-parties/04_nav-item.html.php
Normal file
10
tech/tabs/03-third-parties/04_nav-item.html.php
Normal 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>
|
||||||
113
tech/tabs/03-third-parties/05_content.html.php
Normal file
113
tech/tabs/03-third-parties/05_content.html.php
Normal 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>
|
||||||
7
tech/tabs/03-third-parties/06_modals.html.php
Normal file
7
tech/tabs/03-third-parties/06_modals.html.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php /*
|
||||||
|
FILE: 06_modals.html.php
|
||||||
|
Description: any extra modal, hidden windows that get displayed when user clicks something
|
||||||
|
*/ ?>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
17
tech/tabs/03-third-parties/07_javascript.js
Normal file
17
tech/tabs/03-third-parties/07_javascript.js
Normal 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
|
||||||
|
|
||||||
|
|
||||||
2
tech/tabs/04-schema/01_constants.php
Normal file
2
tech/tabs/04-schema/01_constants.php
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
$schemaFile = $uploadDir.'schema.sql';
|
||||||
5
tech/tabs/04-schema/03_handler.php
Normal file
5
tech/tabs/04-schema/03_handler.php
Normal 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>";
|
||||||
|
}
|
||||||
6
tech/tabs/04-schema/04_nav-item.html.php
Normal file
6
tech/tabs/04-schema/04_nav-item.html.php
Normal 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>
|
||||||
10
tech/tabs/04-schema/05_content.html.php
Normal file
10
tech/tabs/04-schema/05_content.html.php
Normal 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>
|
||||||
14
tech/tabs/04-schema/07_javascript.js
Normal file
14
tech/tabs/04-schema/07_javascript.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
5
tech/tabs/05-actions/03_handler.php
Normal file
5
tech/tabs/05-actions/03_handler.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
if (isset($_POST['resync'])) {
|
||||||
|
require __DIR__ . "/resyncProductList.php";
|
||||||
|
echo "<div class='alert alert-success'>🔄 ReSync executed.</div>";
|
||||||
|
}
|
||||||
6
tech/tabs/05-actions/04_nav-item.html.php
Normal file
6
tech/tabs/05-actions/04_nav-item.html.php
Normal 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>
|
||||||
6
tech/tabs/05-actions/05_content.html.php
Normal file
6
tech/tabs/05-actions/05_content.html.php
Normal 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>
|
||||||
6
tech/tabs/06-backup/04_nav-item.html.php
Normal file
6
tech/tabs/06-backup/04_nav-item.html.php
Normal 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>
|
||||||
4
tech/tabs/06-backup/05_content.html.php
Normal file
4
tech/tabs/06-backup/05_content.html.php
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<!-- Backup Tab -->
|
||||||
|
<div class="tab-pane fade" id="backup">
|
||||||
|
<p class="text-muted">⚙️ Coming soon...</p>
|
||||||
|
</div>
|
||||||
Loading…
x
Reference in New Issue
Block a user