DSP Project first push, date: 29/01/2026
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.git
|
||||
.gitignore
|
||||
.DS_Store
|
||||
node_modules
|
||||
vendor
|
||||
uploads/*
|
||||
!uploads/**/.gitkeep
|
||||
.env
|
||||
21
.env
Normal file
21
.env
Normal file
@@ -0,0 +1,21 @@
|
||||
JUPYTER_EXTERNAL_URL=https://hub.niph.org.kh
|
||||
JUPYTERHUB_PORT=443
|
||||
JUPYTERHUB_AUTH_STRATEGY=oauth
|
||||
JUPYTERHUB_DUMMY_PASSWORD=
|
||||
JUPYTERHUB_COOKIE_SECURE=true
|
||||
JUPYTERHUB_HTTP_TIMEOUT=90
|
||||
JUPYTERHUB_START_TIMEOUT=90
|
||||
JUPYTERHUB_USER_PATH=hub/
|
||||
JUPYTERHUB_USERNAME_TEMPLATE=user_{person_id}
|
||||
DSP_WORKSPACE_ROOT=/home/niph_dev/Documents/dsp/uploads/jupyter_workspace
|
||||
DSP_OAUTH_CLIENT_ID=hub-client
|
||||
DSP_OAUTH_CLIENT_SECRET=hub-client-secret-20251103
|
||||
DSP_OAUTH_REDIRECT_URIS=https://hub.niph.org.kh/hub/oauth_callback
|
||||
DSP_OAUTH_AUTHORIZE_URL=https://dsp.niph.org.kh/oauth/authorize
|
||||
DSP_OAUTH_TOKEN_URL=https://dsp.niph.org.kh/oauth/token
|
||||
DSP_OAUTH_USERINFO_URL=https://dsp.niph.org.kh/oauth/userinfo
|
||||
JUPYTERHUB_OAUTH_CALLBACK=https://hub.niph.org.kh/hub/oauth_callback
|
||||
JUPYTERHUB_CULL_API_TOKEN=kCc1EzULTX2jG8jyvOTA0B6vrMk5SGmWjwDvNqlQ2wY
|
||||
DSP_APP_ORIGINS="https://dsp.niph.org.kh https://hub.niph.org.kh http://localhost:8082 http://192.168.170.226:8082"
|
||||
DSP_FRAME_ANCESTORS="https://dsp.niph.org.kh https://hub.niph.org.kh http://localhost:8082 http://192.168.170.226:8082"
|
||||
APP_PORT=443
|
||||
8
.htaccess
Normal file
8
.htaccess
Normal file
@@ -0,0 +1,8 @@
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Serve existing PHP files when the extension is omitted
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
||||
RewriteRule ^(.+)$ $1.php [L]
|
||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# Dockerfile for DSP PHP application with R support
|
||||
FROM php:8.2-apache
|
||||
|
||||
# Install system dependencies and PHP extensions
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
git \
|
||||
default-mysql-client \
|
||||
r-base \
|
||||
&& docker-php-ext-install pdo_mysql \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install required R packages
|
||||
RUN Rscript -e "install.packages('jsonlite', repos='https://cloud.r-project.org')"
|
||||
|
||||
# Enable Apache modules commonly used by PHP apps
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Copy project files into the container (can be overridden by bind mount in docker-compose)
|
||||
COPY . /var/www/html
|
||||
|
||||
# Ensure upload directories are writable by the web server
|
||||
RUN chown -R www-data:www-data /var/www/html/uploads
|
||||
|
||||
# Increase default PHP upload limits
|
||||
COPY docker/custom.ini /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker/app-entrypoint.sh /usr/local/bin/app-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/app-entrypoint.sh
|
||||
|
||||
# Expose Apache port
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["app-entrypoint.sh"]
|
||||
119
admin/app_log.php
Normal file
119
admin/app_log.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
$logPath = realpath(__DIR__ . '/../logs/app.log');
|
||||
$canReadLog = $logPath && is_readable($logPath);
|
||||
$lineLimit = filter_input(INPUT_GET, 'lines', FILTER_VALIDATE_INT);
|
||||
$lineLimit = $lineLimit ?: 200;
|
||||
$lineLimit = max(50, min(2000, $lineLimit));
|
||||
$logEntries = [];
|
||||
$logMeta = [
|
||||
'size' => null,
|
||||
'modified' => null,
|
||||
];
|
||||
|
||||
if ($canReadLog) {
|
||||
$logMeta['size'] = filesize($logPath);
|
||||
$logMeta['modified'] = filemtime($logPath);
|
||||
|
||||
if (isset($_GET['download'])) {
|
||||
header('Content-Type: text/plain');
|
||||
header('Content-Disposition: attachment; filename="app.log"');
|
||||
header('Content-Length: ' . $logMeta['size']);
|
||||
readfile($logPath);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$logEntries = tail_file($logPath, $lineLimit);
|
||||
} catch (RuntimeException $e) {
|
||||
$canReadLog = false;
|
||||
$logError = $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$logError = 'The application log file is missing or unreadable.';
|
||||
}
|
||||
|
||||
function tail_file(string $path, int $lines): array
|
||||
{
|
||||
$file = new SplFileObject($path, 'r');
|
||||
$file->seek(PHP_INT_MAX);
|
||||
$lastLine = $file->key();
|
||||
$startLine = max($lastLine - $lines + 1, 0);
|
||||
$file->seek($startLine);
|
||||
|
||||
$buffer = [];
|
||||
while (!$file->eof()) {
|
||||
$buffer[] = rtrim($file->current(), "\r\n");
|
||||
$file->next();
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
$lastUpdated = $logMeta['modified'] ? date('Y-m-d H:i:s', $logMeta['modified']) . ' UTC' : 'Unknown';
|
||||
$logSizeHuman = $logMeta['size'] !== null ? number_format($logMeta['size'] / 1024, 1) . ' KB' : 'Unknown';
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php include_once __DIR__ . '/../includes/header_admin.php'; ?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include_once __DIR__ . '/../includes/nav_admin.php'; ?>
|
||||
<div class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h4 mb-1">Application Log</h1>
|
||||
<p class="text-muted mb-0">Streaming the latest <?php echo htmlspecialchars($lineLimit); ?> lines from <code>logs/app.log</code>.</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<small class="text-muted d-block">Last updated: <?php echo htmlspecialchars($lastUpdated); ?></small>
|
||||
<small class="text-muted">Size: <?php echo htmlspecialchars($logSizeHuman); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body d-flex flex-wrap gap-2 align-items-center">
|
||||
<form class="d-flex align-items-center gap-2" method="get">
|
||||
<label for="lines" class="form-label mb-0">Lines to display</label>
|
||||
<select class="form-select form-select-sm w-auto" id="lines" name="lines" onchange="this.form.submit()">
|
||||
<?php foreach ([100, 200, 500, 1000, 2000] as $option): ?>
|
||||
<option value="<?php echo $option; ?>" <?php echo ($lineLimit === $option) ? 'selected' : ''; ?>>
|
||||
<?php echo $option; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
<?php if ($canReadLog): ?>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="?lines=<?php echo $lineLimit; ?>">
|
||||
<i class="fas fa-sync-alt me-1"></i> Refresh
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-primary" href="?download=1">
|
||||
<i class="fas fa-download me-1"></i> Download
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$canReadLog): ?>
|
||||
<div class="alert alert-danger rounded shadow-sm">
|
||||
<strong>Cannot read log file.</strong>
|
||||
<div><?php echo htmlspecialchars($logError ?? 'Unknown error.'); ?></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<pre class="bg-dark text-light p-3 rounded small" style="max-height: 60vh; overflow:auto;"><?php echo htmlspecialchars(implode("\n", $logEntries)); ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php include_once __DIR__ . '/../includes/footer_admin.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
171
admin/dashboard.php
Normal file
171
admin/dashboard.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/User.php';
|
||||
require_once '../classes/Announcement.php';
|
||||
require_once '../classes/DataSource.php'; // Still needed for overall data source counts
|
||||
require_once '../classes/Classifications.php'; // For classification counts
|
||||
require_once '../classes/Aboutus.php';
|
||||
require_once '../classes/Contactus.php'; // For feedback/contact messages
|
||||
require_once '../classes/Faq.php';
|
||||
require_once '../classes/Slide.php'; // Include Slide class
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize classes
|
||||
$user = new User($pdo);
|
||||
$announcement = new Announcement($pdo);
|
||||
$dataSource = new DataSource($pdo);
|
||||
$classification = new Classifications($pdo);
|
||||
$aboutUs = new Aboutus($pdo);
|
||||
$contactUs = new Contactus($pdo);
|
||||
$faq = new Faq($pdo);
|
||||
$slideManager = new Slide($pdo);
|
||||
|
||||
// Fetch dashboard data
|
||||
$totalUsers = $user->getTotalUsers();
|
||||
$totalAnnouncements = $announcement->getTotalAnnouncements();
|
||||
$totalDataSources = $dataSource->getTotalDataSources();
|
||||
$totalCategories = $classification->getTotalCategories();
|
||||
$totalDataTypes = $classification->getTotalDataTypes();
|
||||
$totalFeedback = $contactUs->getTotalFeedback();
|
||||
$totalFaqs = $faq->getTotalFaqs();
|
||||
$totalSlides = $slideManager->getTotalSlides();
|
||||
$pendingPermissions = $dataSource->getPendingPermissionRequestsCount(); // This correctly gets ALL pending for admin dashboard
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Admin Dashboard</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded-pill" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-users card-icon mb-3"></i>
|
||||
<h5 class="card-title">Total Users</h5>
|
||||
<p class="card-text fs-2 fw-bold text-primary"><?php echo $totalUsers; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-bullhorn card-icon mb-3"></i>
|
||||
<h5 class="card-title">Total Announcements</h5>
|
||||
<p class="card-text fs-2 fw-bold text-success"><?php echo $totalAnnouncements; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-database card-icon mb-3"></i>
|
||||
<h5 class="card-title">Total Data Sources</h5>
|
||||
<p class="card-text fs-2 fw-bold text-info"><?php echo $totalDataSources; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-folder-open card-icon mb-3"></i>
|
||||
<h5 class="card-title">Data Categories</h5>
|
||||
<p class="card-text fs-2 fw-bold text-warning"><?php echo $totalCategories; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-file-alt card-icon mb-3"></i>
|
||||
<h5 class="card-title">Data Types</h5>
|
||||
<p class="card-text fs-2 fw-bold text-danger"><?php echo $totalDataTypes; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-handshake card-icon mb-3"></i>
|
||||
<h5 class="card-title">Pending Permissions</h5>
|
||||
<p class="card-text fs-2 fw-bold text-secondary"><?php echo $pendingPermissions; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-comments card-icon mb-3"></i>
|
||||
<h5 class="card-title">New Feedback</h5>
|
||||
<p class="card-text fs-2 fw-bold text-primary"><?php echo $totalFeedback; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-question card-icon mb-3"></i>
|
||||
<h5 class="card-title">Total FAQs</h5>
|
||||
<p class="card-text fs-2 fw-bold text-success"><?php echo $totalFaqs; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="card text-center p-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-images card-icon mb-3"></i>
|
||||
<h5 class="card-title">Total Slides</h5>
|
||||
<p class="card-text fs-2 fw-bold text-warning"><?php echo $totalSlides; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
216
admin/manage_aboutus.php
Normal file
216
admin/manage_aboutus.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Aboutus.php';
|
||||
require_once '../classes/User.php'; // To get person_id for the Aboutus class
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize Aboutus class
|
||||
$aboutUs = new Aboutus($pdo);
|
||||
$user = new User($pdo); // To get the person_id of the logged-in user
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action_type = $_POST['action_type'] ?? ''; // 'add' or 'edit' or 'delete'
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$about_id = $_POST['about_id'] ?? null;
|
||||
|
||||
// Get the person_id of the currently logged-in user
|
||||
$currentUserDetails = $user->getUserDetails($_SESSION['user_id']);
|
||||
$fkisp_id_of = $currentUserDetails['fkisp_id_of'];
|
||||
|
||||
if ($action_type === 'delete') {
|
||||
if ($about_id) {
|
||||
try {
|
||||
$aboutUs->deleteAboutUs($about_id);
|
||||
set_message('About Us entry deleted successfully!', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error deleting About Us entry: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (empty($title) || empty($description)) {
|
||||
set_message('Title and description cannot be empty.', 'danger');
|
||||
} else {
|
||||
try {
|
||||
if ($action_type === 'add') {
|
||||
$aboutUs->addAboutUs($title, $description, $_SESSION['user_id'], $fkisp_id_of);
|
||||
set_message('About Us entry added successfully!', 'success');
|
||||
} elseif ($action_type === 'edit' && $about_id) {
|
||||
$aboutUs->updateAboutUs($about_id, $title, $description, $_SESSION['user_id'], $fkisp_id_of);
|
||||
set_message('About Us entry updated successfully!', 'success');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Error: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
header('Location: manage_aboutus.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch About Us entries for display
|
||||
$aboutUsEntries = $aboutUs->getAllAboutUs();
|
||||
|
||||
// Prepare data for editing if action is 'edit'
|
||||
$editAboutUs = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
$editAboutUs = $aboutUs->getAboutUsById($id);
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage About Us</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><?php echo $editAboutUs ? 'Edit' : 'Add New'; ?> About Us Entry</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_aboutus.php" method="POST">
|
||||
<input type="hidden" name="action_type" value="<?php echo $editAboutUs ? 'edit' : 'add'; ?>">
|
||||
<?php if ($editAboutUs): ?>
|
||||
<input type="hidden" name="about_id" value="<?php echo htmlspecialchars($editAboutUs['pkdspsabout_id']); ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control rounded" id="title" name="title" value="<?php echo htmlspecialchars($editAboutUs['dspsabout_title_en'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label d-flex justify-content-between align-items-center">
|
||||
<span>Description</span>
|
||||
</label>
|
||||
<textarea class="form-control rounded-3" id="description" name="description" rows="8" required><?php echo htmlspecialchars($editAboutUs['dspsabout_description'] ?? ''); ?></textarea>
|
||||
<div class="form-text">Format paragraphs, bullet lists, and links so the public About Us page is easy to read.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-<?php echo $editAboutUs ? 'save' : 'plus'; ?> me-2"></i> <?php echo $editAboutUs ? 'Update' : 'Add'; ?> Entry
|
||||
</button>
|
||||
<?php if ($editAboutUs): ?>
|
||||
<a href="manage_aboutus.php" class="btn btn-secondary rounded ms-2">Cancel Edit</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-white" style="background-color: #28a745;">
|
||||
<h5 class="mb-0">All About Us Entries</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Reg. Date</th>
|
||||
<th>Mod. Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($aboutUsEntries)): ?>
|
||||
<?php foreach ($aboutUsEntries as $entry): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($entry['pkdspsabout_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['dspsabout_title_en']); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr($entry['dspsabout_description'], 0, 100)) . (strlen($entry['dspsabout_description']) > 100 ? '...' : ''); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['dspsabout_reg_datetime']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['dspsabout_mod_datetime']); ?></td>
|
||||
<td>
|
||||
<a href="manage_aboutus.php?action=edit&id=<?php echo htmlspecialchars($entry['pkdspsabout_id']); ?>" class="btn btn-sm btn-warning rounded btn-action">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="manage_aboutus.php" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this About Us entry?');">
|
||||
<input type="hidden" name="action_type" value="delete">
|
||||
<input type="hidden" name="about_id" value="<?php echo htmlspecialchars($entry['pkdspsabout_id']); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded btn-action">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">No About Us entries found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ckeditor/ckeditor5-build-classic@38.1.1/build/ckeditor.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var textarea = document.querySelector('#description');
|
||||
if (textarea && typeof ClassicEditor !== 'undefined') {
|
||||
ClassicEditor
|
||||
.create(textarea, {
|
||||
toolbar: [
|
||||
'heading','|','bold','italic','underline','bulletedList','numberedList','blockQuote',
|
||||
'|','link','insertTable','undo','redo'
|
||||
]
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error('Failed to initialise rich text editor', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
260
admin/manage_announcements.php
Normal file
260
admin/manage_announcements.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Announcement.php';
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize Announcement class
|
||||
$announcement = new Announcement($pdo);
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action_type = $_POST['action_type'] ?? ''; // 'add' or 'edit' or 'delete'
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$status = $_POST['status'] ?? 'Draft';
|
||||
$announcement_id = $_POST['announcement_id'] ?? null;
|
||||
$current_photo = $_POST['current_photo'] ?? ''; // For editing, keep track of existing photo
|
||||
|
||||
if ($action_type === 'delete') {
|
||||
if ($announcement_id) {
|
||||
try {
|
||||
$announcement->deleteAnnouncement($announcement_id);
|
||||
set_message('Announcement deleted successfully!', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error deleting announcement: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle photo upload
|
||||
$photoPath = $current_photo; // Default to current photo if not uploading new
|
||||
if (isset($_FILES['photo']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) {
|
||||
try {
|
||||
$photoPath = $announcement->handlePhotoUpload($_FILES['photo']);
|
||||
// If editing and a new photo is uploaded, delete the old one
|
||||
if ($action_type === 'edit' && !empty($current_photo) && $current_photo !== $photoPath) {
|
||||
unlink('../uploads/announcements/' . $current_photo); // Delete old file
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Photo upload error: ' . $e->getMessage(), 'danger');
|
||||
header('Location: manage_announcements.php');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($title) || empty($description)) {
|
||||
set_message('Title and description cannot be empty.', 'danger');
|
||||
} else {
|
||||
try {
|
||||
if ($action_type === 'add') {
|
||||
$announcement->addAnnouncement($title, $description, $photoPath, $status, $_SESSION['user_id']);
|
||||
set_message('Announcement added successfully!', 'success');
|
||||
} elseif ($action_type === 'edit' && $announcement_id) {
|
||||
$announcement->updateAnnouncement($announcement_id, $title, $description, $photoPath, $status, $_SESSION['user_id']);
|
||||
set_message('Announcement updated successfully!', 'success');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Error: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
header('Location: manage_announcements.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch announcements for display
|
||||
$announcements = $announcement->getAllAnnouncements();
|
||||
|
||||
// Prepare data for editing if action is 'edit'
|
||||
$editAnnouncement = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
$editAnnouncement = $announcement->getAnnouncementById($id);
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage Announcements</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><?php echo $editAnnouncement ? 'Edit' : 'Add New'; ?> Announcement</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_announcements.php" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action_type" value="<?php echo $editAnnouncement ? 'edit' : 'add'; ?>">
|
||||
<?php if ($editAnnouncement): ?>
|
||||
<input type="hidden" name="announcement_id" value="<?php echo htmlspecialchars($editAnnouncement['pkdspsann_id']); ?>">
|
||||
<input type="hidden" name="current_photo" value="<?php echo htmlspecialchars($editAnnouncement['dspsann_photopath']); ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control rounded" id="title" name="title" value="<?php echo htmlspecialchars($editAnnouncement['dspsann_title'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label d-flex justify-content-between align-items-center">
|
||||
<span>Description</span>
|
||||
</label>
|
||||
<textarea class="form-control rounded-3" id="description" name="description" rows="8" required><?php echo htmlspecialchars($editAnnouncement['dspsann_description'] ?? ''); ?></textarea>
|
||||
<div class="form-text">Use the toolbar to format bullet lists, emphasize important actions, and link to additional resources.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="photo" class="form-label">Photo (Optional)</label>
|
||||
<input type="file" class="form-control rounded" id="photo" name="photo" accept="image/*">
|
||||
<?php if ($editAnnouncement && !empty($editAnnouncement['dspsann_photopath'])): ?>
|
||||
<div class="mt-2">
|
||||
Current Photo: <img src="../uploads/announcements/<?php echo htmlspecialchars($editAnnouncement['dspsann_photopath']); ?>" alt="Announcement Photo" class="announcement-img">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select rounded" id="status" name="status" required>
|
||||
<option value="Draft" <?php echo ($editAnnouncement && $editAnnouncement['dspsann_status'] == 'Draft') ? 'selected' : ''; ?>>Draft</option>
|
||||
<option value="Published" <?php echo ($editAnnouncement && $editAnnouncement['dspsann_status'] == 'Published') ? 'selected' : ''; ?>>Published</option>
|
||||
<option value="Archived" <?php echo ($editAnnouncement && $editAnnouncement['dspsann_status'] == 'Archived') ? 'selected' : ''; ?>>Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-<?php echo $editAnnouncement ? 'save' : 'plus'; ?> me-2"></i> <?php echo $editAnnouncement ? 'Update' : 'Add'; ?> Announcement
|
||||
</button>
|
||||
<?php if ($editAnnouncement): ?>
|
||||
<a href="manage_announcements.php" class="btn btn-secondary rounded ms-2">Cancel Edit</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-white" style="background-color: #28a745;">
|
||||
<h5 class="mb-0">All Announcements</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Photo</th>
|
||||
<th>Status</th>
|
||||
<th>Reg. Date</th>
|
||||
<th>Mod. Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($announcements)): ?>
|
||||
<?php foreach ($announcements as $ann): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($ann['pkdspsann_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($ann['dspsann_title']); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr($ann['dspsann_description'], 0, 100)) . (strlen($ann['dspsann_description']) > 100 ? '...' : ''); ?></td>
|
||||
<td>
|
||||
<?php if (!empty($ann['dspsann_photopath'])): ?>
|
||||
<img src="../uploads/announcements/<?php echo htmlspecialchars($ann['dspsann_photopath']); ?>" alt="Photo" class="announcement-img">
|
||||
<?php else: ?>
|
||||
N/A
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><span class="badge bg-<?php
|
||||
if ($ann['dspsann_status'] == 'Published') echo 'success';
|
||||
else if ($ann['dspsann_status'] == 'Draft') echo 'warning';
|
||||
else echo 'secondary';
|
||||
?>"><?php echo htmlspecialchars($ann['dspsann_status']); ?></span></td>
|
||||
<td><?php echo htmlspecialchars($ann['dspsann_reg_datetime']); ?></td>
|
||||
<td><?php echo htmlspecialchars($ann['dspsann_mod_datetime']); ?></td>
|
||||
<td>
|
||||
<a href="manage_announcements.php?action=edit&id=<?php echo htmlspecialchars($ann['pkdspsann_id']); ?>" class="btn btn-sm btn-warning rounded btn-action">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="manage_announcements.php" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this announcement? This action cannot be undone.');">
|
||||
<input type="hidden" name="action_type" value="delete">
|
||||
<input type="hidden" name="announcement_id" value="<?php echo htmlspecialchars($ann['pkdspsann_id']); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded btn-action">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center">No announcements found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ckeditor/ckeditor5-build-classic@38.1.1/build/ckeditor.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var textarea = document.querySelector('#description');
|
||||
if (textarea && typeof ClassicEditor !== 'undefined') {
|
||||
ClassicEditor
|
||||
.create(textarea, {
|
||||
toolbar: [
|
||||
'heading','|','bold','italic','underline','bulletedList','numberedList','blockQuote',
|
||||
'|','link','insertTable','undo','redo'
|
||||
]
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error('Failed to initialise rich text editor', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
269
admin/manage_classifications.php
Normal file
269
admin/manage_classifications.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Classifications.php'; // New Classifications class
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize Classifications class
|
||||
$classification = new Classifications($pdo);
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$type = $_POST['type'] ?? ''; // 'datatype' or 'category'
|
||||
$action_type = $_POST['action_type'] ?? ''; // 'add' or 'edit'
|
||||
$name_en = trim($_POST['name_en'] ?? '');
|
||||
$name_kh = trim($_POST['name_kh'] ?? '');
|
||||
$details = trim($_POST['details'] ?? '');
|
||||
$record_id = $_POST['record_id'] ?? null;
|
||||
|
||||
if (empty($name_en)) {
|
||||
set_message('English name cannot be empty.', 'danger');
|
||||
header('Location: manage_classifications.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
if ($type === 'datatype') {
|
||||
if ($action_type === 'add') {
|
||||
$classification->addDataType($name_en, $name_kh, $_SESSION['user_id']);
|
||||
set_message('Data Type added successfully!', 'success');
|
||||
} elseif ($action_type === 'edit' && $record_id) {
|
||||
$classification->updateDataType($record_id, $name_en, $name_kh, $_SESSION['user_id']);
|
||||
set_message('Data Type updated successfully!', 'success');
|
||||
} elseif ($action_type === 'delete' && $record_id) {
|
||||
$classification->deleteDataType($record_id);
|
||||
set_message('Data Type deleted successfully!', 'success');
|
||||
}
|
||||
} elseif ($type === 'category') {
|
||||
if ($action_type === 'add') {
|
||||
$classification->addCategory($name_en, $details, $_SESSION['user_id']);
|
||||
set_message('Category added successfully!', 'success');
|
||||
} elseif ($action_type === 'edit' && $record_id) {
|
||||
$classification->updateCategory($record_id, $name_en, $details, $_SESSION['user_id']);
|
||||
set_message('Category updated successfully!', 'success');
|
||||
} elseif ($action_type === 'delete' && $record_id) {
|
||||
$classification->deleteCategory($record_id);
|
||||
set_message('Category deleted successfully!', 'success');
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Error: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
header('Location: manage_classifications.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch data for display
|
||||
$dataTypes = $classification->getAllDataTypes();
|
||||
$categories = $classification->getAllCategories();
|
||||
|
||||
// Prepare data for editing if action is 'edit'
|
||||
$editDataType = null;
|
||||
$editCategory = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
if ($_GET['type'] === 'datatype') {
|
||||
$editDataType = $classification->getDataTypeById($id);
|
||||
} elseif ($_GET['type'] === 'category') {
|
||||
$editCategory = $classification->getCategoryById($id);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage Classifications</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Data Type Management -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Manage Data Types</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_classifications.php" method="POST" class="mb-4">
|
||||
<input type="hidden" name="type" value="datatype">
|
||||
<input type="hidden" name="action_type" value="<?php echo $editDataType ? 'edit' : 'add'; ?>">
|
||||
<?php if ($editDataType): ?>
|
||||
<input type="hidden" name="record_id" value="<?php echo htmlspecialchars($editDataType['pkdspstds_id']); ?>">
|
||||
<?php endif; ?>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label for="dataTypeNameEn" class="form-label">Data Type Name (English)</label>
|
||||
<input type="text" class="form-control rounded" id="dataTypeNameEn" name="name_en" value="<?php echo htmlspecialchars($editDataType['dspstds_name_en'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="dataTypeNameKh" class="form-label">Data Type Name (Khmer)</label>
|
||||
<input type="text" class="form-control rounded" id="dataTypeNameKh" name="name_kh" value="<?php echo htmlspecialchars($editDataType['dspstds_name_kh'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100 rounded">
|
||||
<i class="fas fa-<?php echo $editDataType ? 'save' : 'plus'; ?> me-2"></i> <?php echo $editDataType ? 'Update' : 'Add'; ?> Data Type
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>English Name</th>
|
||||
<th>Khmer Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($dataTypes)): ?>
|
||||
<?php foreach ($dataTypes as $dataType): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($dataType['pkdspstds_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($dataType['dspstds_name_en']); ?></td>
|
||||
<td><?php echo htmlspecialchars($dataType['dspstds_name_kh']); ?></td>
|
||||
<td>
|
||||
<a href="manage_classifications.php?action=edit&type=datatype&id=<?php echo htmlspecialchars($dataType['pkdspstds_id']); ?>" class="btn btn-sm btn-warning rounded btn-action">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="manage_classifications.php" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this Data Type?');">
|
||||
<input type="hidden" name="type" value="datatype">
|
||||
<input type="hidden" name="action_type" value="delete">
|
||||
<input type="hidden" name="record_id" value="<?php echo htmlspecialchars($dataType['pkdspstds_id']); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded btn-action">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">No data types found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Management -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">Manage Categories</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_classifications.php" method="POST" class="mb-4">
|
||||
<input type="hidden" name="type" value="category">
|
||||
<input type="hidden" name="action_type" value="<?php echo $editCategory ? 'edit' : 'add'; ?>">
|
||||
<?php if ($editCategory): ?>
|
||||
<input type="hidden" name="record_id" value="<?php echo htmlspecialchars($editCategory['pkdspscate_id']); ?>">
|
||||
<?php endif; ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="categoryTitleEn" class="form-label">Category Title (English)</label>
|
||||
<input type="text" class="form-control rounded" id="categoryTitleEn" name="name_en" value="<?php echo htmlspecialchars($editCategory['dspscate_title_en'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="categoryDetails" class="form-label">Details</label>
|
||||
<textarea class="form-control rounded-3" id="categoryDetails" name="details" rows="3"><?php echo htmlspecialchars($editCategory['dspscate_details'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-success rounded">
|
||||
<i class="fas fa-<?php echo $editCategory ? 'save' : 'plus'; ?> me-2"></i> <?php echo $editCategory ? 'Update' : 'Add'; ?> Category
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>English Title</th>
|
||||
<th>Details</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($categories)): ?>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($category['pkdspscate_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($category['dspscate_title_en']); ?></td>
|
||||
<td><?php echo htmlspecialchars($category['dspscate_details']); ?></td>
|
||||
<td>
|
||||
<a href="manage_classifications.php?action=edit&type=category&id=<?php echo htmlspecialchars($category['pkdspscate_id']); ?>" class="btn btn-sm btn-warning rounded btn-action">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="manage_classifications.php" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this Category?');">
|
||||
<input type="hidden" name="type" value="category">
|
||||
<input type="hidden" name="action_type" value="delete">
|
||||
<input type="hidden" name="record_id" value="<?php echo htmlspecialchars($category['pkdspscate_id']); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded btn-action">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">No categories found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
213
admin/manage_contactus.php
Normal file
213
admin/manage_contactus.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Contactus.php'; // This class handles dsps_tbl_feedback
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize Contactus class (for feedback management)
|
||||
$contactUs = new Contactus($pdo);
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// Handle form submissions for responding to feedback
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action_type = $_POST['action_type'] ?? ''; // 'respond' or 'delete'
|
||||
$feedback_id = $_POST['feedback_id'] ?? null;
|
||||
$respond_text = trim($_POST['respond_text'] ?? '');
|
||||
$status = $_POST['status'] ?? 'New'; // Default status for response
|
||||
|
||||
if ($action_type === 'respond') {
|
||||
if (empty($respond_text)) {
|
||||
set_message('Response text cannot be empty.', 'danger');
|
||||
} elseif ($feedback_id) {
|
||||
try {
|
||||
$contactUs->respondToFeedback($feedback_id, $respond_text, $status, $_SESSION['user_id']);
|
||||
set_message('Feedback responded to successfully!', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error responding to feedback: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
} elseif ($action_type === 'delete') {
|
||||
if ($feedback_id) {
|
||||
try {
|
||||
$contactUs->deleteFeedback($feedback_id);
|
||||
set_message('Feedback deleted successfully!', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error deleting feedback: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
header('Location: manage_contactus.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch feedback entries for display
|
||||
$feedbackEntries = $contactUs->getAllFeedback();
|
||||
|
||||
// Prepare data for responding if action is 'respond'
|
||||
$respondFeedback = null;
|
||||
if ($action === 'respond' && $id) {
|
||||
$respondFeedback = $contactUs->getFeedbackById($id);
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage Contact Us (Feedback)</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if ($respondFeedback): ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Respond to Feedback #<?php echo htmlspecialchars($respondFeedback['pkdspsfb_id']); ?></h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<strong>From:</strong> <?php echo htmlspecialchars($respondFeedback['dspsfb_name']); ?> (<?php echo htmlspecialchars($respondFeedback['dspsfb_email']); ?>)
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>Submitted On:</strong> <?php echo htmlspecialchars($respondFeedback['dspsfb_reg_datetime']); ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>Message:</strong>
|
||||
<p class="border p-3 rounded-3 bg-light"><?php echo nl2br(htmlspecialchars($respondFeedback['dspsfb_body_text'])); ?></p>
|
||||
</div>
|
||||
<?php if (!empty($respondFeedback['dspsfb_respond_text'])): ?>
|
||||
<div class="mb-3">
|
||||
<strong>Previous Response:</strong>
|
||||
<p class="border p-3 rounded-3 bg-light text-muted"><?php echo nl2br(htmlspecialchars($respondFeedback['dspsfb_respond_text'])); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="manage_contactus.php" method="POST">
|
||||
<input type="hidden" name="action_type" value="respond">
|
||||
<input type="hidden" name="feedback_id" value="<?php echo htmlspecialchars($respondFeedback['pkdspsfb_id']); ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="respond_text" class="form-label">Your Response</label>
|
||||
<textarea class="form-control rounded-3" id="respond_text" name="respond_text" rows="5" required><?php echo htmlspecialchars($respondFeedback['dspsfb_respond_text'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select rounded" id="status" name="status" required>
|
||||
<option value="New" <?php echo ($respondFeedback['dspsfb_status'] == 'New') ? 'selected' : ''; ?>>New</option>
|
||||
<option value="In Progress" <?php echo ($respondFeedback['dspsfb_status'] == 'In Progress') ? 'selected' : ''; ?>>In Progress</option>
|
||||
<option value="Resolved" <?php echo ($respondFeedback['dspsfb_status'] == 'Resolved') ? 'selected' : ''; ?>>Resolved</option>
|
||||
<option value="Archived" <?php echo ($respondFeedback['dspsfb_status'] == 'Archived') ? 'selected' : ''; ?>>Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-reply me-2"></i> Send Response
|
||||
</button>
|
||||
<a href="manage_contactus.php" class="btn btn-secondary rounded ms-2">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-white" style="background-color: #28a745;">
|
||||
<h5 class="mb-0">All Feedback Messages</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Message</th>
|
||||
<th>Status</th>
|
||||
<th>Submitted On</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($feedbackEntries)): ?>
|
||||
<?php foreach ($feedbackEntries as $feedback): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($feedback['pkdspsfb_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($feedback['dspsfb_name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($feedback['dspsfb_email']); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr($feedback['dspsfb_body_text'], 0, 100)) . (strlen($feedback['dspsfb_body_text']) > 100 ? '...' : ''); ?></td>
|
||||
<td><span class="badge bg-<?php
|
||||
if ($feedback['dspsfb_status'] == 'New') echo 'danger';
|
||||
else if ($feedback['dspsfb_status'] == 'In Progress') echo 'warning';
|
||||
else if ($feedback['dspsfb_status'] == 'Resolved') echo 'success';
|
||||
else echo 'secondary';
|
||||
?>"><?php echo htmlspecialchars($feedback['dspsfb_status']); ?></span></td>
|
||||
<td><?php echo htmlspecialchars($feedback['dspsfb_reg_datetime']); ?></td>
|
||||
<td>
|
||||
<a href="manage_contactus.php?action=respond&id=<?php echo htmlspecialchars($feedback['pkdspsfb_id']); ?>" class="btn btn-sm btn-primary rounded btn-action">
|
||||
<i class="fas fa-reply"></i> Respond
|
||||
</a>
|
||||
<form action="manage_contactus.php" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this feedback?');">
|
||||
<input type="hidden" name="action_type" value="delete">
|
||||
<input type="hidden" name="feedback_id" value="<?php echo htmlspecialchars($feedback['pkdspsfb_id']); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded btn-action">
|
||||
<i class="fas fa-trash-alt"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">No feedback messages found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
195
admin/manage_datasources.php
Normal file
195
admin/manage_datasources.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
// admin/manage_datasources.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
$search_query = trim($_GET['search'] ?? '');
|
||||
$status_filter = trim($_GET['status_filter'] ?? '');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$datasourceId = isset($_POST['datasource_id']) ? (int)$_POST['datasource_id'] : 0;
|
||||
|
||||
if ($action === 'change_status' && $datasourceId > 0) {
|
||||
$newStatus = trim($_POST['new_status'] ?? '');
|
||||
try {
|
||||
$dataSourceManager->updateDataSourceStatus($datasourceId, $newStatus, (int) $_SESSION['user_id']);
|
||||
set_message('Data source status updated successfully.', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Failed to update status: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
} elseif ($action === 'delete' && $datasourceId > 0) {
|
||||
try {
|
||||
if ($dataSourceManager->deleteDataSource($datasourceId)) {
|
||||
set_message('Data source deleted.', 'success');
|
||||
} else {
|
||||
set_message('Unable to delete data source.', 'danger');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Deletion failed: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
$redirectUrl = 'manage_datasources.php';
|
||||
$params = [];
|
||||
if ($search_query !== '') {
|
||||
$params['search'] = urlencode($search_query);
|
||||
}
|
||||
if ($status_filter !== '') {
|
||||
$params['status_filter'] = urlencode($status_filter);
|
||||
}
|
||||
if (!empty($params)) {
|
||||
$redirectUrl .= '?' . http_build_query($params);
|
||||
}
|
||||
header('Location: ' . $redirectUrl);
|
||||
exit();
|
||||
}
|
||||
|
||||
$dataSources = $dataSourceManager->getAllDataSourcesDetailed(
|
||||
$search_query !== '' ? $search_query : null,
|
||||
$status_filter !== '' ? $status_filter : null
|
||||
);
|
||||
|
||||
$statuses = ['Active', 'Inactive', 'Pending Review', 'Published'];
|
||||
$uploadsWebPath = '../uploads/datasources/';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php include_once("../includes/header_admin.php"); ?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include_once("../includes/nav_admin.php"); ?>
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage Data Sources</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">'
|
||||
. htmlspecialchars($_SESSION['message']) .
|
||||
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message'], $_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Data Sources</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="row g-3 align-items-end mb-4" method="GET" action="manage_datasources.php">
|
||||
<div class="col-md-5">
|
||||
<label for="searchDataSource" class="form-label">Search</label>
|
||||
<input type="text" class="form-control rounded" id="searchDataSource" name="search" value="<?= htmlspecialchars($search_query) ?>" placeholder="Title, owner, or type">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="statusFilter" class="form-label">Status</label>
|
||||
<select class="form-select rounded" id="statusFilter" name="status_filter">
|
||||
<option value="">All statuses</option>
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= htmlspecialchars($status) ?>" <?= $status_filter === $status ? 'selected' : '' ?>><?= htmlspecialchars($status) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-info rounded me-2"><i class="fas fa-filter me-2"></i>Apply</button>
|
||||
<a href="manage_datasources.php" class="btn btn-outline-secondary rounded"><i class="fas fa-sync-alt me-2"></i>Reset</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title</th>
|
||||
<th>Owner</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Files</th>
|
||||
<th>Registered</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($dataSources)): ?>
|
||||
<?php foreach ($dataSources as $ds): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($ds['pkdspsds_id']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['owner_name']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['data_type_name']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['category_name']) ?></td>
|
||||
<td>
|
||||
<form class="d-flex align-items-center gap-2" method="POST">
|
||||
<input type="hidden" name="datasource_id" value="<?= (int)$ds['pkdspsds_id'] ?>">
|
||||
<input type="hidden" name="action" value="change_status">
|
||||
<select class="form-select form-select-sm" name="new_status">
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= htmlspecialchars($status) ?>" <?= $ds['dspsds_status'] === $status ? 'selected' : '' ?>><?= htmlspecialchars($status) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary rounded">Update</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$fileColumns = [
|
||||
'dspsds_filename' => 'Primary',
|
||||
'dspsds_filename1' => 'Doc 1',
|
||||
'dspsds_filename2' => 'Doc 2',
|
||||
'dspsds_filename3' => 'Doc 3',
|
||||
];
|
||||
$links = [];
|
||||
foreach ($fileColumns as $column => $label) {
|
||||
$fileName = $ds[$column] ?? '';
|
||||
if ($fileName === '') {
|
||||
continue;
|
||||
}
|
||||
$isUrl = preg_match('/^https?:\\/\\//i', $fileName) === 1;
|
||||
$target = $isUrl ? $fileName : $uploadsWebPath . rawurlencode($fileName);
|
||||
$links[] = '<a class="btn btn-sm btn-outline-secondary rounded-pill me-1 mb-1" href="' . htmlspecialchars($target) . '" target="_blank" rel="noopener" title="' . htmlspecialchars($label) . '"><i class="fas fa-paperclip"></i></a>';
|
||||
}
|
||||
echo !empty($links) ? implode('', $links) : '<span class="text-muted">—</span>';
|
||||
?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars(date('Y-m-d', strtotime($ds['dspsds_reg_datetime'] ?? 'now'))) ?></td>
|
||||
<td>
|
||||
<form method="POST" onsubmit="return confirm('Delete this data source? This cannot be undone.');">
|
||||
<input type="hidden" name="datasource_id" value="<?= (int)$ds['pkdspsds_id'] ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger rounded"><i class="fas fa-trash-alt"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-4">No data sources found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include_once("../includes/footer_admin.php"); ?>
|
||||
</body>
|
||||
</html>
|
||||
218
admin/manage_faq.php
Normal file
218
admin/manage_faq.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Faq.php';
|
||||
require_once '../classes/User.php'; // To get person_id for the Faq class
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize Faq class
|
||||
$faq = new Faq($pdo);
|
||||
$user = new User($pdo); // To get the person_id of the logged-in user
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action_type = $_POST['action_type'] ?? ''; // 'add' or 'edit' or 'delete'
|
||||
$title = trim($_POST['title'] ?? ''); // This is the question
|
||||
$description = trim($_POST['description'] ?? ''); // This is the answer
|
||||
$faq_id = $_POST['faq_id'] ?? null;
|
||||
|
||||
// Get the person_id of the currently logged-in user
|
||||
$currentUserDetails = $user->getUserDetails($_SESSION['user_id']);
|
||||
$fkisp_id_of = $currentUserDetails['fkisp_id_of'];
|
||||
|
||||
if ($action_type === 'delete') {
|
||||
if ($faq_id) {
|
||||
try {
|
||||
$faq->deleteFaq($faq_id);
|
||||
set_message('FAQ entry deleted successfully!', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error deleting FAQ entry: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (empty($title) || empty($description)) {
|
||||
set_message('Question and Answer cannot be empty.', 'danger');
|
||||
} else {
|
||||
try {
|
||||
if ($action_type === 'add') {
|
||||
$faq->addFaq($title, $description, $_SESSION['user_id'], $fkisp_id_of);
|
||||
set_message('FAQ entry added successfully!', 'success');
|
||||
} elseif ($action_type === 'edit' && $faq_id) {
|
||||
$faq->updateFaq($faq_id, $title, $description, $_SESSION['user_id'], $fkisp_id_of);
|
||||
set_message('FAQ entry updated successfully!', 'success');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Error: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
header('Location: manage_faq.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch FAQ entries for display
|
||||
$faqEntries = $faq->getAllFaqs();
|
||||
|
||||
// Prepare data for editing if action is 'edit'
|
||||
$editFaq = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
$editFaq = $faq->getFaqById($id);
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage FAQ</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><?php echo $editFaq ? 'Edit' : 'Add New'; ?> FAQ Entry</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_faq.php" method="POST">
|
||||
<input type="hidden" name="action_type" value="<?php echo $editFaq ? 'edit' : 'add'; ?>">
|
||||
<?php if ($editFaq): ?>
|
||||
<input type="hidden" name="faq_id" value="<?php echo htmlspecialchars($editFaq['pkdspsfaq_id']); ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Question</label>
|
||||
<input type="text" class="form-control rounded" id="title" name="title" value="<?php echo htmlspecialchars($editFaq['dspsfaq_title_en'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label d-flex justify-content-between align-items-center">
|
||||
<span>Answer</span>
|
||||
</label>
|
||||
<textarea class="form-control rounded-3" id="description" name="description" rows="6" required><?php echo htmlspecialchars($editFaq['dspsfaq_description'] ?? ''); ?></textarea>
|
||||
<div class="form-text">Rich formatting appears on the public FAQ page—emphasise key steps and link to related resources.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-<?php echo $editFaq ? 'save' : 'plus'; ?> me-2"></i> <?php echo $editFaq ? 'Update' : 'Add'; ?> FAQ
|
||||
</button>
|
||||
<?php if ($editFaq): ?>
|
||||
<a href="manage_faq.php" class="btn btn-secondary rounded ms-2">Cancel Edit</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-white" style="background-color: #28a745;">
|
||||
<h5 class="mb-0">All FAQ Entries</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Question</th>
|
||||
<th>Answer</th>
|
||||
<th>Reg. Date</th>
|
||||
<th>Mod. Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($faqEntries)): ?>
|
||||
<?php foreach ($faqEntries as $entry): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($entry['pkdspsfaq_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['dspsfaq_title_en']); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr($entry['dspsfaq_description'], 0, 100)) . (strlen($entry['dspsfaq_description']) > 100 ? '...' : ''); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['dspsfaq_reg_datetime']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['dspsfaq_mod_datetime']); ?></td>
|
||||
<td>
|
||||
<a href="manage_faq.php?action=edit&id=<?php echo htmlspecialchars($entry['pkdspsfaq_id']); ?>" class="btn btn-sm btn-warning rounded btn-action">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="manage_faq.php" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this FAQ entry?');">
|
||||
<input type="hidden" name="action_type" value="delete">
|
||||
<input type="hidden" name="faq_id" value="<?php echo htmlspecialchars($entry['pkdspsfaq_id']); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded btn-action">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">No FAQ entries found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ckeditor/ckeditor5-build-classic@38.1.1/build/ckeditor.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var textarea = document.querySelector('#description');
|
||||
if (textarea && typeof ClassicEditor !== 'undefined') {
|
||||
ClassicEditor
|
||||
.create(textarea, {
|
||||
toolbar: [
|
||||
'heading','|','bold','italic','underline','bulletedList','numberedList','blockQuote',
|
||||
'|','link','insertTable','undo','redo'
|
||||
]
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error('Failed to initialise rich text editor', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
310
admin/manage_permissions_admin.php
Normal file
310
admin/manage_permissions_admin.php
Normal file
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php'; // Contains permission management methods
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize DataSource class
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
|
||||
// --- Handle Search and Filter Parameters ---
|
||||
$search_query = trim($_GET['search'] ?? '');
|
||||
$filter_status = trim($_GET['status_filter'] ?? '');
|
||||
|
||||
// Handle form submissions for updating permission status
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action_type = $_POST['action_type'] ?? '';
|
||||
$permission_id = $_POST['permission_id'] ?? null;
|
||||
|
||||
if ($action_type === 'update_permission_status' && $permission_id) {
|
||||
$new_status = trim($_POST['new_status'] ?? '');
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
|
||||
// --- SECURITY CHECK: Verify permission ID and associated data source existence ---
|
||||
try {
|
||||
$permission_details = $dataSourceManager->getPermissionRequestById($permission_id);
|
||||
|
||||
if (!$permission_details) {
|
||||
set_message("Permission request not found or invalid.", "danger");
|
||||
header('Location: manage_permissions_admin.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Optional: You could add a check here to ensure the data source itself is still active
|
||||
// $dataSource = $dataSourceManager->getDataSourceById($permission_details['fkdspsds_id']);
|
||||
// if (!$dataSource) { /* handle error */ }
|
||||
|
||||
if (!in_array($new_status, ['Approved', 'Pending', 'Rejected', 'Revoked'])) {
|
||||
set_message('Invalid permission status selected.', 'danger');
|
||||
} else {
|
||||
// The reg_by for permission updates is the user who is logged in (DAC Staff)
|
||||
$dataSourceManager->updatePermissionStatus(
|
||||
(int) $permission_id,
|
||||
$new_status,
|
||||
(int) $_SESSION['user_id'],
|
||||
$notes
|
||||
);
|
||||
set_message('Permission status updated successfully!', 'success');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Error updating permission status: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
// Redirect to self, preserving search/filter parameters if they exist
|
||||
$redirect_url = 'manage_permissions_admin.php';
|
||||
$query_params = [];
|
||||
if (!empty($search_query)) {
|
||||
$query_params['search'] = urlencode($search_query);
|
||||
}
|
||||
if (!empty($filter_status)) {
|
||||
$query_params['status_filter'] = urlencode($filter_status);
|
||||
}
|
||||
if (!empty($query_params)) {
|
||||
$redirect_url .= '?' . http_build_query($query_params);
|
||||
}
|
||||
header('Location: ' . $redirect_url);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch all permission requests based on search and filter parameters
|
||||
$allPermissions = $dataSourceManager->getAllPermissionRequests($filter_status, $search_query);
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage All Permissions</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded-pill" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Data Access Requests</h5>
|
||||
<!-- Removed the "Create Mock Data Source" button -->
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Search and Filter Form -->
|
||||
<form action="manage_permissions_admin.php" method="GET" class="mb-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label for="searchPermissionInput" class="form-label visually-hidden">Search Permissions</label>
|
||||
<input type="text" class="form-control rounded-pill" id="searchPermissionInput" name="search" placeholder="Search by data source, requester, owner..." value="<?= htmlspecialchars($search_query) ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="statusFilter" class="form-label visually-hidden">Filter by Status</label>
|
||||
<select class="form-select rounded-pill" id="statusFilter" name="status_filter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="Pending" <?= ($filter_status == 'Pending' ? 'selected' : '') ?>>Pending</option>
|
||||
<option value="Approved" <?= ($filter_status == 'Approved' ? 'selected' : '') ?>>Approved</option>
|
||||
<option value="Rejected" <?= ($filter_status == 'Rejected' ? 'selected' : '') ?>>Rejected</option>
|
||||
<option value="Revoked" <?= ($filter_status == 'Revoked' ? 'selected' : '') ?>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-info rounded-pill me-2"><i class="fas fa-filter me-2"></i>Apply Filter</button>
|
||||
<a href="manage_permissions_admin.php" class="btn btn-secondary rounded-pill"><i class="fas fa-sync-alt me-2"></i>Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Req. ID</th>
|
||||
<th>Data Source</th>
|
||||
<th>Requester</th>
|
||||
<th>Data Owner</th>
|
||||
<th>Permission Type</th>
|
||||
<th>Request Notes</th>
|
||||
<th>Proof</th>
|
||||
<th>Status</th>
|
||||
<th>Requested On</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($allPermissions)): ?>
|
||||
<?php foreach ($allPermissions as $permission): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($permission['pkdspsdsp_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($permission['dspsds_title_en']); ?></td>
|
||||
<td><?php echo htmlspecialchars($permission['requester_firstname'] . ' ' . $permission['requester_lastname']); ?></td>
|
||||
<td><?php echo htmlspecialchars($permission['owner_firstname'] . ' ' . $permission['owner_lastname']); ?></td>
|
||||
<td><?php echo htmlspecialchars($permission['dspsdsp_permission']); ?></td>
|
||||
<td><?php echo !empty($permission['dspsdsp_notes']) ? nl2br(htmlspecialchars($permission['dspsdsp_notes'])) : '<span class="text-muted">—</span>'; ?></td>
|
||||
<td>
|
||||
<?php if (!empty($permission['dspsdsp_proof_path'])): ?>
|
||||
<?php
|
||||
$proofPath = $permission['dspsdsp_proof_path'];
|
||||
$isExternal = preg_match('/^https?:\\/\\//i', $proofPath) === 1;
|
||||
$linkTarget = $isExternal ? $proofPath : '../uploads/' . $proofPath;
|
||||
?>
|
||||
<a href="<?php echo htmlspecialchars($linkTarget); ?>" class="btn btn-sm btn-outline-primary rounded-pill" target="_blank" rel="noopener">
|
||||
<i class="fas fa-file-pdf me-1"></i> View
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-<?php
|
||||
if ($permission['dspsdsp_status'] == 'Approved') echo 'success';
|
||||
else if ($permission['dspsdsp_status'] == 'Pending') echo 'warning';
|
||||
else if ($permission['dspsdsp_status'] == 'Rejected') echo 'danger';
|
||||
else echo 'secondary';
|
||||
?>">
|
||||
<?php echo htmlspecialchars($permission['dspsdsp_status']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($permission['dspsdsp_reg_datetime']); ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-info rounded-pill btn-action" data-bs-toggle="modal" data-bs-target="#managePermissionModal"
|
||||
data-permission-id="<?php echo htmlspecialchars($permission['pkdspsdsp_id']); ?>"
|
||||
data-data-source="<?php echo htmlspecialchars($permission['dspsds_title_en']); ?>"
|
||||
data-requester="<?php echo htmlspecialchars($permission['requester_firstname'] . ' ' . $permission['requester_lastname']); ?>"
|
||||
data-permission-type="<?php echo htmlspecialchars($permission['dspsdsp_permission']); ?>"
|
||||
data-notes="<?php echo htmlspecialchars($permission['dspsdsp_notes']); ?>"
|
||||
data-current-status="<?php echo htmlspecialchars($permission['dspsdsp_status']); ?>">
|
||||
<i class="fas fa-cogs"></i> Manage
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">No permission requests found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manage Permission Modal -->
|
||||
<div class="modal fade" id="managePermissionModal" tabindex="-1" aria-labelledby="managePermissionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header text-white rounded-top" style="background-color: #28a745;">
|
||||
<h5 class="modal-title" id="managePermissionModalLabel">Manage Permission Request</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="manage_permissions_admin.php" method="POST">
|
||||
<input type="hidden" name="action_type" value="update_permission_status">
|
||||
<input type="hidden" name="permission_id" id="modalPermissionId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="modalDataSource" class="form-label">Data Source</label>
|
||||
<input type="text" class="form-control rounded-pill" id="modalDataSource" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalRequester" class="form-label">Requester</label>
|
||||
<input type="text" class="form-control rounded-pill" id="modalRequester" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalPermissionType" class="form-label">Permission Type</label>
|
||||
<input type="text" class="form-control rounded-pill" id="modalPermissionType" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalRequestNotes" class="form-label">Requester Notes</label>
|
||||
<textarea class="form-control rounded-3" id="modalRequestNotes" rows="3" readonly></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newPermissionStatus" class="form-label">Update Status</label>
|
||||
<select class="form-select rounded-pill" id="newPermissionStatus" name="new_status" required>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Approved">Approved</option>
|
||||
<option value="Rejected">Rejected</option>
|
||||
<option value="Revoked">Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="adminNotes" class="form-label">Admin Notes (Optional)</label>
|
||||
<textarea class="form-control rounded-3" id="adminNotes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded-pill">Update Permission</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// JavaScript to populate the modal fields when "Manage" button is clicked
|
||||
var managePermissionModal = document.getElementById('managePermissionModal');
|
||||
managePermissionModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget; // Button that triggered the modal
|
||||
var permissionId = button.getAttribute('data-permission-id');
|
||||
var dataSource = button.getAttribute('data-data-source');
|
||||
var requester = button.getAttribute('data-requester');
|
||||
var permissionType = button.getAttribute('data-permission-type');
|
||||
var notes = button.getAttribute('data-notes');
|
||||
var currentStatus = button.getAttribute('data-current-status');
|
||||
|
||||
var modalPermissionId = managePermissionModal.querySelector('#modalPermissionId');
|
||||
var modalDataSource = managePermissionModal.querySelector('#modalDataSource');
|
||||
var modalRequester = managePermissionModal.querySelector('#modalRequester');
|
||||
var modalPermissionType = managePermissionModal.querySelector('#modalPermissionType');
|
||||
var modalRequestNotes = managePermissionModal.querySelector('#modalRequestNotes');
|
||||
var newPermissionStatusSelect = managePermissionModal.querySelector('#newPermissionStatus');
|
||||
|
||||
modalPermissionId.value = permissionId;
|
||||
modalDataSource.value = dataSource;
|
||||
modalRequester.value = requester;
|
||||
modalPermissionType.value = permissionType;
|
||||
modalRequestNotes.value = notes;
|
||||
newPermissionStatusSelect.value = currentStatus; // Set default selected option to current status
|
||||
// Clear admin notes field for new entry
|
||||
managePermissionModal.querySelector('#adminNotes').value = '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
258
admin/manage_slides.php
Normal file
258
admin/manage_slides.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Slide.php';
|
||||
require_once '../classes/User.php'; // Needed to get fkisp_id_of for slide creation/modification
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize Slide and User classes
|
||||
$slideManager = new Slide($pdo);
|
||||
$userManager = new User($pdo);
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// Get current user's person ID for fkisp_id_of
|
||||
$currentUserDetails = $userManager->getUserDetails($_SESSION['user_id']);
|
||||
$fkisp_id_of = $currentUserDetails['fkisp_id_of'];
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action_type = $_POST['action_type'] ?? ''; // 'add' or 'edit' or 'delete'
|
||||
$title_en = trim($_POST['title_en'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$slide_id = $_POST['slide_id'] ?? null;
|
||||
$current_photo = $_POST['current_photo'] ?? ''; // For editing, keep track of existing photo
|
||||
|
||||
if ($action_type === 'delete') {
|
||||
if ($slide_id) {
|
||||
try {
|
||||
$slideManager->deleteSlide($slide_id);
|
||||
set_message('Slide deleted successfully!', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error deleting slide: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle photo upload
|
||||
$photoPath = $current_photo; // Default to current photo if not uploading new
|
||||
if (isset($_FILES['photo']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) {
|
||||
try {
|
||||
$photoPath = $slideManager->handlePhotoUpload($_FILES['photo']);
|
||||
// If editing and a new photo is uploaded, delete the old one
|
||||
if ($action_type === 'edit' && !empty($current_photo) && $current_photo !== $photoPath) {
|
||||
// Ensure the old photo path is not empty and different from the new one
|
||||
if (!empty($current_photo) && file_exists('../uploads/slides/' . $current_photo)) {
|
||||
unlink('../uploads/slides/' . $current_photo); // Delete old file
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Photo upload error: ' . $e->getMessage(), 'danger');
|
||||
header('Location: manage_slides.php');
|
||||
exit();
|
||||
}
|
||||
} elseif ($action_type === 'add' && (!isset($_FILES['photo']) || $_FILES['photo']['error'] !== UPLOAD_ERR_OK)) {
|
||||
// For adding, a photo is required.
|
||||
set_message('Please upload a photo for the slide.', 'danger');
|
||||
header('Location: manage_slides.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
if (empty($title_en) || empty($description)) {
|
||||
set_message('Title and description cannot be empty.', 'danger');
|
||||
} else {
|
||||
try {
|
||||
if ($action_type === 'add') {
|
||||
$slideManager->addSlide($title_en, $description, $photoPath, $_SESSION['user_id'], $fkisp_id_of);
|
||||
set_message('Slide added successfully!', 'success');
|
||||
} elseif ($action_type === 'edit' && $slide_id) {
|
||||
$slideManager->updateSlide($slide_id, $title_en, $description, $photoPath, $_SESSION['user_id'], $fkisp_id_of);
|
||||
set_message('Slide updated successfully!', 'success');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Error: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
header('Location: manage_slides.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch slides for display
|
||||
$slides = $slideManager->getAllSlides();
|
||||
|
||||
// Prepare data for editing if action is 'edit'
|
||||
$editSlide = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
$editSlide = $slideManager->getSlideById($id);
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage Slides</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><?php echo $editSlide ? 'Edit' : 'Add New'; ?> Slide</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_slides.php" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action_type" value="<?php echo $editSlide ? 'edit' : 'add'; ?>">
|
||||
<?php if ($editSlide): ?>
|
||||
<input type="hidden" name="slide_id" value="<?php echo htmlspecialchars($editSlide['pkdspsslide_id']); ?>">
|
||||
<input type="hidden" name="current_photo" value="<?php echo htmlspecialchars($editSlide['dspsslide_photoname']); ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title_en" class="form-label">Slide Title (English)</label>
|
||||
<input type="text" class="form-control rounded" id="title_en" name="title_en" value="<?php echo htmlspecialchars($editSlide['dspsslide_title_en'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label d-flex justify-content-between align-items-center">
|
||||
<span>Description</span>
|
||||
</label>
|
||||
<textarea class="form-control rounded-3" id="description" name="description" rows="5" required><?php echo htmlspecialchars($editSlide['dspsslide_description'] ?? ''); ?></textarea>
|
||||
<div class="form-text">Formatted text appears on the public carousel, so emphasise key phrases and provide concise summaries.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="photo" class="form-label">Slide Photo (JPG, PNG, GIF)</label>
|
||||
<input type="file" class="form-control rounded" id="photo" name="photo" accept="image/*" <?php echo $editSlide ? '' : 'required'; ?>>
|
||||
<?php if ($editSlide && !empty($editSlide['dspsslide_photoname'])): ?>
|
||||
<div class="mt-2">
|
||||
Current Photo: <img src="../uploads/slides/<?php echo htmlspecialchars($editSlide['dspsslide_photoname']); ?>" alt="Slide Photo" class="slide-img-thumbnail">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-<?php echo $editSlide ? 'save' : 'plus'; ?> me-2"></i> <?php echo $editSlide ? 'Update' : 'Add'; ?> Slide
|
||||
</button>
|
||||
<?php if ($editSlide): ?>
|
||||
<a href="manage_slides.php" class="btn btn-secondary rounded ms-2">Cancel Edit</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-white" style="background-color: #28a745;">
|
||||
<h5 class="mb-0">All Slides</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Photo</th>
|
||||
<th>Reg. Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($slides)): ?>
|
||||
<?php foreach ($slides as $slide): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($slide['pkdspsslide_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($slide['dspsslide_title_en']); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr($slide['dspsslide_description'], 0, 100)) . (strlen($slide['dspsslide_description']) > 100 ? '...' : ''); ?></td>
|
||||
<td>
|
||||
<?php if (!empty($slide['dspsslide_photoname'])): ?>
|
||||
<img src="../uploads/slides/<?php echo htmlspecialchars($slide['dspsslide_photoname']); ?>" alt="Slide Photo" class="slide-img-thumbnail">
|
||||
<?php else: ?>
|
||||
N/A
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($slide['dspsslide_reg_datetime']); ?></td>
|
||||
<td>
|
||||
<a href="manage_slides.php?action=edit&id=<?php echo htmlspecialchars($slide['pkdspsslide_id']); ?>" class="btn btn-sm btn-warning rounded btn-action">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="manage_slides.php" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this slide? This action cannot be undone.');">
|
||||
<input type="hidden" name="action_type" value="delete">
|
||||
<input type="hidden" name="slide_id" value="<?php echo htmlspecialchars($slide['pkdspsslide_id']); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded btn-action">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">No slides found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ckeditor/ckeditor5-build-classic@38.1.1/build/ckeditor.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var textarea = document.querySelector('#description');
|
||||
if (textarea && typeof ClassicEditor !== 'undefined') {
|
||||
ClassicEditor
|
||||
.create(textarea, {
|
||||
toolbar: [
|
||||
'heading','|','bold','italic','underline','bulletedList','numberedList','blockQuote',
|
||||
'|','link','insertTable','undo','redo'
|
||||
]
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error('Failed to initialise rich text editor', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
782
admin/manage_users.php
Normal file
782
admin/manage_users.php
Normal file
@@ -0,0 +1,782 @@
|
||||
<?php
|
||||
// Start session and include necessary files
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/User.php';
|
||||
|
||||
// Redirect if not logged in or not a DAC Staff
|
||||
redirect_if_not_logged_in('../index.php');
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
// Initialize User class
|
||||
$userManager = new User($pdo);
|
||||
|
||||
// --- Handle Search and Filter Parameters ---
|
||||
$search_query = trim($_GET['search'] ?? '');
|
||||
$filter_status = trim($_GET['status_filter'] ?? ''); // New filter for user status
|
||||
$filters_active = ($search_query !== '' || $filter_status !== '');
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action_type = $_POST['action_type'] ?? '';
|
||||
$user_id = $_POST['user_id'] ?? null;
|
||||
|
||||
if ($action_type === 'update_status' && $user_id) {
|
||||
$new_status = $_POST['new_status'] ?? '';
|
||||
$requested_r_access = $_POST['can_run_r'] ?? null;
|
||||
$has_valid_r_flag = in_array($requested_r_access, ['0', '1'], true);
|
||||
|
||||
// Prevent a DAC Staff from deactivating themselves
|
||||
if ($user_id == $_SESSION['user_id'] && $new_status == 'Inactive') {
|
||||
set_message('You cannot deactivate your own account.', 'danger');
|
||||
header('Location: manage_users.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!in_array($new_status, ['DAC Staff', 'Data Contributor', 'Data Owner', 'Data User', 'Inactive'])) {
|
||||
set_message('Invalid user status selected.', 'danger');
|
||||
} else {
|
||||
try {
|
||||
$admin_user_id = (int) $_SESSION['user_id'];
|
||||
$userManager->updateUserStatus($user_id, $new_status, $admin_user_id);
|
||||
|
||||
if ($has_valid_r_flag) {
|
||||
$userManager->updateUserRJupyterAccess($user_id, $requested_r_access === '1', $admin_user_id);
|
||||
|
||||
if ((int)$user_id === (int)$_SESSION['user_id']) {
|
||||
$_SESSION['can_run_r'] = ($requested_r_access === '1');
|
||||
}
|
||||
}
|
||||
|
||||
$message = 'User status updated successfully!';
|
||||
if ($has_valid_r_flag) {
|
||||
$message = 'User status and R/Jupyter access updated successfully!';
|
||||
}
|
||||
set_message($message, 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error updating user status: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
} elseif ($action_type === 'reset_password' && $user_id) {
|
||||
$new_password = $_POST['new_password'] ?? '';
|
||||
$confirm_password = $_POST['confirm_password'] ?? '';
|
||||
|
||||
if (empty($new_password) || empty($confirm_password)) {
|
||||
set_message('Please provide and confirm the new password.', 'danger');
|
||||
} elseif ($new_password !== $confirm_password) {
|
||||
set_message('Passwords do not match. Please try again.', 'danger');
|
||||
} elseif (strlen($new_password) < 8) {
|
||||
set_message('Password must be at least 8 characters long.', 'danger');
|
||||
} else {
|
||||
try {
|
||||
$admin_user_id = (int) $_SESSION['user_id'];
|
||||
$userManager->changePassword((int)$user_id, $new_password, $admin_user_id);
|
||||
set_message('Password reset successfully.', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Error resetting password: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
} elseif ($action_type === 'add_user') {
|
||||
// --- Handle Add New User Submission ---
|
||||
$id_card = trim($_POST['id_card'] ?? '');
|
||||
$first_name_en = trim($_POST['first_name_en'] ?? '');
|
||||
$last_name_en = trim($_POST['last_name_en'] ?? '');
|
||||
$sex = trim($_POST['sex'] ?? '');
|
||||
$dob = trim($_POST['dob'] ?? '');
|
||||
$phone_number = trim($_POST['phone_number'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$confirm_password = $_POST['confirm_password'] ?? '';
|
||||
$user_role_new = trim($_POST['user_role_new'] ?? 'Data User'); // Role for new user
|
||||
|
||||
// Server-side validation for new user
|
||||
if (empty($first_name_en) || empty($last_name_en) || empty($sex) || empty($dob) || empty($username) || empty($password) || empty($confirm_password)) {
|
||||
set_message("All required fields for new user must be filled.", "danger");
|
||||
} elseif ($password !== $confirm_password) {
|
||||
set_message("Passwords do not match for new user.", "danger");
|
||||
} elseif (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
set_message("Invalid email format for new user.", "danger");
|
||||
} else {
|
||||
// Prepare data for User class
|
||||
$person_data = [
|
||||
'id_card' => $id_card,
|
||||
'first_name_en' => $first_name_en,
|
||||
'last_name_en' => $last_name_en,
|
||||
'sex' => $sex,
|
||||
'dob' => $dob,
|
||||
'pob' => null, // Add if you collect this
|
||||
'nationality' => 'Cambodian', // Default or collect
|
||||
'marital_status' => 'Single', // Default or collect
|
||||
'phone_number' => $phone_number,
|
||||
'email' => $email,
|
||||
'telegram' => null, // Add if you collect this
|
||||
'note' => null // Add if you collect this
|
||||
];
|
||||
|
||||
$user_data = [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'status' => 'Data User', // Default status for new registrations
|
||||
'can_run_r' => !empty($_POST['user_can_run_r'])
|
||||
];
|
||||
|
||||
try {
|
||||
if ($userManager->registerUser($person_data, $user_data)) {
|
||||
set_message("New user '" . htmlspecialchars($username) . "' registered successfully!", "success");
|
||||
} else {
|
||||
// This else might be redundant if registerUser always throws on failure
|
||||
set_message("Failed to register new user due to an unknown error.", "danger");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message('Error registering new user: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to self, preserving search/filter parameters if they exist
|
||||
$redirect_url = 'manage_users.php';
|
||||
$query_params = [];
|
||||
if (!empty($search_query)) {
|
||||
$query_params['search'] = urlencode($search_query);
|
||||
}
|
||||
if (!empty($filter_status)) {
|
||||
$query_params['status_filter'] = urlencode($filter_status);
|
||||
}
|
||||
if (!empty($query_params)) {
|
||||
$redirect_url .= '?' . http_build_query($query_params);
|
||||
}
|
||||
header('Location: ' . $redirect_url);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch users based on search and filter parameters
|
||||
// We will modify getAllUsers in classes/User.php to accept these parameters
|
||||
$users = $userManager->getAllUsers($search_query, $filter_status);
|
||||
|
||||
$totalUsers = count($users);
|
||||
$activeUsers = 0;
|
||||
$inactiveUsers = 0;
|
||||
$dacStaffCount = 0;
|
||||
$ownerCount = 0;
|
||||
$contributorCount = 0;
|
||||
$rAccessCount = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
$status = $user['isu_status'] ?? '';
|
||||
$isActive = $status !== 'Inactive';
|
||||
if ($isActive) {
|
||||
$activeUsers++;
|
||||
} else {
|
||||
$inactiveUsers++;
|
||||
}
|
||||
|
||||
if ($status === 'DAC Staff') {
|
||||
$dacStaffCount++;
|
||||
} elseif ($status === 'Data Owner') {
|
||||
$ownerCount++;
|
||||
} elseif ($status === 'Data Contributor') {
|
||||
$contributorCount++;
|
||||
}
|
||||
|
||||
if (!empty($user['isu_can_run_r'])) {
|
||||
$rAccessCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$summaryMetrics = [
|
||||
[
|
||||
'label' => 'Total Users',
|
||||
'value' => $totalUsers,
|
||||
'icon' => 'fa-users',
|
||||
'class' => 'bg-primary-subtle text-primary',
|
||||
'icon_class' => 'text-primary'
|
||||
],
|
||||
[
|
||||
'label' => 'Active Accounts',
|
||||
'value' => $activeUsers,
|
||||
'icon' => 'fa-user-check',
|
||||
'class' => 'bg-success-subtle text-success',
|
||||
'icon_class' => 'text-success'
|
||||
],
|
||||
[
|
||||
'label' => 'With R/Jupyter',
|
||||
'value' => $rAccessCount,
|
||||
'icon' => 'fa-flask',
|
||||
'class' => 'bg-info-subtle text-info',
|
||||
'icon_class' => 'text-info'
|
||||
],
|
||||
[
|
||||
'label' => 'Inactive',
|
||||
'value' => $inactiveUsers,
|
||||
'icon' => 'fa-user-slash',
|
||||
'class' => 'bg-warning-subtle text-warning',
|
||||
'icon_class' => 'text-warning'
|
||||
],
|
||||
];
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_admin.php");
|
||||
?>
|
||||
<style>
|
||||
.summary-cards .card {
|
||||
border: none;
|
||||
border-left: 4px solid rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.summary-cards .card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 0.75rem 1.5rem rgba(15, 68, 130, 0.15);
|
||||
}
|
||||
.summary-icon {
|
||||
font-size: 1.75rem;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.summary-label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status-badges .badge {
|
||||
font-weight: 600;
|
||||
padding: 0.45rem 0.65rem;
|
||||
}
|
||||
.table thead th {
|
||||
background-color: #f1f5f9;
|
||||
border-bottom: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(20, 86, 185, 0.05);
|
||||
}
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table-responsive {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.table-responsive::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
.table-responsive::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.btn-action-group .btn {
|
||||
min-width: 9rem;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.summary-value {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.btn-action-group .btn {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_admin.php");
|
||||
?>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Manage Users</a>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="navbar-text d-none d-md-inline">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
<button class="btn btn-outline-primary btn-sm d-md-none" type="button" data-bs-toggle="collapse" data-bs-target="#manageUsersToolbar" aria-expanded="false" aria-controls="manageUsersToolbar">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Display session messages
|
||||
if (isset($_SESSION['message'])) {
|
||||
echo '<div class="alert alert-' . $_SESSION['message_type'] . ' alert-dismissible fade show rounded" role="alert">' . htmlspecialchars($_SESSION['message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="summary-cards row g-3 mb-3">
|
||||
<?php foreach ($summaryMetrics as $metric): ?>
|
||||
<div class="col-12 col-md-6 col-xl-3">
|
||||
<div class="card shadow-sm rounded-4 <?php echo $metric['class']; ?>">
|
||||
<div class="card-body d-flex align-items-center gap-3">
|
||||
<div class="summary-icon <?php echo htmlspecialchars($metric['icon_class']); ?>">
|
||||
<i class="fas <?php echo htmlspecialchars($metric['icon']); ?>"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="summary-value"><?php echo number_format($metric['value']); ?></div>
|
||||
<div class="summary-label text-secondary-emphasis">
|
||||
<?php echo htmlspecialchars($metric['label']); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="status-badges d-flex flex-wrap gap-2 mb-4">
|
||||
<span class="badge bg-primary-subtle text-primary">DAC Staff: <?php echo number_format($dacStaffCount); ?></span>
|
||||
<span class="badge bg-info-subtle text-info">Data Owners: <?php echo number_format($ownerCount); ?></span>
|
||||
<span class="badge bg-primary-subtle text-primary">Contributors: <?php echo number_format($contributorCount); ?></span>
|
||||
<span class="badge bg-success-subtle text-success">With R/Jupyter: <?php echo number_format($rAccessCount); ?></span>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Registered Users</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-light d-none d-md-inline-flex rounded" data-bs-toggle="collapse" data-bs-target="#manageUsersToolbar" aria-expanded="<?php echo $filters_active ? 'true' : 'false'; ?>" aria-controls="manageUsersToolbar">
|
||||
<i class="fas fa-filter me-2"></i>Filters
|
||||
</button>
|
||||
<button type="button" class="btn btn-success rounded" data-bs-toggle="modal" data-bs-target="#addUserModal">
|
||||
<i class="fas fa-user-plus me-2"></i> Add New User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Search and Filter Form -->
|
||||
<div class="collapse<?php echo $filters_active ? ' show' : ''; ?>" id="manageUsersToolbar">
|
||||
<form action="manage_users.php" method="GET" class="mb-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-md-5">
|
||||
<label for="searchUserInput" class="form-label visually-hidden">Search Users</label>
|
||||
<div class="input-group shadow-sm rounded-pill overflow-hidden">
|
||||
<span class="input-group-text bg-white border-0 text-muted">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control border-0" id="searchUserInput" name="search" placeholder="Search by username, name, email..." value="<?= htmlspecialchars($search_query) ?>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="statusFilter" class="form-label visually-hidden">Filter by Role</label>
|
||||
<div class="input-group shadow-sm rounded-pill overflow-hidden">
|
||||
<span class="input-group-text bg-white border-0 text-muted">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</span>
|
||||
<select class="form-select border-0" id="statusFilter" name="status_filter">
|
||||
<option value="">All Roles</option>
|
||||
<option value="DAC Staff" <?= ($filter_status == 'DAC Staff' ? 'selected' : '') ?>>DAC Staff</option>
|
||||
<option value="Data Contributor" <?= ($filter_status == 'Data Contributor' ? 'selected' : '') ?>>Data Contributor</option>
|
||||
<option value="Data Owner" <?= ($filter_status == 'Data Owner' ? 'selected' : '') ?>>Data Owner</option>
|
||||
<option value="Data User" <?= ($filter_status == 'Data User' ? 'selected' : '') ?>>Data User</option>
|
||||
<option value="Inactive" <?= ($filter_status == 'Inactive' ? 'selected' : '') ?>>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="d-flex gap-2 justify-content-md-end">
|
||||
<button type="submit" class="btn btn-info rounded-pill flex-fill flex-md-grow-0 px-md-4">
|
||||
<i class="fas fa-filter me-2"></i>Apply
|
||||
</button>
|
||||
<a href="manage_users.php" class="btn btn-outline-secondary rounded-pill flex-fill flex-md-grow-0 px-md-4">
|
||||
<i class="fas fa-sync-alt me-2"></i>Reset
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th>
|
||||
<th>Username</th>
|
||||
<th>Full Name</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Current Role</th>
|
||||
<th>R/Jupyter</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($users)): ?>
|
||||
<?php $rowNumber = 1; ?>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td><?php echo $rowNumber++; ?></td>
|
||||
<td><?php echo htmlspecialchars($user['isu_name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($user['isp_firstname_en'] . ' ' . $user['isp_lastname_en']); ?></td>
|
||||
<td><?php echo htmlspecialchars($user['isp_email'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo htmlspecialchars($user['isp_phone_number'] ?? 'N/A'); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$roleClass = 'bg-secondary-subtle text-secondary';
|
||||
if ($user['isu_status'] == 'DAC Staff') {
|
||||
$roleClass = 'bg-danger-subtle text-danger';
|
||||
} elseif ($user['isu_status'] == 'Data Owner') {
|
||||
$roleClass = 'bg-info-subtle text-info';
|
||||
} elseif ($user['isu_status'] == 'Data User') {
|
||||
$roleClass = 'bg-success-subtle text-success';
|
||||
} elseif ($user['isu_status'] == 'Data Contributor') {
|
||||
$roleClass = 'bg-primary-subtle text-primary';
|
||||
}
|
||||
?>
|
||||
<span class="badge rounded-pill <?php echo $roleClass; ?>">
|
||||
<?php echo htmlspecialchars($user['isu_status']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($user['isu_can_run_r'])): ?>
|
||||
<span class="badge rounded-pill bg-success-subtle text-success">Enabled</span>
|
||||
<?php else: ?>
|
||||
<span class="badge rounded-pill bg-secondary-subtle text-secondary">Disabled</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2 btn-action-group">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary rounded-pill"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editUserModal"
|
||||
data-bs-tooltip="true"
|
||||
title="Change role or adjust R/Jupyter access"
|
||||
data-user-id="<?php echo htmlspecialchars($user['pkisu_id']); ?>"
|
||||
data-username="<?php echo htmlspecialchars($user['isu_name']); ?>"
|
||||
data-current-status="<?php echo htmlspecialchars($user['isu_status']); ?>"
|
||||
data-can-run-r="<?php echo !empty($user['isu_can_run_r']) ? '1' : '0'; ?>">
|
||||
<i class="fas fa-user-gear"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger rounded-pill"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#resetPasswordModal"
|
||||
data-bs-tooltip="true"
|
||||
title="Set a new password for this account"
|
||||
data-user-id="<?php echo htmlspecialchars($user['pkisu_id']); ?>"
|
||||
data-username="<?php echo htmlspecialchars($user['isu_name']); ?>">
|
||||
<i class="fas fa-key"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-muted">No users match the current filters.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit User Role Modal -->
|
||||
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header text-white rounded-top" style="background-color: #28a745;">
|
||||
<h5 class="modal-title" id="editUserModalLabel">Change User Role</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="manage_users.php" method="POST">
|
||||
<input type="hidden" name="action_type" value="update_status">
|
||||
<input type="hidden" name="user_id" id="modalUserId">
|
||||
<div class="mb-3">
|
||||
<label for="modalUsername" class="form-label">Username</label>
|
||||
<input type="text" class="form-control rounded" id="modalUsername" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalCurrentStatus" class="form-label">Current Role</label>
|
||||
<input type="text" class="form-control rounded" id="modalCurrentStatus" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">New Role</label>
|
||||
<select class="form-select rounded" id="newStatus" name="new_status" required>
|
||||
<option value="Data User">Data User</option>
|
||||
<option value="Data Contributor">Data Contributor</option>
|
||||
<option value="Data Owner">Data Owner</option>
|
||||
<option value="DAC Staff">DAC Staff</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalRAccess" class="form-label">R/Jupyter Access</label>
|
||||
<select class="form-select rounded" id="modalRAccess" name="can_run_r" required>
|
||||
<option value="1">Enabled</option>
|
||||
<option value="0">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded">Update Role</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Password Modal -->
|
||||
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header text-white rounded-top" style="background-color: #28a745;">
|
||||
<h5 class="modal-title" id="resetPasswordModalLabel">Reset User Password</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="manage_users.php" method="POST" id="resetPasswordForm" autocomplete="off">
|
||||
<input type="hidden" name="action_type" value="reset_password">
|
||||
<input type="hidden" name="user_id" id="resetUserId">
|
||||
<div class="mb-3">
|
||||
<label for="resetUsername" class="form-label">Username</label>
|
||||
<input type="text" class="form-control rounded" id="resetUsername" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newPassword" class="form-label">New Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control rounded" id="newPassword" name="new_password" required minlength="8">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirmNewPassword" class="form-label">Confirm New Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control rounded" id="confirmNewPassword" name="confirm_password" required minlength="8">
|
||||
</div>
|
||||
<div class="alert alert-warning rounded d-flex align-items-start gap-2">
|
||||
<i class="fas fa-exclamation-triangle mt-1"></i>
|
||||
<span>Resetting the password will immediately replace the user's existing credentials. Provide the new password to the user securely.</span>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded"><i class="fas fa-key me-2"></i>Reset Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add New User Modal -->
|
||||
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header text-white rounded-top" style="background-color: #28a745;">
|
||||
<h5 class="modal-title" id="addUserModalLabel">Add New User</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="manage_users.php" method="POST" id="addUserForm">
|
||||
<input type="hidden" name="action_type" value="add_user">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-3 text-primary">Personal Information</h6>
|
||||
<div class="mb-3">
|
||||
<label for="addFirstName" class="form-label">First Name (EN) <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control rounded" id="addFirstName" name="first_name_en" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addLastName" class="form-label">Last Name (EN) <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control rounded" id="addLastName" name="last_name_en" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addSex" class="form-label">Sex <span class="text-danger">*</span></label>
|
||||
<select class="form-select rounded" id="addSex" name="sex" required>
|
||||
<option value="">Select...</option>
|
||||
<option value="Male">Male</option>
|
||||
<option value="Female">Female</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addDob" class="form-label">Date of Birth <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control rounded" id="addDob" name="dob" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addPob" class="form-label">Place of Birth</label>
|
||||
<input type="text" class="form-control rounded" id="addPob" name="pob">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addNationality" class="form-label">Nationality</label>
|
||||
<input type="text" class="form-control rounded" id="addNationality" name="nationality" value="Cambodian">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-3 text-primary">Contact Information</h6>
|
||||
<div class="mb-3">
|
||||
<label for="addPhoneNumber" class="form-label">Phone Number</label>
|
||||
<input type="tel" class="form-control rounded" id="addPhoneNumber" name="phone_number">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addEmail" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control rounded" id="addEmail" name="email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addTelegram" class="form-label">Telegram</label>
|
||||
<input type="text" class="form-control rounded" id="addTelegram" name="telegram">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addNote" class="form-label">Note</label>
|
||||
<textarea class="form-control rounded" id="addNote" name="note" rows="2"></textarea>
|
||||
</div>
|
||||
<h6 class="mb-3 text-primary">Account Information</h6>
|
||||
<div class="mb-3">
|
||||
<label for="addUsername" class="form-label">Username <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control rounded" id="addUsername" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addPassword" class="form-label">Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control rounded" id="addPassword" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addConfirmPassword" class="form-label">Confirm Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control rounded" id="addConfirmPassword" name="confirm_password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addUserRoleNew" class="form-label">Assign Role</label>
|
||||
<select class="form-select rounded" id="addUserRoleNew" name="user_role_new" required>
|
||||
<option value="Data User">Data User</option>
|
||||
<option value="Data Contributor">Data Contributor</option>
|
||||
<option value="Data Owner">Data Owner</option>
|
||||
<option value="DAC Staff">DAC Staff</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="addUserRAccess" name="user_can_run_r" value="1">
|
||||
<label class="form-check-label" for="addUserRAccess">Allow R/Jupyter Access</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid mt-4">
|
||||
<button type="submit" class="btn btn-primary rounded">Add User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-tooltip="true"]'));
|
||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
||||
if (window.bootstrap && bootstrap.Tooltip) {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
}
|
||||
});
|
||||
|
||||
// JavaScript to populate the modal fields when "Change Role" button is clicked
|
||||
var editUserModal = document.getElementById('editUserModal');
|
||||
if (editUserModal) {
|
||||
editUserModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget; // Button that triggered the modal
|
||||
var userId = button.getAttribute('data-user-id');
|
||||
var username = button.getAttribute('data-username');
|
||||
var currentStatus = button.getAttribute('data-current-status');
|
||||
var canRunR = button.getAttribute('data-can-run-r') || '0';
|
||||
|
||||
var modalUserId = editUserModal.querySelector('#modalUserId');
|
||||
var modalUsername = editUserModal.querySelector('#modalUsername');
|
||||
var modalCurrentStatus = editUserModal.querySelector('#modalCurrentStatus');
|
||||
var newStatusSelect = editUserModal.querySelector('#newStatus');
|
||||
var modalRAccess = editUserModal.querySelector('#modalRAccess');
|
||||
|
||||
modalUserId.value = userId;
|
||||
modalUsername.value = username;
|
||||
modalCurrentStatus.value = currentStatus;
|
||||
newStatusSelect.value = currentStatus; // Set default selected option to current status
|
||||
if (modalRAccess) {
|
||||
modalRAccess.value = canRunR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// JavaScript for password confirmation in Add New User Modal
|
||||
var addUserForm = document.getElementById('addUserForm');
|
||||
if (addUserForm) {
|
||||
addUserForm.addEventListener('submit', function(event) {
|
||||
var password = document.getElementById('addPassword').value;
|
||||
var confirmPassword = document.getElementById('addConfirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
alert('Passwords do not match!');
|
||||
event.preventDefault(); // Prevent form submission
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Populate Reset Password modal
|
||||
var resetPasswordModal = document.getElementById('resetPasswordModal');
|
||||
if (resetPasswordModal) {
|
||||
resetPasswordModal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event.relatedTarget;
|
||||
var userIdField = document.getElementById('resetUserId');
|
||||
var usernameField = document.getElementById('resetUsername');
|
||||
var newPasswordField = document.getElementById('newPassword');
|
||||
var confirmPasswordField = document.getElementById('confirmNewPassword');
|
||||
|
||||
if (trigger && userIdField && usernameField) {
|
||||
userIdField.value = trigger.getAttribute('data-user-id') || '';
|
||||
usernameField.value = trigger.getAttribute('data-username') || '';
|
||||
}
|
||||
if (newPasswordField) newPasswordField.value = '';
|
||||
if (confirmPasswordField) confirmPasswordField.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
var resetPasswordForm = document.getElementById('resetPasswordForm');
|
||||
if (resetPasswordForm) {
|
||||
resetPasswordForm.addEventListener('submit', function (event) {
|
||||
var newPassword = document.getElementById('newPassword').value;
|
||||
var confirmPassword = document.getElementById('confirmNewPassword').value;
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert('Passwords do not match!');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
alert('Password must be at least 8 characters.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_admin.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
162
admin/r_in_jupyter.php
Normal file
162
admin/r_in_jupyter.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
// admin/r_in_jupyter.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
require_once '../includes/jupyter_helpers.php';
|
||||
|
||||
// Limit access to DAC Staff (Admin role)
|
||||
redirect_if_not_role('DAC Staff', '../index.php');
|
||||
|
||||
$hasRJupyterAccess = has_r_access();
|
||||
$workspaceSync = ['synced' => [], 'missing' => [], 'workspace_dir' => null];
|
||||
$workspaceRelativeDir = null;
|
||||
$workspaceError = null;
|
||||
|
||||
if ($hasRJupyterAccess && isset($_SESSION['person_id'])) {
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
try {
|
||||
$workspaceSync = $dataSourceManager->prepareJupyterWorkspace(
|
||||
(int) $_SESSION['person_id'],
|
||||
dirname(__DIR__) . '/uploads/jupyter_workspace'
|
||||
);
|
||||
$workspaceRelativeDir = 'datasources/user_' . (int) $_SESSION['person_id'];
|
||||
} catch (Exception $e) {
|
||||
$workspaceError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$jupyterBaseUrl = dsp_jupyter_base_url();
|
||||
$jupyterToken = dsp_jupyter_token();
|
||||
$jupyterIframeUrl = dsp_jupyter_iframe_url(
|
||||
$jupyterBaseUrl,
|
||||
$jupyterToken,
|
||||
isset($_SESSION['person_id']) ? (int) $_SESSION['person_id'] : null
|
||||
);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php include_once("../includes/header_admin.php"); ?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include_once("../includes/nav_admin.php"); ?>
|
||||
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">R in JupyterHub</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-header bg-light rounded-top d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">Full JupyterLab Access</h5>
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<span class="badge bg-success-subtle text-success rounded-pill">
|
||||
<i class="fas fa-flask me-1"></i> Enabled
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning-subtle text-warning rounded-pill">
|
||||
<i class="fas fa-lock me-1"></i> Disabled
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<?php if ($workspaceError): ?>
|
||||
<div class="alert alert-danger rounded mb-3">
|
||||
<strong>Workspace error:</strong> <?= htmlspecialchars($workspaceError) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="mb-3">
|
||||
Approved data sources have been synced to
|
||||
<code><?= htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?></code>
|
||||
inside the Jupyter environment. Only files you are approved to use are available.
|
||||
</p>
|
||||
<?php if (!empty($workspaceSync['synced'])): ?>
|
||||
<div class="table-responsive mb-3">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Data Source</th>
|
||||
<th>Data Type</th>
|
||||
<th>Category</th>
|
||||
<th>Filename</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($workspaceSync['synced'] as $idx => $syncedItem): ?>
|
||||
<tr>
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['title']) ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['category'] ?? 'N/A') ?></td>
|
||||
<td><code><?= htmlspecialchars(basename($syncedItem['relative_path'])) ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-3">
|
||||
No approved data sources were found for your account. Use Manage Users to approve access.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($workspaceSync['missing'])): ?>
|
||||
<div class="alert alert-warning rounded mb-3">
|
||||
<strong>Some datasets could not be synced:</strong>
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($workspaceSync['missing'] as $missingItem): ?>
|
||||
<li><?= htmlspecialchars($missingItem['title']) ?> — <?= htmlspecialchars($missingItem['reason']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<p class="mb-3">
|
||||
Use the embedded Jupyter workspace to manage R notebooks, explore uploaded datasets, and
|
||||
collaborate with Data Owners. This view runs with your admin permissions.
|
||||
</p>
|
||||
<div class="ratio ratio-16x9 border rounded overflow-hidden">
|
||||
<iframe
|
||||
src="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>"
|
||||
title="R in JupyterHub for DAC Staff"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
></iframe>
|
||||
</div>
|
||||
<p class="mt-3 mb-0">
|
||||
Prefer the full window? <a href="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">Open Jupyter in a new tab</a>.
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning rounded d-flex align-items-start gap-3">
|
||||
<i class="fas fa-circle-info mt-1"></i>
|
||||
<div>
|
||||
<strong>R in JupyterHub is currently disabled for your account.</strong><br>
|
||||
Visit <a href="manage_users.php">Manage Users</a> to enable R/Jupyter access for yourself or ask another DAC Staff member to toggle the permission.
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
Once access is enabled, refresh this page to launch the JupyterLab workspace.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-secondary border-0 bg-light-subtle text-muted small">
|
||||
Need the current Jupyter configuration? Visit
|
||||
<a href="../install_config.php#r-in-jupyter-service" class="alert-link">Install & Configuration</a>
|
||||
for defaults, overrides, and runtime details.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include_once("../includes/footer_admin.php"); ?>
|
||||
</body>
|
||||
</html>
|
||||
203
api/run_r_script.php
Normal file
203
api/run_r_script.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
// api/run_r_script.php
|
||||
session_start();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
|
||||
// Get the user details from the session. This relies on your login system.
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
$person_id = $_SESSION['person_id'] ?? null;
|
||||
|
||||
// Only allow POST requests and authenticated users with explicit R access.
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($user_id)) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized. Please log in to run this script.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!has_r_access()) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'You do not have permission to run R/Jupyter scripts.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (empty($person_id)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Missing user context for R execution.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if ($data === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid JSON input.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$script_name = $data['script_name'] ?? null;
|
||||
$data_source_id = isset($data['data_source_id']) ? (int)$data['data_source_id'] : null;
|
||||
$parameters = $data['parameters'] ?? [];
|
||||
|
||||
$r_script_path_dir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'r_scripts' . DIRECTORY_SEPARATOR;
|
||||
|
||||
$allowed_r_scripts = [
|
||||
'data_summary.R' => 'Basic Data Summary',
|
||||
'descriptive_stats.R' => 'Numeric column descriptive statistics',
|
||||
'category_frequency.R' => 'Categorical frequency distribution',
|
||||
];
|
||||
|
||||
if (!isset($allowed_r_scripts[$script_name])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid R script selected.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (empty($data_source_id)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data source ID is required.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
$data_source_details = $dataSourceManager->getDataSourceById($data_source_id);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to lookup data source.', 'debug' => $e->getMessage()]);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!$data_source_details) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data source not found.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$data_source_owner_person_id = $data_source_details['fkisp_id_of'] ?? null;
|
||||
$has_access_to_data = ($data_source_owner_person_id && (int)$data_source_owner_person_id === (int)$person_id);
|
||||
|
||||
if (!$has_access_to_data) {
|
||||
try {
|
||||
$has_access_to_data = $dataSourceManager->hasPermission((int)$person_id, $data_source_id, 'Analyze');
|
||||
} catch (Exception $e) {
|
||||
error_log('Analyze permission check failed: ' . $e->getMessage());
|
||||
$has_access_to_data = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_access_to_data) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'You are not allowed to analyze this data source.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$source_filename = $data_source_details['dspsds_filename'] ?? '';
|
||||
if (empty($source_filename)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'No associated data file found for this data source.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$upload_dir = $dataSourceManager->getUploadDir();
|
||||
$upload_dir_real = realpath($upload_dir) ?: $upload_dir;
|
||||
$full_data_source_path = realpath($upload_dir . $source_filename);
|
||||
|
||||
if ($full_data_source_path === false || strpos($full_data_source_path, $upload_dir_real) !== 0 || !is_file($full_data_source_path)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data source file could not be located.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// --- Create a temporary file for the R script to use ---
|
||||
// This is a security best practice to prevent R from accessing arbitrary files.
|
||||
$temp_data_file = tempnam(sys_get_temp_dir(), 'rdata_') . '.csv';
|
||||
copy($full_data_source_path, $temp_data_file);
|
||||
|
||||
// Log the usage of the data source
|
||||
error_log("Logging usage for data source: $data_source_id by user: $user_id for action: Ran Analysis");
|
||||
|
||||
// --- Path and Permission Checks ---
|
||||
$r_executable_path = getenv('RSCRIPT_PATH') ?: '/usr/bin/Rscript';
|
||||
$r_script_full_path = $r_script_path_dir . $script_name;
|
||||
|
||||
if (!file_exists($r_executable_path) || !is_executable($r_executable_path)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => "Rscript executable not found or not executable at: {$r_executable_path}"]);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!file_exists($r_script_full_path) || !is_readable($r_script_full_path)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => "R script not found or not readable at: {$r_script_full_path}"]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Build the command, specifying the R executable directly.
|
||||
$params_json_str = json_encode($parameters, JSON_UNESCAPED_SLASHES);
|
||||
if ($params_json_str === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to encode parameters to JSON.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$command = escapeshellcmd($r_executable_path) . " " . escapeshellarg($r_script_full_path) . " " . escapeshellarg($temp_data_file) . " " . escapeshellarg($params_json_str);
|
||||
|
||||
// --- Use proc_open for better error capture ---
|
||||
$descriptorspec = array(
|
||||
0 => array("pipe", "r"), // stdin
|
||||
1 => array("pipe", "w"), // stdout
|
||||
2 => array("pipe", "w") // stderr
|
||||
);
|
||||
|
||||
$process = proc_open($command, $descriptorspec, $pipes);
|
||||
|
||||
if (!is_resource($process)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to open process to R.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[2]);
|
||||
|
||||
$return_code = proc_close($process);
|
||||
|
||||
// Clean up the temporary data file
|
||||
if (file_exists($temp_data_file)) {
|
||||
unlink($temp_data_file);
|
||||
}
|
||||
|
||||
if ($return_code !== 0) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => "R script execution failed. Exit code: {$return_code}",
|
||||
'r_stdout' => $stdout,
|
||||
'r_stderr' => $stderr
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$r_results = json_decode($stdout, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to decode JSON from R script output.', 'r_stdout' => $stdout, 'r_stderr' => $stderr]);
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
$dataSourceManager->logDataSourceUsage($data_source_id, (int)$person_id, 'Ran Analysis via API', (int)$user_id);
|
||||
} catch (Exception $e) {
|
||||
error_log('Failed to log R analysis usage: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'success', 'results' => $r_results]);
|
||||
BIN
assets/.DS_Store
vendored
Normal file
BIN
assets/.DS_Store
vendored
Normal file
Binary file not shown.
54
assets/diagrams/data_ecosystem.mmd
Normal file
54
assets/diagrams/data_ecosystem.mmd
Normal file
@@ -0,0 +1,54 @@
|
||||
%% Data Ecosystem Diagram for NIPH DSP
|
||||
%% Render with Mermaid (https://mermaid.js.org/)
|
||||
|
||||
flowchart LR
|
||||
subgraph Clients
|
||||
U1[Public/Authenticated Users]
|
||||
U2[DAC Staff]
|
||||
end
|
||||
|
||||
subgraph WebTier[PHP + Apache (dsp_app)]
|
||||
UI[Portal UI<br/>Dashboards & Workflows]
|
||||
API[PHP APIs<br/>Uploads & R runners]
|
||||
end
|
||||
|
||||
subgraph Database[(MySQL 8.0<br/>dsp_db)]
|
||||
DS[(dsps_tbl_datasource)]
|
||||
PERM[(dsps_tbl_datasource_permission)]
|
||||
PEOPLE[(ist_tbl_people & ist_tbl_users)]
|
||||
USAGE[(dsps_tbl_datasource_used)]
|
||||
end
|
||||
|
||||
subgraph Storage[/Shared Volumes/]
|
||||
FILES[(uploads/datasources)]
|
||||
RSCRIPTS[(r_scripts)]
|
||||
JWS[(uploads/jupyter_workspace)]
|
||||
end
|
||||
|
||||
subgraph Tooling
|
||||
PMA[phpMyAdmin<br/>dsp_phpmyadmin]
|
||||
JUP[Jupyter (R kernel)<br/>dsp_jupyter]
|
||||
end
|
||||
|
||||
U1 -->|HTTPS :8082| UI
|
||||
U2 -->|HTTPS :8082| UI
|
||||
U2 -->|Admin Ops| PMA
|
||||
|
||||
UI --> API
|
||||
API -->|SQL queries| Database
|
||||
PMA -->|SQL admin| Database
|
||||
|
||||
API -->|File uploads/downloads| FILES
|
||||
API -->|Sync approved files| JWS
|
||||
API -->|Whitelisted scripts| RSCRIPTS
|
||||
|
||||
JUP -->|Mounts| FILES
|
||||
JUP -->|Mounts| RSCRIPTS
|
||||
JUP -->|Per-user workspace| JWS
|
||||
JUP -->|Notebook insights| U1
|
||||
|
||||
Database -->|Metadata feeds| UI
|
||||
Database -->|Permission checks| API
|
||||
Database -->|Audit logs| DAC[DAC Reports]
|
||||
|
||||
style DAC fill:#f7f9fb,stroke:#93a1c3,stroke-width:1px
|
||||
BIN
assets/images/niph_dsp_data_ecosystem.png
Normal file
BIN
assets/images/niph_dsp_data_ecosystem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 541 KiB |
BIN
assets/images/niphlogo.ico
Normal file
BIN
assets/images/niphlogo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
assets/images/niphlogo.png
Normal file
BIN
assets/images/niphlogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
247
browse_datasources.php
Normal file
247
browse_datasources.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
// browse_datasources.php
|
||||
// This file displays a list of data sources based on optional search and category filters.
|
||||
// It is intended to be included by index.php or accessed directly via a link.
|
||||
|
||||
// --- DEBUGGING START ---
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
// --- DEBUGGING END ---
|
||||
|
||||
// Ensure all necessary dependencies are included
|
||||
require_once 'config.php';
|
||||
require_once 'classes/DataSource.php';
|
||||
require_once 'classes/Classifications.php';
|
||||
require_once 'classes/PermissionManager.php'; // Ensure this class is available
|
||||
|
||||
// Instantiate the DataSource and Classifications classes
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
$classification_manager = new Classifications($pdo);
|
||||
$permissionManager = new PermissionManager($pdo); // Assuming this is available
|
||||
|
||||
// Get filter parameters from the URL
|
||||
$category_id = $_GET['category_id'] ?? null;
|
||||
$search_query = $_GET['search'] ?? null;
|
||||
|
||||
// Sanitize the input to prevent security vulnerabilities
|
||||
$category_id = filter_var($category_id, FILTER_SANITIZE_NUMBER_INT);
|
||||
$search_query = filter_var($search_query, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
// Fetch data sources from the database based on the filters
|
||||
$data_sources = [];
|
||||
try {
|
||||
$data_sources = $data_source_manager->getFilteredDataSources($category_id, $search_query);
|
||||
} catch (Exception $e) {
|
||||
// Log the error for debugging.
|
||||
error_log("Error fetching data sources: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Fetch all categories for the filter dropdown
|
||||
$all_categories = [];
|
||||
try {
|
||||
$all_categories = $classification_manager->getAllCategories();
|
||||
} catch (Exception $e) {
|
||||
error_log("Error fetching categories: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// These variables should be defined in your main index.php
|
||||
// For this file to work correctly, you need to ensure $is_logged_in, $person_id,
|
||||
// and the $permissionManager object are created and available.
|
||||
$is_logged_in = false; // Placeholder, replace with actual logic
|
||||
$person_id = null; // Placeholder, replace with actual logic
|
||||
|
||||
$uploadsWebPath = 'uploads/datasources/';
|
||||
?>
|
||||
|
||||
<div class="container main-content">
|
||||
<h2 class="text-center mb-4">Browse Data Sources</h2>
|
||||
|
||||
<!-- Filter and Search Form -->
|
||||
<div class="card shadow-sm rounded-3 mb-4 p-3">
|
||||
<div class="card-body">
|
||||
<form action="index.php" method="GET" class="row g-3 align-items-end">
|
||||
<!-- IMPORTANT: This hidden input tells index.php to load this page's content -->
|
||||
<input type="hidden" name="page" value="browse_datasources">
|
||||
<div class="col-md-4">
|
||||
<label for="categoryFilter" class="form-label">Filter by Category:</label>
|
||||
<select class="form-select rounded" id="categoryFilter" name="category_id">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach ($all_categories as $category): ?>
|
||||
<?php
|
||||
$category_id_val = $category['pkdspscate_id'] ?? '';
|
||||
$category_title = $category['dspscate_title_en'] ?? 'Unknown Category';
|
||||
?>
|
||||
<option value="<?= htmlspecialchars($category_id_val) ?>"
|
||||
<?= ($category_id == $category_id_val) ? 'selected' : ''; ?>>
|
||||
<?= htmlspecialchars($category_title) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="searchQuery" class="form-label">Search by Title/Description:</label>
|
||||
<input type="text" class="form-control rounded" id="searchQuery" name="search"
|
||||
value="<?= htmlspecialchars($search_query) ?>" placeholder="Enter keywords...">
|
||||
</div>
|
||||
<div class="col-md-3 d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-search me-2"></i> Search Data
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($search_query) || !empty($category_id)): ?>
|
||||
<div class="alert alert-info rounded shadow-sm text-center">
|
||||
<?php
|
||||
$message = "Showing results for: ";
|
||||
$filters = [];
|
||||
if (!empty($search_query)) {
|
||||
$filters[] = "Search Term: '<strong>" . htmlspecialchars($search_query) . "</strong>'";
|
||||
}
|
||||
if (!empty($category_id)) {
|
||||
// Get category details for the message
|
||||
$category_details = null;
|
||||
foreach ($all_categories as $cat) {
|
||||
if (($cat['pkdspscate_id'] ?? '') == $category_id) {
|
||||
$category_details = $cat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($category_details) {
|
||||
$category_title = $category_details['dspscate_title_en'] ?? 'Unknown Category';
|
||||
$filters[] = "Category: '<strong>" . htmlspecialchars($category_title) . "</strong>'";
|
||||
}
|
||||
}
|
||||
echo $message . implode(" and ", $filters);
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data_sources)): ?>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
<?php foreach ($data_sources as $ds): ?>
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm rounded-3 overflow-hidden border border-gray-200">
|
||||
<?php
|
||||
$title = $ds['dspsds_title_en'] ?? '';
|
||||
$description = $ds['dspsds_description'] ?? '';
|
||||
$id = $ds['pkdspsds_id'] ?? '';
|
||||
|
||||
?>
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title text-primary fw-bold"><?= htmlspecialchars($title) ?></h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Category: <?= htmlspecialchars($ds['category_name']) ?></h6>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Type: <?= htmlspecialchars($ds['data_type_name']) ?></h6>
|
||||
<p class="card-text text-muted flex-grow-1">
|
||||
<?php
|
||||
$shortDescription = substr($description, 0, 100);
|
||||
if (strlen($description) > 100) {
|
||||
$shortDescription .= '...';
|
||||
}
|
||||
echo htmlspecialchars($shortDescription);
|
||||
?>
|
||||
</p>
|
||||
<div class="mt-auto">
|
||||
<ul class="list-unstyled small text-muted">
|
||||
<li><i class="fas fa-user me-1"></i> Data Owner: <?= htmlspecialchars($ds['isp_firstname_en'] . ' ' . $ds['isp_lastname_en']); ?></li>
|
||||
<li><i class="fas fa-calendar-alt me-1"></i> Published:
|
||||
<?php if (!empty($ds['dspsds_public_date'])): ?>
|
||||
<?= htmlspecialchars(date('M d, Y', strtotime($ds['dspsds_public_date']))); ?>
|
||||
<?php else: ?>
|
||||
Not specified
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<?php
|
||||
$supportingFiles = [
|
||||
'dspsds_filename1' => ['label' => 'Questionnaire / Data Dictionary', 'icon' => 'fa-clipboard-list'],
|
||||
'dspsds_filename2' => ['label' => 'Protocol / User Guide', 'icon' => 'fa-book'],
|
||||
'dspsds_filename3' => ['label' => 'Other Supporting Document', 'icon' => 'fa-file-alt'],
|
||||
];
|
||||
?>
|
||||
<div class="bg-light-subtle border rounded p-3 mb-3">
|
||||
<span class="d-block text-uppercase text-muted fw-semibold small mb-2">Supporting Documents</span>
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
<?php foreach ($supportingFiles as $column => $meta): ?>
|
||||
<?php
|
||||
$fileName = $ds[$column] ?? '';
|
||||
$label = $meta['label'];
|
||||
$icon = $meta['icon'];
|
||||
?>
|
||||
<li class="mb-2">
|
||||
<i class="fas <?= htmlspecialchars($icon, ENT_QUOTES, 'UTF-8') ?> me-1"></i>
|
||||
<?php if (!empty($fileName)): ?>
|
||||
<?php
|
||||
$isUrl = preg_match('/^https?:\/\//i', $fileName) === 1;
|
||||
$linkTarget = $isUrl ? $fileName : $uploadsWebPath . rawurlencode($fileName);
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($linkTarget, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars($label) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted"><?= htmlspecialchars($label) ?> (Not provided)</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php if ($is_logged_in): ?>
|
||||
<?php
|
||||
$has_read_permission = false;
|
||||
$has_download_permission = false;
|
||||
try {
|
||||
$has_read_permission = $permissionManager->hasPermission($person_id, $ds['pkdspsds_id'], 'Read');
|
||||
$has_download_permission = $permissionManager->hasPermission($person_id, $ds['pkdspsds_id'], 'Download');
|
||||
} catch (Exception $e) {
|
||||
error_log("Permission check error for user " . $person_id . " on DS " . $ds['pkdspsds_id'] . ": " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<?php if ($has_read_permission): ?>
|
||||
<a href="#" class="btn btn-sm btn-outline-success rounded me-2 disabled">
|
||||
<i class="fas fa-check-circle me-1"></i> Read Access Granted
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-success rounded me-2" data-bs-toggle="modal" data-bs-target="#requestPermissionModal"
|
||||
data-ds-id="<?= htmlspecialchars($ds['pkdspsds_id']); ?>"
|
||||
data-ds-title="<?= htmlspecialchars($ds['dspsds_title_en']); ?>"
|
||||
data-permission-type="Read">
|
||||
<i class="fas fa-file-alt me-1"></i> Request Read Access
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($has_download_permission): ?>
|
||||
<a href="download.php?dspsds_id=<?= htmlspecialchars($ds['pkdspsds_id']); ?>" class="btn btn-sm btn-outline-primary rounded">
|
||||
<i class="fas fa-download me-1"></i> Download File
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-primary rounded" data-bs-toggle="modal" data-bs-target="#requestPermissionModal"
|
||||
data-ds-id="<?= htmlspecialchars($ds['pkdspsds_id']); ?>"
|
||||
data-ds-title="<?= htmlspecialchars($ds['dspsds_title_en']); ?>"
|
||||
data-permission-type="Download">
|
||||
<i class="fas fa-cloud-download-alt me-1"></i> Request Download
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-secondary rounded" data-bs-toggle="modal" data-bs-target="#loginModal">
|
||||
<i class="fas fa-sign-in-alt me-1"></i> Login to Request Access
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning text-center rounded-3">
|
||||
<h4 class="alert-heading">No Data Sources Found</h4>
|
||||
<p>We couldn't find any data sources matching your criteria. Try adjusting your search term or selecting a different category.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
127
classes/Aboutus.php
Normal file
127
classes/Aboutus.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
class Aboutus {
|
||||
private $pdo;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new "About Us" entry to the database.
|
||||
*
|
||||
* @param string $title_en The English title (e.g., Vision, Mission, Goal).
|
||||
* @param string $description The detailed description.
|
||||
* @param int $reg_by The ID of the user who registered this entry (from ist_tbl_users).
|
||||
* @param int $fkisp_id_of The ID of the person associated with this entry (from ist_tbl_people).
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or title already exists.
|
||||
*/
|
||||
public function addAboutUs(string $title_en, string $description, int $reg_by, int $fkisp_id_of): bool {
|
||||
$sql = "INSERT INTO dsps_tbl_dspsabout (dspsabout_title_en, dspsabout_description, dspsabout_reg_by, fkisp_id_of)
|
||||
VALUES (:title_en, :description, :reg_by, :fkisp_id_of)";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':reg_by', $reg_by);
|
||||
$stmt->bindParam(':fkisp_id_of', $fkisp_id_of);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == '23000') { // Integrity constraint violation (e.g., duplicate title if UNIQUE)
|
||||
throw new Exception("An 'About Us' entry with this title already exists.");
|
||||
}
|
||||
error_log("Error adding About Us entry: " . $e->getMessage());
|
||||
throw new Exception("Could not add About Us entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing "About Us" entry.
|
||||
*
|
||||
* @param int $id The ID of the entry to update.
|
||||
* @param string $title_en The new English title.
|
||||
* @param string $description The new description.
|
||||
* @param int $mod_by The ID of the user who modified this entry.
|
||||
* @param int $fkisp_id_of The ID of the person associated with this entry.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or title already exists.
|
||||
*/
|
||||
public function updateAboutUs(int $id, string $title_en, string $description, int $mod_by, int $fkisp_id_of): bool {
|
||||
$sql = "UPDATE dsps_tbl_dspsabout
|
||||
SET dspsabout_title_en = :title_en, dspsabout_description = :description,
|
||||
dspsabout_mod_datetime = CURRENT_TIMESTAMP, dspsabout_reg_by = :mod_by, fkisp_id_of = :fkisp_id_of
|
||||
WHERE pkdspsabout_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':mod_by', $mod_by);
|
||||
$stmt->bindParam(':fkisp_id_of', $fkisp_id_of);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == '23000') {
|
||||
throw new Exception("An 'About Us' entry with this title already exists.");
|
||||
}
|
||||
error_log("Error updating About Us entry (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not update About Us entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an "About Us" entry.
|
||||
*
|
||||
* @param int $id The ID of the entry to delete.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function deleteAboutUs(int $id): bool {
|
||||
$sql = "DELETE FROM dsps_tbl_dspsabout WHERE pkdspsabout_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error deleting About Us entry (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not delete About Us entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single "About Us" entry by its ID.
|
||||
*
|
||||
* @param int $id The ID of the entry.
|
||||
* @return array|false The entry data as an associative array, or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAboutUsById(int $id) {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspsabout WHERE pkdspsabout_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching About Us entry by ID ($id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve About Us entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all "About Us" entries.
|
||||
*
|
||||
* @return array An array of "About Us" entry data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllAboutUs(): array {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspsabout ORDER BY dspsabout_reg_datetime ASC";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all About Us entries: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve About Us entries. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
213
classes/Announcement.php
Normal file
213
classes/Announcement.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
class Announcement {
|
||||
private $pdo;
|
||||
private string $uploadDir;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
$this->uploadDir = __DIR__ . '/../uploads/announcements/';
|
||||
// Ensure upload directory exists
|
||||
if (!is_dir($this->uploadDir) && !mkdir($this->uploadDir, 0775, true) && !is_dir($this->uploadDir)) {
|
||||
throw new RuntimeException('Unable to create announcements upload directory.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new announcement to the database.
|
||||
*
|
||||
* @param string $title The title of the announcement.
|
||||
* @param string $description The full description of the announcement.
|
||||
* @param string|null $photopath The filename of the uploaded photo, or null if no photo.
|
||||
* @param string $status The status of the announcement (e.g., 'Draft', 'Published', 'Archived').
|
||||
* @param int $reg_by The ID of the user who registered the announcement.
|
||||
* @return bool True on success, false on failure.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function addAnnouncement(string $title, string $description, ?string $photopath, string $status, int $reg_by): bool {
|
||||
$sql = "INSERT INTO dsps_tbl_announcement (dspsann_title, dspsann_description, dspsann_photopath, dspsann_status, dspsann_reg_by)
|
||||
VALUES (:title, :description, :photopath, :status, :reg_by)";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title', $title);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':photopath', $photopath);
|
||||
$stmt->bindParam(':status', $status);
|
||||
$stmt->bindParam(':reg_by', $reg_by);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error adding announcement: " . $e->getMessage());
|
||||
throw new Exception("Could not add announcement. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing announcement in the database.
|
||||
*
|
||||
* @param int $id The ID of the announcement to update.
|
||||
* @param string $title The new title.
|
||||
* @param string $description The new description.
|
||||
* @param string|null $photopath The new filename of the photo, or null.
|
||||
* @param string $status The new status.
|
||||
* @param int $mod_by The ID of the user who modified the announcement.
|
||||
* @return bool True on success, false on failure.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function updateAnnouncement(int $id, string $title, string $description, ?string $photopath, string $status, int $mod_by): bool {
|
||||
$sql = "UPDATE dsps_tbl_announcement
|
||||
SET dspsann_title = :title, dspsann_description = :description, dspsann_photopath = :photopath,
|
||||
dspsann_status = :status, dspsann_mod_datetime = CURRENT_TIMESTAMP, dspsann_reg_by = :mod_by
|
||||
WHERE pkdspsann_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title', $title);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':photopath', $photopath);
|
||||
$stmt->bindParam(':status', $status);
|
||||
$stmt->bindParam(':mod_by', $mod_by);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error updating announcement (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not update announcement. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an announcement from the database and its associated photo file.
|
||||
*
|
||||
* @param int $id The ID of the announcement to delete.
|
||||
* @return bool True on success, false on failure.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function deleteAnnouncement(int $id): bool {
|
||||
// First, get the photo path to delete the file
|
||||
$announcement = $this->getAnnouncementById($id);
|
||||
if ($announcement && !empty($announcement['dspsann_photopath'])) {
|
||||
$filePath = $this->uploadDir . $announcement['dspsann_photopath'];
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath); // Delete the file
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM dsps_tbl_announcement WHERE pkdspsann_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error deleting announcement (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not delete announcement. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single announcement by its ID.
|
||||
*
|
||||
* @param int $id The ID of the announcement.
|
||||
* @return array|false The announcement data as an associative array, or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAnnouncementById(int $id) {
|
||||
$sql = "SELECT * FROM dsps_tbl_announcement WHERE pkdspsann_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching announcement by ID ($id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve announcement. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all announcements, optionally filtered by status.
|
||||
*
|
||||
* @param string|null $status Optional status to filter by (e.g., 'Published').
|
||||
* @param int|null $limit Optional limit for the number of results.
|
||||
* @return array An array of announcement data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllAnnouncements(?string $status = null, ?int $limit = null): array {
|
||||
$sql = "SELECT * FROM dsps_tbl_announcement";
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if ($status) {
|
||||
$conditions[] = "dspsann_status = :status";
|
||||
$params[':status'] = $status;
|
||||
}
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$sql .= " WHERE " . implode(" AND ", $conditions);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY dspsann_reg_datetime DESC";
|
||||
|
||||
if ($limit) {
|
||||
$sql .= " LIMIT :limit";
|
||||
$params[':limit'] = $limit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($params as $key => &$val) {
|
||||
$stmt->bindParam($key, $val, is_int($val) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all announcements: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve announcements. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of announcements.
|
||||
*
|
||||
* @return int The total number of announcements.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getTotalAnnouncements(): int {
|
||||
$sql = "SELECT COUNT(*) FROM dsps_tbl_announcement";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting total announcements count: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve announcement count. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the upload of an announcement photo.
|
||||
*
|
||||
* @param array $file The $_FILES array for the uploaded photo.
|
||||
* @return string The unique filename of the uploaded photo.
|
||||
* @throws Exception If the upload fails or file type is invalid.
|
||||
*/
|
||||
public function handlePhotoUpload(array $file): string {
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception('File upload error: ' . $file['error']);
|
||||
}
|
||||
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($file['tmp_name']);
|
||||
|
||||
if (!in_array($mimeType, $allowedTypes)) {
|
||||
throw new Exception('Invalid file type. Only JPEG, PNG, and GIF images are allowed.');
|
||||
}
|
||||
|
||||
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$uniqueFilename = uniqid('announcement_') . '.' . $extension;
|
||||
$destination = $this->uploadDir . $uniqueFilename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
throw new Exception('Failed to move uploaded file.');
|
||||
}
|
||||
|
||||
return $uniqueFilename;
|
||||
}
|
||||
}
|
||||
293
classes/Classifications.php
Normal file
293
classes/Classifications.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
class Classifications {
|
||||
private $pdo;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
// --- Data Type Management (dsps_tbl_typedatasource) ---
|
||||
|
||||
/**
|
||||
* Adds a new data type.
|
||||
*
|
||||
* @param string $name_en English name of the data type.
|
||||
* @param string|null $name_kh Khmer name of the data type.
|
||||
* @param int $reg_by User ID who registered it.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or name already exists.
|
||||
*/
|
||||
public function addDataType(string $name_en, ?string $name_kh, int $reg_by): bool {
|
||||
$sql = "INSERT INTO dsps_tbl_typedatasource (dspstds_name_en, dspstds_name_kh, dspstds_reg_by)
|
||||
VALUES (:name_en, :name_kh, :reg_by)";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':name_en', $name_en);
|
||||
$stmt->bindParam(':name_kh', $name_kh);
|
||||
$stmt->bindParam(':reg_by', $reg_by);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == '23000') { // Integrity constraint violation (duplicate entry)
|
||||
throw new Exception("Data Type with this English name already exists.");
|
||||
}
|
||||
error_log("Error adding data type: " . $e->getMessage());
|
||||
throw new Exception("Could not add data type. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing data type.
|
||||
*
|
||||
* @param int $id ID of the data type to update.
|
||||
* @param string $name_en New English name.
|
||||
* @param string|null $name_kh New Khmer name.
|
||||
* @param int $mod_by User ID who modified it.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or name already exists.
|
||||
*/
|
||||
public function updateDataType(int $id, string $name_en, ?string $name_kh, int $mod_by): bool {
|
||||
$sql = "UPDATE dsps_tbl_typedatasource
|
||||
SET dspstds_name_en = :name_en, dspstds_name_kh = :name_kh,
|
||||
dspstds_mod_datetime = CURRENT_TIMESTAMP, dspstds_reg_by = :mod_by
|
||||
WHERE pkdspstds_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':name_en', $name_en);
|
||||
$stmt->bindParam(':name_kh', $name_kh);
|
||||
$stmt->bindParam(':mod_by', $mod_by);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == '23000') {
|
||||
throw new Exception("Data Type with this English name already exists.");
|
||||
}
|
||||
error_log("Error updating data type (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not update data type. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a data type.
|
||||
*
|
||||
* @param int $id ID of the data type to delete.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or data type is in use.
|
||||
*/
|
||||
public function deleteDataType(int $id): bool {
|
||||
// Check if any data sources are using this data type
|
||||
$checkSql = "SELECT COUNT(*) FROM dsps_tbl_datasource WHERE fkdspstds_id = :id";
|
||||
$stmtCheck = $this->pdo->prepare($checkSql);
|
||||
$stmtCheck->bindParam(':id', $id);
|
||||
$stmtCheck->execute();
|
||||
if ($stmtCheck->fetchColumn() > 0) {
|
||||
throw new Exception("Cannot delete Data Type: It is currently used by one or more data sources.");
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM dsps_tbl_typedatasource WHERE pkdspstds_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error deleting data type (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not delete data type. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single data type by ID.
|
||||
*
|
||||
* @param int $id ID of the data type.
|
||||
* @return array|false Data type data or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getDataTypeById(int $id) {
|
||||
$sql = "SELECT * FROM dsps_tbl_typedatasource WHERE pkdspstds_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching data type by ID ($id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve data type. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all data types.
|
||||
*
|
||||
* @return array An array of data type data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllDataTypes(): array {
|
||||
$sql = "SELECT * FROM dsps_tbl_typedatasource ORDER BY dspstds_name_en ASC";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all data types: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve data types. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of data types.
|
||||
*
|
||||
* @return int The total number of data types.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getTotalDataTypes(): int {
|
||||
$sql = "SELECT COUNT(*) FROM dsps_tbl_typedatasource";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting total data types count: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve data type count. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Category Management (dsps_tbl_dspscategory) ---
|
||||
|
||||
/**
|
||||
* Adds a new category.
|
||||
*
|
||||
* @param string $title_en English title of the category.
|
||||
* @param string|null $details Details about the category.
|
||||
* @param int $reg_by User ID who registered it.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or title already exists.
|
||||
*/
|
||||
public function addCategory(string $title_en, ?string $details, int $reg_by): bool {
|
||||
$sql = "INSERT INTO dsps_tbl_dspscategory (dspscate_title_en, dspscate_details, dspscate_reg_by)
|
||||
VALUES (:title_en, :details, :reg_by)";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':details', $details);
|
||||
$stmt->bindParam(':reg_by', $reg_by);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == '23000') {
|
||||
throw new Exception("Category with this English title already exists.");
|
||||
}
|
||||
error_log("Error adding category: " . $e->getMessage());
|
||||
throw new Exception("Could not add category. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing category.
|
||||
*
|
||||
* @param int $id ID of the category to update.
|
||||
* @param string $title_en New English title.
|
||||
* @param string|null $details New details.
|
||||
* @param int $mod_by User ID who modified it.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or title already exists.
|
||||
*/
|
||||
public function updateCategory(int $id, string $title_en, ?string $details, int $mod_by): bool {
|
||||
$sql = "UPDATE dsps_tbl_dspscategory
|
||||
SET dspscate_title_en = :title_en, dspscate_details = :details,
|
||||
dspscate_mod_datetime = CURRENT_TIMESTAMP, dspscate_reg_by = :mod_by
|
||||
WHERE pkdspscate_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':details', $details);
|
||||
$stmt->bindParam(':mod_by', $mod_by);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == '23000') {
|
||||
throw new Exception("Category with this English title already exists.");
|
||||
}
|
||||
error_log("Error updating category (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not update category. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a category.
|
||||
*
|
||||
* @param int $id ID of the category to delete.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or category is in use.
|
||||
*/
|
||||
public function deleteCategory(int $id): bool {
|
||||
// Check if any data sources are using this category
|
||||
$checkSql = "SELECT COUNT(*) FROM dsps_tbl_datasource WHERE fkdspscate_id = :id";
|
||||
$stmtCheck = $this->pdo->prepare($checkSql);
|
||||
$stmtCheck->bindParam(':id', $id);
|
||||
$stmtCheck->execute();
|
||||
if ($stmtCheck->fetchColumn() > 0) {
|
||||
throw new Exception("Cannot delete Category: It is currently used by one or more data sources.");
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM dsps_tbl_dspscategory WHERE pkdspscate_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error deleting category (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not delete category. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single category by ID.
|
||||
*
|
||||
* @param int $id ID of the category.
|
||||
* @return array|false Category data or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getCategoryById(int $id) {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspscategory WHERE pkdspscate_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching category by ID ($id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve category. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all categories.
|
||||
*
|
||||
* @return array An array of category data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllCategories(): array {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspscategory ORDER BY dspscate_title_en ASC";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all categories: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve categories. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of categories.
|
||||
*
|
||||
* @return int The total number of categories.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getTotalCategories(): int {
|
||||
$sql = "SELECT COUNT(*) FROM dsps_tbl_dspscategory";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting total categories count: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve category count. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
172
classes/Contactus.php
Normal file
172
classes/Contactus.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
class Contactus {
|
||||
private $pdo;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits a new feedback message from a user/visitor.
|
||||
*
|
||||
* @param string $name The name of the person submitting feedback.
|
||||
* @param string|null $email The email of the person, if provided.
|
||||
* @param string $body_text The main body of the feedback message.
|
||||
* @param string|null $client_ip The IP address of the client.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function submitFeedback(string $name, ?string $email, string $body_text, ?string $client_ip): bool {
|
||||
$sql = "INSERT INTO dsps_tbl_feedback (dspsfb_name, dspsfb_email, dspsfb_body_text, dspsfb_client_ip, dspsfb_status)
|
||||
VALUES (:name, :email, :body_text, :client_ip, 'New')";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':name', $name);
|
||||
$stmt->bindParam(':email', $email);
|
||||
$stmt->bindParam(':body_text', $body_text);
|
||||
$stmt->bindParam(':client_ip', $client_ip);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error submitting feedback: " . $e->getMessage());
|
||||
throw new Exception("Could not submit feedback. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a DAC Staff user to respond to a feedback message.
|
||||
*
|
||||
* @param int $feedback_id The ID of the feedback message to respond to.
|
||||
* @param string $respond_text The response text from the DAC Staff.
|
||||
* @param string $status The new status of the feedback (e.g., 'In Progress', 'Resolved').
|
||||
* @param int $res_by The user ID of the DAC Staff who responded.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function respondToFeedback(int $feedback_id, string $respond_text, string $status, int $res_by): bool {
|
||||
$sql = "UPDATE dsps_tbl_feedback
|
||||
SET dspsfb_respond_text = :respond_text, dspsfb_status = :status,
|
||||
dspsfb_res_datetime = CURRENT_TIMESTAMP, dspsfb_res_by = :res_by
|
||||
WHERE pkdspsfb_id = :feedback_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':respond_text', $respond_text);
|
||||
$stmt->bindParam(':status', $status);
|
||||
$stmt->bindParam(':res_by', $res_by);
|
||||
$stmt->bindParam(':feedback_id', $feedback_id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error responding to feedback (ID: $feedback_id): " . $e->getMessage());
|
||||
throw new Exception("Could not respond to feedback. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a feedback message.
|
||||
*
|
||||
* @param int $feedback_id The ID of the feedback message to delete.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function deleteFeedback(int $feedback_id): bool {
|
||||
$sql = "DELETE FROM dsps_tbl_feedback WHERE pkdspsfb_id = :feedback_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':feedback_id', $feedback_id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error deleting feedback (ID: $feedback_id): " . $e->getMessage());
|
||||
throw new Exception("Could not delete feedback. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single feedback message by its ID.
|
||||
*
|
||||
* @param int $feedback_id The ID of the feedback message.
|
||||
* @return array|false The feedback data as an associative array, or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getFeedbackById(int $feedback_id) {
|
||||
$sql = "SELECT * FROM dsps_tbl_feedback WHERE pkdspsfb_id = :feedback_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':feedback_id', $feedback_id);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching feedback by ID ($feedback_id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve feedback. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all feedback messages, optionally filtered by status.
|
||||
*
|
||||
* @param string|null $status Optional status to filter by (e.g., 'New', 'Resolved').
|
||||
* @return array An array of feedback data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllFeedback(?string $status = null): array {
|
||||
$sql = "SELECT * FROM dsps_tbl_feedback";
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if ($status) {
|
||||
$conditions[] = "dspsfb_status = :status";
|
||||
$params[':status'] = $status;
|
||||
}
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$sql .= " WHERE " . implode(" AND ", $conditions);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY dspsfb_reg_datetime DESC";
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($params as $key => &$val) {
|
||||
$stmt->bindParam($key, $val, is_int($val) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all feedback: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve feedback messages. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of feedback messages, optionally filtered by status.
|
||||
*
|
||||
* @param string|null $status Optional status to filter by.
|
||||
* @return int The total number of feedback messages.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getTotalFeedback(?string $status = null): int {
|
||||
$sql = "SELECT COUNT(*) FROM dsps_tbl_feedback";
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if ($status) {
|
||||
$conditions[] = "dspsfb_status = :status";
|
||||
$params[':status'] = $status;
|
||||
}
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$sql .= " WHERE " . implode(" AND ", $conditions);
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($params as $key => &$val) {
|
||||
$stmt->bindParam($key, $val, is_int($val) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->execute();
|
||||
return $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting total feedback count: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve feedback count. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
1489
classes/DataSource.php
Normal file
1489
classes/DataSource.php
Normal file
File diff suppressed because it is too large
Load Diff
138
classes/Faq.php
Normal file
138
classes/Faq.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
class Faq {
|
||||
private $pdo;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new FAQ entry to the database.
|
||||
*
|
||||
* @param string $title_en The English question for the FAQ.
|
||||
* @param string $description The English answer for the FAQ.
|
||||
* @param int $reg_by The ID of the user who registered this entry (from ist_tbl_users).
|
||||
* @param int $fkisp_id_of The ID of the person associated with this entry (from ist_tbl_people).
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function addFaq(string $title_en, string $description, int $reg_by, int $fkisp_id_of): bool {
|
||||
$sql = "INSERT INTO dsps_tbl_dspsfaq (dspsfaq_title_en, dspsfaq_description, dspsfaq_reg_by, fkisp_id_of)
|
||||
VALUES (:title_en, :description, :reg_by, :fkisp_id_of)";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':reg_by', $reg_by);
|
||||
$stmt->bindParam(':fkisp_id_of', $fkisp_id_of);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error adding FAQ entry: " . $e->getMessage());
|
||||
throw new Exception("Could not add FAQ entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing FAQ entry.
|
||||
*
|
||||
* @param int $id The ID of the FAQ entry to update.
|
||||
* @param string $title_en The new English question.
|
||||
* @param string $description The new English answer.
|
||||
* @param int $mod_by The ID of the user who modified this entry.
|
||||
* @param int $fkisp_id_of The ID of the person associated with this entry.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function updateFaq(int $id, string $title_en, string $description, int $mod_by, int $fkisp_id_of): bool {
|
||||
$sql = "UPDATE dsps_tbl_dspsfaq
|
||||
SET dspsfaq_title_en = :title_en, dspsfaq_description = :description,
|
||||
dspsfaq_mod_datetime = CURRENT_TIMESTAMP, dspsfaq_reg_by = :mod_by, fkisp_id_of = :fkisp_id_of
|
||||
WHERE pkdspsfaq_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':mod_by', $mod_by);
|
||||
$stmt->bindParam(':fkisp_id_of', $fkisp_id_of);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error updating FAQ entry (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not update FAQ entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an FAQ entry.
|
||||
*
|
||||
* @param int $id The ID of the FAQ entry to delete.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function deleteFaq(int $id): bool {
|
||||
$sql = "DELETE FROM dsps_tbl_dspsfaq WHERE pkdspsfaq_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error deleting FAQ entry (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not delete FAQ entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single FAQ entry by its ID.
|
||||
*
|
||||
* @param int $id The ID of the FAQ entry.
|
||||
* @return array|false The entry data as an associative array, or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getFaqById(int $id) {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspsfaq WHERE pkdspsfaq_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching FAQ entry by ID ($id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve FAQ entry. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all FAQ entries.
|
||||
*
|
||||
* @return array An array of FAQ entry data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllFaqs(): array {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspsfaq ORDER BY dspsfaq_reg_datetime ASC";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all FAQ entries: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve FAQ entries. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of FAQ entries.
|
||||
*
|
||||
* @return int The total number of FAQ entries.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getTotalFaqs(): int {
|
||||
$sql = "SELECT COUNT(*) FROM dsps_tbl_dspsfaq";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting total FAQ count: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve FAQ count. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
257
classes/OAuth.php
Normal file
257
classes/OAuth.php
Normal file
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight OAuth 2.0 data/access service for DSP -> JupyterHub integration.
|
||||
*/
|
||||
class OAuthService
|
||||
{
|
||||
private const AUTH_CODE_TTL = 600; // 10 minutes
|
||||
private const ACCESS_TOKEN_TTL = 3600; // 1 hour
|
||||
private const REFRESH_TOKEN_TTL = 2592000; // 30 days
|
||||
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function getClient(string $clientId): ?array
|
||||
{
|
||||
$sql = "SELECT client_id, client_name, client_secret_hash, redirect_uris, allowed_scopes, is_confidential
|
||||
FROM dsp_oauth_clients
|
||||
WHERE client_id = :client_id AND is_revoked = 0";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':client_id' => $clientId]);
|
||||
$client = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $client ?: null;
|
||||
}
|
||||
|
||||
public function verifyClientSecret(array $client, string $candidate): bool
|
||||
{
|
||||
if (empty($client['client_secret_hash'])) {
|
||||
return $candidate === '';
|
||||
}
|
||||
|
||||
return password_verify($candidate, $client['client_secret_hash']);
|
||||
}
|
||||
|
||||
public function isRedirectUriAllowed(array $client, string $redirectUri): bool
|
||||
{
|
||||
$allowed = array_filter(array_map('trim', preg_split('/[\s,]+/', (string) ($client['redirect_uris'] ?? ''))));
|
||||
if (empty($allowed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($allowed as $prefix) {
|
||||
if (stripos($redirectUri, $prefix) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isScopeAllowed(array $client, ?string $requestedScope): bool
|
||||
{
|
||||
$requestedScope = trim((string) $requestedScope);
|
||||
if ($requestedScope === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$allowedScopes = array_filter(array_map('trim', explode(' ', (string) ($client['allowed_scopes'] ?? ''))));
|
||||
|
||||
if (empty($allowedScopes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (explode(' ', $requestedScope) as $scope) {
|
||||
if (!in_array($scope, $allowedScopes, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function issueAuthorizationCode(string $clientId, int $personId, string $redirectUri, ?string $scope = null): array
|
||||
{
|
||||
$code = $this->generateToken(32);
|
||||
$codeHash = $this->hashToken($code);
|
||||
$expiresAt = time() + self::AUTH_CODE_TTL;
|
||||
|
||||
$sql = "INSERT INTO dsp_oauth_auth_codes
|
||||
(code_hash, client_id, person_id, scope, redirect_uri, expires_at, created_at)
|
||||
VALUES (:code_hash, :client_id, :person_id, :scope, :redirect_uri, FROM_UNIXTIME(:expires_at), NOW())";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':code_hash' => $codeHash,
|
||||
':client_id' => $clientId,
|
||||
':person_id' => $personId,
|
||||
':scope' => $scope,
|
||||
':redirect_uri' => $redirectUri,
|
||||
':expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'expires_at' => $expiresAt,
|
||||
];
|
||||
}
|
||||
|
||||
public function consumeAuthorizationCode(string $code, string $clientId): ?array
|
||||
{
|
||||
$codeHash = $this->hashToken($code);
|
||||
|
||||
$sql = "SELECT code_hash, client_id, person_id, scope, redirect_uri, UNIX_TIMESTAMP(expires_at) AS expires_at
|
||||
FROM dsp_oauth_auth_codes
|
||||
WHERE code_hash = :code_hash";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':code_hash' => $codeHash]);
|
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Delete regardless of outcome
|
||||
$deleteStmt = $this->pdo->prepare("DELETE FROM dsp_oauth_auth_codes WHERE code_hash = :code_hash");
|
||||
$deleteStmt->execute([':code_hash' => $codeHash]);
|
||||
|
||||
if ((int) $record['expires_at'] < time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($record['client_id'] !== $clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function issueTokens(string $clientId, int $personId, ?string $scope = null, bool $includeRefresh = true): array
|
||||
{
|
||||
$accessToken = $this->generateToken(43);
|
||||
$accessHash = $this->hashToken($accessToken);
|
||||
$accessExpiresAt = time() + self::ACCESS_TOKEN_TTL;
|
||||
|
||||
$refreshToken = null;
|
||||
$refreshHash = null;
|
||||
$refreshExpiresAt = null;
|
||||
|
||||
if ($includeRefresh) {
|
||||
$refreshToken = $this->generateToken(43);
|
||||
$refreshHash = $this->hashToken($refreshToken);
|
||||
$refreshExpiresAt = time() + self::REFRESH_TOKEN_TTL;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO dsp_oauth_access_tokens
|
||||
(token_hash, client_id, person_id, scope, expires_at, refresh_token_hash, refresh_expires_at, created_at)
|
||||
VALUES (:token_hash, :client_id, :person_id, :scope, FROM_UNIXTIME(:expires_at),
|
||||
:refresh_hash, " . ($refreshExpiresAt ? "FROM_UNIXTIME(:refresh_expires_at)" : "NULL") . ", NOW())";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':token_hash' => $accessHash,
|
||||
':client_id' => $clientId,
|
||||
':person_id' => $personId,
|
||||
':scope' => $scope,
|
||||
':expires_at' => $accessExpiresAt,
|
||||
':refresh_hash' => $refreshHash,
|
||||
':refresh_expires_at' => $refreshExpiresAt,
|
||||
]);
|
||||
|
||||
return [
|
||||
'access_token' => $accessToken,
|
||||
'access_expires_at' => $accessExpiresAt,
|
||||
'refresh_token' => $refreshToken,
|
||||
'refresh_expires_at' => $refreshExpiresAt,
|
||||
'token_type' => 'Bearer',
|
||||
'scope' => $scope,
|
||||
];
|
||||
}
|
||||
|
||||
public function exchangeRefreshToken(string $clientId, string $refreshToken): ?array
|
||||
{
|
||||
$refreshHash = $this->hashToken($refreshToken);
|
||||
|
||||
$sql = "SELECT token_hash, client_id, person_id, scope, UNIX_TIMESTAMP(refresh_expires_at) AS refresh_expires_at
|
||||
FROM dsp_oauth_access_tokens
|
||||
WHERE refresh_token_hash = :refresh_hash AND is_revoked = 0";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':refresh_hash' => $refreshHash]);
|
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($record['client_id'] !== $clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!empty($record['refresh_expires_at']) && (int) $record['refresh_expires_at'] < time()) {
|
||||
$this->revokeTokenByHash($record['token_hash']);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Revoke old access token
|
||||
$this->revokeTokenByHash($record['token_hash']);
|
||||
|
||||
// Issue new pair
|
||||
return $this->issueTokens($clientId, (int) $record['person_id'], $record['scope'], true);
|
||||
}
|
||||
|
||||
public function getAccessToken(string $token): ?array
|
||||
{
|
||||
$hash = $this->hashToken($token);
|
||||
$sql = "SELECT token_hash, client_id, person_id, scope,
|
||||
UNIX_TIMESTAMP(expires_at) AS expires_at,
|
||||
refresh_token_hash,
|
||||
UNIX_TIMESTAMP(refresh_expires_at) AS refresh_expires_at
|
||||
FROM dsp_oauth_access_tokens
|
||||
WHERE token_hash = :hash AND is_revoked = 0";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':hash' => $hash]);
|
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((int) $record['expires_at'] < time()) {
|
||||
$this->revokeTokenByHash($record['token_hash']);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function revokeTokenByHash(string $hash): void
|
||||
{
|
||||
$sql = "UPDATE dsp_oauth_access_tokens SET is_revoked = 1, revoked_at = NOW()
|
||||
WHERE token_hash = :hash";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':hash' => $hash]);
|
||||
}
|
||||
|
||||
public function recordTokenUsage(string $hash): void
|
||||
{
|
||||
$sql = "UPDATE dsp_oauth_access_tokens SET last_used_at = NOW() WHERE token_hash = :hash";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':hash' => $hash]);
|
||||
}
|
||||
|
||||
private function generateToken(int $length): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode(random_bytes($length)), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
private function hashToken(string $token): string
|
||||
{
|
||||
return hash('sha256', $token);
|
||||
}
|
||||
}
|
||||
144
classes/Permission.php
Normal file
144
classes/Permission.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
// classes/Permission.php
|
||||
class Permission {
|
||||
private $pdo;
|
||||
private $columnExistenceCache = [];
|
||||
|
||||
public function __construct($pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has a specific permission for a data source.
|
||||
* @param int $personId The person's ID (fkisp_id).
|
||||
* @param int $dataSourceId The data source ID (pkdspsds_id).
|
||||
* @param string $permissionType The type of permission (e.g., 'Read', 'Download').
|
||||
* @return bool True if the permission is granted, false otherwise.
|
||||
*/
|
||||
public function hasPermission($personId, $dataSourceId, $permissionType) {
|
||||
$sql = "SELECT COUNT(*) FROM dsps_tbl_datasource_permission
|
||||
WHERE fkisp_id_of = ? AND fkdspsds_id = ?
|
||||
AND dspsdsp_permission = ? AND dspsdsp_status = 'Approved'";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([$personId, $dataSourceId, $permissionType]);
|
||||
return $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pending request for a user and data source, if one exists.
|
||||
* @param int $personId The person's ID (fkisp_id).
|
||||
* @param int $dataSourceId The data source ID (pkdspsds_id).
|
||||
* @param string $permissionType The type of permission.
|
||||
* @return array|false The request data as an array, or false if not found.
|
||||
*/
|
||||
public function getPendingRequest($personId, $dataSourceId, $permissionType) {
|
||||
$sql = "SELECT * FROM dsps_tbl_datasource_permission
|
||||
WHERE fkisp_id_of = ? AND fkdspsds_id = ?
|
||||
AND dspsdsp_permission = ? AND dspsdsp_status = 'Pending'";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([$personId, $dataSourceId, $permissionType]);
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new permission request to the database.
|
||||
* @param int $personId The person's ID (fkisp_id).
|
||||
* @param int $dataSourceId The data source ID (pkdspsds_id).
|
||||
* @param string $permissionType The type of permission requested.
|
||||
* @param string $status The initial status of the request (e.g., 'Pending').
|
||||
* @param string $notes The user's justification for the request.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function addPermissionRequest($personId, $dataSourceId, $permissionType, $status, $notes, ?string $proofPath = null) {
|
||||
$hasProofColumn = $this->ensurePermissionProofColumn();
|
||||
|
||||
if ($hasProofColumn) {
|
||||
$sql = "INSERT INTO dsps_tbl_datasource_permission (fkisp_id_of, fkdspsds_id, dspsdsp_permission, dspsdsp_notes, dspsdsp_proof_path, dspsdsp_status, dspsdsp_datetime)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())";
|
||||
$params = [$personId, $dataSourceId, $permissionType, $notes, $proofPath, $status];
|
||||
} else {
|
||||
$sql = "INSERT INTO dsps_tbl_datasource_permission (fkisp_id_of, fkdspsds_id, dspsdsp_permission, dspsdsp_notes, dspsdsp_status, dspsdsp_datetime)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())";
|
||||
$params = [$personId, $dataSourceId, $permissionType, $notes, $status];
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
return $stmt->execute($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all permission requests for a specific user.
|
||||
* This method is needed for the 'my_permissions.php' script.
|
||||
* @param int $personId The person's ID (fkisp_id).
|
||||
* @return array An array of all permission requests for the given person.
|
||||
*/
|
||||
public function getPermissionsByPersonId($personId) {
|
||||
$hasProofColumn = $this->ensurePermissionProofColumn();
|
||||
$proofSelect = $hasProofColumn
|
||||
? 'pr.dspsdsp_proof_path AS dspspr_proof_path'
|
||||
: 'NULL AS dspspr_proof_path';
|
||||
|
||||
$sql = "SELECT
|
||||
ds.dspsds_title_en AS ds_title,
|
||||
pr.dspsdsp_permission AS dspspr_permission_type,
|
||||
pr.dspsdsp_reg_datetime AS dspspr_request_date,
|
||||
pr.dspsdsp_status AS dspspr_status,
|
||||
pr.dspsdsp_notes AS dspspr_notes,
|
||||
$proofSelect
|
||||
FROM dsps_tbl_datasource_permission pr
|
||||
JOIN dsps_tbl_datasource ds ON pr.fkdspsds_id = ds.pkdspsds_id
|
||||
WHERE pr.fkisp_id_of = ?";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([$personId]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
private function tableColumnExists(string $table, string $column): bool {
|
||||
$cacheKey = $table . '.' . $column;
|
||||
if (array_key_exists($cacheKey, $this->columnExistenceCache)) {
|
||||
return $this->columnExistenceCache[$cacheKey];
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = sprintf('SHOW COLUMNS FROM `%s` LIKE :column', $table);
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':column', $column, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
$exists = (bool) $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log('Error checking column existence: ' . $e->getMessage());
|
||||
// Assume the column exists if we cannot verify (safer than silently skipping writes)
|
||||
$exists = true;
|
||||
}
|
||||
|
||||
$this->columnExistenceCache[$cacheKey] = $exists;
|
||||
return $exists;
|
||||
}
|
||||
|
||||
private function ensurePermissionProofColumn(): bool {
|
||||
$table = 'dsps_tbl_datasource_permission';
|
||||
$column = 'dspsdsp_proof_path';
|
||||
$cacheKey = $table . '.' . $column;
|
||||
|
||||
if ($this->tableColumnExists($table, $column)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$alterSql = "ALTER TABLE `{$table}` ADD COLUMN `{$column}` VARCHAR(255) DEFAULT NULL AFTER dspsdsp_notes";
|
||||
try {
|
||||
$this->pdo->exec($alterSql);
|
||||
$this->columnExistenceCache[$cacheKey] = true;
|
||||
return true;
|
||||
} catch (PDOException $e) {
|
||||
error_log('Failed to add proof column: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
classes/PermissionManager.php
Normal file
45
classes/PermissionManager.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// classes/PermissionManager.php
|
||||
// This class handles all logic related to checking and requesting user permissions for data sources.
|
||||
|
||||
class PermissionManager
|
||||
{
|
||||
private $pdo;
|
||||
|
||||
public function __construct($pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific person has a specific permission for a data source.
|
||||
* @param int $personId The ID of the person.
|
||||
* @param int $dataSourceId The ID of the data source.
|
||||
* @param string $permissionType The type of permission to check ('Read' or 'Download').
|
||||
* @return bool True if the permission exists, false otherwise.
|
||||
*/
|
||||
public function hasPermission($personId, $dataSourceId, $permissionType)
|
||||
{
|
||||
try {
|
||||
// Using a prepared statement to prevent SQL injection
|
||||
$sql = "SELECT COUNT(*) FROM dspsds_person_permissions
|
||||
WHERE fk_dspsdspp_person_id = :personId
|
||||
AND fk_dspsdspp_dspsds_id = :dataSourceId
|
||||
AND dspsdspp_permission = :permissionType";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':personId', $personId, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':dataSourceId', $dataSourceId, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':permissionType', $permissionType, PDO::PARAM_STR);
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
return $stmt->fetchColumn() > 0;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Log the error but don't expose it to the user
|
||||
error_log("Database error in hasPermission: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
188
classes/Slide.php
Normal file
188
classes/Slide.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
class Slide {
|
||||
private $pdo;
|
||||
private string $uploadDir;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
$this->uploadDir = __DIR__ . '/../uploads/slides/';
|
||||
// Ensure upload directory exists
|
||||
if (!is_dir($this->uploadDir) && !mkdir($this->uploadDir, 0775, true) && !is_dir($this->uploadDir)) {
|
||||
throw new RuntimeException('Unable to create slides upload directory.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new slide to the database.
|
||||
*
|
||||
* @param string $title_en The English title of the slide.
|
||||
* @param string $description The full description of the slide.
|
||||
* @param string $photoname The filename of the uploaded photo.
|
||||
* @param int $reg_by The ID of the user who registered the slide.
|
||||
* @param int $fkisp_id_of The person ID of the user who registered the slide.
|
||||
* @return bool True on success, false on failure.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function addSlide(string $title_en, string $description, string $photoname, int $reg_by, int $fkisp_id_of): bool {
|
||||
$sql = "INSERT INTO dsps_tbl_dspsslide (dspsslide_title_en, dspsslide_description, dspsslide_photoname, dspsslide_reg_by, fkisp_id_of)
|
||||
VALUES (:title_en, :description, :photoname, :reg_by, :fkisp_id_of)";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':photoname', $photoname);
|
||||
$stmt->bindParam(':reg_by', $reg_by, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':fkisp_id_of', $fkisp_id_of, PDO::PARAM_INT);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error adding slide: " . $e->getMessage());
|
||||
throw new Exception("Could not add slide. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing slide in the database.
|
||||
*
|
||||
* @param int $id The ID of the slide to update.
|
||||
* @param string $title_en The new English title.
|
||||
* @param string $description The new description.
|
||||
* @param string $photoname The new filename of the photo.
|
||||
* @param int $mod_by The ID of the user who modified the slide.
|
||||
* @param int $fkisp_id_of The person ID of the user who modified the slide.
|
||||
* @return bool True on success, false on failure.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function updateSlide(int $id, string $title_en, string $description, string $photoname, int $mod_by, int $fkisp_id_of): bool {
|
||||
$sql = "UPDATE dsps_tbl_dspsslide
|
||||
SET dspsslide_title_en = :title_en, dspsslide_description = :description, dspsslide_photoname = :photoname,
|
||||
dspsslide_mod_datetime = CURRENT_TIMESTAMP, dspsslide_reg_by = :mod_by, fkisp_id_of = :fkisp_id_of
|
||||
WHERE pkdspsslide_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':title_en', $title_en);
|
||||
$stmt->bindParam(':description', $description);
|
||||
$stmt->bindParam(':photoname', $photoname);
|
||||
$stmt->bindParam(':mod_by', $mod_by, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':fkisp_id_of', $fkisp_id_of, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error updating slide (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not update slide. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a slide from the database and its associated photo file.
|
||||
*
|
||||
* @param int $id The ID of the slide to delete.
|
||||
* @return bool True on success, false on failure.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function deleteSlide(int $id): bool {
|
||||
// First, get the photo path to delete the file
|
||||
$slide = $this->getSlideById($id);
|
||||
if ($slide && !empty($slide['dspsslide_photoname'])) {
|
||||
$filePath = $this->uploadDir . $slide['dspsslide_photoname'];
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath); // Delete the file
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM dsps_tbl_dspsslide WHERE pkdspsslide_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error deleting slide (ID: $id): " . $e->getMessage());
|
||||
throw new Exception("Could not delete slide. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single slide by its ID.
|
||||
*
|
||||
* @param int $id The ID of the slide.
|
||||
* @return array|false The slide data as an associative array, or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getSlideById(int $id) {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspsslide WHERE pkdspsslide_id = :id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching slide by ID ($id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve slide. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all slides.
|
||||
*
|
||||
* @return array An array of slide data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllSlides(): array {
|
||||
$sql = "SELECT * FROM dsps_tbl_dspsslide ORDER BY pkdspsslide_id ASC"; // Order by ID or a custom sort order
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all slides: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve slides. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of slides.
|
||||
*
|
||||
* @return int The total number of slides.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getTotalSlides(): int {
|
||||
$sql = "SELECT COUNT(*) FROM dsps_tbl_dspsslide";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting total slides count: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve slide count. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the upload of a slide photo.
|
||||
*
|
||||
* @param array $file The $_FILES array for the uploaded photo.
|
||||
* @return string The unique filename of the uploaded photo.
|
||||
* @throws Exception If the upload fails or file type is invalid.
|
||||
*/
|
||||
public function handlePhotoUpload(array $file): string {
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception('File upload error: ' . $file['error']);
|
||||
}
|
||||
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($file['tmp_name']);
|
||||
|
||||
if (!in_array($mimeType, $allowedTypes)) {
|
||||
throw new Exception('Invalid file type. Only JPEG, PNG, and GIF images are allowed.');
|
||||
}
|
||||
|
||||
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$uniqueFilename = uniqid('slide_') . '.' . $extension;
|
||||
$destination = $this->uploadDir . $uniqueFilename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
throw new Exception('Failed to move uploaded file.');
|
||||
}
|
||||
|
||||
return $uniqueFilename;
|
||||
}
|
||||
}
|
||||
386
classes/User.php
Normal file
386
classes/User.php
Normal file
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
class User {
|
||||
private $pdo;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user and their personal information in the database.
|
||||
* @param array $person_data An array of personal information.
|
||||
* @param array $user_data An array of user account data.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function registerUser($person_data, $user_data)
|
||||
{
|
||||
// Start a transaction to ensure both person and user data are saved or neither is
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
// Check for duplicate ID card, phone, or email before inserting
|
||||
$dupConditions = [];
|
||||
$dupParams = [];
|
||||
$duplicateLabels = [];
|
||||
if (!empty($person_data['id_card'])) {
|
||||
$dupConditions[] = "isp_idcard = :id_card";
|
||||
$dupParams[':id_card'] = $person_data['id_card'];
|
||||
$duplicateLabels[] = 'ID card';
|
||||
}
|
||||
if (!empty($person_data['phone_number'])) {
|
||||
$dupConditions[] = "isp_phone_number = :phone";
|
||||
$dupParams[':phone'] = $person_data['phone_number'];
|
||||
$duplicateLabels[] = 'phone number';
|
||||
}
|
||||
if (!empty($person_data['email'])) {
|
||||
$dupConditions[] = "isp_email = :email";
|
||||
$dupParams[':email'] = $person_data['email'];
|
||||
$duplicateLabels[] = 'email';
|
||||
}
|
||||
|
||||
if (!empty($dupConditions)) {
|
||||
$check_sql = "SELECT pkisp_id FROM ist_tbl_people WHERE " . implode(' OR ', $dupConditions);
|
||||
$check_stmt = $this->pdo->prepare($check_sql);
|
||||
$check_stmt->execute($dupParams);
|
||||
|
||||
if ($check_stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$this->pdo->rollBack();
|
||||
$duplicateMessage = 'information';
|
||||
if (!empty($duplicateLabels)) {
|
||||
if (count($duplicateLabels) === 1) {
|
||||
$duplicateMessage = $duplicateLabels[0];
|
||||
} elseif (count($duplicateLabels) === 2) {
|
||||
$duplicateMessage = implode(' or ', $duplicateLabels);
|
||||
} else {
|
||||
$last = array_pop($duplicateLabels);
|
||||
$duplicateMessage = implode(', ', $duplicateLabels) . ", or {$last}";
|
||||
}
|
||||
}
|
||||
set_message("A user with this {$duplicateMessage} already exists.", "danger");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate username
|
||||
$check_username_sql = "SELECT pkisu_id FROM ist_tbl_users WHERE isu_name = :username";
|
||||
$check_username_stmt = $this->pdo->prepare($check_username_sql);
|
||||
$check_username_stmt->execute([':username' => $user_data['username']]);
|
||||
|
||||
if ($check_username_stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$this->pdo->rollBack();
|
||||
set_message("This username is already taken. Please choose another one.", "danger");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Insert into ist_tbl_people
|
||||
$person_sql = "
|
||||
INSERT INTO ist_tbl_people (
|
||||
isp_idcard, isp_firstname_en, isp_lastname_en, isp_sex,
|
||||
isp_dob, isp_pob, isp_nationality, isp_marital_status,
|
||||
isp_phone_number, isp_email, isp_telegram, isp_note
|
||||
) VALUES (
|
||||
:id_card, :first_name_en, :last_name_en, :sex,
|
||||
:dob, :pob, :nationality, :marital_status,
|
||||
:phone_number, :email, :telegram, :note
|
||||
)
|
||||
";
|
||||
$person_stmt = $this->pdo->prepare($person_sql);
|
||||
$person_stmt->execute([
|
||||
':id_card' => ($person_data['id_card'] ?? '') !== '' ? $person_data['id_card'] : null,
|
||||
':first_name_en' => $person_data['first_name_en'],
|
||||
':last_name_en' => $person_data['last_name_en'],
|
||||
':sex' => $person_data['sex'],
|
||||
':dob' => $person_data['dob'],
|
||||
':pob' => $person_data['pob'],
|
||||
':nationality' => $person_data['nationality'],
|
||||
':marital_status' => $person_data['marital_status'],
|
||||
':phone_number' => $person_data['phone_number'],
|
||||
':email' => $person_data['email'],
|
||||
':telegram' => $person_data['telegram'],
|
||||
':note' => $person_data['note']
|
||||
]);
|
||||
|
||||
// Get the ID of the newly inserted person record
|
||||
$person_id = $this->pdo->lastInsertId();
|
||||
|
||||
// 2. Insert into ist_tbl_users
|
||||
$user_sql = "
|
||||
INSERT INTO ist_tbl_users (
|
||||
fkisp_id_of, isu_name, isu_password, isu_status, isu_can_run_r
|
||||
) VALUES (
|
||||
:fkisp_id_of, :username, :password, :status, :can_run_r
|
||||
)
|
||||
";
|
||||
|
||||
$user_stmt = $this->pdo->prepare($user_sql);
|
||||
$user_stmt->execute([
|
||||
':fkisp_id_of' => $person_id,
|
||||
':username' => $user_data['username'],
|
||||
':password' => password_hash($user_data['password'], PASSWORD_DEFAULT), // Hash the password
|
||||
':status' => $user_data['status'],
|
||||
':can_run_r' => empty($user_data['can_run_r']) ? 0 : 1
|
||||
]);
|
||||
|
||||
// Commit the transaction
|
||||
$this->pdo->commit();
|
||||
return true;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Roll back the transaction on any error
|
||||
$this->pdo->rollBack();
|
||||
// Log the detailed error
|
||||
error_log("Registration failed: " . $e->getMessage());
|
||||
set_message("Registration failed due to a database error. Please try again.", "danger");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a user based on username and password.
|
||||
*
|
||||
* @param string $username The user's username.
|
||||
* @param string $password The user's plain-text password.
|
||||
* @return array|false User data (pkisu_id, fkisp_id_of, isu_name, isu_status) on success, false on failure.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function authenticateUser(string $username, string $password) {
|
||||
$sql = "SELECT pkisu_id, fkisp_id_of, isu_name, isu_password, isu_status, isu_can_run_r
|
||||
FROM ist_tbl_users
|
||||
WHERE isu_name = :username AND isu_status != 'Inactive'"; // Do not allow login for inactive users
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':username', $username);
|
||||
$stmt->execute();
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($user && password_verify($password, $user['isu_password'])) {
|
||||
// Remove password hash before returning user data
|
||||
unset($user['isu_password']);
|
||||
return $user;
|
||||
}
|
||||
return false;
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error authenticating user: " . $e->getMessage());
|
||||
throw new Exception("Authentication failed due to a server error. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves full user details by user ID (pkisu_id).
|
||||
*
|
||||
* @param int $user_id The pkisu_id of the user.
|
||||
* @return array|false The combined user and person data, or false if not found.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getUserDetails(int $user_id) {
|
||||
$sql = "SELECT u.*, p.*
|
||||
FROM ist_tbl_users u
|
||||
JOIN ist_tbl_people p ON u.fkisp_id_of = p.pkisp_id
|
||||
WHERE u.pkisu_id = :user_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching user details for ID ($user_id): " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve user details. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all users, optionally filtered by status and/or search query.
|
||||
*
|
||||
* @param string|null $search_query Optional search term for username, first name, last name, email, phone.
|
||||
* @param string|null $status_filter Optional status to filter by (e.g., 'Data Owner').
|
||||
* @return array An array of user data.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getAllUsers(?string $search_query = null, ?string $status_filter = null): array {
|
||||
$sql = "SELECT u.pkisu_id, u.isu_name, u.isu_status, u.isu_reg_datetime, u.isu_mod_datetime,
|
||||
u.isu_can_run_r,
|
||||
p.isp_firstname_en, p.isp_lastname_en, p.isp_email, p.isp_phone_number
|
||||
FROM ist_tbl_users u
|
||||
JOIN ist_tbl_people p ON u.fkisp_id_of = p.pkisp_id";
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if ($status_filter) {
|
||||
$conditions[] = "u.isu_status = :status_filter";
|
||||
$params[':status_filter'] = $status_filter;
|
||||
}
|
||||
|
||||
if ($search_query) {
|
||||
$search_term = '%' . $search_query . '%';
|
||||
$conditions[] = "(u.isu_name LIKE :search_query OR
|
||||
p.isp_firstname_en LIKE :search_query OR
|
||||
p.isp_lastname_en LIKE :search_query OR
|
||||
p.isp_email LIKE :search_query OR
|
||||
p.isp_phone_number LIKE :search_query)";
|
||||
$params[':search_query'] = $search_term;
|
||||
}
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$sql .= " WHERE " . implode(" AND ", $conditions);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY u.isu_reg_datetime DESC";
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($params as $key => &$val) {
|
||||
// Use PDO::PARAM_STR for all search/filter parameters, as they are strings
|
||||
$stmt->bindParam($key, $val, PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching all users: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve user list. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of registered users.
|
||||
*
|
||||
* @return int The total number of users.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function getTotalUsers(): int {
|
||||
$sql = "SELECT COUNT(*) FROM ist_tbl_users";
|
||||
try {
|
||||
$stmt = $this->pdo->query($sql);
|
||||
return $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting total users count: " . $e->getMessage());
|
||||
throw new Exception("Could not retrieve user count. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a user's status (role).
|
||||
*
|
||||
* @param int $user_id The ID of the user to update.
|
||||
* @param string $new_status The new status ('DAC Staff', 'Data Owner', 'Data User', 'Inactive').
|
||||
* @param int $mod_by The ID of the user performing the modification.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function updateUserStatus(int $user_id, string $new_status, int $mod_by): bool {
|
||||
$sql = "UPDATE ist_tbl_users
|
||||
SET isu_status = :new_status, isu_mod_datetime = CURRENT_TIMESTAMP, isu_regby_id = :mod_by
|
||||
WHERE pkisu_id = :user_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':new_status', $new_status);
|
||||
$stmt->bindParam(':mod_by', $mod_by, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error updating user status (ID: $user_id): " . $e->getMessage());
|
||||
throw new Exception("Could not update user status. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants or revokes R/Jupyter access for a user.
|
||||
*
|
||||
* @param int $user_id The pkisu_id of the user.
|
||||
* @param bool $can_run_r Whether the user should have access.
|
||||
* @param int $mod_by The ID of the admin performing the change.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs.
|
||||
*/
|
||||
public function updateUserRJupyterAccess(int $user_id, bool $can_run_r, int $mod_by): bool {
|
||||
$sql = "UPDATE ist_tbl_users
|
||||
SET isu_can_run_r = :can_run_r, isu_mod_datetime = CURRENT_TIMESTAMP, isu_regby_id = :mod_by
|
||||
WHERE pkisu_id = :user_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$flag = $can_run_r ? 1 : 0;
|
||||
$stmt->bindParam(':can_run_r', $flag, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':mod_by', $mod_by, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error updating R/Jupyter access (ID: $user_id): " . $e->getMessage());
|
||||
throw new Exception("Could not update R/Jupyter access. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a user's personal information.
|
||||
*
|
||||
* @param int $person_id The pkisp_id of the person to update.
|
||||
* @param array $person_data Associative array with fields to update (e.g., isp_firstname_en, isp_phone_number).
|
||||
* @param int $mod_by The ID of the user performing the modification.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If a database error occurs or duplicate entry.
|
||||
*/
|
||||
public function updatePersonInfo(int $person_id, array $person_data, int $mod_by): bool {
|
||||
$setClauses = [];
|
||||
$params = [':person_id' => $person_id, ':mod_by' => $mod_by];
|
||||
|
||||
foreach ($person_data as $key => $value) {
|
||||
// Only allow specific fields to be updated
|
||||
if (in_array($key, [
|
||||
'isp_idcard', 'isp_firstname_en', 'isp_lastname_en', 'isp_sex', 'isp_dob',
|
||||
'isp_pob', 'isp_nationality', 'isp_marital_status', 'isp_phone_number',
|
||||
'isp_email', 'isp_telegram', 'isp_note'
|
||||
])) {
|
||||
$setClauses[] = "$key = :$key";
|
||||
$params[":$key"] = ($value === '' ? null : $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($setClauses)) {
|
||||
return false; // No fields to update
|
||||
}
|
||||
|
||||
$sql = "UPDATE ist_tbl_people
|
||||
SET " . implode(', ', $setClauses) . ", isp_mod_datetime = CURRENT_TIMESTAMP, isp_regby_id = :mod_by
|
||||
WHERE pkisp_id = :person_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->rowCount() > 0;
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == '23000') {
|
||||
throw new Exception("A duplicate entry was found for ID card, email, or phone number.");
|
||||
}
|
||||
error_log("Error updating person info (ID: $person_id): " . $e->getMessage());
|
||||
throw new Exception("Could not update personal information. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a user's password.
|
||||
*
|
||||
* @param int $user_id The pkisu_id of the user.
|
||||
* @param string $new_password The new plain-text password.
|
||||
* @param int $mod_by The ID of the user performing the modification.
|
||||
* @return bool True on success.
|
||||
* @throws Exception If password hashing fails or database error.
|
||||
*/
|
||||
public function changePassword(int $user_id, string $new_password, int $mod_by): bool {
|
||||
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
|
||||
if ($hashed_password === false) {
|
||||
throw new Exception("Failed to hash new password.");
|
||||
}
|
||||
|
||||
$sql = "UPDATE ist_tbl_users
|
||||
SET isu_password = :hashed_password, isu_mod_datetime = CURRENT_TIMESTAMP, isu_regby_id = :mod_by
|
||||
WHERE pkisu_id = :user_id";
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindParam(':hashed_password', $hashed_password);
|
||||
$stmt->bindParam(':mod_by', $mod_by, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
|
||||
return $stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error changing password for user (ID: $user_id): " . $e->getMessage());
|
||||
throw new Exception("Could not change password. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
25
composer.json
Normal file
25
composer.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "niph/dsp",
|
||||
"type": "project",
|
||||
"description": "Data Sharing Platform (DSP) with JupyterHub integration",
|
||||
"require": {
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"classes/"
|
||||
],
|
||||
"files": [
|
||||
"includes/jupyter_helpers.php",
|
||||
"includes/auth.php"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
1688
composer.lock
generated
Normal file
1688
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
config.php
Normal file
35
config.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// config.php
|
||||
// Central file for database connection and configuration.
|
||||
|
||||
// Ensure application log directory exists for capturing warnings/errors.
|
||||
$logDir = __DIR__ . '/logs';
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0775, true);
|
||||
}
|
||||
|
||||
if (is_dir($logDir) && is_writable($logDir)) {
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', $logDir . '/app.log');
|
||||
}
|
||||
|
||||
// Database credentials (allow override via environment variables for Docker support)
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
||||
define('DB_PORT', getenv('DB_PORT') ?: '3306');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASS') !== false ? getenv('DB_PASS') : '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'niph_dsps');
|
||||
|
||||
// Establish PDO connection
|
||||
try {
|
||||
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_PORT, DB_NAME);
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS);
|
||||
// Set PDO attributes for error handling and default fetch mode
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
// In a production environment, log the error and show a generic message
|
||||
error_log("Database connection failed: " . $e->getMessage());
|
||||
die("A problem occurred with the database connection. Please try again later.");
|
||||
}
|
||||
?>
|
||||
341
data_hybrid/browse_datasources.php
Normal file
341
data_hybrid/browse_datasources.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
// data_user/browse_datasources.php
|
||||
// This page allows users (including guests) to browse available data sources.
|
||||
|
||||
// Enable detailed error reporting for debugging purposes (log only)
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// Check if a session is not already active before starting one.
|
||||
// This prevents the "session already active" notice.
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Use __DIR__ to get the absolute path to this file's directory,
|
||||
// ensuring the path to the required files is always correct.
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
require_once(__DIR__ . '/../includes/auth.php');
|
||||
require_once(__DIR__ . '/../classes/DataSource.php');
|
||||
require_once(__DIR__ . '/../classes/Classifications.php');
|
||||
require_once(__DIR__ . '/../classes/User.php');
|
||||
require_once(__DIR__ . '/../classes/Permission.php'); // Correctly include the Permission class
|
||||
|
||||
// Check if user is logged in
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
$person_id = $_SESSION['person_id'] ?? null;
|
||||
$username = $_SESSION['username'] ?? null;
|
||||
$usernameLabel = $username ? htmlspecialchars($username, ENT_QUOTES, 'UTF-8') : 'Guest';
|
||||
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
// Instantiate classes
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
$classificationManager = new Classifications($pdo);
|
||||
$userManager = new User($pdo);
|
||||
$permissionManager = new Permission($pdo); // Instantiate the Permission class
|
||||
|
||||
// Get user details if logged in
|
||||
$currentUserDetails = $is_logged_in ? $userManager->getUserDetails($user_id) : null;
|
||||
|
||||
$uploadsWebPath = '../uploads/datasources/';
|
||||
|
||||
// Get filter parameters from GET request
|
||||
$filter_category_id = $_GET['category_id'] ?? null;
|
||||
if ($filter_category_id !== null) {
|
||||
$filter_category_id = filter_var($filter_category_id, FILTER_VALIDATE_INT);
|
||||
if ($filter_category_id === false) {
|
||||
$filter_category_id = null;
|
||||
}
|
||||
}
|
||||
$search_query = htmlspecialchars($_GET['search'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Fetch data sources based on filters
|
||||
$data_sources = [];
|
||||
try {
|
||||
$data_sources = $dataSourceManager->getDataSources(
|
||||
null, // No owner filter for public browsing
|
||||
'Active',
|
||||
$filter_category_id,
|
||||
$search_query
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
set_message('Error retrieving data sources: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
|
||||
// Fetch all categories for the filter dropdown
|
||||
$all_categories = [];
|
||||
try {
|
||||
$all_categories = $classificationManager->getAllCategories();
|
||||
} catch (Exception $e) {
|
||||
error_log("Error fetching categories for browse_datasources: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_contributor.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_contributor.php");
|
||||
?>
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> Browse All Data</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo $usernameLabel; ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter and Search Form -->
|
||||
<div class="card shadow-sm rounded mb-4 p-3">
|
||||
<div class="card-body">
|
||||
<form action="browse_datasources.php" method="GET" class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label for="categoryFilter" class="form-label">Filter by Category:</label>
|
||||
<select class="form-select rounded" id="categoryFilter" name="category_id">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach ($all_categories as $category): ?>
|
||||
<option value="<?php echo htmlspecialchars($category['pkdspscate_id']); ?>"
|
||||
<?php echo ($filter_category_id == $category['pkdspscate_id']) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($category['dspscate_title_en']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="searchQuery" class="form-label">Search by Title/Description:</label>
|
||||
<input type="text" class="form-control rounded" id="searchQuery" name="search"
|
||||
value="<?php echo htmlspecialchars($search_query); ?>" placeholder="Enter keywords...">
|
||||
</div>
|
||||
<div class="col-md-3 d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-filter me-2"></i> Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Sources List -->
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 mt-4">
|
||||
<?php if (!empty($data_sources)): ?>
|
||||
<?php foreach ($data_sources as $ds): ?>
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm rounded">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title text-primary"><?php echo htmlspecialchars($ds['dspsds_title_en']); ?></h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Category: <?php echo htmlspecialchars($ds['category_name']); ?></h6>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Type: <?php echo htmlspecialchars($ds['data_type_name']); ?></h6>
|
||||
<p class="card-text flex-grow-1"><?php echo htmlspecialchars(substr($ds['dspsds_description'], 0, 150)) . (strlen($ds['dspsds_description']) > 150 ? '...' : ''); ?></p>
|
||||
<div class="mt-auto">
|
||||
<ul class="list-unstyled small text-muted">
|
||||
<li><i class="fas fa-user me-1"></i> Data Owner: <?php echo htmlspecialchars($ds['isp_firstname_en'] . ' ' . $ds['isp_lastname_en']); ?></li>
|
||||
<li><i class="fas fa-calendar-alt me-1"></i> Published:
|
||||
<?php if (!empty($ds['dspsds_public_date'])): ?>
|
||||
<?= htmlspecialchars(date('M d, Y', strtotime($ds['dspsds_public_date']))); ?>
|
||||
<?php else: ?>
|
||||
Not specified
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
<?php
|
||||
$supportingFiles = [
|
||||
'dspsds_filename1' => ['label' => 'Questionnaire / Data Dictionary', 'icon' => 'fa-clipboard-list'],
|
||||
'dspsds_filename2' => ['label' => 'Protocol / User Guide', 'icon' => 'fa-book'],
|
||||
'dspsds_filename3' => ['label' => 'Other Supporting Document', 'icon' => 'fa-file-alt'],
|
||||
];
|
||||
?>
|
||||
<div class="bg-light-subtle border rounded p-3 mb-3">
|
||||
<span class="d-block text-uppercase text-muted fw-semibold small mb-2">Supporting Documents</span>
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
<?php foreach ($supportingFiles as $column => $meta): ?>
|
||||
<?php
|
||||
$fileName = $ds[$column] ?? '';
|
||||
$label = $meta['label'];
|
||||
$icon = $meta['icon'];
|
||||
?>
|
||||
<li class="mb-2">
|
||||
<i class="fas <?= htmlspecialchars($icon, ENT_QUOTES, 'UTF-8') ?> me-1"></i>
|
||||
<?php if (!empty($fileName)): ?>
|
||||
<?php
|
||||
$isUrl = preg_match('/^https?:\/\//i', $fileName) === 1;
|
||||
$linkTarget = $isUrl ? $fileName : $uploadsWebPath . rawurlencode($fileName);
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($linkTarget, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars($label) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted"><?= htmlspecialchars($label) ?> (Not provided)</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php if ($is_logged_in): ?>
|
||||
<?php
|
||||
$has_read_permission = false;
|
||||
$has_download_permission = false;
|
||||
try {
|
||||
$has_read_permission = $permissionManager->hasPermission($person_id, $ds['pkdspsds_id'], 'Read');
|
||||
$has_download_permission = $permissionManager->hasPermission($person_id, $ds['pkdspsds_id'], 'Download');
|
||||
} catch (Exception $e) {
|
||||
error_log("Permission check error for user " . $person_id . " on DS " . $ds['pkdspsds_id'] . ": " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<?php if ($has_read_permission): ?>
|
||||
<a href="#" class="btn btn-sm btn-outline-success rounded me-2 disabled">
|
||||
<i class="fas fa-check-circle me-1"></i> Read Access Granted
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-success rounded me-2" data-bs-toggle="modal" data-bs-target="#requestPermissionModal"
|
||||
data-ds-id="<?php echo htmlspecialchars($ds['pkdspsds_id']); ?>"
|
||||
data-ds-title="<?php echo htmlspecialchars($ds['dspsds_title_en']); ?>"
|
||||
data-permission-type="Read">
|
||||
<i class="fas fa-file-alt me-1"></i> Request Read Access
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($has_download_permission): ?>
|
||||
<a href="download.php?dspsds_id=<?php echo htmlspecialchars($ds['pkdspsds_id']); ?>" class="btn btn-sm btn-outline-primary rounded">
|
||||
<i class="fas fa-download me-1"></i> Download File
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-primary rounded" data-bs-toggle="modal" data-bs-target="#requestPermissionModal"
|
||||
data-ds-id="<?php echo htmlspecialchars($ds['pkdspsds_id']); ?>"
|
||||
data-ds-title="<?php echo htmlspecialchars($ds['dspsds_title_en']); ?>"
|
||||
data-permission-type="Download">
|
||||
<i class="fas fa-cloud-download-alt me-1"></i> Request Download
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-secondary rounded" data-bs-toggle="modal" data-bs-target="#loginModal">
|
||||
<i class="fas fa-sign-in-alt me-1"></i> Login to Request Access
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info rounded text-center">
|
||||
No active data sources found matching your criteria.
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Permission Modal -->
|
||||
<div class="modal fade" id="requestPermissionModal" tabindex="-1" aria-labelledby="requestPermissionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header bg-success text-white rounded-top">
|
||||
<h5 class="modal-title" id="requestPermissionModalLabel">Request Data Access Permission</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="../data_user/process_request_permission.php" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="data_source_id" id="modalDataSourceId">
|
||||
<input type="hidden" name="permission_type" id="modalPermissionType">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="modalDataSourceTitle" class="form-label">Data Source</label>
|
||||
<input type="text" class="form-control rounded" id="modalDataSourceTitle" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalRequestedPermission" class="form-label">Requested Permission</label>
|
||||
<input type="text" class="form-control rounded" id="modalRequestedPermission" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="requestNotes" class="form-label">Reason for Request (Required)</label>
|
||||
<textarea class="form-control rounded-3" id="requestNotes" name="notes" rows="4" placeholder="Please explain why you need this data and how you plan to use it." required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="proofFile" class="form-label">Upload Proof (PDF only)</label>
|
||||
<input type="file" class="form-control rounded" id="proofFile" name="proof_file" accept="application/pdf" required>
|
||||
<div class="form-text">Attach an official letter, approval memo, or supporting document in PDF format (max 10 MB).</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success rounded">Submit Request</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal for guests -->
|
||||
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header bg-primary text-white rounded-top">
|
||||
<h5 class="modal-title" id="loginModalLabel">Login Required</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center p-4">
|
||||
<p>You must be logged in to request access to data sources.</p>
|
||||
<a href="../index.php?page=login" class="btn btn-primary rounded me-2">Login</a>
|
||||
<a href="../index.php?page=register" class="btn btn-outline-primary rounded">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_contributor.php");
|
||||
?>
|
||||
<script>
|
||||
// JavaScript to populate the permission request modal
|
||||
var requestPermissionModal = document.getElementById('requestPermissionModal');
|
||||
if (requestPermissionModal) {
|
||||
requestPermissionModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget; // Button that triggered the modal
|
||||
var dsId = button.getAttribute('data-ds-id');
|
||||
var dsTitle = button.getAttribute('data-ds-title');
|
||||
var permissionType = button.getAttribute('data-permission-type');
|
||||
|
||||
var modalDataSourceId = requestPermissionModal.querySelector('#modalDataSourceId');
|
||||
var modalDataSourceTitle = requestPermissionModal.querySelector('#modalDataSourceTitle');
|
||||
var modalPermissionType = requestPermissionModal.querySelector('#modalPermissionType');
|
||||
var modalRequestedPermission = requestPermissionModal.querySelector('#modalRequestedPermission');
|
||||
|
||||
modalDataSourceId.value = dsId;
|
||||
modalDataSourceTitle.value = dsTitle;
|
||||
modalPermissionType.value = permissionType;
|
||||
modalRequestedPermission.value = permissionType; // Display the type in the modal
|
||||
requestPermissionModal.querySelector('#requestNotes').value = ''; // Clear notes on new open
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
213
data_hybrid/dashboard.php
Normal file
213
data_hybrid/dashboard.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
// data_hybrid/dashboard.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/User.php';
|
||||
require_once '../classes/DataSource.php'; // For counts
|
||||
// Ensure only Data Contributor can access this dashboard
|
||||
redirect_if_not_role('Data Contributor');
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id']; // The fkisp_id_of for the data owner
|
||||
$username = $_SESSION['username'];
|
||||
$user_status = $_SESSION['user_status'];
|
||||
|
||||
// Instantiate classes
|
||||
$user_manager = new User($pdo);
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
|
||||
// Get dynamic counts for the Data Owner
|
||||
$my_data_sources_count = count($data_source_manager->getDataSources($person_id));
|
||||
$pending_permissions_count = count($data_source_manager->getPermissionRequestsForOwner($person_id, 'Pending'));
|
||||
$usageByDataSource = $data_source_manager->getUsageByDataSourceForUser($person_id, 8);
|
||||
$data_accesses_last_30_days = 0;
|
||||
if (!empty($usageByDataSource)) {
|
||||
foreach ($usageByDataSource as $row) {
|
||||
$data_accesses_last_30_days += (int) ($row['usage_count'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_contributor.php");
|
||||
?>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_contributor.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> Dashboard</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary"><i class="fas fa-database me-2"></i> My Data Sources</h5>
|
||||
<p class="card-text fs-2"><?= $my_data_sources_count ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning"><i class="fas fa-hourglass-half me-2"></i> Pending Permissions</h5>
|
||||
<p class="card-text fs-2"><?= $pending_permissions_count ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success"><i class="fas fa-download me-2"></i> Data Accesses (Last 30 Days)</h5>
|
||||
<p class="card-text fs-2"><?= $data_accesses_last_30_days ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$chartLabels = [];
|
||||
$chartValues = [];
|
||||
if (!empty($usageByDataSource)) {
|
||||
foreach ($usageByDataSource as $row) {
|
||||
$chartLabels[] = $row['dspsds_title_en'] ?? ('Data Source #' . $row['pkdspsds_id']);
|
||||
$chartValues[] = (int) ($row['usage_count'] ?? 0);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm rounded h-100">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0">Usage of Data Sources</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($chartValues)): ?>
|
||||
<canvas id="contributorUsagePieChart" height="300"></canvas>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">No usage recorded yet for your account.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<h3>Quick Actions</h3>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a href="manage_my_datasources.php?action=add" class="btn btn-primary rounded"><i class="fas fa-plus-circle me-2"></i> Add New Data Source</a>
|
||||
<a href="manage_permissions.php" class="btn btn-warning rounded"><i class="fas fa-user-check me-2"></i> Review Permissions</a>
|
||||
<a href="my_analytics.php" class="btn btn-info rounded"><i class="fas fa-chart-pie me-2"></i> View My Data Usage</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder for recent activity related to their data sources -->
|
||||
<div class="mt-5">
|
||||
<h3>Recent Activity on My Data Sources</h3>
|
||||
<ul class="list-group shadow-sm rounded">
|
||||
<?php
|
||||
// Example recent activities (replace with actual data from dsps_tbl_datasource_used and dsps_tbl_anonymous)
|
||||
// You'd need to fetch these from the database, potentially joining with dsps_tbl_datasource
|
||||
$recent_activities = [
|
||||
['text' => 'User John Doe requested access to \'Population Census 2023\'.', 'time' => 'Just now', 'type' => 'info'],
|
||||
['text' => '\'Health Data Q1 2024\' was downloaded 5 times today.', 'time' => '2 hours ago', 'type' => 'success'],
|
||||
['text' => 'You updated \'Education Statistics 2022\'.', 'time' => 'Yesterday', 'type' => 'secondary'],
|
||||
];
|
||||
foreach ($recent_activities as $activity) {
|
||||
echo '<li class="list-group-item d-flex justify-content-between align-items-center">';
|
||||
echo htmlspecialchars($activity['text']);
|
||||
echo '<span class="badge bg-' . htmlspecialchars($activity['type']) . '">' . htmlspecialchars($activity['time']) . '</span>';
|
||||
echo '</li>';
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_contributor.php");
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js" integrity="sha256-VjZ1tcHTul3e8DqRL3OjaxAg/P070MqxsVXni4eWh05=" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const labels = <?= json_encode(array_map('htmlspecialchars', $chartLabels)) ?>;
|
||||
const values = <?= json_encode($chartValues) ?>;
|
||||
|
||||
if (!Array.isArray(labels) || !Array.isArray(values) || labels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('contributorUsagePieChart');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundColors = [
|
||||
'#0d6efd', '#198754', '#dc3545', '#fd7e14', '#6f42c1', '#20c997', '#0dcaf0', '#ffc107'
|
||||
];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: values,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
return `${label}: ${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
111
data_hybrid/download.php
Normal file
111
data_hybrid/download.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
// This script handles the file download and logs the action to the database.
|
||||
|
||||
// Start the session to access user info
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php'; // Make sure your auth.php includes a redirect_if_not_logged_in or similar function
|
||||
|
||||
// --- 1. Get User and Datasource IDs ---
|
||||
// Ensure the user is logged in
|
||||
redirect_if_not_logged_in('../login.php');
|
||||
|
||||
// Get the user's person_id from the session
|
||||
$person_id = $_SESSION['person_id'];
|
||||
|
||||
// Get the datasource_id from the URL parameter
|
||||
$datasource_id = $_GET['dspsds_id'] ?? null;
|
||||
|
||||
// Validate the datasource_id
|
||||
if (!$datasource_id || !filter_var($datasource_id, FILTER_VALIDATE_INT)) {
|
||||
die("Invalid or missing datasource ID.");
|
||||
}
|
||||
|
||||
// --- 2. Log the Download Action ---
|
||||
// This code inserts a new record for every download.
|
||||
try {
|
||||
$sql_insert = "
|
||||
INSERT INTO dsps_tbl_datasource_used
|
||||
(fkdspsdsused_id, fkisp_id_of, dspsdspused_action)
|
||||
VALUES
|
||||
(?, ?, ?)
|
||||
";
|
||||
$stmt_insert = $pdo->prepare($sql_insert);
|
||||
$action = "Downloaded";
|
||||
$stmt_insert->execute([$datasource_id, $person_id, $action]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// We now log the error and set a user-facing message
|
||||
error_log("Error logging download: " . $e->getMessage());
|
||||
// Redirect with an error message, but still try to serve the file
|
||||
set_message("An error occurred while logging the download.", "danger");
|
||||
// We do not die here, as we still want to try and serve the file
|
||||
}
|
||||
|
||||
// --- 3. Retrieve File Path and Name ---
|
||||
$file_path = null;
|
||||
$file_name = null;
|
||||
try {
|
||||
$sql_select = "
|
||||
SELECT dspsds_filename, dspsds_title_en
|
||||
FROM dsps_tbl_datasource
|
||||
WHERE pkdspsds_id = ?
|
||||
";
|
||||
$stmt = $pdo->prepare($sql_select);
|
||||
$stmt->execute([$datasource_id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if ($row) {
|
||||
$file_name = $row['dspsds_filename'];
|
||||
$download_label = $row['dspsds_title_en'] ?: 'datasource_' . $datasource_id;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error retrieving file info: " . $e->getMessage());
|
||||
die("An error occurred while retrieving file information.");
|
||||
}
|
||||
|
||||
if (empty($file_name)) {
|
||||
die("File not found in the database.");
|
||||
}
|
||||
|
||||
// Handle external URLs
|
||||
if (preg_match('/^https?:\\/\\//i', $file_name)) {
|
||||
header('Location: ' . $file_name);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uploadsDir = realpath(__DIR__ . '/../uploads/datasources');
|
||||
if (!$uploadsDir) {
|
||||
error_log('Uploads directory not found for download.');
|
||||
die('File storage directory is unavailable.');
|
||||
}
|
||||
|
||||
$file_path = $uploadsDir . '/' . $file_name;
|
||||
|
||||
// --- 4. Serve the File to the User ---
|
||||
// Check if the file exists on the server
|
||||
if (file_exists($file_path)) {
|
||||
// Set headers to force a download
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . basename($download_label . '_' . $file_name) . '"');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('Content-Length: ' . filesize($file_path));
|
||||
|
||||
// Clear output buffer
|
||||
if (ob_get_level()) {
|
||||
ob_clean();
|
||||
}
|
||||
flush();
|
||||
|
||||
// Read the file and send it to the output buffer
|
||||
readfile($file_path);
|
||||
exit;
|
||||
} else {
|
||||
die("The file could not be found on the server at the specified path.");
|
||||
}
|
||||
?>
|
||||
537
data_hybrid/manage_my_datasources.php
Normal file
537
data_hybrid/manage_my_datasources.php
Normal file
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
// data_owner/manage_my_datasources.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
|
||||
// Ensure only Data Owners can access this page
|
||||
redirect_if_not_role('Data Contributor');
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$owner_person_id = $_SESSION['person_id'];
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$ds_id = $_GET['id'] ?? null;
|
||||
$datasource_data = [];
|
||||
|
||||
// Fetch dropdown data
|
||||
$data_types = $data_source_manager->getAllDataTypes();
|
||||
$categories = $data_source_manager->getAllCategories();
|
||||
$primaryRulesMap = [];
|
||||
foreach ($data_types as $type) {
|
||||
$typeName = $type['dspstds_name_en'] ?? null;
|
||||
$rules = $data_source_manager->getPrimaryFileRulesForType($typeName);
|
||||
$acceptList = [];
|
||||
foreach ($rules['extensions'] ?? [] as $ext) {
|
||||
$acceptList[] = '.' . strtolower($ext);
|
||||
}
|
||||
$primaryRulesMap[$type['pkdspstds_id']] = [
|
||||
'accept' => $acceptList,
|
||||
'description' => $rules['description'] ?? 'CSV, JSON, PDF, XLS, XLSX',
|
||||
];
|
||||
}
|
||||
$defaultPrimaryRules = $data_source_manager->getPrimaryFileRulesForType(null);
|
||||
$defaultPrimaryAccept = [];
|
||||
foreach ($defaultPrimaryRules['extensions'] ?? [] as $ext) {
|
||||
$defaultPrimaryAccept[] = '.' . strtolower($ext);
|
||||
}
|
||||
$initialPrimaryDescription = $defaultPrimaryRules['description'] ?? 'CSV, JSON, PDF, XLS, XLSX';
|
||||
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title_en = trim($_POST['title_en'] ?? '');
|
||||
$title_kh = trim($_POST['title_kh'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$type_id = filter_var($_POST['type_id'] ?? '', FILTER_SANITIZE_NUMBER_INT);
|
||||
$category_id = filter_var($_POST['category_id'] ?? '', FILTER_SANITIZE_NUMBER_INT);
|
||||
$public_date = trim($_POST['public_date'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'Pending Review');
|
||||
$selectedDataType = null;
|
||||
if (!empty($type_id)) {
|
||||
$selectedDataType = $data_source_manager->getDataTypeById((int)$type_id);
|
||||
}
|
||||
$current_files = [
|
||||
'dspsds_filename' => trim($_POST['current_filename'] ?? ''),
|
||||
'dspsds_filename1' => trim($_POST['current_filename1'] ?? ''),
|
||||
'dspsds_filename2' => trim($_POST['current_filename2'] ?? ''),
|
||||
'dspsds_filename3' => trim($_POST['current_filename3'] ?? ''),
|
||||
];
|
||||
|
||||
$final_files = $current_files;
|
||||
$file_inputs = [
|
||||
'dspsds_filename' => 'data_file',
|
||||
'dspsds_filename1' => 'data_file1',
|
||||
'dspsds_filename2' => 'data_file2',
|
||||
'dspsds_filename3' => 'data_file3',
|
||||
];
|
||||
$file_labels = [
|
||||
'dspsds_filename' => 'Primary Data File',
|
||||
'dspsds_filename1' => 'Questionnaire / Data Dictionary',
|
||||
'dspsds_filename2' => 'Protocol / User Guide',
|
||||
'dspsds_filename3' => 'Other Supporting Document',
|
||||
];
|
||||
$remove_files = $_POST['remove_files'] ?? [];
|
||||
if (!is_array($remove_files)) {
|
||||
$remove_files = [$remove_files];
|
||||
}
|
||||
|
||||
foreach ($file_inputs as $column => $inputName) {
|
||||
if (!isset($_FILES[$inputName]) || $_FILES[$inputName]['error'] === UPLOAD_ERR_NO_FILE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($_FILES[$inputName]['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception('Upload error code: ' . $_FILES[$inputName]['error']);
|
||||
}
|
||||
$fileRules = null;
|
||||
if ($column === 'dspsds_filename') {
|
||||
$fileRules = $data_source_manager->getPrimaryFileRulesForType($selectedDataType['dspstds_name_en'] ?? null);
|
||||
}
|
||||
$uploadedName = $data_source_manager->handleDataSourceFileUpload($_FILES[$inputName], $fileRules);
|
||||
if ($uploadedName) {
|
||||
if (!empty($current_files[$column]) && $current_files[$column] !== $uploadedName) {
|
||||
$oldPath = $data_source_manager->getUploadDir() . $current_files[$column];
|
||||
if (is_file($oldPath)) {
|
||||
unlink($oldPath);
|
||||
}
|
||||
}
|
||||
$final_files[$column] = $uploadedName;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$friendlyLabel = $file_labels[$column] ?? $inputName;
|
||||
set_message('File upload failed for ' . htmlspecialchars($friendlyLabel) . ': ' . $e->getMessage(), 'danger');
|
||||
$final_files[$column] = $current_files[$column];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($remove_files as $column) {
|
||||
if (!array_key_exists($column, $final_files)) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($current_files[$column])) {
|
||||
$oldPath = $data_source_manager->getUploadDir() . $current_files[$column];
|
||||
if (is_file($oldPath)) {
|
||||
unlink($oldPath);
|
||||
}
|
||||
}
|
||||
$final_files[$column] = '';
|
||||
}
|
||||
|
||||
// Basic validation for required fields
|
||||
if (empty($title_en) || empty($type_id) || empty($category_id)) {
|
||||
set_message("Title, Data Type, and Category are required.", "danger");
|
||||
// Redirect to preserve form data or re-display form with errors
|
||||
// For now, we'll just redirect to list, but a better UX would be to stay on the form
|
||||
header("Location: manage_my_datasources.php?action=" . ($action === 'add_submit' ? 'add' : 'edit&id=' . $ds_id));
|
||||
exit();
|
||||
}
|
||||
|
||||
// Determine the public date to pass to the add/update methods
|
||||
// The DataSource class's add/update methods have logic for this, so we'll pass it as a string or null
|
||||
$final_public_date = (!empty($public_date) && $status === 'Active') ? $public_date : null;
|
||||
|
||||
|
||||
if ($action === 'add_submit') {
|
||||
try {
|
||||
// Corrected call to addDataSource
|
||||
if ($data_source_manager->addDataSource(
|
||||
$type_id,
|
||||
$category_id,
|
||||
$owner_person_id, // Data owner is the logged-in person
|
||||
$final_files['dspsds_filename'],
|
||||
$title_en,
|
||||
$title_kh,
|
||||
$description,
|
||||
$status,
|
||||
$user_id, // User who registered it (logged-in user)
|
||||
$final_files['dspsds_filename1'],
|
||||
$final_files['dspsds_filename2'],
|
||||
$final_files['dspsds_filename3']
|
||||
)) {
|
||||
set_message("Data source added successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to add data source.", "danger");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message("Error adding data source: " . $e->getMessage(), "danger");
|
||||
}
|
||||
} elseif ($action === 'edit_submit' && $ds_id) {
|
||||
try {
|
||||
// Corrected call to updateDataSource
|
||||
if ($data_source_manager->updateDataSource(
|
||||
$ds_id,
|
||||
$type_id,
|
||||
$category_id,
|
||||
$owner_person_id, // Data owner is the logged-in person
|
||||
$final_files['dspsds_filename'],
|
||||
$title_en,
|
||||
$title_kh,
|
||||
$description,
|
||||
$status,
|
||||
$user_id, // User who modified it (logged-in user)
|
||||
$final_files['dspsds_filename1'],
|
||||
$final_files['dspsds_filename2'],
|
||||
$final_files['dspsds_filename3']
|
||||
)) {
|
||||
set_message("Data source updated successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to update data source.", "danger");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message("Error updating data source: " . $e->getMessage(), "danger");
|
||||
}
|
||||
}
|
||||
// Redirect after POST to prevent form resubmission
|
||||
header("Location: manage_my_datasources.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Handle GET actions
|
||||
if ($action === 'edit' && $ds_id) {
|
||||
$datasource_data = $data_source_manager->getDataSourceById($ds_id);
|
||||
// Crucial security check: Ensure the logged-in owner actually owns this data source
|
||||
if (!$datasource_data || $datasource_data['fkisp_id_of'] != $owner_person_id) {
|
||||
set_message("Data source not found or you don't have permission to edit it.", "danger");
|
||||
header("Location: manage_my_datasources.php");
|
||||
exit();
|
||||
}
|
||||
} elseif ($action === 'delete' && $ds_id) {
|
||||
$datasource = $data_source_manager->getDataSourceById($ds_id);
|
||||
// Crucial security check: Ensure the logged-in owner actually owns this data source
|
||||
if ($datasource && $datasource['fkisp_id_of'] == $owner_person_id) {
|
||||
// Delete associated file on the server
|
||||
$fileColumns = ['dspsds_filename', 'dspsds_filename1', 'dspsds_filename2', 'dspsds_filename3'];
|
||||
foreach ($fileColumns as $column) {
|
||||
if (!empty($datasource[$column])) {
|
||||
$filePath = $data_source_manager->getUploadDir() . $datasource[$column];
|
||||
if (is_file($filePath)) {
|
||||
unlink($filePath); // Delete the file
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($data_source_manager->deleteDataSource($ds_id)) {
|
||||
set_message("Data source deleted successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to delete data source.", "danger");
|
||||
}
|
||||
} else {
|
||||
set_message("Data source not found or you don't have permission to delete it.", "warning");
|
||||
}
|
||||
header("Location: manage_my_datasources.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch data sources for the current owner for display
|
||||
$my_data_sources = $data_source_manager->getDataSources($owner_person_id);
|
||||
$uploadsWebPath = '../uploads/datasources/';
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_contributor.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_contributor.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> My Data Sources</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<a href="manage_my_datasources.php?action=add" class="btn btn-primary rounded">
|
||||
<i class="fas fa-plus-circle me-2"></i> Add New Data Source
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= htmlspecialchars($_SESSION['message']) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($action === 'add' || $action === 'edit'): ?>
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0"><?= ($action === 'add' ? 'Add New' : 'Edit') ?> Data Source</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_my_datasources.php?action=<?= ($action === 'add' ? 'add_submit' : 'edit_submit&id=' . htmlspecialchars($ds_id)) ?>" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="current_filename" value="<?= htmlspecialchars($datasource_data['dspsds_filename'] ?? '') ?>">
|
||||
<input type="hidden" name="current_filename1" value="<?= htmlspecialchars($datasource_data['dspsds_filename1'] ?? '') ?>">
|
||||
<input type="hidden" name="current_filename2" value="<?= htmlspecialchars($datasource_data['dspsds_filename2'] ?? '') ?>">
|
||||
<input type="hidden" name="current_filename3" value="<?= htmlspecialchars($datasource_data['dspsds_filename3'] ?? '') ?>">
|
||||
<div class="mb-3">
|
||||
<label for="dsTitleEn" class="form-label">Title (English)</label>
|
||||
<input type="text" class="form-control rounded" id="dsTitleEn" name="title_en" value="<?= htmlspecialchars($datasource_data['dspsds_title_en'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsTitleKh" class="form-label">Title (Khmer)</label>
|
||||
<input type="text" class="form-control rounded" id="dsTitleKh" name="title_kh" value="<?= htmlspecialchars($datasource_data['dspsds_title_kh'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsDescription" class="form-label">Description</label>
|
||||
<textarea class="form-control rounded" id="dsDescription" name="description" rows="5"><?= htmlspecialchars($datasource_data['dspsds_description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsType" class="form-label">Data Type</label>
|
||||
<select class="form-select rounded" id="dsType" name="type_id" required>
|
||||
<option value="">Select Type...</option>
|
||||
<?php foreach ($data_types as $type): ?>
|
||||
<option value="<?= htmlspecialchars($type['pkdspstds_id']) ?>"
|
||||
<?= (isset($datasource_data['fkdspstds_id']) && $datasource_data['fkdspstds_id'] == $type['pkdspstds_id']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($type['dspstds_name_en']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsCategory" class="form-label">Category</label>
|
||||
<select class="form-select rounded" id="dsCategory" name="category_id" required>
|
||||
<option value="">Select Category...</option>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<option value="<?= htmlspecialchars($category['pkdspscate_id']) ?>"
|
||||
<?= (isset($datasource_data['fkdspscate_id']) && $datasource_data['fkdspscate_id'] == $category['pkdspscate_id']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($category['dspscate_title_en']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php
|
||||
$currentTypeForDescription = $datasource_data['fkdspstds_id'] ?? null;
|
||||
if ($currentTypeForDescription && isset($primaryRulesMap[$currentTypeForDescription])) {
|
||||
$initialPrimaryDescription = $primaryRulesMap[$currentTypeForDescription]['description'] ?? $initialPrimaryDescription;
|
||||
}
|
||||
$fileInputsConfig = [
|
||||
[
|
||||
'label' => 'Primary Data File',
|
||||
'id' => 'dsDataFile',
|
||||
'name' => 'data_file',
|
||||
'column' => 'dspsds_filename',
|
||||
'help' => 'Upload a file that matches the selected data type.',
|
||||
],
|
||||
[
|
||||
'label' => 'Questionnaire / Data Dictionary',
|
||||
'id' => 'dsDataFile1',
|
||||
'name' => 'data_file1',
|
||||
'column' => 'dspsds_filename1',
|
||||
'help' => 'Upload a supporting document (PDF, XLSX, etc.)',
|
||||
],
|
||||
[
|
||||
'label' => 'Protocol / User Guide',
|
||||
'id' => 'dsDataFile2',
|
||||
'name' => 'data_file2',
|
||||
'column' => 'dspsds_filename2',
|
||||
'help' => 'Upload a protocol or user guide (PDF, DOCX, etc.)',
|
||||
],
|
||||
[
|
||||
'label' => 'Other Supporting Document',
|
||||
'id' => 'dsDataFile3',
|
||||
'name' => 'data_file3',
|
||||
'column' => 'dspsds_filename3',
|
||||
'help' => 'Optional additional document.',
|
||||
],
|
||||
];
|
||||
?>
|
||||
|
||||
<?php foreach ($fileInputsConfig as $config): ?>
|
||||
<?php $existingFile = $datasource_data[$config['column']] ?? ''; ?>
|
||||
<div class="mb-3">
|
||||
<label for="<?= htmlspecialchars($config['id']) ?>" class="form-label"><?= htmlspecialchars($config['label']) ?></label>
|
||||
<input type="file" class="form-control rounded" id="<?= htmlspecialchars($config['id']) ?>" name="<?= htmlspecialchars($config['name']) ?>">
|
||||
<?php if ($config['column'] === 'dspsds_filename'): ?>
|
||||
<small class="form-text text-muted">
|
||||
Allowed formats: <span id="primaryFileFormats"><?= htmlspecialchars($initialPrimaryDescription) ?></span>
|
||||
</small>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($existingFile)): ?>
|
||||
<?php
|
||||
$isUrl = preg_match('/^https?:\/\//i', $existingFile) === 1;
|
||||
$linkTarget = $isUrl ? $existingFile : $uploadsWebPath . rawurlencode($existingFile);
|
||||
$linkLabel = $isUrl ? 'View Link' : 'Download File';
|
||||
?>
|
||||
<small class="form-text text-muted mt-2">
|
||||
Current file: <a href="<?= htmlspecialchars($linkTarget) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($linkLabel) ?></a>
|
||||
</small>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="remove_<?= htmlspecialchars($config['id']) ?>" name="remove_files[]" value="<?= htmlspecialchars($config['column']) ?>">
|
||||
<label class="form-check-label" for="remove_<?= htmlspecialchars($config['id']) ?>">Remove existing file</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($config['help'])): ?>
|
||||
<small class="form-text text-muted"><?= htmlspecialchars($config['help']) ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="mb-3">
|
||||
<label for="dsPublicDate" class="form-label">Public Date (Optional)</label>
|
||||
<input type="date" class="form-control rounded" id="dsPublicDate" name="public_date" value="<?= htmlspecialchars($datasource_data['dspsds_public_date'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsStatus" class="form-label">Status</label>
|
||||
<select class="form-select rounded" id="dsStatus" name="status" required>
|
||||
<option value="Pending Review" <?= (isset($datasource_data['dspsds_status']) && $datasource_data['dspsds_status'] == 'Pending Review') ? 'selected' : '' ?>>Pending Review</option>
|
||||
<option value="Active" <?= (isset($datasource_data['dspsds_status']) && $datasource_data['dspsds_status'] == 'Active') ? 'selected' : '' ?>>Active</option>
|
||||
<option value="Inactive" <?= (isset($datasource_data['dspsds_status']) && $datasource_data['dspsds_status'] == 'Inactive') ? 'selected' : '' ?>>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="manage_my_datasources.php" class="btn btn-secondary rounded">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary rounded"><?= ($action === 'add' ? 'Add' : 'Update') ?> Data Source</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0">My Data Sources</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($my_data_sources)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title (EN)</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Data Source</th>
|
||||
<th>Questionnaire</th>
|
||||
<th>User Guide</th>
|
||||
<th>Supporting Doc</th>
|
||||
<th>Registered Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($my_data_sources as $ds): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($ds['pkdspsds_id']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['data_type_name']) ?></td> <!-- Corrected key -->
|
||||
<td><?= htmlspecialchars($ds['category_name']) ?></td> <!-- Corrected key -->
|
||||
<td>
|
||||
<span class="badge <?= ($ds['dspsds_status'] == 'Active' ? 'bg-success' : ($ds['dspsds_status'] == 'Pending Review' ? 'bg-warning' : 'bg-secondary')) ?>">
|
||||
<?= htmlspecialchars($ds['dspsds_status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php
|
||||
$primaryFile = $ds['dspsds_filename'] ?? '';
|
||||
echo '<td>';
|
||||
if (!empty($primaryFile)) {
|
||||
$isUrlPrimary = preg_match('/^https?:\/\//i', $primaryFile) === 1;
|
||||
$primaryTarget = $isUrlPrimary ? $primaryFile : $uploadsWebPath . rawurlencode($primaryFile);
|
||||
$primaryTitle = $isUrlPrimary ? 'External link' : 'Download Data Source';
|
||||
echo '<a class="btn btn-sm btn-outline-primary rounded-pill" href="' . htmlspecialchars($primaryTarget) . '" target="_blank" rel="noopener" title="' . htmlspecialchars($primaryTitle) . '"><i class="fas fa-database"></i></a>';
|
||||
} else {
|
||||
echo '<span class="text-muted">—</span>';
|
||||
}
|
||||
echo '</td>';
|
||||
?>
|
||||
<?php
|
||||
$fileCells = [
|
||||
'dspsds_filename1' => 'Questionnaire / Data Dictionary',
|
||||
'dspsds_filename2' => 'Protocol / User Guide',
|
||||
'dspsds_filename3' => 'Other Supporting Document',
|
||||
];
|
||||
foreach ($fileCells as $column => $label) {
|
||||
$fileName = $ds[$column] ?? '';
|
||||
echo '<td>';
|
||||
if (!empty($fileName)) {
|
||||
$isUrl = preg_match('/^https?:\/\//i', $fileName) === 1;
|
||||
$linkTarget = $isUrl ? $fileName : $uploadsWebPath . rawurlencode($fileName);
|
||||
$titleAttr = $isUrl ? 'External link' : 'Download ' . $label;
|
||||
echo '<a class="btn btn-sm btn-outline-primary rounded-pill" href="' . htmlspecialchars($linkTarget) . '" target="_blank" rel="noopener" title="' . htmlspecialchars($titleAttr) . '"><i class="fas fa-paperclip"></i></a>';
|
||||
} else {
|
||||
echo '<span class="text-muted">—</span>';
|
||||
}
|
||||
echo '</td>';
|
||||
}
|
||||
?>
|
||||
<td><?= date('Y-m-d H:i', strtotime($ds['dspsds_reg_datetime'])) ?></td>
|
||||
<td>
|
||||
<a href="manage_my_datasources.php?action=edit&id=<?= htmlspecialchars($ds['pkdspsds_id']) ?>" class="btn btn-sm btn-info rounded me-1" title="Edit"><i class="fas fa-edit"></i></a>
|
||||
<a href="manage_my_datasources.php?action=delete&id=<?= htmlspecialchars($ds['pkdspsds_id']) ?>" class="btn btn-sm btn-danger rounded" title="Delete" onclick="return confirm('Are you sure you want to delete this data source? This will also delete related permissions and usage logs.');"><i class="fas fa-trash-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">You have not added any data sources yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_contributor.php");
|
||||
?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var typeSelect = document.getElementById('dsType');
|
||||
var fileInput = document.getElementById('dsDataFile');
|
||||
var formatsLabel = document.getElementById('primaryFileFormats');
|
||||
var primaryRules = <?php echo json_encode($primaryRulesMap, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
var defaultRule = <?php echo json_encode([
|
||||
'accept' => $defaultPrimaryAccept,
|
||||
'description' => $defaultPrimaryRules['description'] ?? 'CSV, JSON, PDF, XLS, XLSX',
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
function applyPrimaryFileRules() {
|
||||
if (!fileInput) {
|
||||
return;
|
||||
}
|
||||
var selected = typeSelect ? typeSelect.value : null;
|
||||
var rule = (selected && primaryRules[selected]) ? primaryRules[selected] : defaultRule;
|
||||
if (rule && Array.isArray(rule.accept) && rule.accept.length > 0) {
|
||||
fileInput.setAttribute('accept', rule.accept.join(','));
|
||||
} else {
|
||||
fileInput.removeAttribute('accept');
|
||||
}
|
||||
if (formatsLabel && rule && rule.description) {
|
||||
formatsLabel.textContent = rule.description;
|
||||
}
|
||||
}
|
||||
|
||||
applyPrimaryFileRules();
|
||||
if (typeSelect) {
|
||||
typeSelect.addEventListener('change', applyPrimaryFileRules);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
252
data_hybrid/manage_permissions.php
Normal file
252
data_hybrid/manage_permissions.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
// data_owner/manage_permissions.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php'; // For permission methods
|
||||
|
||||
// Ensure only Data Owners can access this page
|
||||
redirect_if_not_role('Data Contributor');
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$owner_person_id = $_SESSION['person_id'];
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$permission_id = $_GET['id'] ?? null;
|
||||
|
||||
// Handle form submissions for updating permission status
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action_type']) && $_POST['action_type'] === 'update_permission') {
|
||||
$permission_id_to_update = filter_var($_POST['permission_id'], FILTER_SANITIZE_NUMBER_INT);
|
||||
$new_status = trim($_POST['new_status']);
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
|
||||
// Basic validation
|
||||
if (empty($permission_id_to_update) || !in_array($new_status, ['Approved', 'Rejected', 'Revoked'])) {
|
||||
set_message("Invalid request to update permission.", "danger");
|
||||
header("Location: manage_permissions.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// You might want to add a check here to ensure the data owner is indeed the owner of the data source
|
||||
// related to this permission_id, to prevent tampering.
|
||||
// This would involve fetching the permission request and then checking the data source's fkisp_id_of.
|
||||
|
||||
if ($data_source_manager->updatePermissionStatus($permission_id_to_update, $new_status, $user_id, $notes)) {
|
||||
set_message("Permission request updated successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to update permission request.", "danger");
|
||||
}
|
||||
header("Location: manage_permissions.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$pending_requests = $data_source_manager->getPermissionRequestsForOwner($owner_person_id, 'Pending');
|
||||
$all_requests = $data_source_manager->getPermissionRequestsForOwner($owner_person_id); // All statuses
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_contributor.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_contributor.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> Permissions</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3 class="mb-3">Pending Requests</h3>
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-body">
|
||||
<?php if (!empty($pending_requests)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Data Source</th>
|
||||
<th>Requested By</th>
|
||||
<th>Permission Type</th>
|
||||
<th>Requested Date</th>
|
||||
<th>Notes</th>
|
||||
<th>Proof</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pending_requests as $req): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($req['pkdspsdsp_id']) ?></td>
|
||||
<td><?= htmlspecialchars($req['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?></td>
|
||||
<td><span class="badge bg-info"><?= htmlspecialchars($req['dspsdsp_permission']) ?></span></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$notes = $req['dspsdsp_notes'] ?? '';
|
||||
echo $notes !== ''
|
||||
? nl2br(htmlspecialchars($notes))
|
||||
: '<span class="text-muted">—</span>';
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($req['dspsdsp_proof_path'])): ?>
|
||||
<?php
|
||||
$proofPath = $req['dspsdsp_proof_path'];
|
||||
$isExternal = preg_match('/^https?:\\/\\//i', $proofPath) === 1;
|
||||
$cleanPath = ltrim($proofPath, '/');
|
||||
$linkTarget = $isExternal ? $proofPath : '../uploads/' . $cleanPath;
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($linkTarget) ?>" class="btn btn-sm btn-outline-primary rounded-pill" target="_blank" rel="noopener">
|
||||
<i class="fas fa-file-pdf me-1"></i> View
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<form action="manage_permissions.php" method="POST" class="d-inline">
|
||||
<input type="hidden" name="action_type" value="update_permission">
|
||||
<input type="hidden" name="permission_id" value="<?= htmlspecialchars($req['pkdspsdsp_id']) ?>">
|
||||
<input type="hidden" name="new_status" value="Approved">
|
||||
<button type="submit" class="btn btn-sm btn-success rounded me-1" title="Approve">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="manage_permissions.php" method="POST" class="d-inline">
|
||||
<input type="hidden" name="action_type" value="update_permission">
|
||||
<input type="hidden" name="permission_id" value="<?= htmlspecialchars($req['pkdspsdsp_id']) ?>">
|
||||
<input type="hidden" name="new_status" value="Rejected">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded" title="Reject">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">No pending permission requests.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-3 mt-5">All Permission Requests</h3>
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<?php if (!empty($all_requests)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Data Source</th>
|
||||
<th>Requested By</th>
|
||||
<th>Permission Type</th>
|
||||
<th>Status</th>
|
||||
<th>Requested Date</th>
|
||||
<th>Notes</th>
|
||||
<th>Proof</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($all_requests as $req): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($req['pkdspsdsp_id']) ?></td>
|
||||
<td><?= htmlspecialchars($req['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?></td>
|
||||
<td><span class="badge bg-info"><?= htmlspecialchars($req['dspsdsp_permission']) ?></span></td>
|
||||
<td>
|
||||
<span class="badge <?= ($req['dspsdsp_status'] == 'Approved' ? 'bg-success' : ($req['dspsdsp_status'] == 'Pending' ? 'bg-warning' : 'bg-danger')) ?>">
|
||||
<?= htmlspecialchars($req['dspsdsp_status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$notes = $req['dspsdsp_notes'] ?? '';
|
||||
echo $notes !== ''
|
||||
? nl2br(htmlspecialchars($notes))
|
||||
: '<span class="text-muted">—</span>';
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($req['dspsdsp_proof_path'])): ?>
|
||||
<?php
|
||||
$proofPath = $req['dspsdsp_proof_path'];
|
||||
$isExternal = preg_match('/^https?:\\/\\//i', $proofPath) === 1;
|
||||
$cleanPath = ltrim($proofPath, '/');
|
||||
$linkTarget = $isExternal ? $proofPath : '../uploads/' . $cleanPath;
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($linkTarget) ?>" class="btn btn-sm btn-outline-primary rounded-pill" target="_blank" rel="noopener">
|
||||
<i class="fas fa-file-pdf me-1"></i> View
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($req['dspsdsp_status'] == 'Approved'): ?>
|
||||
<form action="manage_permissions.php" method="POST" class="d-inline">
|
||||
<input type="hidden" name="action_type" value="update_permission">
|
||||
<input type="hidden" name="permission_id" value="<?= htmlspecialchars($req['pkdspsdsp_id']) ?>">
|
||||
<input type="hidden" name="new_status" value="Revoked">
|
||||
<button type="submit" class="btn btn-sm btn-secondary rounded" title="Revoke" onclick="return confirm('Are you sure you want to revoke this permission?');">
|
||||
<i class="fas fa-ban"></i> Revoke
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">No action</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">No permission requests found.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_contributor.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
134
data_hybrid/my_downloads.php
Normal file
134
data_hybrid/my_downloads.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
// data_user/my_downloads.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
|
||||
// Ensure only Data Users can access this page
|
||||
redirect_if_not_role('Data Contributor');
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id']; // This is the correct ID to use for the join
|
||||
$username = $_SESSION['username'];
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
|
||||
// Fetch download history for the current user
|
||||
$download_history = [];
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT dsu.*,
|
||||
ds.dspsds_title_en,
|
||||
ds.dspsds_filename,
|
||||
tds.dspstds_name_en
|
||||
FROM dsps_tbl_datasource_used dsu
|
||||
JOIN dsps_tbl_datasource ds
|
||||
ON dsu.fkdspsdsused_id = ds.pkdspsds_id
|
||||
JOIN dsps_tbl_typedatasource tds
|
||||
ON ds.fkdspstds_id = tds.pkdspstds_id
|
||||
WHERE dsu.fkisp_id_of = :person_id
|
||||
AND dsu.dspsdspused_action = 'Downloaded'
|
||||
ORDER BY dsu.dspsdspused_datetime DESC
|
||||
");
|
||||
$stmt->execute(['person_id' => $person_id]);
|
||||
$download_history = $stmt->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching download history: " . $e->getMessage());
|
||||
set_message("Error fetching download history.", "danger");
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_contributor.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_contributor.php");
|
||||
?>
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> My Downloads History</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0">Downloaded Data Sources</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($download_history)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data Source</th>
|
||||
<th>Type</th>
|
||||
<th>Download Date</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($download_history as $item): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($item['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($item['dspstds_name_en']) ?></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($item['dspsdspused_datetime'])) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($item['dspsds_filename'])): ?>
|
||||
<!--
|
||||
This is the CORRECTED line. It now points to download.php
|
||||
and passes the ID of the data source.
|
||||
-->
|
||||
<a href="download.php?dspsds_id=<?= htmlspecialchars($item['fkdspsdsused_id']) ?>" class="btn btn-sm btn-outline-primary text-dark badge">
|
||||
<i class="fas fa-download me-1"></i> Download Again
|
||||
</a>
|
||||
<?php else: ?>
|
||||
N/A (API or no direct file)
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">You have not downloaded any data sources yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_contributor.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
140
data_hybrid/my_permissions.php
Normal file
140
data_hybrid/my_permissions.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
// data_user/my_permissions.php
|
||||
// This page shows the user a list of all their data access requests and their statuses.
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
// We'll assume these files exist based on your original code
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Permission.php';
|
||||
require_once '../classes/User.php';
|
||||
|
||||
// Redirect if not logged in
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: ../index.php?page=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id'];
|
||||
$username = $_SESSION['username'];
|
||||
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
// Instantiate classes
|
||||
$permissionManager = new Permission($pdo);
|
||||
$userManager = new User($pdo);
|
||||
|
||||
// Get user details
|
||||
$currentUserDetails = $userManager->getUserDetails($user_id);
|
||||
|
||||
// Fetch all permission requests for the logged-in user
|
||||
$permissionRequests = [];
|
||||
try {
|
||||
$permissionRequests = $permissionManager->getPermissionsByPersonId($person_id);
|
||||
} catch (Exception $e) {
|
||||
set_message('Error retrieving permission requests: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_contributor.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_contributor.php");
|
||||
?>
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> My Permissions</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded-lg">
|
||||
<div class="card-body p-4">
|
||||
<?php if (!empty($permissionRequests)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-borderless align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">Data Source</th>
|
||||
<th scope="col">Requested For</th>
|
||||
<th scope="col">Date Submitted</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($permissionRequests as $request): ?>
|
||||
<tr>
|
||||
<td class="fw-bold"><?= htmlspecialchars($request['ds_title']) ?></td>
|
||||
<td><?= htmlspecialchars($request['dspspr_permission_type']) ?></td>
|
||||
<td><?= htmlspecialchars(date('M d, Y', strtotime($request['dspspr_request_date']))) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$status_class = '';
|
||||
switch ($request['dspspr_status']) {
|
||||
case 'Approved':
|
||||
$status_class = 'bg-success badge text-white';
|
||||
break;
|
||||
case 'Pending':
|
||||
$status_class = 'bg-warning badge text-dark';
|
||||
break;
|
||||
case 'Denied':
|
||||
$status_class = 'bg-danger badge text-white';
|
||||
break;
|
||||
default:
|
||||
$status_class = 'bg-secondary badge text-white';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<span class="status-badge <?= $status_class ?>"><?= htmlspecialchars($request['dspspr_status']) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info text-center rounded m-0">
|
||||
You have not submitted any permission requests yet. <a href="browse_datasources.php" class="alert-link">Browse data sources</a> to get started.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_user.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
162
data_hybrid/r_in_jupyter.php
Normal file
162
data_hybrid/r_in_jupyter.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
// data_hybrid/r_in_jupyter.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
require_once '../includes/jupyter_helpers.php';
|
||||
|
||||
redirect_if_not_role('Data Contributor');
|
||||
|
||||
$hasRJupyterAccess = has_r_access();
|
||||
$workspaceSync = ['synced' => [], 'missing' => [], 'workspace_dir' => null];
|
||||
$workspaceRelativeDir = null;
|
||||
$workspaceError = null;
|
||||
|
||||
if ($hasRJupyterAccess && isset($_SESSION['person_id'])) {
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
try {
|
||||
$workspaceSync = $dataSourceManager->prepareJupyterWorkspace(
|
||||
(int) $_SESSION['person_id'],
|
||||
dirname(__DIR__) . '/uploads/jupyter_workspace'
|
||||
);
|
||||
$workspaceRelativeDir = 'datasources/user_' . (int) $_SESSION['person_id'];
|
||||
} catch (Exception $e) {
|
||||
$workspaceError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$jupyterBaseUrl = dsp_jupyter_base_url();
|
||||
$jupyterToken = dsp_jupyter_token();
|
||||
$jupyterIframeUrl = dsp_jupyter_iframe_url(
|
||||
$jupyterBaseUrl,
|
||||
$jupyterToken,
|
||||
isset($_SESSION['person_id']) ? (int) $_SESSION['person_id'] : null
|
||||
);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php include_once("../includes/header_contributor.php"); ?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include_once("../includes/nav_contributor.php"); ?>
|
||||
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">R in JupyterHub</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message'], $_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-header bg-light rounded-top d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">Collaborative R Workspace</h5>
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<span class="badge bg-success-subtle text-success rounded-pill"><i class="fas fa-flask me-1"></i> Enabled</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning-subtle text-warning rounded-pill"><i class="fas fa-lock me-1"></i> Disabled</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<?php if ($workspaceError): ?>
|
||||
<div class="alert alert-danger rounded mb-3">
|
||||
<strong>Workspace error:</strong> <?= htmlspecialchars($workspaceError) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="mb-3">
|
||||
Only Approved data sources are copied into
|
||||
<code><?= htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?></code>
|
||||
inside Jupyter. Use these files when collaborating with Data Owners.
|
||||
</p>
|
||||
<?php if (!empty($workspaceSync['synced'])): ?>
|
||||
<div class="table-responsive mb-3">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Data Source</th>
|
||||
<th>Data Type</th>
|
||||
<th>Category</th>
|
||||
<th>Filename</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($workspaceSync['synced'] as $idx => $syncedItem): ?>
|
||||
<tr>
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['title']) ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['category'] ?? 'N/A') ?></td>
|
||||
<td><code><?= htmlspecialchars(basename($syncedItem['relative_path'])) ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-3">
|
||||
You do not have any Approved data sources yet. Once your requests are approved, refresh this page.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($workspaceSync['missing'])): ?>
|
||||
<div class="alert alert-warning rounded mb-3">
|
||||
<strong>Some items were skipped:</strong>
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($workspaceSync['missing'] as $missingItem): ?>
|
||||
<li><?= htmlspecialchars($missingItem['title']) ?> — <?= htmlspecialchars($missingItem['reason']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<p class="mb-3">
|
||||
Launch the embedded JupyterLab environment to build notebooks, explore shared datasets, and collaborate with Data Owners.
|
||||
Use the <em>Files</em> panel to open existing work or create a new R Notebook from the launcher.
|
||||
</p>
|
||||
<div class="ratio ratio-16x9 border rounded overflow-hidden">
|
||||
<iframe
|
||||
src="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>"
|
||||
title="R in JupyterHub"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
></iframe>
|
||||
</div>
|
||||
<p class="mt-3 mb-0">
|
||||
Prefer a dedicated window? <a href="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">Open Jupyter in a new tab</a>.
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning rounded d-flex align-items-start gap-3">
|
||||
<i class="fas fa-circle-info mt-1"></i>
|
||||
<div>
|
||||
<strong>R in JupyterHub is currently disabled for your account.</strong><br>
|
||||
Contact a DAC Staff administrator to enable R/Jupyter access so you can analyse data directly from this workspace.
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
Once access is enabled, refresh this page to launch the JupyterLab environment.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include_once("../includes/footer_contributor.php"); ?>
|
||||
</body>
|
||||
</html>
|
||||
214
data_owner/dashboard.php
Normal file
214
data_owner/dashboard.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
// data_owner/dashboard.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/User.php';
|
||||
require_once '../classes/DataSource.php'; // For counts
|
||||
|
||||
// Ensure only Data Owners can access this dashboard
|
||||
redirect_if_not_role('Data Owner');
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id']; // The fkisp_id_of for the data owner
|
||||
$username = $_SESSION['username'];
|
||||
$user_status = $_SESSION['user_status'];
|
||||
|
||||
// Instantiate classes
|
||||
$user_manager = new User($pdo);
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
|
||||
// Get dynamic counts for the Data Owner
|
||||
$my_data_sources_count = count($data_source_manager->getDataSources($person_id));
|
||||
$pending_permissions_count = count($data_source_manager->getPermissionRequestsForOwner($person_id, 'Pending'));
|
||||
$usageByUser = $data_source_manager->getUsageByUserForOwner($person_id, 6);
|
||||
$data_accesses_last_30_days = 0;
|
||||
if (!empty($usageByUser)) {
|
||||
foreach ($usageByUser as $row) {
|
||||
$data_accesses_last_30_days += (int) ($row['usage_count'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_owner.php");
|
||||
?>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_owner.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> Dashboard</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary"><i class="fas fa-database me-2"></i> My Data Sources</h5>
|
||||
<p class="card-text fs-2"><?= $my_data_sources_count ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning"><i class="fas fa-hourglass-half me-2"></i> Pending Permissions</h5>
|
||||
<p class="card-text fs-2"><?= $pending_permissions_count ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success"><i class="fas fa-download me-2"></i> Data Accesses (Last 30 Days)</h5>
|
||||
<p class="card-text fs-2"><?= $data_accesses_last_30_days ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$chartLabels = [];
|
||||
$chartValues = [];
|
||||
if (!empty($usageByUser)) {
|
||||
foreach ($usageByUser as $row) {
|
||||
$chartLabels[] = $row['username'] ?? 'Unknown';
|
||||
$chartValues[] = (int) ($row['usage_count'] ?? 0);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm rounded h-100">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0">Usage Breakdown by User</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($chartValues)): ?>
|
||||
<canvas id="usagePieChart" height="280"></canvas>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">No data usage recorded yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<h3>Quick Actions</h3>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a href="manage_my_datasources.php?action=add" class="btn btn-primary rounded"><i class="fas fa-plus-circle me-2"></i> Add New Data Source</a>
|
||||
<a href="manage_permissions.php" class="btn btn-warning rounded"><i class="fas fa-user-check me-2"></i> Review Permissions</a>
|
||||
<a href="my_analytics.php" class="btn btn-info rounded"><i class="fas fa-chart-pie me-2"></i> View My Data Usage</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder for recent activity related to their data sources -->
|
||||
<div class="mt-5">
|
||||
<h3>Recent Activity on My Data Sources</h3>
|
||||
<ul class="list-group shadow-sm rounded">
|
||||
<?php
|
||||
// Example recent activities (replace with actual data from dsps_tbl_datasource_used and dsps_tbl_anonymous)
|
||||
// You'd need to fetch these from the database, potentially joining with dsps_tbl_datasource
|
||||
$recent_activities = [
|
||||
['text' => 'User John Doe requested access to \'Population Census 2023\'.', 'time' => 'Just now', 'type' => 'info'],
|
||||
['text' => '\'Health Data Q1 2024\' was downloaded 5 times today.', 'time' => '2 hours ago', 'type' => 'success'],
|
||||
['text' => 'You updated \'Education Statistics 2022\'.', 'time' => 'Yesterday', 'type' => 'secondary'],
|
||||
];
|
||||
foreach ($recent_activities as $activity) {
|
||||
echo '<li class="list-group-item d-flex justify-content-between align-items-center">';
|
||||
echo htmlspecialchars($activity['text']);
|
||||
echo '<span class="badge bg-' . htmlspecialchars($activity['type']) . '">' . htmlspecialchars($activity['time']) . '</span>';
|
||||
echo '</li>';
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_owner.php");
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js" integrity="sha256-VjZ1tcHTul3e8DqRL3OjaxAg/P070MqxsVXni4eWh05=" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const labels = <?= json_encode(array_map('htmlspecialchars', $chartLabels)) ?>;
|
||||
const values = <?= json_encode($chartValues) ?>;
|
||||
|
||||
if (!Array.isArray(labels) || !Array.isArray(values) || labels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('usagePieChart');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundColors = [
|
||||
'#0d6efd', '#198754', '#dc3545', '#fd7e14', '#6f42c1', '#20c997', '#0dcaf0'
|
||||
];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: values,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
return `${label}: ${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
542
data_owner/manage_my_datasources.php
Normal file
542
data_owner/manage_my_datasources.php
Normal file
@@ -0,0 +1,542 @@
|
||||
<?php
|
||||
// data_owner/manage_my_datasources.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
|
||||
// Ensure only Data Owners can access this page
|
||||
redirect_if_not_role('Data Owner');
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$owner_person_id = $_SESSION['person_id'];
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$ds_id = $_GET['id'] ?? null;
|
||||
$datasource_data = [];
|
||||
|
||||
// Fetch dropdown data
|
||||
$data_types = $data_source_manager->getAllDataTypes();
|
||||
$categories = $data_source_manager->getAllCategories();
|
||||
$primaryRulesMap = [];
|
||||
foreach ($data_types as $type) {
|
||||
$typeName = $type['dspstds_name_en'] ?? null;
|
||||
$rules = $data_source_manager->getPrimaryFileRulesForType($typeName);
|
||||
$acceptList = [];
|
||||
foreach ($rules['extensions'] ?? [] as $ext) {
|
||||
$acceptList[] = '.' . strtolower($ext);
|
||||
}
|
||||
$primaryRulesMap[$type['pkdspstds_id']] = [
|
||||
'accept' => $acceptList,
|
||||
'description' => $rules['description'] ?? 'CSV, JSON, PDF, XLS, XLSX',
|
||||
];
|
||||
}
|
||||
$defaultPrimaryRules = $data_source_manager->getPrimaryFileRulesForType(null);
|
||||
$defaultPrimaryAccept = [];
|
||||
foreach ($defaultPrimaryRules['extensions'] ?? [] as $ext) {
|
||||
$defaultPrimaryAccept[] = '.' . strtolower($ext);
|
||||
}
|
||||
$initialPrimaryDescription = $defaultPrimaryRules['description'] ?? 'CSV, JSON, PDF, XLS, XLSX';
|
||||
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title_en = trim($_POST['title_en'] ?? '');
|
||||
$title_kh = trim($_POST['title_kh'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$type_id = filter_var($_POST['type_id'] ?? '', FILTER_SANITIZE_NUMBER_INT);
|
||||
$category_id = filter_var($_POST['category_id'] ?? '', FILTER_SANITIZE_NUMBER_INT);
|
||||
$public_date = trim($_POST['public_date'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'Pending Review');
|
||||
$selectedDataType = null;
|
||||
if (!empty($type_id)) {
|
||||
$selectedDataType = $data_source_manager->getDataTypeById((int)$type_id);
|
||||
}
|
||||
$current_files = [
|
||||
'dspsds_filename' => trim($_POST['current_filename'] ?? ''),
|
||||
'dspsds_filename1' => trim($_POST['current_filename1'] ?? ''),
|
||||
'dspsds_filename2' => trim($_POST['current_filename2'] ?? ''),
|
||||
'dspsds_filename3' => trim($_POST['current_filename3'] ?? ''),
|
||||
];
|
||||
|
||||
$final_files = $current_files;
|
||||
$file_inputs = [
|
||||
'dspsds_filename' => 'data_file',
|
||||
'dspsds_filename1' => 'data_file1',
|
||||
'dspsds_filename2' => 'data_file2',
|
||||
'dspsds_filename3' => 'data_file3',
|
||||
];
|
||||
$file_labels = [
|
||||
'dspsds_filename' => 'Primary Data File',
|
||||
'dspsds_filename1' => 'Questionnaire / Data Dictionary',
|
||||
'dspsds_filename2' => 'Protocol / User Guide',
|
||||
'dspsds_filename3' => 'Other Supporting Document',
|
||||
];
|
||||
$remove_files = $_POST['remove_files'] ?? [];
|
||||
if (!is_array($remove_files)) {
|
||||
$remove_files = [$remove_files];
|
||||
}
|
||||
|
||||
foreach ($file_inputs as $column => $inputName) {
|
||||
if (!isset($_FILES[$inputName]) || $_FILES[$inputName]['error'] === UPLOAD_ERR_NO_FILE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($_FILES[$inputName]['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception('Upload error code: ' . $_FILES[$inputName]['error']);
|
||||
}
|
||||
$fileRules = null;
|
||||
if ($column === 'dspsds_filename') {
|
||||
$fileRules = $data_source_manager->getPrimaryFileRulesForType($selectedDataType['dspstds_name_en'] ?? null);
|
||||
}
|
||||
$uploadedName = $data_source_manager->handleDataSourceFileUpload($_FILES[$inputName], $fileRules);
|
||||
if ($uploadedName) {
|
||||
if (!empty($current_files[$column]) && $current_files[$column] !== $uploadedName) {
|
||||
$oldPath = $data_source_manager->getUploadDir() . $current_files[$column];
|
||||
if (is_file($oldPath)) {
|
||||
unlink($oldPath);
|
||||
}
|
||||
}
|
||||
$final_files[$column] = $uploadedName;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$friendlyLabel = $file_labels[$column] ?? $inputName;
|
||||
set_message('File upload failed for ' . htmlspecialchars($friendlyLabel) . ': ' . $e->getMessage(), 'danger');
|
||||
$final_files[$column] = $current_files[$column];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($remove_files as $column) {
|
||||
if (!array_key_exists($column, $final_files)) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($current_files[$column])) {
|
||||
$oldPath = $data_source_manager->getUploadDir() . $current_files[$column];
|
||||
if (is_file($oldPath)) {
|
||||
unlink($oldPath);
|
||||
}
|
||||
}
|
||||
$final_files[$column] = '';
|
||||
}
|
||||
|
||||
// Basic validation for required fields
|
||||
if (empty($title_en) || empty($type_id) || empty($category_id)) {
|
||||
set_message("Title, Data Type, and Category are required.", "danger");
|
||||
// Redirect to preserve form data or re-display form with errors
|
||||
// For now, we'll just redirect to list, but a better UX would be to stay on the form
|
||||
header("Location: manage_my_datasources.php?action=" . ($action === 'add_submit' ? 'add' : 'edit&id=' . $ds_id));
|
||||
exit();
|
||||
}
|
||||
|
||||
// Determine the public date to pass to the add/update methods
|
||||
// The DataSource class's add/update methods have logic for this, so we'll pass it as a string or null
|
||||
$final_public_date = (!empty($public_date) && $status === 'Active') ? $public_date : null;
|
||||
|
||||
|
||||
if ($action === 'add_submit') {
|
||||
try {
|
||||
// Corrected call to addDataSource
|
||||
if ($data_source_manager->addDataSource(
|
||||
$type_id,
|
||||
$category_id,
|
||||
$owner_person_id, // Data owner is the logged-in person
|
||||
$final_files['dspsds_filename'],
|
||||
$title_en,
|
||||
$title_kh,
|
||||
$description,
|
||||
$status,
|
||||
$user_id, // User who registered it (logged-in user)
|
||||
$final_files['dspsds_filename1'],
|
||||
$final_files['dspsds_filename2'],
|
||||
$final_files['dspsds_filename3']
|
||||
)) {
|
||||
set_message("Data source added successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to add data source.", "danger");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message("Error adding data source: " . $e->getMessage(), "danger");
|
||||
}
|
||||
} elseif ($action === 'edit_submit' && $ds_id) {
|
||||
try {
|
||||
// Corrected call to updateDataSource
|
||||
if ($data_source_manager->updateDataSource(
|
||||
$ds_id,
|
||||
$type_id,
|
||||
$category_id,
|
||||
$owner_person_id, // Data owner is the logged-in person
|
||||
$final_files['dspsds_filename'],
|
||||
$title_en,
|
||||
$title_kh,
|
||||
$description,
|
||||
$status,
|
||||
$user_id, // User who modified it (logged-in user)
|
||||
$final_files['dspsds_filename1'],
|
||||
$final_files['dspsds_filename2'],
|
||||
$final_files['dspsds_filename3']
|
||||
)) {
|
||||
set_message("Data source updated successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to update data source.", "danger");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
set_message("Error updating data source: " . $e->getMessage(), "danger");
|
||||
}
|
||||
}
|
||||
// Redirect after POST to prevent form resubmission
|
||||
header("Location: manage_my_datasources.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Handle GET actions
|
||||
if ($action === 'edit' && $ds_id) {
|
||||
$datasource_data = $data_source_manager->getDataSourceById($ds_id);
|
||||
// Crucial security check: Ensure the logged-in owner actually owns this data source
|
||||
if (!$datasource_data || $datasource_data['fkisp_id_of'] != $owner_person_id) {
|
||||
set_message("Data source not found or you don't have permission to edit it.", "danger");
|
||||
header("Location: manage_my_datasources.php");
|
||||
exit();
|
||||
}
|
||||
} elseif ($action === 'delete' && $ds_id) {
|
||||
$datasource = $data_source_manager->getDataSourceById($ds_id);
|
||||
// Crucial security check: Ensure the logged-in owner actually owns this data source
|
||||
if ($datasource && $datasource['fkisp_id_of'] == $owner_person_id) {
|
||||
// Delete associated file on the server
|
||||
$fileColumns = ['dspsds_filename', 'dspsds_filename1', 'dspsds_filename2', 'dspsds_filename3'];
|
||||
foreach ($fileColumns as $column) {
|
||||
if (!empty($datasource[$column])) {
|
||||
$filePath = $data_source_manager->getUploadDir() . $datasource[$column];
|
||||
if (is_file($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($data_source_manager->deleteDataSource($ds_id)) {
|
||||
set_message("Data source deleted successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to delete data source.", "danger");
|
||||
}
|
||||
} else {
|
||||
set_message("Data source not found or you don't have permission to delete it.", "warning");
|
||||
}
|
||||
header("Location: manage_my_datasources.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch data sources for the current owner for display
|
||||
$my_data_sources = $data_source_manager->getDataSources($owner_person_id);
|
||||
$uploadsWebPath = '../uploads/datasources/';
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_owner.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_owner.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> My Data Sources</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<a href="manage_my_datasources.php?action=add" class="btn btn-primary rounded">
|
||||
<i class="fas fa-plus-circle me-2"></i> Add New Data Source
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= htmlspecialchars($_SESSION['message']) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($action === 'add' || $action === 'edit'): ?>
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0"><?= ($action === 'add' ? 'Add New' : 'Edit') ?> Data Source</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="manage_my_datasources.php?action=<?= ($action === 'add' ? 'add_submit' : 'edit_submit&id=' . htmlspecialchars($ds_id)) ?>" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="current_filename" value="<?= htmlspecialchars($datasource_data['dspsds_filename'] ?? '') ?>">
|
||||
<input type="hidden" name="current_filename1" value="<?= htmlspecialchars($datasource_data['dspsds_filename1'] ?? '') ?>">
|
||||
<input type="hidden" name="current_filename2" value="<?= htmlspecialchars($datasource_data['dspsds_filename2'] ?? '') ?>">
|
||||
<input type="hidden" name="current_filename3" value="<?= htmlspecialchars($datasource_data['dspsds_filename3'] ?? '') ?>">
|
||||
<div class="mb-3">
|
||||
<label for="dsTitleEn" class="form-label">Title (English)</label>
|
||||
<input type="text" class="form-control rounded" id="dsTitleEn" name="title_en" value="<?= htmlspecialchars($datasource_data['dspsds_title_en'] ?? '') ?>" required>
|
||||
</div>
|
||||
<!--
|
||||
<div class="mb-3">
|
||||
<label for="dsTitleKh" class="form-label">Title (Khmer)</label>
|
||||
<input type="text" class="form-control rounded" id="dsTitleKh" name="title_kh" value="<?//= htmlspecialchars($datasource_data['dspsds_title_kh'] ?? '') ?>">
|
||||
</div>
|
||||
-->
|
||||
<div class="mb-3">
|
||||
<label for="dsDescription" class="form-label">Description</label>
|
||||
<textarea class="form-control rounded" id="dsDescription" name="description" rows="5"><?= htmlspecialchars($datasource_data['dspsds_description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsType" class="form-label">Data Type</label>
|
||||
<select class="form-select rounded" id="dsType" name="type_id" required>
|
||||
<option value="">Select Type...</option>
|
||||
<?php foreach ($data_types as $type): ?>
|
||||
<option value="<?= htmlspecialchars($type['pkdspstds_id']) ?>"
|
||||
<?= (isset($datasource_data['fkdspstds_id']) && $datasource_data['fkdspstds_id'] == $type['pkdspstds_id']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($type['dspstds_name_en']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsCategory" class="form-label">Category</label>
|
||||
<select class="form-select rounded" id="dsCategory" name="category_id" required>
|
||||
<option value="">Select Category...</option>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<option value="<?= htmlspecialchars($category['pkdspscate_id']) ?>"
|
||||
<?= (isset($datasource_data['fkdspscate_id']) && $datasource_data['fkdspscate_id'] == $category['pkdspscate_id']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($category['dspscate_title_en']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
$currentTypeForDescription = $datasource_data['fkdspstds_id'] ?? null;
|
||||
if ($currentTypeForDescription && isset($primaryRulesMap[$currentTypeForDescription])) {
|
||||
$initialPrimaryDescription = $primaryRulesMap[$currentTypeForDescription]['description'] ?? $initialPrimaryDescription;
|
||||
}
|
||||
$fileInputsConfig = [
|
||||
[
|
||||
'label' => 'Primary Data File',
|
||||
'id' => 'dsDataFile',
|
||||
'name' => 'data_file',
|
||||
'column' => 'dspsds_filename',
|
||||
'help' => 'Upload a file that matches the selected data type.',
|
||||
],
|
||||
[
|
||||
'label' => 'Questionnaire / Data Dictionary',
|
||||
'id' => 'dsDataFile1',
|
||||
'name' => 'data_file1',
|
||||
'column' => 'dspsds_filename1',
|
||||
'help' => 'Upload a supporting document (PDF, XLSX, etc.)',
|
||||
],
|
||||
[
|
||||
'label' => 'Protocol / User Guide',
|
||||
'id' => 'dsDataFile2',
|
||||
'name' => 'data_file2',
|
||||
'column' => 'dspsds_filename2',
|
||||
'help' => 'Upload a protocol or user guide (PDF, DOCX, etc.)',
|
||||
],
|
||||
[
|
||||
'label' => 'Other Supporting Document',
|
||||
'id' => 'dsDataFile3',
|
||||
'name' => 'data_file3',
|
||||
'column' => 'dspsds_filename3',
|
||||
'help' => 'Optional additional document.',
|
||||
],
|
||||
];
|
||||
?>
|
||||
|
||||
<?php foreach ($fileInputsConfig as $config): ?>
|
||||
<?php $existingFile = $datasource_data[$config['column']] ?? ''; ?>
|
||||
<div class="mb-3">
|
||||
<label for="<?= htmlspecialchars($config['id']) ?>" class="form-label"><?= htmlspecialchars($config['label']) ?></label>
|
||||
<input type="file" class="form-control rounded" id="<?= htmlspecialchars($config['id']) ?>" name="<?= htmlspecialchars($config['name']) ?>">
|
||||
<?php if ($config['column'] === 'dspsds_filename'): ?>
|
||||
<small class="form-text text-muted">
|
||||
Allowed formats: <span id="primaryFileFormats"><?= htmlspecialchars($initialPrimaryDescription) ?></span>
|
||||
</small>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($existingFile)): ?>
|
||||
<?php
|
||||
$isUrl = preg_match('/^https?:\/\//i', $existingFile) === 1;
|
||||
$linkTarget = $isUrl ? $existingFile : $uploadsWebPath . rawurlencode($existingFile);
|
||||
$linkLabel = $isUrl ? 'View Link' : 'Download File';
|
||||
?>
|
||||
<small class="form-text text-muted mt-2">
|
||||
Current file: <a href="<?= htmlspecialchars($linkTarget) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($linkLabel) ?></a>
|
||||
</small>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="remove_<?= htmlspecialchars($config['id']) ?>" name="remove_files[]" value="<?= htmlspecialchars($config['column']) ?>">
|
||||
<label class="form-check-label" for="remove_<?= htmlspecialchars($config['id']) ?>">Remove existing file</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($config['help'])): ?>
|
||||
<small class="form-text text-muted"><?= htmlspecialchars($config['help']) ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dsPublicDate" class="form-label">Public Date (Optional)</label>
|
||||
<input type="date" class="form-control rounded" id="dsPublicDate" name="public_date" value="<?= htmlspecialchars($datasource_data['dspsds_public_date'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dsStatus" class="form-label">Status</label>
|
||||
<select class="form-select rounded" id="dsStatus" name="status" required>
|
||||
<option value="Pending Review" <?= (isset($datasource_data['dspsds_status']) && $datasource_data['dspsds_status'] == 'Pending Review') ? 'selected' : '' ?>>Pending Review</option>
|
||||
<option value="Active" <?= (isset($datasource_data['dspsds_status']) && $datasource_data['dspsds_status'] == 'Active') ? 'selected' : '' ?>>Active</option>
|
||||
<option value="Inactive" <?= (isset($datasource_data['dspsds_status']) && $datasource_data['dspsds_status'] == 'Inactive') ? 'selected' : '' ?>>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="manage_my_datasources.php" class="btn btn-secondary rounded">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary rounded"><?= ($action === 'add' ? 'Add' : 'Update') ?> Data Source</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0">My Data Sources</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($my_data_sources)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title (EN)</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Data Source</th>
|
||||
<th>Questionnaire</th>
|
||||
<th>User Guide</th>
|
||||
<th>Supporting Doc</th>
|
||||
<th>Registered Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($my_data_sources as $ds): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($ds['pkdspsds_id']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($ds['data_type_name']) ?></td> <!-- Corrected key -->
|
||||
<td><?= htmlspecialchars($ds['category_name']) ?></td> <!-- Corrected key -->
|
||||
<td>
|
||||
<span class="badge <?= ($ds['dspsds_status'] == 'Active' ? 'bg-success' : ($ds['dspsds_status'] == 'Pending Review' ? 'bg-warning' : 'bg-secondary')) ?>">
|
||||
<?= htmlspecialchars($ds['dspsds_status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php
|
||||
$primaryFile = $ds['dspsds_filename'] ?? '';
|
||||
echo '<td>';
|
||||
if (!empty($primaryFile)) {
|
||||
$isUrlPrimary = preg_match('/^https?:\/\//i', $primaryFile) === 1;
|
||||
$primaryTarget = $isUrlPrimary ? $primaryFile : $uploadsWebPath . rawurlencode($primaryFile);
|
||||
$primaryTitle = $isUrlPrimary ? 'External link' : 'Download Data Source';
|
||||
echo '<a class="btn btn-sm btn-outline-primary rounded-pill" href="' . htmlspecialchars($primaryTarget) . '" target="_blank" rel="noopener" title="' . htmlspecialchars($primaryTitle) . '"><i class="fas fa-database"></i></a>';
|
||||
} else {
|
||||
echo '<span class="text-muted">—</span>';
|
||||
}
|
||||
echo '</td>';
|
||||
?>
|
||||
<?php
|
||||
$fileCells = [
|
||||
'dspsds_filename1' => 'Questionnaire / Data Dictionary',
|
||||
'dspsds_filename2' => 'Protocol / User Guide',
|
||||
'dspsds_filename3' => 'Other Supporting Document',
|
||||
];
|
||||
foreach ($fileCells as $column => $label) {
|
||||
$fileName = $ds[$column] ?? '';
|
||||
echo '<td>';
|
||||
if (!empty($fileName)) {
|
||||
$isUrl = preg_match('/^https?:\/\//i', $fileName) === 1;
|
||||
$linkTarget = $isUrl ? $fileName : $uploadsWebPath . rawurlencode($fileName);
|
||||
$titleAttr = $isUrl ? 'External link' : 'Download ' . $label;
|
||||
echo '<a class="btn btn-sm btn-outline-primary rounded-pill" href="' . htmlspecialchars($linkTarget) . '" target="_blank" rel="noopener" title="' . htmlspecialchars($titleAttr) . '"><i class="fas fa-paperclip"></i></a>';
|
||||
} else {
|
||||
echo '<span class="text-muted">—</span>';
|
||||
}
|
||||
echo '</td>';
|
||||
}
|
||||
?>
|
||||
<td><?= date('Y-m-d H:i', strtotime($ds['dspsds_reg_datetime'])) ?></td>
|
||||
<td>
|
||||
<a href="manage_my_datasources.php?action=edit&id=<?= htmlspecialchars($ds['pkdspsds_id']) ?>" class="btn btn-sm btn-info rounded me-1" title="Edit"><i class="fas fa-edit"></i></a>
|
||||
<a href="manage_my_datasources.php?action=delete&id=<?= htmlspecialchars($ds['pkdspsds_id']) ?>" class="btn btn-sm btn-danger rounded" title="Delete" onclick="return confirm('Are you sure you want to delete this data source? This will also delete related permissions and usage logs.');"><i class="fas fa-trash-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">You have not added any data sources yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_owner.php");
|
||||
?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var typeSelect = document.getElementById('dsType');
|
||||
var fileInput = document.getElementById('dsDataFile');
|
||||
var formatsLabel = document.getElementById('primaryFileFormats');
|
||||
var primaryRules = <?php echo json_encode($primaryRulesMap, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
var defaultRule = <?php echo json_encode([
|
||||
'accept' => $defaultPrimaryAccept,
|
||||
'description' => $defaultPrimaryRules['description'] ?? 'CSV, JSON, PDF, XLS, XLSX',
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
function applyPrimaryFileRules() {
|
||||
if (!fileInput) {
|
||||
return;
|
||||
}
|
||||
var selected = typeSelect ? typeSelect.value : null;
|
||||
var rule = (selected && primaryRules[selected]) ? primaryRules[selected] : defaultRule;
|
||||
if (rule && Array.isArray(rule.accept) && rule.accept.length > 0) {
|
||||
fileInput.setAttribute('accept', rule.accept.join(','));
|
||||
} else {
|
||||
fileInput.removeAttribute('accept');
|
||||
}
|
||||
if (formatsLabel && rule && rule.description) {
|
||||
formatsLabel.textContent = rule.description;
|
||||
}
|
||||
}
|
||||
|
||||
applyPrimaryFileRules();
|
||||
if (typeSelect) {
|
||||
typeSelect.addEventListener('change', applyPrimaryFileRules);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
252
data_owner/manage_permissions.php
Normal file
252
data_owner/manage_permissions.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
// data_owner/manage_permissions.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php'; // For permission methods
|
||||
|
||||
// Ensure only Data Owners can access this page
|
||||
redirect_if_not_role('Data Owner');
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$owner_person_id = $_SESSION['person_id'];
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$permission_id = $_GET['id'] ?? null;
|
||||
|
||||
// Handle form submissions for updating permission status
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action_type']) && $_POST['action_type'] === 'update_permission') {
|
||||
$permission_id_to_update = filter_var($_POST['permission_id'], FILTER_SANITIZE_NUMBER_INT);
|
||||
$new_status = trim($_POST['new_status']);
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
|
||||
// Basic validation
|
||||
if (empty($permission_id_to_update) || !in_array($new_status, ['Approved', 'Rejected', 'Revoked'])) {
|
||||
set_message("Invalid request to update permission.", "danger");
|
||||
header("Location: manage_permissions.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// You might want to add a check here to ensure the data owner is indeed the owner of the data source
|
||||
// related to this permission_id, to prevent tampering.
|
||||
// This would involve fetching the permission request and then checking the data source's fkisp_id_of.
|
||||
|
||||
if ($data_source_manager->updatePermissionStatus($permission_id_to_update, $new_status, $user_id, $notes)) {
|
||||
set_message("Permission request updated successfully!", "success");
|
||||
} else {
|
||||
set_message("Failed to update permission request.", "danger");
|
||||
}
|
||||
header("Location: manage_permissions.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$pending_requests = $data_source_manager->getPermissionRequestsForOwner($owner_person_id, 'Pending');
|
||||
$all_requests = $data_source_manager->getPermissionRequestsForOwner($owner_person_id); // All statuses
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_owner.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_owner.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> Permissions</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3 class="mb-3">Pending Requests</h3>
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-body">
|
||||
<?php if (!empty($pending_requests)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Data Source</th>
|
||||
<th>Requested By</th>
|
||||
<th>Permission Type</th>
|
||||
<th>Requested Date</th>
|
||||
<th>Notes</th>
|
||||
<th>Proof</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pending_requests as $req): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($req['pkdspsdsp_id']) ?></td>
|
||||
<td><?= htmlspecialchars($req['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?></td>
|
||||
<td><span class="badge bg-info"><?= htmlspecialchars($req['dspsdsp_permission']) ?></span></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$notes = $req['dspsdsp_notes'] ?? '';
|
||||
echo $notes !== ''
|
||||
? nl2br(htmlspecialchars($notes))
|
||||
: '<span class="text-muted">—</span>';
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($req['dspsdsp_proof_path'])): ?>
|
||||
<?php
|
||||
$proofPath = $req['dspsdsp_proof_path'];
|
||||
$isExternal = preg_match('/^https?:\\/\\//i', $proofPath) === 1;
|
||||
$cleanPath = ltrim($proofPath, '/');
|
||||
$linkTarget = $isExternal ? $proofPath : '../uploads/' . $cleanPath;
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($linkTarget) ?>" class="btn btn-sm btn-outline-primary rounded-pill" target="_blank" rel="noopener">
|
||||
<i class="fas fa-file-pdf me-1"></i> View
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<form action="manage_permissions.php" method="POST" class="d-inline">
|
||||
<input type="hidden" name="action_type" value="update_permission">
|
||||
<input type="hidden" name="permission_id" value="<?= htmlspecialchars($req['pkdspsdsp_id']) ?>">
|
||||
<input type="hidden" name="new_status" value="Approved">
|
||||
<button type="submit" class="btn btn-sm btn-success rounded me-1" title="Approve">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="manage_permissions.php" method="POST" class="d-inline">
|
||||
<input type="hidden" name="action_type" value="update_permission">
|
||||
<input type="hidden" name="permission_id" value="<?= htmlspecialchars($req['pkdspsdsp_id']) ?>">
|
||||
<input type="hidden" name="new_status" value="Rejected">
|
||||
<button type="submit" class="btn btn-sm btn-danger rounded" title="Reject">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">No pending permission requests.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-3 mt-5">All Permission Requests</h3>
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<?php if (!empty($all_requests)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Data Source</th>
|
||||
<th>Requested By</th>
|
||||
<th>Permission Type</th>
|
||||
<th>Status</th>
|
||||
<th>Requested Date</th>
|
||||
<th>Notes</th>
|
||||
<th>Proof</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($all_requests as $req): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($req['pkdspsdsp_id']) ?></td>
|
||||
<td><?= htmlspecialchars($req['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?></td>
|
||||
<td><span class="badge bg-info"><?= htmlspecialchars($req['dspsdsp_permission']) ?></span></td>
|
||||
<td>
|
||||
<span class="badge <?= ($req['dspsdsp_status'] == 'Approved' ? 'bg-success' : ($req['dspsdsp_status'] == 'Pending' ? 'bg-warning' : 'bg-danger')) ?>">
|
||||
<?= htmlspecialchars($req['dspsdsp_status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$notes = $req['dspsdsp_notes'] ?? '';
|
||||
echo $notes !== ''
|
||||
? nl2br(htmlspecialchars($notes))
|
||||
: '<span class="text-muted">—</span>';
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($req['dspsdsp_proof_path'])): ?>
|
||||
<?php
|
||||
$proofPath = $req['dspsdsp_proof_path'];
|
||||
$isExternal = preg_match('/^https?:\\/\\//i', $proofPath) === 1;
|
||||
$cleanPath = ltrim($proofPath, '/');
|
||||
$linkTarget = $isExternal ? $proofPath : '../uploads/' . $cleanPath;
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($linkTarget) ?>" class="btn btn-sm btn-outline-primary rounded-pill" target="_blank" rel="noopener">
|
||||
<i class="fas fa-file-pdf me-1"></i> View
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($req['dspsdsp_status'] == 'Approved'): ?>
|
||||
<form action="manage_permissions.php" method="POST" class="d-inline">
|
||||
<input type="hidden" name="action_type" value="update_permission">
|
||||
<input type="hidden" name="permission_id" value="<?= htmlspecialchars($req['pkdspsdsp_id']) ?>">
|
||||
<input type="hidden" name="new_status" value="Revoked">
|
||||
<button type="submit" class="btn btn-sm btn-secondary rounded" title="Revoke" onclick="return confirm('Are you sure you want to revoke this permission?');">
|
||||
<i class="fas fa-ban"></i> Revoke
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">No action</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">No permission requests found.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_owner.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
140
data_owner/my_analytics.php
Normal file
140
data_owner/my_analytics.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
// data_owner/my_analytics.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
|
||||
// Ensure only Data Owners can access this page
|
||||
redirect_if_not_role('Data Owner');
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$owner_person_id = $_SESSION['person_id'];
|
||||
$username = $_SESSION['username'];
|
||||
|
||||
// Fetch analytics data (placeholders for now)
|
||||
// You would typically query dsps_tbl_datasource_used and dsps_tbl_anonymous
|
||||
// filtered by data sources owned by $owner_person_id.
|
||||
|
||||
// Example: Total downloads for my data sources
|
||||
$total_downloads = 0;
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(dsu.pkdspsdspused_id)
|
||||
FROM dsps_tbl_datasource_used dsu
|
||||
JOIN dsps_tbl_datasource ds ON dsu.fkdspsdsused_id = ds.pkdspsds_id
|
||||
WHERE ds.fkisp_id_of = :owner_person_id AND dsu.dspsdspused_action = 'Downloaded'
|
||||
");
|
||||
$stmt->execute(['owner_person_id' => $owner_person_id]);
|
||||
$total_downloads = $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching total downloads: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Example: Most viewed data sources (from anonymous views or usage logs)
|
||||
$most_viewed_datasources = [];
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT ds.dspsds_title_en, COUNT(da.pkdspsano_id) AS view_count
|
||||
FROM dsps_tbl_anonymous da
|
||||
JOIN dsps_tbl_datasource ds ON da.fkdspsds_id = ds.pkdspsds_id
|
||||
WHERE ds.fkisp_id_of = :owner_person_id
|
||||
GROUP BY ds.pkdspsds_id, ds.dspsds_title_en
|
||||
ORDER BY view_count DESC
|
||||
LIMIT 5
|
||||
");
|
||||
$stmt->execute(['owner_person_id' => $owner_person_id]);
|
||||
$most_viewed_datasources = $stmt->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching most viewed data sources: " . $e->getMessage());
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_owner.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_owner.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> My Data Analytics</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary"><i class="fas fa-download me-2"></i> Total Downloads of My Data</h5>
|
||||
<p class="card-text fs-2"><?= $total_downloads ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info"><i class="fas fa-eye me-2"></i> Total Views of My Data Introductions</h5>
|
||||
<p class="card-text fs-2">1500</p> <!-- Placeholder, implement actual count from dsps_tbl_anonymous -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm rounded mb-5">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0">Most Viewed Data Sources (Top 5)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($most_viewed_datasources)): ?>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach ($most_viewed_datasources as $ds): ?>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<?= htmlspecialchars($ds['dspsds_title_en']) ?>
|
||||
<span class="badge bg-primary rounded"><?= htmlspecialchars($ds['view_count']) ?> Views</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">No view data available for your sources yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_owner.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
166
data_owner/run_r_scripts.php
Normal file
166
data_owner/run_r_scripts.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
// data_owner/run_r_scripts.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
require_once '../includes/jupyter_helpers.php';
|
||||
// Ensure only Data Owners can access this page
|
||||
redirect_if_not_role('Data Owner');
|
||||
|
||||
// Build the Jupyter URL for the iframe. Default to localhost access with the dev token.
|
||||
$jupyterBaseUrl = dsp_jupyter_base_url();
|
||||
$jupyterToken = dsp_jupyter_token();
|
||||
$jupyterIframeUrl = dsp_jupyter_iframe_url(
|
||||
$jupyterBaseUrl,
|
||||
$jupyterToken,
|
||||
isset($_SESSION['person_id']) ? (int) $_SESSION['person_id'] : null
|
||||
);
|
||||
$hasRJupyterAccess = has_r_access();
|
||||
$workspaceSync = ['synced' => [], 'missing' => [], 'workspace_dir' => null];
|
||||
$workspaceRelativeDir = null;
|
||||
$workspaceError = null;
|
||||
|
||||
if ($hasRJupyterAccess && isset($_SESSION['person_id'])) {
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
try {
|
||||
$workspaceSync = $dataSourceManager->prepareJupyterWorkspace(
|
||||
(int) $_SESSION['person_id'],
|
||||
dirname(__DIR__) . '/uploads/jupyter_workspace'
|
||||
);
|
||||
$workspaceRelativeDir = 'datasources/user_' . (int) $_SESSION['person_id'];
|
||||
} catch (Exception $e) {
|
||||
$workspaceError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_owner.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_owner.php");
|
||||
?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">R in JupyterHub</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-header bg-light rounded-top d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">Work with R inside JupyterLab</h5>
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<span class="badge bg-success-subtle text-success rounded-pill"><i class="fas fa-flask me-1"></i> Enabled</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning-subtle text-warning rounded-pill"><i class="fas fa-lock me-1"></i> Disabled</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<?php if ($workspaceError): ?>
|
||||
<div class="alert alert-danger rounded mb-3">
|
||||
<strong>Workspace error:</strong> <?= htmlspecialchars($workspaceError) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="mb-3">
|
||||
Approved data sources you manage or have access to are synced to
|
||||
<code><?= htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?></code> inside Jupyter.
|
||||
Only datasets with Approved permissions appear here.
|
||||
</p>
|
||||
<?php if (!empty($workspaceSync['synced'])): ?>
|
||||
<div class="table-responsive mb-3">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Data Source</th>
|
||||
<th>Data Type</th>
|
||||
<th>Category</th>
|
||||
<th>Filename</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($workspaceSync['synced'] as $idx => $syncedItem): ?>
|
||||
<tr>
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['title']) ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['category'] ?? 'N/A') ?></td>
|
||||
<td><code><?= htmlspecialchars(basename($syncedItem['relative_path'])) ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-3">
|
||||
You do not currently have any Approved data sources. Approve requests in the permissions panel to make them available here.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($workspaceSync['missing'])): ?>
|
||||
<div class="alert alert-warning rounded mb-3">
|
||||
<strong>Some files were skipped:</strong>
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($workspaceSync['missing'] as $missingItem): ?>
|
||||
<li><?= htmlspecialchars($missingItem['title']) ?> — <?= htmlspecialchars($missingItem['reason']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<p class="mb-3">
|
||||
Launch the embedded Jupyter environment below to build, run, and share your R notebooks. Use the
|
||||
toolbar menu inside Jupyter to create new R notebooks and access saved work under <em>Files</em>.
|
||||
</p>
|
||||
<div class="ratio ratio-16x9 border rounded overflow-hidden">
|
||||
<iframe
|
||||
src="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>"
|
||||
title="R in JupyterHub"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
></iframe>
|
||||
</div>
|
||||
<p class="mt-3 mb-0">
|
||||
Need more room? <a href="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">Open Jupyter in a new tab</a>.
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning rounded d-flex align-items-start gap-3">
|
||||
<i class="fas fa-circle-info mt-1"></i>
|
||||
<div>
|
||||
<strong>R in JupyterHub is currently disabled for your account.</strong><br>
|
||||
Contact a DAC Staff administrator to enable R/Jupyter access so you can analyse datasets directly from this workspace.
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
After your access is approved, refresh this page to launch the embedded notebook environment.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_owner.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
341
data_user/browse_datasources.php
Normal file
341
data_user/browse_datasources.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
// data_user/browse_datasources.php
|
||||
// This page allows users (including guests) to browse available data sources.
|
||||
|
||||
// Enable detailed error reporting for debugging purposes (log only)
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// Check if a session is not already active before starting one.
|
||||
// This prevents the "session already active" notice.
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Use __DIR__ to get the absolute path to this file's directory,
|
||||
// ensuring the path to the required files is always correct.
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
require_once(__DIR__ . '/../includes/auth.php');
|
||||
require_once(__DIR__ . '/../classes/DataSource.php');
|
||||
require_once(__DIR__ . '/../classes/Classifications.php');
|
||||
require_once(__DIR__ . '/../classes/User.php');
|
||||
require_once(__DIR__ . '/../classes/Permission.php'); // Correctly include the Permission class
|
||||
|
||||
// Check if user is logged in
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
$person_id = $_SESSION['person_id'] ?? null;
|
||||
$username = $_SESSION['username'] ?? null;
|
||||
$usernameLabel = $username ? htmlspecialchars($username, ENT_QUOTES, 'UTF-8') : 'Guest';
|
||||
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
// Instantiate classes
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
$classificationManager = new Classifications($pdo);
|
||||
$userManager = new User($pdo);
|
||||
$permissionManager = new Permission($pdo); // Instantiate the Permission class
|
||||
|
||||
// Get user details if logged in
|
||||
$currentUserDetails = $is_logged_in ? $userManager->getUserDetails($user_id) : null;
|
||||
|
||||
$uploadsWebPath = '../uploads/datasources/';
|
||||
|
||||
// Get filter parameters from GET request
|
||||
$filter_category_id = $_GET['category_id'] ?? null;
|
||||
if ($filter_category_id !== null) {
|
||||
$filter_category_id = filter_var($filter_category_id, FILTER_VALIDATE_INT);
|
||||
if ($filter_category_id === false) {
|
||||
$filter_category_id = null;
|
||||
}
|
||||
}
|
||||
$search_query = htmlspecialchars($_GET['search'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Fetch data sources based on filters
|
||||
$data_sources = [];
|
||||
try {
|
||||
$data_sources = $dataSourceManager->getDataSources(
|
||||
null, // No owner filter for public browsing
|
||||
'Active',
|
||||
$filter_category_id,
|
||||
$search_query
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
set_message('Error retrieving data sources: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
|
||||
// Fetch all categories for the filter dropdown
|
||||
$all_categories = [];
|
||||
try {
|
||||
$all_categories = $classificationManager->getAllCategories();
|
||||
} catch (Exception $e) {
|
||||
error_log("Error fetching categories for browse_datasources: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_user.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_user.php");
|
||||
?>
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> Browse All Data</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo $usernameLabel; ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter and Search Form -->
|
||||
<div class="card shadow-sm rounded mb-4 p-3">
|
||||
<div class="card-body">
|
||||
<form action="browse_datasources.php" method="GET" class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label for="categoryFilter" class="form-label">Filter by Category:</label>
|
||||
<select class="form-select rounded" id="categoryFilter" name="category_id">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach ($all_categories as $category): ?>
|
||||
<option value="<?php echo htmlspecialchars($category['pkdspscate_id']); ?>"
|
||||
<?php echo ($filter_category_id == $category['pkdspscate_id']) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($category['dspscate_title_en']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="searchQuery" class="form-label">Search by Title/Description:</label>
|
||||
<input type="text" class="form-control rounded" id="searchQuery" name="search"
|
||||
value="<?php echo htmlspecialchars($search_query); ?>" placeholder="Enter keywords...">
|
||||
</div>
|
||||
<div class="col-md-3 d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded">
|
||||
<i class="fas fa-filter me-2"></i> Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Sources List -->
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 mt-4">
|
||||
<?php if (!empty($data_sources)): ?>
|
||||
<?php foreach ($data_sources as $ds): ?>
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm rounded">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title text-primary"><?php echo htmlspecialchars($ds['dspsds_title_en']); ?></h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Category: <?php echo htmlspecialchars($ds['category_name']); ?></h6>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Type: <?php echo htmlspecialchars($ds['data_type_name']); ?></h6>
|
||||
<p class="card-text flex-grow-1"><?php echo htmlspecialchars(substr($ds['dspsds_description'], 0, 150)) . (strlen($ds['dspsds_description']) > 150 ? '...' : ''); ?></p>
|
||||
<div class="mt-auto">
|
||||
<ul class="list-unstyled small text-muted">
|
||||
<li><i class="fas fa-user me-1"></i> Data Owner: <?php echo htmlspecialchars($ds['isp_firstname_en'] . ' ' . $ds['isp_lastname_en']); ?></li>
|
||||
<li><i class="fas fa-calendar-alt me-1"></i> Published:
|
||||
<?php if (!empty($ds['dspsds_public_date'])): ?>
|
||||
<?= htmlspecialchars(date('M d, Y', strtotime($ds['dspsds_public_date']))); ?>
|
||||
<?php else: ?>
|
||||
Not specified
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
<?php
|
||||
$supportingFiles = [
|
||||
'dspsds_filename1' => ['label' => 'Questionnaire / Data Dictionary', 'icon' => 'fa-clipboard-list'],
|
||||
'dspsds_filename2' => ['label' => 'Protocol / User Guide', 'icon' => 'fa-book'],
|
||||
'dspsds_filename3' => ['label' => 'Other Supporting Document', 'icon' => 'fa-file-alt'],
|
||||
];
|
||||
?>
|
||||
<div class="bg-light-subtle border rounded p-3 mb-3">
|
||||
<span class="d-block text-uppercase text-muted fw-semibold small mb-2">Supporting Documents</span>
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
<?php foreach ($supportingFiles as $column => $meta): ?>
|
||||
<?php
|
||||
$fileName = $ds[$column] ?? '';
|
||||
$label = $meta['label'];
|
||||
$icon = $meta['icon'];
|
||||
?>
|
||||
<li class="mb-2">
|
||||
<i class="fas <?= htmlspecialchars($icon, ENT_QUOTES, 'UTF-8') ?> me-1"></i>
|
||||
<?php if (!empty($fileName)): ?>
|
||||
<?php
|
||||
$isUrl = preg_match('/^https?:\/\//i', $fileName) === 1;
|
||||
$linkTarget = $isUrl ? $fileName : $uploadsWebPath . rawurlencode($fileName);
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($linkTarget, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars($label) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted"><?= htmlspecialchars($label) ?> (Not provided)</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php if ($is_logged_in): ?>
|
||||
<?php
|
||||
$has_read_permission = false;
|
||||
$has_download_permission = false;
|
||||
try {
|
||||
$has_read_permission = $permissionManager->hasPermission($person_id, $ds['pkdspsds_id'], 'Read');
|
||||
$has_download_permission = $permissionManager->hasPermission($person_id, $ds['pkdspsds_id'], 'Download');
|
||||
} catch (Exception $e) {
|
||||
error_log("Permission check error for user " . $person_id . " on DS " . $ds['pkdspsds_id'] . ": " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<?php if ($has_read_permission): ?>
|
||||
<a href="#" class="btn btn-sm btn-outline-success rounded me-2 disabled">
|
||||
<i class="fas fa-check-circle me-1"></i> Read Access Granted
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-success rounded me-2" data-bs-toggle="modal" data-bs-target="#requestPermissionModal"
|
||||
data-ds-id="<?php echo htmlspecialchars($ds['pkdspsds_id']); ?>"
|
||||
data-ds-title="<?php echo htmlspecialchars($ds['dspsds_title_en']); ?>"
|
||||
data-permission-type="Read">
|
||||
<i class="fas fa-file-alt me-1"></i> Request Read Access
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($has_download_permission): ?>
|
||||
<a href="download.php?dspsds_id=<?php echo htmlspecialchars($ds['pkdspsds_id']); ?>" class="btn btn-sm btn-outline-primary rounded">
|
||||
<i class="fas fa-download me-1"></i> Download File
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-primary rounded" data-bs-toggle="modal" data-bs-target="#requestPermissionModal"
|
||||
data-ds-id="<?php echo htmlspecialchars($ds['pkdspsds_id']); ?>"
|
||||
data-ds-title="<?php echo htmlspecialchars($ds['dspsds_title_en']); ?>"
|
||||
data-permission-type="Download">
|
||||
<i class="fas fa-cloud-download-alt me-1"></i> Request Download
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-secondary rounded" data-bs-toggle="modal" data-bs-target="#loginModal">
|
||||
<i class="fas fa-sign-in-alt me-1"></i> Login to Request Access
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info rounded text-center">
|
||||
No active data sources found matching your criteria.
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Permission Modal -->
|
||||
<div class="modal fade" id="requestPermissionModal" tabindex="-1" aria-labelledby="requestPermissionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header bg-success text-white rounded-top">
|
||||
<h5 class="modal-title" id="requestPermissionModalLabel">Request Data Access Permission</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="../data_user/process_request_permission.php" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="data_source_id" id="modalDataSourceId">
|
||||
<input type="hidden" name="permission_type" id="modalPermissionType">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="modalDataSourceTitle" class="form-label">Data Source</label>
|
||||
<input type="text" class="form-control rounded" id="modalDataSourceTitle" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalRequestedPermission" class="form-label">Requested Permission</label>
|
||||
<input type="text" class="form-control rounded" id="modalRequestedPermission" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="requestNotes" class="form-label">Reason for Request (Required)</label>
|
||||
<textarea class="form-control rounded-3" id="requestNotes" name="notes" rows="4" placeholder="Please explain why you need this data and how you plan to use it." required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="proofFile" class="form-label">Upload Proof (PDF only)</label>
|
||||
<input type="file" class="form-control rounded" id="proofFile" name="proof_file" accept="application/pdf" required>
|
||||
<div class="form-text">Attach an official letter, approval memo, or supporting document in PDF format (max 10 MB).</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success rounded">Submit Request</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal for guests -->
|
||||
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header bg-primary text-white rounded-top">
|
||||
<h5 class="modal-title" id="loginModalLabel">Login Required</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center p-4">
|
||||
<p>You must be logged in to request access to data sources.</p>
|
||||
<a href="../index.php?page=login" class="btn btn-primary rounded me-2">Login</a>
|
||||
<a href="../index.php?page=register" class="btn btn-outline-primary rounded">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_user.php");
|
||||
?>
|
||||
<script>
|
||||
// JavaScript to populate the permission request modal
|
||||
var requestPermissionModal = document.getElementById('requestPermissionModal');
|
||||
if (requestPermissionModal) {
|
||||
requestPermissionModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget; // Button that triggered the modal
|
||||
var dsId = button.getAttribute('data-ds-id');
|
||||
var dsTitle = button.getAttribute('data-ds-title');
|
||||
var permissionType = button.getAttribute('data-permission-type');
|
||||
|
||||
var modalDataSourceId = requestPermissionModal.querySelector('#modalDataSourceId');
|
||||
var modalDataSourceTitle = requestPermissionModal.querySelector('#modalDataSourceTitle');
|
||||
var modalPermissionType = requestPermissionModal.querySelector('#modalPermissionType');
|
||||
var modalRequestedPermission = requestPermissionModal.querySelector('#modalRequestedPermission');
|
||||
|
||||
modalDataSourceId.value = dsId;
|
||||
modalDataSourceTitle.value = dsTitle;
|
||||
modalPermissionType.value = permissionType;
|
||||
modalRequestedPermission.value = permissionType; // Display the type in the modal
|
||||
requestPermissionModal.querySelector('#requestNotes').value = ''; // Clear notes on new open
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
166
data_user/dashboard.php
Normal file
166
data_user/dashboard.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
// data_user/dashboard.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php'; // For data source related counts
|
||||
|
||||
// Ensure only Data Users can access this dashboard
|
||||
redirect_if_not_role('Data User');
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id'];
|
||||
$username = $_SESSION['username'];
|
||||
$user_status = $_SESSION['user_status'];
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
|
||||
// Get counts for Data User
|
||||
$approved_datasources_count = 0;
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(DISTINCT dp.fkdspsds_id)
|
||||
FROM dsps_tbl_datasource_permission dp
|
||||
WHERE dp.fkisp_id_of = :person_id AND dp.dspsdsp_status = 'Approved'
|
||||
");
|
||||
$stmt->execute(['person_id' => $person_id]);
|
||||
$approved_datasources_count = $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching approved datasources count: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$pending_requests_count = 0;
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(dp.pkdspsdsp_id)
|
||||
FROM dsps_tbl_datasource_permission dp
|
||||
WHERE dp.fkisp_id_of = :person_id AND dp.dspsdsp_status = 'Pending'
|
||||
");
|
||||
$stmt->execute(['person_id' => $person_id]);
|
||||
$pending_requests_count = $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching pending requests count: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$my_downloads_count = 0;
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(*) FROM dsps_tbl_datasource_used
|
||||
WHERE fkisp_id_of = :person_id AND dspsdspused_action = 'Downloaded'
|
||||
");
|
||||
$stmt->execute(['person_id' => $person_id]);
|
||||
$my_downloads_count = $stmt->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching my downloads count: " . $e->getMessage());
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_user.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_user.php");
|
||||
?>
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> Dashboard</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success"><i class="fas fa-check-circle me-2"></i> Approved Data Sources</h5>
|
||||
<p class="card-text fs-2"><?= $approved_datasources_count ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning"><i class="fas fa-hourglass-start me-2"></i> Pending Requests</h5>
|
||||
<p class="card-text fs-2"><?= $pending_requests_count ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info"><i class="fas fa-download me-2"></i> My Total Downloads</h5>
|
||||
<p class="card-text fs-2"><?= $my_downloads_count ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<h3>Quick Actions</h3>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a href="browse_datasources.php" class="btn btn-primary rounded"><i class="fas fa-search me-2"></i> Browse All Data</a>
|
||||
<a href="my_permissions.php" class="btn btn-warning rounded"><i class="fas fa-handshake me-2"></i> View My Permissions</a>
|
||||
<a href="my_downloads.php" class="btn btn-info rounded"><i class="fas fa-download me-2"></i> My Download History</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder for recently accessed data sources -->
|
||||
<div class="mt-5">
|
||||
<h3>Recently Accessed Data Sources</h3>
|
||||
<ul class="list-group shadow-sm rounded">
|
||||
<?php
|
||||
// Example recent activities (replace with actual data from dsps_tbl_datasource_used)
|
||||
// You'd need to fetch these from the database, filtered by fkisp_id_of = $person_id
|
||||
$recent_accesses = [
|
||||
['title' => 'Health Survey 2023', 'action' => 'Downloaded', 'time' => '1 hour ago'],
|
||||
['title' => 'Education Statistics 2022', 'action' => 'Viewed Details', 'time' => 'Yesterday'],
|
||||
['title' => 'Climate Data Phnom Penh', 'action' => 'Ran Analysis', 'time' => '3 days ago'],
|
||||
];
|
||||
if (!empty($recent_accesses)) {
|
||||
foreach ($recent_accesses as $access) {
|
||||
echo '<li class="list-group-item d-flex justify-content-between align-items-center">';
|
||||
echo htmlspecialchars($access['title']) . ' - ' . htmlspecialchars($access['action']);
|
||||
echo '<span class="badge bg-secondary">' . htmlspecialchars($access['time']) . '</span>';
|
||||
echo '</li>';
|
||||
}
|
||||
} else {
|
||||
echo '<li class="list-group-item text-center text-muted">No recent activity.</li>';
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_user.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
109
data_user/download.php
Normal file
109
data_user/download.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
// This script handles the file download and logs the action to the database.
|
||||
|
||||
// Start the session to access user info
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php'; // Make sure your auth.php includes a redirect_if_not_logged_in or similar function
|
||||
|
||||
// --- 1. Get User and Datasource IDs ---
|
||||
// Ensure the user is logged in
|
||||
redirect_if_not_logged_in('../login.php');
|
||||
|
||||
// Get the user's person_id from the session
|
||||
$person_id = $_SESSION['person_id'];
|
||||
|
||||
// Get the datasource_id from the URL parameter
|
||||
$datasource_id = $_GET['dspsds_id'] ?? null;
|
||||
|
||||
// Validate the datasource_id
|
||||
if (!$datasource_id || !filter_var($datasource_id, FILTER_VALIDATE_INT)) {
|
||||
die("Invalid or missing datasource ID.");
|
||||
}
|
||||
|
||||
// --- 2. Log the Download Action ---
|
||||
// This code inserts a new record for every download.
|
||||
try {
|
||||
$sql_insert = "
|
||||
INSERT INTO dsps_tbl_datasource_used
|
||||
(fkdspsdsused_id, fkisp_id_of, dspsdspused_action)
|
||||
VALUES
|
||||
(?, ?, ?)
|
||||
";
|
||||
$stmt_insert = $pdo->prepare($sql_insert);
|
||||
$action = "Downloaded";
|
||||
$stmt_insert->execute([$datasource_id, $person_id, $action]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// We now log the error and set a user-facing message
|
||||
error_log("Error logging download: " . $e->getMessage());
|
||||
// Redirect with an error message, but still try to serve the file
|
||||
set_message("An error occurred while logging the download.", "danger");
|
||||
// We do not die here, as we still want to try and serve the file
|
||||
}
|
||||
|
||||
// --- 3. Retrieve File Path and Name ---
|
||||
$file_path = null;
|
||||
$file_name = null;
|
||||
try {
|
||||
$sql_select = "
|
||||
SELECT dspsds_filename, dspsds_title_en
|
||||
FROM dsps_tbl_datasource
|
||||
WHERE pkdspsds_id = ?
|
||||
";
|
||||
$stmt = $pdo->prepare($sql_select);
|
||||
$stmt->execute([$datasource_id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if ($row) {
|
||||
$file_name = $row['dspsds_filename'];
|
||||
$download_label = $row['dspsds_title_en'] ?: 'datasource_' . $datasource_id;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error retrieving file info: " . $e->getMessage());
|
||||
die("An error occurred while retrieving file information.");
|
||||
}
|
||||
|
||||
if (empty($file_name)) {
|
||||
die("File not found in the database.");
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\\/\\//i', $file_name)) {
|
||||
header('Location: ' . $file_name);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uploadsDir = realpath(__DIR__ . '/../uploads/datasources');
|
||||
if (!$uploadsDir) {
|
||||
error_log('Uploads directory not found for download.');
|
||||
die('File storage directory is unavailable.');
|
||||
}
|
||||
|
||||
$file_path = $uploadsDir . '/' . $file_name;
|
||||
|
||||
// --- 4. Serve the File to the User ---
|
||||
// Check if the file exists on the server
|
||||
if (file_exists($file_path)) {
|
||||
// Set headers to force a download
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . basename($download_label . '_' . $file_name) . '"');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('Content-Length: ' . filesize($file_path));
|
||||
|
||||
if (ob_get_level()) {
|
||||
ob_clean();
|
||||
}
|
||||
flush();
|
||||
|
||||
// Read the file and send it to the output buffer
|
||||
readfile($file_path);
|
||||
exit;
|
||||
} else {
|
||||
die("The file could not be found on the server at the specified path.");
|
||||
}
|
||||
?>
|
||||
58
data_user/indexTesting.php
Normal file
58
data_user/indexTesting.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// dsp26072025/index.php
|
||||
// This is the main front controller for the application.
|
||||
|
||||
// Define the base path for consistent includes
|
||||
// This will correctly point to the root directory of your project
|
||||
define('BASE_PATH', __DIR__ . '/');
|
||||
|
||||
// Start the session once at the very beginning
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Global Dependencies - All pages will have access to these.
|
||||
// NOTE: Make sure these files exist in the specified paths.
|
||||
require_once BASE_PATH . 'config.php';
|
||||
require_once BASE_PATH . 'includes/auth.php';
|
||||
require_once BASE_PATH . 'includes/functions.php';
|
||||
require_once BASE_PATH . 'classes/DataSource.php';
|
||||
require_once BASE_PATH . 'classes/Classifications.php';
|
||||
require_once BASE_PATH . 'classes/User.php';
|
||||
require_once BASE_PATH . 'classes/Permission.php';
|
||||
require_once BASE_PATH . 'classes/Request.php';
|
||||
require_once BASE_PATH . 'classes/Log.php';
|
||||
|
||||
// Get the requested page from the URL. Default to 'dashboard' if not set.
|
||||
$page = $_GET['page'] ?? 'dashboard';
|
||||
|
||||
// A simple routing system to include the correct file
|
||||
switch ($page) {
|
||||
case 'dashboard':
|
||||
include BASE_PATH . 'data_user/dashboard.php';
|
||||
break;
|
||||
case 'browse_datasources':
|
||||
include BASE_PATH . 'data_user/browse_datasources.php';
|
||||
break;
|
||||
case 'my_permissions':
|
||||
include BASE_PATH . 'data_user/my_permissions.php';
|
||||
break;
|
||||
case 'my_downloads':
|
||||
include BASE_PATH . 'data_user/my_downloads.php';
|
||||
break;
|
||||
case 'login':
|
||||
include BASE_PATH . 'data_user/login.php';
|
||||
break;
|
||||
case 'register':
|
||||
include BASE_PATH . 'data_user/register.php';
|
||||
break;
|
||||
case 'process_request_permission':
|
||||
include BASE_PATH . 'data_user/process_request_permission.php';
|
||||
break;
|
||||
default:
|
||||
// Handle pages not found
|
||||
header("HTTP/1.0 404 Not Found");
|
||||
include BASE_PATH . 'data_user/404.php';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
134
data_user/my_downloads.php
Normal file
134
data_user/my_downloads.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
// data_user/my_downloads.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
|
||||
// Ensure only Data Users can access this page
|
||||
redirect_if_not_role('Data User');
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id']; // This is the correct ID to use for the join
|
||||
$username = $_SESSION['username'];
|
||||
|
||||
$data_source_manager = new DataSource($pdo);
|
||||
|
||||
// Fetch download history for the current user
|
||||
$download_history = [];
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT dsu.*,
|
||||
ds.dspsds_title_en,
|
||||
ds.dspsds_filename,
|
||||
tds.dspstds_name_en
|
||||
FROM dsps_tbl_datasource_used dsu
|
||||
JOIN dsps_tbl_datasource ds
|
||||
ON dsu.fkdspsdsused_id = ds.pkdspsds_id
|
||||
JOIN dsps_tbl_typedatasource tds
|
||||
ON ds.fkdspstds_id = tds.pkdspstds_id
|
||||
WHERE dsu.fkisp_id_of = :person_id
|
||||
AND dsu.dspsdspused_action = 'Downloaded'
|
||||
ORDER BY dsu.dspsdspused_datetime DESC
|
||||
");
|
||||
$stmt->execute(['person_id' => $person_id]);
|
||||
$download_history = $stmt->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching download history: " . $e->getMessage());
|
||||
set_message("Error fetching download history.", "danger");
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_user.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_user.php");
|
||||
?>
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> My Downloads History</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded">
|
||||
<div class="card-header bg-light rounded-top">
|
||||
<h5 class="mb-0">Downloaded Data Sources</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($download_history)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data Source</th>
|
||||
<th>Type</th>
|
||||
<th>Download Date</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($download_history as $item): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($item['dspsds_title_en']) ?></td>
|
||||
<td><?= htmlspecialchars($item['dspstds_name_en']) ?></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($item['dspsdspused_datetime'])) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($item['dspsds_filename'])): ?>
|
||||
<!--
|
||||
This is the CORRECTED line. It now points to download.php
|
||||
and passes the ID of the data source.
|
||||
-->
|
||||
<a href="download.php?dspsds_id=<?= htmlspecialchars($item['fkdspsdsused_id']) ?>" class="btn btn-sm btn-outline-primary text-dark badge">
|
||||
<i class="fas fa-download me-1"></i> Download Again
|
||||
</a>
|
||||
<?php else: ?>
|
||||
N/A (API or no direct file)
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-0">You have not downloaded any data sources yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_user.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
150
data_user/my_permissions.php
Normal file
150
data_user/my_permissions.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
// data_user/my_permissions.php
|
||||
// This page shows the user a list of all their data access requests and their statuses.
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
// We'll assume these files exist based on your original code
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/Permission.php';
|
||||
require_once '../classes/User.php';
|
||||
|
||||
// Redirect if not logged in
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: ../index.php?page=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id'];
|
||||
$username = $_SESSION['username'];
|
||||
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
// Instantiate classes
|
||||
$permissionManager = new Permission($pdo);
|
||||
$userManager = new User($pdo);
|
||||
|
||||
// Get user details
|
||||
$currentUserDetails = $userManager->getUserDetails($user_id);
|
||||
|
||||
// Fetch all permission requests for the logged-in user
|
||||
$permissionRequests = [];
|
||||
try {
|
||||
$permissionRequests = $permissionManager->getPermissionsByPersonId($person_id);
|
||||
} catch (Exception $e) {
|
||||
set_message('Error retrieving permission requests: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/header_user.php");
|
||||
?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php
|
||||
// Include header file for admin pages
|
||||
include_once("../includes/nav_user.php");
|
||||
?>
|
||||
<!-- Page Content -->
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"> My Permissions</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded-lg">
|
||||
<div class="card-body p-4">
|
||||
<?php if (!empty($permissionRequests)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-borderless align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">Data Source</th>
|
||||
<th scope="col">Requested For</th>
|
||||
<th scope="col">Date Submitted</th>
|
||||
<th scope="col">Proof</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($permissionRequests as $request): ?>
|
||||
<tr>
|
||||
<td class="fw-bold"><?= htmlspecialchars($request['ds_title']) ?></td>
|
||||
<td><?= htmlspecialchars($request['dspspr_permission_type']) ?></td>
|
||||
<td><?= htmlspecialchars(date('M d, Y', strtotime($request['dspspr_request_date']))) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($request['dspspr_proof_path'])): ?>
|
||||
<a href="../uploads/<?= htmlspecialchars($request['dspspr_proof_path']) ?>" class="btn btn-sm btn-outline-primary rounded-pill" target="_blank" rel="noopener">
|
||||
<i class="fas fa-file-pdf me-1"></i> View
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$status_class = '';
|
||||
switch ($request['dspspr_status']) {
|
||||
case 'Approved':
|
||||
$status_class = 'bg-success badge text-white';
|
||||
break;
|
||||
case 'Pending':
|
||||
$status_class = 'bg-warning badge text-dark';
|
||||
break;
|
||||
case 'Denied':
|
||||
$status_class = 'bg-danger badge text-white';
|
||||
break;
|
||||
default:
|
||||
$status_class = 'bg-secondary badge text-white';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<span class="status-badge <?= $status_class ?>"><?= htmlspecialchars($request['dspspr_status']) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info text-center rounded m-0">
|
||||
You have not submitted any permission requests yet. <a href="browse_datasources.php" class="alert-link">Browse data sources</a> to get started.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
// Include Footer file for owner pages
|
||||
include_once("../includes/footer_user.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
147
data_user/process_request_permission.php
Normal file
147
data_user/process_request_permission.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
// data_user/process_request_permission.php
|
||||
// This script handles the submission of data access permission requests.
|
||||
|
||||
// Enable detailed error reporting for debugging purposes
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
// Start a session if one is not already active
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Check if the request method is POST. If not, redirect to prevent direct access.
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
// Redirect to the home page or an error page
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Use __DIR__ to get the absolute path to this file's directory,
|
||||
// ensuring the path to the required files is always correct.
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
require_once(__DIR__ . '/../includes/auth.php'); // Not strictly needed, but good practice
|
||||
require_once(__DIR__ . '/../classes/Permission.php');
|
||||
require_once(__DIR__ . '/../classes/User.php');
|
||||
|
||||
/**
|
||||
* Handles the upload of a proof document.
|
||||
*
|
||||
* @param array|null $file The uploaded file array from $_FILES.
|
||||
* @param int $personId The requesting user's person ID to help namespace the file.
|
||||
* @return string Relative path (within uploads) to the stored file.
|
||||
*/
|
||||
function handle_proof_upload(?array $file, int $personId): string {
|
||||
if ($file === null || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
||||
set_and_redirect('Please upload a PDF proof document for your request.', 'danger');
|
||||
}
|
||||
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
set_and_redirect('There was a problem uploading your proof document. Please try again.', 'danger');
|
||||
}
|
||||
|
||||
$maxSize = 10 * 1024 * 1024; // 10 MB
|
||||
if (($file['size'] ?? 0) > $maxSize) {
|
||||
set_and_redirect('Proof files must be smaller than 10 MB.', 'danger');
|
||||
}
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($file['tmp_name']) ?: '';
|
||||
if ($mimeType !== 'application/pdf') {
|
||||
set_and_redirect('Only PDF files are accepted as proof.', 'danger');
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../uploads/permission_proofs';
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0775, true)) {
|
||||
set_and_redirect('Unable to create the proof upload directory. Contact an administrator.', 'danger');
|
||||
}
|
||||
|
||||
if (!is_writable($uploadDir)) {
|
||||
set_and_redirect('The proof upload directory is not writable. Contact an administrator.', 'danger');
|
||||
}
|
||||
|
||||
$random = bin2hex(random_bytes(8));
|
||||
$filename = sprintf('%d_%s.pdf', $personId, $random);
|
||||
$destination = $uploadDir . '/' . $filename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
set_and_redirect('Failed to store your proof document. Please try again.', 'danger');
|
||||
}
|
||||
|
||||
return 'permission_proofs/' . $filename;
|
||||
}
|
||||
|
||||
// A helper function to set a session message and redirect
|
||||
function set_and_redirect($message, $type, $page = 'browse_datasources.php') {
|
||||
$_SESSION['message'] = $message;
|
||||
$_SESSION['message_type'] = $type;
|
||||
// Check if headers have already been sent.
|
||||
// This is the most common reason for redirects to fail.
|
||||
if (headers_sent()) {
|
||||
echo "<div class='alert alert-danger'>Redirect failed. Headers already sent. Please go back to <a href='$page'>the previous page</a> to view the message.</div>";
|
||||
echo "Message: " . htmlspecialchars($message);
|
||||
exit;
|
||||
} else {
|
||||
header('Location: ' . $page);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Check if the user is logged in
|
||||
if (!isset($_SESSION['person_id']) || !isset($_SESSION['user_id'])) {
|
||||
set_and_redirect('You must be logged in to request permission.', 'danger');
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$person_id = $_SESSION['person_id'];
|
||||
|
||||
// 2. Validate and sanitize POST data
|
||||
$dataSourceId = filter_input(INPUT_POST, 'data_source_id', FILTER_VALIDATE_INT);
|
||||
|
||||
// Replace deprecated FILTER_SANITIZE_STRING
|
||||
$permissionType = trim(filter_input(INPUT_POST, 'permission_type', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW));
|
||||
$notes = trim(filter_input(INPUT_POST, 'notes', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW));
|
||||
|
||||
// Check if required fields are missing or invalid
|
||||
if (!$dataSourceId || empty($permissionType) || empty($notes)) {
|
||||
set_and_redirect('Invalid or missing request details. Please try again.', 'danger');
|
||||
}
|
||||
|
||||
$proofPath = handle_proof_upload($_FILES['proof_file'] ?? null, $person_id);
|
||||
|
||||
// 3. Instantiate the Permission class and process the request
|
||||
try {
|
||||
$permissionManager = new Permission($pdo);
|
||||
|
||||
// Check if a similar request (for the same user, DS, and type) already exists.
|
||||
$existingRequest = $permissionManager->getPendingRequest($person_id, $dataSourceId, $permissionType);
|
||||
|
||||
if ($existingRequest) {
|
||||
set_and_redirect('A request for this permission type is already pending.', 'warning');
|
||||
}
|
||||
|
||||
// Attempt to add the new permission request to the database.
|
||||
$success = $permissionManager->addPermissionRequest(
|
||||
$person_id,
|
||||
$dataSourceId,
|
||||
$permissionType,
|
||||
'Pending', // Set status to Pending
|
||||
$notes,
|
||||
$proofPath
|
||||
);
|
||||
|
||||
if ($success) {
|
||||
set_and_redirect('Your request for ' . htmlspecialchars($permissionType) . ' access has been submitted successfully.', 'success');
|
||||
} else {
|
||||
set_and_redirect('Failed to submit your request. Please try again later.', 'danger');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Log the detailed error for debugging, but show a generic message to the user.
|
||||
error_log("Error submitting permission request: " . $e->getMessage());
|
||||
set_and_redirect('An unexpected error occurred. Please try again.', 'danger');
|
||||
}
|
||||
|
||||
?>
|
||||
161
data_user/r_in_jupyter.php
Normal file
161
data_user/r_in_jupyter.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
// data_user/r_in_jupyter.php
|
||||
session_start();
|
||||
require_once '../config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../classes/DataSource.php';
|
||||
require_once '../includes/jupyter_helpers.php';
|
||||
|
||||
redirect_if_not_role('Data User');
|
||||
|
||||
$hasRJupyterAccess = has_r_access();
|
||||
$workspaceSync = ['synced' => [], 'missing' => [], 'workspace_dir' => null];
|
||||
$workspaceRelativeDir = null;
|
||||
$workspaceError = null;
|
||||
|
||||
if ($hasRJupyterAccess && isset($_SESSION['person_id'])) {
|
||||
$dataSourceManager = new DataSource($pdo);
|
||||
try {
|
||||
$workspaceSync = $dataSourceManager->prepareJupyterWorkspace(
|
||||
(int) $_SESSION['person_id'],
|
||||
dirname(__DIR__) . '/uploads/jupyter_workspace'
|
||||
);
|
||||
$workspaceRelativeDir = 'datasources/user_' . (int) $_SESSION['person_id'];
|
||||
} catch (Exception $e) {
|
||||
$workspaceError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$jupyterBaseUrl = dsp_jupyter_base_url();
|
||||
$jupyterToken = dsp_jupyter_token();
|
||||
$jupyterIframeUrl = dsp_jupyter_iframe_url(
|
||||
$jupyterBaseUrl,
|
||||
$jupyterToken,
|
||||
isset($_SESSION['person_id']) ? (int) $_SESSION['person_id'] : null
|
||||
);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php include_once("../includes/header_user.php"); ?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include_once("../includes/nav_user.php"); ?>
|
||||
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">R in JupyterHub</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text me-3">
|
||||
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message'], $_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm rounded mb-4">
|
||||
<div class="card-header bg-light rounded-top d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">R Notebook Workspace</h5>
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<span class="badge bg-success-subtle text-success rounded-pill"><i class="fas fa-flask me-1"></i> Enabled</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning-subtle text-warning rounded-pill"><i class="fas fa-lock me-1"></i> Disabled</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($hasRJupyterAccess): ?>
|
||||
<?php if ($workspaceError): ?>
|
||||
<div class="alert alert-danger rounded mb-3">
|
||||
<strong>Workspace error:</strong> <?= htmlspecialchars($workspaceError) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="mb-3">
|
||||
Approved datasets are available inside Jupyter at
|
||||
<code><?= htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?></code>.
|
||||
Only data sources you have Approved access to will appear.
|
||||
</p>
|
||||
<?php if (!empty($workspaceSync['synced'])): ?>
|
||||
<div class="table-responsive mb-3">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Data Source</th>
|
||||
<th>Data Type</th>
|
||||
<th>Category</th>
|
||||
<th>Filename</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($workspaceSync['synced'] as $idx => $syncedItem): ?>
|
||||
<tr>
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['title']) ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?></td>
|
||||
<td><?= htmlspecialchars($syncedItem['category'] ?? 'N/A') ?></td>
|
||||
<td><code><?= htmlspecialchars(basename($syncedItem['relative_path'])) ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info rounded mb-3">
|
||||
You currently have no Approved data sources. Request access from DAC Staff or Data Owners to populate this workspace.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($workspaceSync['missing'])): ?>
|
||||
<div class="alert alert-warning rounded mb-3">
|
||||
<strong>Some datasets were skipped:</strong>
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($workspaceSync['missing'] as $missingItem): ?>
|
||||
<li><?= htmlspecialchars($missingItem['title']) ?> — <?= htmlspecialchars($missingItem['reason']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<p class="mb-3">
|
||||
Use the embedded JupyterLab session to explore approved datasets, run R notebooks, or collaborate with Data Owners and DAC Staff.
|
||||
</p>
|
||||
<div class="ratio ratio-16x9 border rounded overflow-hidden">
|
||||
<iframe
|
||||
src="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>"
|
||||
title="R in JupyterHub"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
></iframe>
|
||||
</div>
|
||||
<p class="mt-3 mb-0">
|
||||
Need more space? <a href="<?= htmlspecialchars($jupyterIframeUrl, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">Open Jupyter in a new tab</a>.
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning rounded d-flex align-items-start gap-3">
|
||||
<i class="fas fa-circle-info mt-1"></i>
|
||||
<div>
|
||||
<strong>R in JupyterHub is currently disabled for your account.</strong><br>
|
||||
Request R/Jupyter access from DAC Staff so you can run notebooks directly from this portal.
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
After your access is approved, revisit this page to launch the notebook workspace.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include_once("../includes/footer_user.php"); ?>
|
||||
</body>
|
||||
</html>
|
||||
83
db/migrations/20241103_oauth_tables.sql
Normal file
83
db/migrations/20241103_oauth_tables.sql
Normal file
@@ -0,0 +1,83 @@
|
||||
--
|
||||
-- DSP OAuth schema for JupyterHub integration
|
||||
--
|
||||
-- Run inside the MySQL container, e.g.:
|
||||
-- docker-compose exec db mysql -u root -p niph_dsps < db/migrations/20241103_oauth_tables.sql
|
||||
--
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `dsp_oauth_clients` (
|
||||
`client_id` varchar(128) NOT NULL,
|
||||
`client_name` varchar(255) NOT NULL,
|
||||
`client_secret_hash` varchar(255) DEFAULT NULL,
|
||||
`redirect_uris` text NOT NULL,
|
||||
`allowed_scopes` varchar(255) DEFAULT NULL,
|
||||
`is_confidential` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`is_revoked` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`client_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `dsp_oauth_auth_codes` (
|
||||
`code_hash` char(64) NOT NULL,
|
||||
`client_id` varchar(128) NOT NULL,
|
||||
`person_id` int(11) NOT NULL,
|
||||
`scope` varchar(255) DEFAULT NULL,
|
||||
`redirect_uri` varchar(2000) NOT NULL,
|
||||
`expires_at` datetime NOT NULL,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`code_hash`),
|
||||
KEY `idx_oauth_auth_client` (`client_id`),
|
||||
KEY `idx_oauth_auth_expires` (`expires_at`),
|
||||
CONSTRAINT `dsp_oauth_auth_codes_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `dsp_oauth_clients` (`client_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `dsp_oauth_auth_codes_ibfk_2` FOREIGN KEY (`person_id`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `dsp_oauth_access_tokens` (
|
||||
`token_hash` char(64) NOT NULL,
|
||||
`client_id` varchar(128) NOT NULL,
|
||||
`person_id` int(11) NOT NULL,
|
||||
`scope` varchar(255) DEFAULT NULL,
|
||||
`expires_at` datetime NOT NULL,
|
||||
`refresh_token_hash` char(64) DEFAULT NULL,
|
||||
`refresh_expires_at` datetime DEFAULT NULL,
|
||||
`is_revoked` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_used_at` datetime DEFAULT NULL,
|
||||
`revoked_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`token_hash`),
|
||||
KEY `idx_oauth_access_client` (`client_id`),
|
||||
KEY `idx_oauth_access_person` (`person_id`),
|
||||
KEY `idx_oauth_access_refresh` (`refresh_token_hash`),
|
||||
KEY `idx_oauth_access_expires` (`expires_at`),
|
||||
CONSTRAINT `dsp_oauth_access_tokens_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `dsp_oauth_clients` (`client_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `dsp_oauth_access_tokens_ibfk_2` FOREIGN KEY (`person_id`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
COMMIT;
|
||||
|
||||
--
|
||||
-- Optional helper to register the JupyterHub client.
|
||||
-- Replace the placeholder secret/redirects before running.
|
||||
--
|
||||
-- INSERT INTO dsp_oauth_clients (
|
||||
-- client_id,
|
||||
-- client_name,
|
||||
-- client_secret_hash,
|
||||
-- redirect_uris,
|
||||
-- allowed_scopes
|
||||
-- ) VALUES (
|
||||
-- 'hub-client',
|
||||
-- 'DSP JupyterHub',
|
||||
-- '$2y$10$replace_this_with_password_hash',
|
||||
-- 'https://hub.example.com/hub/oauth_callback',
|
||||
-- 'profile'
|
||||
-- )
|
||||
-- ON DUPLICATE KEY UPDATE
|
||||
-- client_name = VALUES(client_name),
|
||||
-- client_secret_hash = VALUES(client_secret_hash),
|
||||
-- redirect_uris = VALUES(redirect_uris),
|
||||
-- allowed_scopes = VALUES(allowed_scopes),
|
||||
-- updated_at = NOW();
|
||||
2
db/migrations/20250114_add_permission_proof_path.sql
Normal file
2
db/migrations/20250114_add_permission_proof_path.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE dsps_tbl_datasource_permission
|
||||
ADD COLUMN dspsdsp_proof_path VARCHAR(255) DEFAULT NULL AFTER dspsdsp_notes;
|
||||
782
db/niph_dsps.sql
Normal file
782
db/niph_dsps.sql
Normal file
@@ -0,0 +1,782 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.2.1
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Host: localhost
|
||||
-- Generation Time: Oct 04, 2025 at 08:56 AM
|
||||
-- Server version: 10.4.28-MariaDB
|
||||
-- PHP Version: 8.2.4
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Database: `niph_dsps`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_announcement`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_announcement` (
|
||||
`pkdspsann_id` int(11) NOT NULL,
|
||||
`dspsann_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsann_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspsann_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users',
|
||||
`dspsann_title` varchar(255) NOT NULL,
|
||||
`dspsann_description` text NOT NULL,
|
||||
`dspsann_photopath` varchar(255) DEFAULT NULL COMMENT 'Optional image path for announcement',
|
||||
`dspsann_status` enum('Published','Draft','Archived') NOT NULL DEFAULT 'Draft'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_announcement`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_announcement` (`pkdspsann_id`, `dspsann_reg_datetime`, `dspsann_mod_datetime`, `dspsann_reg_by`, `dspsann_title`, `dspsann_description`, `dspsann_photopath`, `dspsann_status`) VALUES
|
||||
(1, '2025-07-19 16:31:56', '2025-07-19 16:33:19', 1, 'Important Policy Update for Data Access', 'We have updated our data access policy to streamline the request process. Please review the new guidelines in the \"My Permissions\" section for Data Users and \"Manage Permissions\" for Data Owners.', NULL, 'Published'),
|
||||
(2, '2025-07-19 16:31:56', '2025-07-19 16:33:23', 1, 'New Research Datasets on Infectious Diseases', 'Exciting new datasets related to recent infectious disease outbreaks are now available for approved researchers. These include anonymized patient data and epidemiological trends.', NULL, 'Published'),
|
||||
(3, '2025-07-19 16:31:56', '2025-07-19 16:31:56', 1, 'Platform Maintenance Scheduled for July 25th', 'Please be advised that the DSPS will undergo scheduled maintenance on July 25th, 2025, from 10:00 PM to 2:00 AM (ICT). During this period, the platform may be temporarily unavailable.', NULL, 'Published'),
|
||||
(4, '2025-07-19 16:31:56', '2025-08-11 08:47:22', 1, 'Call for Data Sharing Proposals', 'NIPH is inviting proposals from researchers and institutions interested in sharing their public health datasets through our platform. Visit the \"Data Owner\" section for more details.', '', 'Archived'),
|
||||
(5, '2025-07-19 16:31:56', '2025-08-11 08:47:15', 1, 'Draft Announcement - Internal Review', 'This is a draft announcement for internal review only. It is not visible to the public.', '', 'Archived');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_anonymous`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_anonymous` (
|
||||
`pkdspsano_id` int(11) NOT NULL,
|
||||
`dspsano_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`fkdspsds_id` int(11) NOT NULL COMMENT 'FK to dsps_tbl_datasource',
|
||||
`dspsano_client_ip` varchar(45) DEFAULT NULL COMMENT 'IPv4 or IPv6 address',
|
||||
`dspsano_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsano_action` varchar(100) DEFAULT NULL COMMENT 'e.g., View Introduction, Clicked Link'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_datasource`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_datasource` (
|
||||
`pkdspsds_id` int(11) NOT NULL,
|
||||
`dspsds_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsds_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspsds_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users (who registered this data source)',
|
||||
`fkdspstds_id` int(11) NOT NULL COMMENT 'FK to dsps_tbl_typedatasource',
|
||||
`fkdspscate_id` int(11) NOT NULL COMMENT 'FK to dsps_tbl_dspscategory',
|
||||
`fkisp_id_of` int(11) NOT NULL COMMENT 'FK to ist_tbl_people (Data Owner of this source)',
|
||||
`dspsds_filename` varchar(255) DEFAULT NULL COMMENT 'File path/name for CSV/JSON/PDF, or API endpoint URL',
|
||||
`dspsds_title_en` varchar(255) NOT NULL,
|
||||
`dspsds_title_kh` varchar(255) DEFAULT NULL,
|
||||
`dspsds_description` text DEFAULT NULL,
|
||||
`dspsds_public_date` date DEFAULT NULL COMMENT 'Date when data source was made public',
|
||||
`dspsds_status` varchar(20) NOT NULL DEFAULT 'Pending Review',
|
||||
`dspsds_filename1` varchar(250) DEFAULT NULL,
|
||||
`dspsds_filename2` varchar(250) DEFAULT NULL,
|
||||
`dspsds_filename3` varchar(250) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_datasource`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_datasource` (`pkdspsds_id`, `dspsds_reg_datetime`, `dspsds_mod_datetime`, `dspsds_reg_by`, `fkdspstds_id`, `fkdspscate_id`, `fkisp_id_of`, `dspsds_filename`, `dspsds_title_en`, `dspsds_title_kh`, `dspsds_description`, `dspsds_public_date`, `dspsds_status`, `dspsds_filename1`, `dspsds_filename2`, `dspsds_filename3`) VALUES
|
||||
(4, '2025-07-20 12:43:12', '2025-07-20 13:01:06', 2, 1, 3, 2, 'population_health_2023.csv', 'National Population Health Survey 2023', 'ការស្ទង់មតិសុខភាពប្រជាជនជាតិឆ្នាំ២០២៣', 'Comprehensive dataset on health indicators, demographics, and disease prevalence across Cambodia. Data collected in 2023.', NULL, 'Active', '', NULL, ''),
|
||||
(9, '2025-07-20 12:46:56', '2025-07-26 10:26:42', 2, 1, 1, 2, 'infectious_disease_api_endpoint.json', 'API Endpoint for Infectious Disease Trends', 'ចំណុចបញ្ចប់ API សម្រាប់និន្នាការជំងឺឆ្លង', 'Real-time data access for common infectious diseases, including incidence rates and geographical distribution. Requires API key.', NULL, 'Active', '', NULL, ''),
|
||||
(10, '2025-08-09 12:36:52', '2025-08-09 12:36:52', 2, 1, 2, 2, 'datasource_6896de743a3f0.csv', 'TEST001', 'test001', 'test', NULL, 'Active', '', NULL, ''),
|
||||
(11, '2025-08-30 13:55:40', '2025-08-30 13:55:40', 206, 4, 1, 104, 'datasource_68b2a06c7245e.pdf', 'test for Contributor', 'test', 'test for Contributor', NULL, 'Active', '', NULL, ''),
|
||||
(12, '2025-09-06 15:55:39', '2025-09-06 15:55:39', 2, 1, 2, 2, 'datasource_68bbf70b91bc0.xlsx', 'Test5555', '', 'Test5555', NULL, 'Active', NULL, NULL, NULL);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_datasource_permission`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_datasource_permission` (
|
||||
`pkdspsdsp_id` int(11) NOT NULL,
|
||||
`dspsdsp_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsdsp_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspsdsp_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users (who granted permission, usually Data Owner)',
|
||||
`fkdspsds_id` int(11) NOT NULL COMMENT 'FK to dsps_tbl_datasource',
|
||||
`fkisp_id_of` int(11) NOT NULL COMMENT 'FK to ist_tbl_people (User who is granted permission)',
|
||||
`dspsdsp_datetime` datetime DEFAULT current_timestamp() COMMENT 'When permission was granted/requested',
|
||||
`dspsdsp_permission` enum('Read','Download','Analyze') NOT NULL,
|
||||
`dspsdsp_notes` text DEFAULT NULL,
|
||||
`dspsdsp_proof_path` varchar(255) DEFAULT NULL,
|
||||
`dspsdsp_status` enum('Approved','Pending','Rejected','Revoked') NOT NULL DEFAULT 'Pending'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_datasource_permission`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_datasource_permission` (`pkdspsdsp_id`, `dspsdsp_reg_datetime`, `dspsdsp_mod_datetime`, `dspsdsp_reg_by`, `fkdspsds_id`, `fkisp_id_of`, `dspsdsp_datetime`, `dspsdsp_permission`, `dspsdsp_notes`, `dspsdsp_proof_path`, `dspsdsp_status`) VALUES
|
||||
(1, '2025-08-09 11:23:07', '2025-08-09 11:46:06', 2, 9, 3, '2025-08-09 11:23:07', 'Download', '', NULL, 'Approved'),
|
||||
(4, '2025-08-09 11:39:22', '2025-08-09 11:46:03', 2, 4, 3, '2025-08-09 11:39:22', 'Read', '', NULL, 'Approved'),
|
||||
(5, '2025-08-09 12:48:09', '2025-08-09 12:48:39', 2, 10, 3, '2025-08-09 12:48:09', 'Download', '', NULL, 'Approved'),
|
||||
(6, '2025-08-30 14:00:25', '2025-08-30 14:01:09', 206, 11, 105, '2025-08-30 14:00:25', 'Download', '', NULL, 'Approved'),
|
||||
(7, '2025-08-30 14:26:17', '2025-08-30 14:29:27', 2, 10, 104, '2025-08-30 14:26:17', 'Download', '', NULL, 'Approved');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_datasource_used`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_datasource_used` (
|
||||
`pkdspsdspused_id` int(11) NOT NULL,
|
||||
`dspsdspused_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsdspused_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspsdspused_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users (who performed the action)',
|
||||
`fkdspsdsused_id` int(11) NOT NULL COMMENT 'FK to dsps_tbl_datasource (the data source that was used)',
|
||||
`fkisp_id_of` int(11) NOT NULL COMMENT 'FK to ist_tbl_people (the user who used it)',
|
||||
`dspsdspused_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsdspused_action` varchar(100) DEFAULT NULL COMMENT 'e.g., Downloaded, Accessed API, Ran Analysis'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_datasource_used`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_datasource_used` (`pkdspsdspused_id`, `dspsdspused_reg_datetime`, `dspsdspused_mod_datetime`, `dspsdspused_reg_by`, `fkdspsdsused_id`, `fkisp_id_of`, `dspsdspused_datetime`, `dspsdspused_action`) VALUES
|
||||
(1, '2025-08-09 13:08:06', '2025-08-09 13:08:06', NULL, 10, 3, '2025-08-09 13:08:06', 'Downloaded'),
|
||||
(2, '2025-08-09 13:09:06', '2025-08-09 13:09:06', NULL, 9, 3, '2025-08-09 13:09:06', 'Downloaded'),
|
||||
(3, '2025-08-09 14:34:08', '2025-08-09 14:34:08', NULL, 10, 3, '2025-08-09 14:34:08', 'Downloaded'),
|
||||
(4, '2025-08-09 14:34:15', '2025-08-09 14:34:15', NULL, 10, 3, '2025-08-09 14:34:15', 'Downloaded'),
|
||||
(5, '2025-08-30 14:30:39', '2025-08-30 14:30:39', NULL, 10, 104, '2025-08-30 14:30:39', 'Downloaded');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_dspsabout`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_dspsabout` (
|
||||
`pkdspsabout_id` int(11) NOT NULL,
|
||||
`dspsabout_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsabout_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspsabout_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users',
|
||||
`fkisp_id_of` int(11) NOT NULL COMMENT 'FK to ist_tbl_people (who created/modified this about entry)',
|
||||
`dspsabout_title_en` varchar(255) NOT NULL,
|
||||
`dspsabout_description` text DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_dspsabout`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_dspsabout` (`pkdspsabout_id`, `dspsabout_reg_datetime`, `dspsabout_mod_datetime`, `dspsabout_reg_by`, `fkisp_id_of`, `dspsabout_title_en`, `dspsabout_description`) VALUES
|
||||
(1, '2025-07-19 16:35:41', '2025-07-19 16:35:41', 1, 1, 'Our Vision', 'To be a leading institution in public health, fostering a healthier and more resilient community through evidence-based practices and collaborative data sharing.'),
|
||||
(2, '2025-07-19 16:35:41', '2025-07-19 16:35:41', 1, 1, 'Our Mission', 'To protect and promote the health of the population through scientific research, education, and effective public health interventions, facilitated by accessible and secure data.'),
|
||||
(3, '2025-07-19 16:35:41', '2025-07-19 16:35:41', 1, 1, 'Our Goals', '1. Enhance data accessibility for public health research. 2. Promote data-driven decision-making. 3. Strengthen collaboration among health stakeholders. 4. Ensure data security and privacy.'),
|
||||
(4, '2025-07-19 16:35:41', '2025-07-19 16:35:41', 1, 1, 'About NIPH', 'The National Institute of Public Health (NIPH) is a governmental institution under the Ministry of Health, Cambodia. Established to conduct research, provide training, and offer public health services, NIPH plays a crucial role in improving the health status of the Cambodian population. This data sharing platform is an initiative to further our mission by enabling secure and efficient data exchange.');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_dspscategory`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_dspscategory` (
|
||||
`pkdspscate_id` int(11) NOT NULL,
|
||||
`dspscate_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspscate_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspscate_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users',
|
||||
`dspscate_title_en` varchar(255) NOT NULL,
|
||||
`dspscate_details` text DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_dspscategory`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_dspscategory` (`pkdspscate_id`, `dspscate_reg_datetime`, `dspscate_mod_datetime`, `dspscate_reg_by`, `dspscate_title_en`, `dspscate_details`) VALUES
|
||||
(1, '2025-07-12 16:54:58', '2025-07-19 14:50:31', NULL, 'Public Health', 'Data related to public health, diseases, and demographics.'),
|
||||
(2, '2025-07-12 16:54:58', '2025-07-19 14:50:38', NULL, 'Education Statistics', 'Statistical data on schools, students, and educational outcomes.'),
|
||||
(3, '2025-07-12 16:54:58', '2025-07-19 14:50:41', NULL, 'Environmental Data', 'Data concerning climate, pollution, and natural resources.');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_dspsfaq`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_dspsfaq` (
|
||||
`pkdspsfaq_id` int(11) NOT NULL,
|
||||
`dspsfaq_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsfaq_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspsfaq_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users',
|
||||
`fkisp_id_of` int(11) NOT NULL COMMENT 'FK to ist_tbl_people (who created/modified this FAQ)',
|
||||
`dspsfaq_title_en` varchar(255) NOT NULL COMMENT 'Question',
|
||||
`dspsfaq_description` text NOT NULL COMMENT 'Answer'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_dspsfaq`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_dspsfaq` (`pkdspsfaq_id`, `dspsfaq_reg_datetime`, `dspsfaq_mod_datetime`, `dspsfaq_reg_by`, `fkisp_id_of`, `dspsfaq_title_en`, `dspsfaq_description`) VALUES
|
||||
(1, '2025-07-19 16:38:29', '2025-07-26 22:23:24', 1, 1, 'What is the NIPH Data Sharing Platform?', 'The NIPH Data Sharing Platform (DSP) is a secure online portal designed to facilitate the sharing and access of public health data among authorized researchers, policymakers, and the public. It aims to promote data-driven decision-making and collaborative research.'),
|
||||
(2, '2025-07-19 16:38:29', '2025-07-19 16:38:29', 1, 1, 'How can I request access to data?', 'Data Users can browse available data sources and submit a formal request for access through the platform. The request will be reviewed by the respective Data Owner, who will approve or deny access based on the data\'s sensitivity and the user\'s justification.'),
|
||||
(3, '2025-07-19 16:38:29', '2025-07-19 16:38:29', 1, 1, 'What types of data are available?', 'The platform hosts various types of public health data, including but not limited to epidemiological surveillance data, survey results, laboratory data, and research findings. Data formats may include CSV, JSON, PDF, and potentially API access.'),
|
||||
(4, '2025-07-19 16:38:29', '2025-07-19 16:38:29', 1, 1, 'Is my data secure on this platform?', 'Yes, data security and privacy are paramount. The platform employs robust security measures, including secure authentication, role-based access control, and data encryption. All sensitive data is handled in compliance with national data protection regulations.'),
|
||||
(5, '2025-07-19 16:38:29', '2025-07-19 16:38:29', 1, 1, 'How can I contribute my data?', 'If you are a Data Owner (e.g., a researcher or institution with relevant public health data), you can register on the platform and use the \"Manage My Data Sources\" section to upload and describe your datasets. All contributions are subject to review by DAC Staff.');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_dspsslide`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_dspsslide` (
|
||||
`pkdspsslide_id` int(11) NOT NULL,
|
||||
`dspsslide_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsslide_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspsslide_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users',
|
||||
`fkisp_id_of` int(11) NOT NULL COMMENT 'FK to ist_tbl_people (who created/modified this slide)',
|
||||
`dspsslide_title_en` varchar(255) NOT NULL,
|
||||
`dspsslide_description` text DEFAULT NULL,
|
||||
`dspsslide_photoname` varchar(255) NOT NULL COMMENT 'File path/name of the slide image'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_dspsslide`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_dspsslide` (`pkdspsslide_id`, `dspsslide_reg_datetime`, `dspsslide_mod_datetime`, `dspsslide_reg_by`, `fkisp_id_of`, `dspsslide_title_en`, `dspsslide_description`, `dspsslide_photoname`) VALUES
|
||||
(1, '2025-07-20 10:24:56', '2025-08-12 10:01:29', 1, 1, 'Welcome to NIPH Data Sharing Platform', 'Your central hub for public health data and collaborative research.', 'slide_689aae8993e8c.jpg'),
|
||||
(2, '2025-07-20 10:24:56', '2025-07-20 10:29:44', 1, 1, 'Explore Diverse Datasets', 'Access a wide range of epidemiological, clinical, and environmental health data.', 'slide_68f5da6a43a8d.jpg'),
|
||||
(3, '2025-07-20 10:24:56', '2025-07-20 10:29:46', 1, 1, 'Empowering Data-Driven Decisions', 'Facilitating informed policy-making and public health interventions.', 'slide_68f5da6a43a8a.jpg');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_feedback`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_feedback` (
|
||||
`pkdspsfb_id` int(11) NOT NULL,
|
||||
`dspsfb_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspsfb_res_datetime` datetime DEFAULT NULL COMMENT 'Response datetime',
|
||||
`dspsfb_res_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users (who responded)',
|
||||
`dspsfb_client_ip` varchar(45) DEFAULT NULL COMMENT 'IP of the feedback submitter',
|
||||
`dspsfb_name` varchar(255) NOT NULL,
|
||||
`dspsfb_email` varchar(255) DEFAULT NULL,
|
||||
`dspsfb_body_text` text NOT NULL,
|
||||
`dspsfb_respond_text` text DEFAULT NULL COMMENT 'Response from DAC staff/admin',
|
||||
`dspsfb_status` enum('New','In Progress','Resolved','Archived') NOT NULL DEFAULT 'New'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_social`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_social` (
|
||||
`pkdspssocial_id` int(11) NOT NULL,
|
||||
`dspssocial_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspssocial_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspssocial_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users',
|
||||
`dspssocial_name` varchar(100) NOT NULL COMMENT 'e.g., Facebook, YouTube, Telegram',
|
||||
`dspssocial_link` varchar(255) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `dsps_tbl_typedatasource`
|
||||
--
|
||||
|
||||
CREATE TABLE `dsps_tbl_typedatasource` (
|
||||
`pkdspstds_id` int(11) NOT NULL,
|
||||
`dspstds_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`dspstds_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`dspstds_reg_by` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users',
|
||||
`dspstds_name_en` varchar(100) NOT NULL,
|
||||
`dspstds_name_kh` varchar(100) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `dsps_tbl_typedatasource`
|
||||
--
|
||||
|
||||
INSERT INTO `dsps_tbl_typedatasource` (`pkdspstds_id`, `dspstds_reg_datetime`, `dspstds_mod_datetime`, `dspstds_reg_by`, `dspstds_name_en`, `dspstds_name_kh`) VALUES
|
||||
(1, '2025-07-12 16:54:58', '2025-07-12 16:54:58', NULL, 'CSV', 'ស៊ីអេសវី'),
|
||||
(2, '2025-07-12 16:54:58', '2025-07-12 16:54:58', NULL, 'JSON', 'ជេសអិន'),
|
||||
(3, '2025-07-12 16:54:58', '2025-07-12 16:54:58', NULL, 'API', 'អេភីអាយ'),
|
||||
(4, '2025-07-12 16:54:58', '2025-07-12 16:54:58', NULL, 'PDF', 'ភីឌីអេហ្វ');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `ist_tbl_people`
|
||||
--
|
||||
|
||||
CREATE TABLE `ist_tbl_people` (
|
||||
`pkisp_id` int(11) NOT NULL,
|
||||
`isp_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`isp_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`isp_regby_id` int(11) DEFAULT NULL COMMENT 'FK to ist_tbl_users if registered by another user, or NULL if self-registered',
|
||||
`isp_idcard` varchar(50) DEFAULT NULL,
|
||||
`isp_firstname_en` varchar(100) NOT NULL,
|
||||
`isp_lastname_en` varchar(100) NOT NULL,
|
||||
`isp_sex` enum('Male','Female','Other') NOT NULL,
|
||||
`isp_dob` date NOT NULL,
|
||||
`isp_pob` varchar(255) DEFAULT NULL COMMENT 'Place of Birth',
|
||||
`isp_nationality` varchar(100) DEFAULT 'Cambodian',
|
||||
`isp_marital_status` enum('Single','Married','Divorced','Widowed') DEFAULT 'Single',
|
||||
`isp_phone_number` varchar(20) DEFAULT NULL,
|
||||
`isp_email` varchar(255) DEFAULT NULL,
|
||||
`isp_telegram` varchar(255) DEFAULT NULL,
|
||||
`isp_note` text DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `ist_tbl_people`
|
||||
--
|
||||
|
||||
INSERT INTO `ist_tbl_people` (`pkisp_id`, `isp_reg_datetime`, `isp_mod_datetime`, `isp_regby_id`, `isp_idcard`, `isp_firstname_en`, `isp_lastname_en`, `isp_sex`, `isp_dob`, `isp_pob`, `isp_nationality`, `isp_marital_status`, `isp_phone_number`, `isp_email`, `isp_telegram`, `isp_note`) VALUES
|
||||
(1, '2025-07-12 16:16:55', '2025-07-12 16:16:55', NULL, '123456789', 'Admin', 'User', 'Male', '1990-01-01', NULL, 'Cambodian', 'Single', '0123456789', 'admin@example.com', NULL, NULL),
|
||||
(2, '2025-07-12 16:16:55', '2025-07-12 16:16:55', NULL, '987654321', 'Data', 'Owner', 'Female', '1985-05-10', NULL, 'Cambodian', 'Single', '0987654321', 'owner@example.com', NULL, NULL),
|
||||
(3, '2025-07-12 16:16:55', '2025-07-12 16:16:55', NULL, '112233445', 'Data', 'User', 'Male', '1992-11-20', NULL, 'Cambodian', 'Single', '0112233445', 'user@example.com', NULL, NULL),
|
||||
(101, '2025-07-20 12:43:02', '2025-07-20 12:43:02', NULL, '1234567890', 'Mock', 'Owner', 'Male', '1980-01-01', NULL, 'Cambodian', 'Single', '111222333', 'mock.owner@example.com', NULL, NULL),
|
||||
(102, '2025-08-30 08:34:25', '2025-08-30 08:34:25', NULL, '11', '11', '111', 'Male', '2025-08-30', NULL, 'Cambodian', 'Single', '', '', NULL, NULL),
|
||||
(103, '2025-08-30 08:37:51', '2025-08-30 08:37:51', NULL, 'N001', 'Pisey', 'Um', 'Female', '2025-08-22', NULL, 'Cambodian', 'Single', '012', 'pisey@gmail.com', NULL, NULL),
|
||||
(104, '2025-08-30 09:07:13', '2025-08-30 09:07:13', NULL, 'N002', 'sp', 'ch', 'Male', '2025-08-30', NULL, 'Cambodian', 'Single', '092', 'sp@gmail.com', NULL, NULL),
|
||||
(105, '2025-08-30 10:46:55', '2025-08-30 10:46:55', NULL, 'p001', 'pp', 'ppp', 'Male', '2025-08-30', NULL, 'Cambodian', 'Single', '123', 'pp@gmail.com', NULL, NULL);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `ist_tbl_users`
|
||||
--
|
||||
|
||||
CREATE TABLE `ist_tbl_users` (
|
||||
`pkisu_id` int(11) NOT NULL,
|
||||
`isu_reg_datetime` datetime DEFAULT current_timestamp(),
|
||||
`isu_mod_datetime` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`isu_regby_id` int(11) DEFAULT NULL COMMENT 'FK to pkisu_id if registered by another user, or NULL if self-registered',
|
||||
`fkisp_id_of` int(11) NOT NULL COMMENT 'FK to ist_tbl_people',
|
||||
`isu_name` varchar(100) NOT NULL COMMENT 'Username',
|
||||
`isu_password` varchar(255) NOT NULL COMMENT 'Hashed password',
|
||||
`isu_status` enum('DAC Staff','Data Contributor','Data Owner','Data User','Inactive') NOT NULL DEFAULT 'Data User',
|
||||
`isu_can_run_r` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 if user may run R/Jupyter integrations'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `ist_tbl_users`
|
||||
--
|
||||
|
||||
INSERT INTO `ist_tbl_users` (`pkisu_id`, `isu_reg_datetime`, `isu_mod_datetime`, `isu_regby_id`, `fkisp_id_of`, `isu_name`, `isu_password`, `isu_status`, `isu_can_run_r`) VALUES
|
||||
(1, '2025-07-12 16:16:55', '2025-07-20 10:48:49', 1, 1, 'admin', '$2y$10$2h/CFUM6D8d0nFSOgUQebOL/ow5pyTAsDRcqZAv0XrkAmyhVs2fIe', 'DAC Staff', 1),
|
||||
(2, '2025-07-12 16:16:55', '2025-07-20 12:05:18', 1, 2, 'owner', '$2y$10$2h/CFUM6D8d0nFSOgUQebOL/ow5pyTAsDRcqZAv0XrkAmyhVs2fIe', 'Data Owner', 1),
|
||||
(3, '2025-07-12 16:16:55', '2025-08-11 08:41:57', 1, 3, 'user', '$2y$10$2h/CFUM6D8d0nFSOgUQebOL/ow5pyTAsDRcqZAv0XrkAmyhVs2fIe', 'Data User', 0),
|
||||
(203, '2025-07-12 16:16:55', '2025-08-11 08:41:57', 3, 101, 'mockup', '$2y$10$2h/CFUM6D8d0nFSOgUQebOL/ow5pyTAsDRcqZAv0XrkAmyhVs2fIe', 'Data User', 0),
|
||||
(204, '2025-08-30 08:34:25', '2025-08-30 08:34:25', NULL, 102, 'ttt', '$2y$10$aOBNcCE9b1Jh.c.g7tP5gOSoD6RKujzPVV3AMGJH02jm.Uom0.GxS', 'Data User', 0),
|
||||
(205, '2025-08-30 08:37:51', '2025-08-30 09:10:33', 1, 103, 'pisey', '$2y$10$DYdxGJLZ3XFJWZI.Tcq2IO18DOhXw2KBwnzAXZ3SD8zD0Bw/vtyOO', 'Data Owner', 1),
|
||||
(206, '2025-08-30 09:07:14', '2025-08-30 10:37:43', NULL, 104, 'sp', '$2y$10$yy9L1fK5Il2e3sSw03pyTukxGRRxU5bxc4Zp09fSZ4GvfhkdcZy4W', 'Data Contributor', 0),
|
||||
(207, '2025-08-30 10:46:56', '2025-08-30 10:46:56', NULL, 105, 'pp', '$2y$10$JBTBNdWoifyQ3kodJjJfc.c8CQlbVNKVvfJ.lSKiBcVU5W8PkyVom', 'Data User', 0);
|
||||
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_announcement`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_announcement`
|
||||
ADD PRIMARY KEY (`pkdspsann_id`),
|
||||
ADD KEY `dspsann_reg_by` (`dspsann_reg_by`),
|
||||
ADD KEY `idx_dspsann_status` (`dspsann_status`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_anonymous`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_anonymous`
|
||||
ADD PRIMARY KEY (`pkdspsano_id`),
|
||||
ADD KEY `fkdspsds_id` (`fkdspsds_id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_datasource`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource`
|
||||
ADD PRIMARY KEY (`pkdspsds_id`),
|
||||
ADD KEY `dspsds_reg_by` (`dspsds_reg_by`),
|
||||
ADD KEY `fkdspstds_id` (`fkdspstds_id`),
|
||||
ADD KEY `fkdspscate_id` (`fkdspscate_id`),
|
||||
ADD KEY `fkisp_id_of` (`fkisp_id_of`),
|
||||
ADD KEY `idx_dspsds_status` (`dspsds_status`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_datasource_permission`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource_permission`
|
||||
ADD PRIMARY KEY (`pkdspsdsp_id`),
|
||||
ADD UNIQUE KEY `fkdspsds_id` (`fkdspsds_id`,`fkisp_id_of`),
|
||||
ADD KEY `dspsdsp_reg_by` (`dspsdsp_reg_by`),
|
||||
ADD KEY `fkisp_id_of` (`fkisp_id_of`),
|
||||
ADD KEY `idx_dspsdsp_status` (`dspsdsp_status`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_datasource_used`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource_used`
|
||||
ADD PRIMARY KEY (`pkdspsdspused_id`),
|
||||
ADD KEY `dspsdspused_reg_by` (`dspsdspused_reg_by`),
|
||||
ADD KEY `fkdspsdsused_id` (`fkdspsdsused_id`),
|
||||
ADD KEY `fkisp_id_of` (`fkisp_id_of`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_dspsabout`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsabout`
|
||||
ADD PRIMARY KEY (`pkdspsabout_id`),
|
||||
ADD KEY `fkisp_id_of` (`fkisp_id_of`),
|
||||
ADD KEY `dspsabout_reg_by` (`dspsabout_reg_by`),
|
||||
ADD KEY `idx_dspsabout_title` (`dspsabout_title_en`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_dspscategory`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspscategory`
|
||||
ADD PRIMARY KEY (`pkdspscate_id`),
|
||||
ADD UNIQUE KEY `dspscate_title_en` (`dspscate_title_en`),
|
||||
ADD KEY `dspscate_reg_by` (`dspscate_reg_by`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_dspsfaq`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsfaq`
|
||||
ADD PRIMARY KEY (`pkdspsfaq_id`),
|
||||
ADD KEY `fkisp_id_of` (`fkisp_id_of`),
|
||||
ADD KEY `dspsfaq_reg_by` (`dspsfaq_reg_by`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_dspsslide`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsslide`
|
||||
ADD PRIMARY KEY (`pkdspsslide_id`),
|
||||
ADD KEY `fkisp_id_of` (`fkisp_id_of`),
|
||||
ADD KEY `dspsslide_reg_by` (`dspsslide_reg_by`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_feedback`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_feedback`
|
||||
ADD PRIMARY KEY (`pkdspsfb_id`),
|
||||
ADD KEY `dspsfb_res_by` (`dspsfb_res_by`),
|
||||
ADD KEY `idx_dspsfb_status` (`dspsfb_status`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_social`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_social`
|
||||
ADD PRIMARY KEY (`pkdspssocial_id`),
|
||||
ADD UNIQUE KEY `dspssocial_name` (`dspssocial_name`),
|
||||
ADD KEY `dspssocial_reg_by` (`dspssocial_reg_by`);
|
||||
|
||||
--
|
||||
-- Indexes for table `dsps_tbl_typedatasource`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_typedatasource`
|
||||
ADD PRIMARY KEY (`pkdspstds_id`),
|
||||
ADD UNIQUE KEY `dspstds_name_en` (`dspstds_name_en`),
|
||||
ADD UNIQUE KEY `dspstds_name_kh` (`dspstds_name_kh`),
|
||||
ADD KEY `dspstds_reg_by` (`dspstds_reg_by`);
|
||||
|
||||
--
|
||||
-- Indexes for table `ist_tbl_people`
|
||||
--
|
||||
ALTER TABLE `ist_tbl_people`
|
||||
ADD PRIMARY KEY (`pkisp_id`),
|
||||
ADD UNIQUE KEY `isp_idcard` (`isp_idcard`),
|
||||
ADD UNIQUE KEY `isp_phone_number` (`isp_phone_number`),
|
||||
ADD UNIQUE KEY `isp_email` (`isp_email`),
|
||||
ADD KEY `idx_isp_idcard` (`isp_idcard`),
|
||||
ADD KEY `idx_isp_name` (`isp_firstname_en`,`isp_lastname_en`);
|
||||
|
||||
--
|
||||
-- Indexes for table `ist_tbl_users`
|
||||
--
|
||||
ALTER TABLE `ist_tbl_users`
|
||||
ADD PRIMARY KEY (`pkisu_id`),
|
||||
ADD UNIQUE KEY `fkisp_id_of` (`fkisp_id_of`),
|
||||
ADD UNIQUE KEY `isu_name` (`isu_name`),
|
||||
ADD KEY `isu_regby_id` (`isu_regby_id`),
|
||||
ADD KEY `idx_isu_name` (`isu_name`),
|
||||
ADD KEY `idx_isu_status` (`isu_status`);
|
||||
|
||||
--
|
||||
-- Table structure for table `dsp_oauth_clients`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `dsp_oauth_clients`;
|
||||
CREATE TABLE `dsp_oauth_clients` (
|
||||
`client_id` varchar(128) NOT NULL,
|
||||
`client_name` varchar(255) NOT NULL,
|
||||
`client_secret_hash` varchar(255) DEFAULT NULL,
|
||||
`redirect_uris` text NOT NULL,
|
||||
`allowed_scopes` varchar(255) DEFAULT NULL,
|
||||
`is_confidential` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`is_revoked` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`client_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `dsp_oauth_auth_codes`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `dsp_oauth_auth_codes`;
|
||||
CREATE TABLE `dsp_oauth_auth_codes` (
|
||||
`code_hash` char(64) NOT NULL,
|
||||
`client_id` varchar(128) NOT NULL,
|
||||
`person_id` int(11) NOT NULL,
|
||||
`scope` varchar(255) DEFAULT NULL,
|
||||
`redirect_uri` varchar(2000) NOT NULL,
|
||||
`expires_at` datetime NOT NULL,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`code_hash`),
|
||||
KEY `idx_oauth_auth_client` (`client_id`),
|
||||
KEY `idx_oauth_auth_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `dsp_oauth_access_tokens`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `dsp_oauth_access_tokens`;
|
||||
CREATE TABLE `dsp_oauth_access_tokens` (
|
||||
`token_hash` char(64) NOT NULL,
|
||||
`client_id` varchar(128) NOT NULL,
|
||||
`person_id` int(11) NOT NULL,
|
||||
`scope` varchar(255) DEFAULT NULL,
|
||||
`expires_at` datetime NOT NULL,
|
||||
`refresh_token_hash` char(64) DEFAULT NULL,
|
||||
`refresh_expires_at` datetime DEFAULT NULL,
|
||||
`is_revoked` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_used_at` datetime DEFAULT NULL,
|
||||
`revoked_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`token_hash`),
|
||||
KEY `idx_oauth_access_client` (`client_id`),
|
||||
KEY `idx_oauth_access_person` (`person_id`),
|
||||
KEY `idx_oauth_access_refresh` (`refresh_token_hash`),
|
||||
KEY `idx_oauth_access_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_announcement`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_announcement`
|
||||
MODIFY `pkdspsann_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_anonymous`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_anonymous`
|
||||
MODIFY `pkdspsano_id` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_datasource`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource`
|
||||
MODIFY `pkdspsds_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_datasource_permission`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource_permission`
|
||||
MODIFY `pkdspsdsp_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=8;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_datasource_used`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource_used`
|
||||
MODIFY `pkdspsdspused_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_dspsabout`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsabout`
|
||||
MODIFY `pkdspsabout_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_dspscategory`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspscategory`
|
||||
MODIFY `pkdspscate_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_dspsfaq`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsfaq`
|
||||
MODIFY `pkdspsfaq_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_dspsslide`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsslide`
|
||||
MODIFY `pkdspsslide_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_feedback`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_feedback`
|
||||
MODIFY `pkdspsfb_id` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_social`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_social`
|
||||
MODIFY `pkdspssocial_id` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `dsps_tbl_typedatasource`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_typedatasource`
|
||||
MODIFY `pkdspstds_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `ist_tbl_people`
|
||||
--
|
||||
ALTER TABLE `ist_tbl_people`
|
||||
MODIFY `pkisp_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=106;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `ist_tbl_users`
|
||||
--
|
||||
ALTER TABLE `ist_tbl_users`
|
||||
MODIFY `pkisu_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=208;
|
||||
|
||||
--
|
||||
-- Constraints for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_announcement`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_announcement`
|
||||
ADD CONSTRAINT `dsps_tbl_announcement_ibfk_1` FOREIGN KEY (`dspsann_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_anonymous`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_anonymous`
|
||||
ADD CONSTRAINT `dsps_tbl_anonymous_ibfk_1` FOREIGN KEY (`fkdspsds_id`) REFERENCES `dsps_tbl_datasource` (`pkdspsds_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_datasource`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource`
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_ibfk_1` FOREIGN KEY (`dspsds_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_ibfk_2` FOREIGN KEY (`fkdspstds_id`) REFERENCES `dsps_tbl_typedatasource` (`pkdspstds_id`) ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_ibfk_3` FOREIGN KEY (`fkdspscate_id`) REFERENCES `dsps_tbl_dspscategory` (`pkdspscate_id`) ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_ibfk_4` FOREIGN KEY (`fkisp_id_of`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_datasource_permission`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource_permission`
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_permission_ibfk_1` FOREIGN KEY (`dspsdsp_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_permission_ibfk_2` FOREIGN KEY (`fkdspsds_id`) REFERENCES `dsps_tbl_datasource` (`pkdspsds_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_permission_ibfk_3` FOREIGN KEY (`fkisp_id_of`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_datasource_used`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_datasource_used`
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_used_ibfk_1` FOREIGN KEY (`dspsdspused_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_used_ibfk_2` FOREIGN KEY (`fkdspsdsused_id`) REFERENCES `dsps_tbl_datasource` (`pkdspsds_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_datasource_used_ibfk_3` FOREIGN KEY (`fkisp_id_of`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_dspsabout`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsabout`
|
||||
ADD CONSTRAINT `dsps_tbl_dspsabout_ibfk_1` FOREIGN KEY (`fkisp_id_of`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_dspsabout_ibfk_2` FOREIGN KEY (`dspsabout_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_dspscategory`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspscategory`
|
||||
ADD CONSTRAINT `dsps_tbl_dspscategory_ibfk_1` FOREIGN KEY (`dspscate_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_dspsfaq`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsfaq`
|
||||
ADD CONSTRAINT `dsps_tbl_dspsfaq_ibfk_1` FOREIGN KEY (`fkisp_id_of`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_dspsfaq_ibfk_2` FOREIGN KEY (`dspsfaq_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_dspsslide`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_dspsslide`
|
||||
ADD CONSTRAINT `dsps_tbl_dspsslide_ibfk_1` FOREIGN KEY (`fkisp_id_of`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsps_tbl_dspsslide_ibfk_2` FOREIGN KEY (`dspsslide_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_feedback`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_feedback`
|
||||
ADD CONSTRAINT `dsps_tbl_feedback_ibfk_1` FOREIGN KEY (`dspsfb_res_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_social`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_social`
|
||||
ADD CONSTRAINT `dsps_tbl_social_ibfk_1` FOREIGN KEY (`dspssocial_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsps_tbl_typedatasource`
|
||||
--
|
||||
ALTER TABLE `dsps_tbl_typedatasource`
|
||||
ADD CONSTRAINT `dsps_tbl_typedatasource_ibfk_1` FOREIGN KEY (`dspstds_reg_by`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsp_oauth_auth_codes`
|
||||
--
|
||||
ALTER TABLE `dsp_oauth_auth_codes`
|
||||
ADD CONSTRAINT `dsp_oauth_auth_codes_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `dsp_oauth_clients` (`client_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsp_oauth_auth_codes_ibfk_2` FOREIGN KEY (`person_id`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `dsp_oauth_access_tokens`
|
||||
--
|
||||
ALTER TABLE `dsp_oauth_access_tokens`
|
||||
ADD CONSTRAINT `dsp_oauth_access_tokens_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `dsp_oauth_clients` (`client_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `dsp_oauth_access_tokens_ibfk_2` FOREIGN KEY (`person_id`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ist_tbl_users`
|
||||
--
|
||||
ALTER TABLE `ist_tbl_users`
|
||||
ADD CONSTRAINT `ist_tbl_users_ibfk_1` FOREIGN KEY (`fkisp_id_of`) REFERENCES `ist_tbl_people` (`pkisp_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `ist_tbl_users_ibfk_2` FOREIGN KEY (`isu_regby_id`) REFERENCES `ist_tbl_users` (`pkisu_id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
78
docker-compose.yml
Normal file
78
docker-compose.yml
Normal file
@@ -0,0 +1,78 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: dsp_app
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "4010:80"
|
||||
- "8082:80"
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_PORT: 3306
|
||||
DB_NAME: niph_dsps
|
||||
DB_USER: dsp_user
|
||||
DB_PASS: dsp_pass
|
||||
JUPYTER_EXTERNAL_URL: ${JUPYTER_EXTERNAL_URL:-}
|
||||
JUPYTER_PORT: ${JUPYTER_PORT:-443}
|
||||
DSP_APP_ORIGINS: ${DSP_APP_ORIGINS:-}
|
||||
DSP_FRAME_ANCESTORS: ${DSP_FRAME_ANCESTORS:-}
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: dsp_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
MYSQL_DATABASE: niph_dsps
|
||||
MYSQL_USER: dsp_user
|
||||
MYSQL_PASSWORD: dsp_pass
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./db/niph_dsps.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:latest
|
||||
container_name: dsp_phpmyadmin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:80"
|
||||
environment:
|
||||
PMA_HOST: db
|
||||
PMA_USER: dsp_user
|
||||
PMA_PASSWORD: dsp_pass
|
||||
UPLOAD_LIMIT: 64M
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
jupyterhub:
|
||||
build:
|
||||
context: ./docker/jupyterhub
|
||||
container_name: dsp_jupyterhub
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "${JUPYTERHUB_PORT:-443}:8000"
|
||||
- "8888:8000"
|
||||
environment:
|
||||
DSP_JH_NETWORK: dsp_default
|
||||
DSP_APP_CONTAINER: dsp_app
|
||||
DSP_WORKSPACE_ROOT: ${DSP_WORKSPACE_ROOT:-/var/www/html/uploads/jupyter_workspace}
|
||||
volumes:
|
||||
- ./uploads/jupyter_workspace:/var/www/html/uploads/jupyter_workspace
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
12
docker/app-entrypoint.sh
Normal file
12
docker/app-entrypoint.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Ensure uploads subdirectories exist and are writable
|
||||
UPLOAD_ROOT="/var/www/html/uploads"
|
||||
mkdir -p "$UPLOAD_ROOT/announcements" "$UPLOAD_ROOT/slides" "$UPLOAD_ROOT/datasources"
|
||||
|
||||
# Relax permissions so Apache (www-data) can write when using bind mounts
|
||||
chown -R www-data:www-data "$UPLOAD_ROOT"
|
||||
chmod -R 775 "$UPLOAD_ROOT"
|
||||
|
||||
exec apache2-foreground
|
||||
2
docker/custom.ini
Normal file
2
docker/custom.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
upload_max_filesize=2048M
|
||||
post_max_size=2048M
|
||||
45
docker/jupyter/jupyter_server_config.py
Normal file
45
docker/jupyter/jupyter_server_config.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Jupyter Server configuration for DSP docker stack."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
c = get_config() # noqa: F821 - provided by Jupyter at runtime
|
||||
|
||||
default_app_origins = [
|
||||
"http://localhost:8082",
|
||||
"http://127.0.0.1:8082",
|
||||
]
|
||||
extra_app_origins = [
|
||||
value.rstrip("/") for value in os.getenv("DSP_APP_ORIGINS", "").split() if value
|
||||
]
|
||||
allowed_app_origins = list(
|
||||
dict.fromkeys([origin.rstrip("/") for origin in default_app_origins + extra_app_origins])
|
||||
)
|
||||
|
||||
if allowed_app_origins:
|
||||
c.ServerApp.allow_origin = allowed_app_origins[0]
|
||||
if len(allowed_app_origins) > 1:
|
||||
escaped_origins = [re.escape(origin) for origin in allowed_app_origins]
|
||||
pattern = "^(" + "|".join(escaped_origins) + ")$"
|
||||
c.ServerApp.allow_origin_pat = pattern
|
||||
|
||||
c.ServerApp.allow_remote_access = True
|
||||
c.ServerApp.disable_check_xsrf = True
|
||||
|
||||
default_frame_ancestors = [
|
||||
"'self'",
|
||||
"http://localhost:8082",
|
||||
"http://127.0.0.1:8082",
|
||||
]
|
||||
extra_frame_ancestors = [value for value in os.getenv("DSP_FRAME_ANCESTORS", "").split() if value]
|
||||
frame_ancestors = " ".join(
|
||||
dict.fromkeys(default_frame_ancestors + [origin.rstrip("/") for origin in extra_frame_ancestors])
|
||||
)
|
||||
|
||||
c.ServerApp.tornado_settings = {
|
||||
"headers": {
|
||||
"Content-Security-Policy": f"frame-ancestors {frame_ancestors}",
|
||||
"X-Frame-Options": "ALLOWALL",
|
||||
}
|
||||
}
|
||||
9
docker/jupyterhub/Dockerfile
Normal file
9
docker/jupyterhub/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM jupyterhub/jupyterhub:4.1
|
||||
|
||||
# Install required Python packages and Docker CLI for pre-spawn syncing
|
||||
RUN pip install --no-cache-dir oauthenticator dockerspawner jupyterhub-idle-culler && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends docker.io && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py
|
||||
144
docker/jupyterhub/jupyterhub_config.py
Normal file
144
docker/jupyterhub/jupyterhub_config.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Dict, Optional
|
||||
|
||||
from oauthenticator.generic import GenericOAuthenticator
|
||||
from jupyterhub.auth import DummyAuthenticator
|
||||
from dockerspawner import DockerSpawner
|
||||
|
||||
c = get_config()
|
||||
|
||||
# Authenticator selection (environment-driven)
|
||||
c.Authenticator.enable_auth_state = False
|
||||
auth_strategy = os.environ.get("JUPYTERHUB_AUTH_STRATEGY", "oauth").strip().lower()
|
||||
|
||||
if auth_strategy == "dummy":
|
||||
c.JupyterHub.authenticator_class = DummyAuthenticator
|
||||
dummy_password = os.environ.get("JUPYTERHUB_DUMMY_PASSWORD")
|
||||
if dummy_password:
|
||||
c.DummyAuthenticator.password = dummy_password
|
||||
else:
|
||||
c.JupyterHub.authenticator_class = GenericOAuthenticator
|
||||
c.GenericOAuthenticator.client_id = os.environ.get("DSP_OAUTH_CLIENT_ID", "")
|
||||
c.GenericOAuthenticator.client_secret = os.environ.get("DSP_OAUTH_CLIENT_SECRET", "")
|
||||
c.GenericOAuthenticator.authorize_url = os.environ.get("DSP_OAUTH_AUTHORIZE_URL", "")
|
||||
c.GenericOAuthenticator.token_url = os.environ.get("DSP_OAUTH_TOKEN_URL", "")
|
||||
c.GenericOAuthenticator.userdata_url = os.environ.get("DSP_OAUTH_USERINFO_URL", "")
|
||||
c.GenericOAuthenticator.oauth_callback_url = os.environ.get("JUPYTERHUB_OAUTH_CALLBACK", "")
|
||||
c.GenericOAuthenticator.scope = ["profile"]
|
||||
c.GenericOAuthenticator.username_claim = "hub_username"
|
||||
c.GenericOAuthenticator.username_key = "hub_username"
|
||||
c.GenericOAuthenticator.auto_login = True
|
||||
|
||||
# Explicitly acknowledge HTTP when running behind an external TLS terminator.
|
||||
c.JupyterHub.confirm_no_ssl = True
|
||||
c.Spawner.http_timeout = int(os.getenv("JUPYTERHUB_HTTP_TIMEOUT", "90"))
|
||||
c.Spawner.start_timeout = int(os.getenv("JUPYTERHUB_START_TIMEOUT", "90"))
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
value = os.getenv(name)
|
||||
if value is None:
|
||||
return default
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
tornado_settings = getattr(c.JupyterHub, "tornado_settings", {})
|
||||
if isinstance(tornado_settings, dict):
|
||||
merged_settings = tornado_settings.copy()
|
||||
else:
|
||||
merged_settings = {}
|
||||
|
||||
default_frame_ancestors = ["'self'", "http://localhost:8082", "http://127.0.0.1:8082"]
|
||||
app_origin_values = [value.rstrip("/") for value in os.getenv("DSP_APP_ORIGINS", "").split() if value]
|
||||
extra_frame_ancestors = [value.rstrip("/") for value in os.getenv("DSP_FRAME_ANCESTORS", "").split() if value]
|
||||
frame_ancestors = " ".join(
|
||||
dict.fromkeys(default_frame_ancestors + app_origin_values + extra_frame_ancestors)
|
||||
)
|
||||
|
||||
header_settings = {
|
||||
"Content-Security-Policy": f"frame-ancestors {frame_ancestors}",
|
||||
"X-Frame-Options": "ALLOWALL",
|
||||
}
|
||||
|
||||
existing_headers = merged_settings.get("headers", {})
|
||||
existing_headers.update(header_settings)
|
||||
merged_settings["headers"] = existing_headers
|
||||
|
||||
external_url = os.getenv("JUPYTER_EXTERNAL_URL", "")
|
||||
cookie_secure_default = external_url.startswith("https://")
|
||||
merged_settings["cookie_options"] = {
|
||||
"SameSite": "None",
|
||||
"Secure": _env_bool("JUPYTERHUB_COOKIE_SECURE", cookie_secure_default),
|
||||
}
|
||||
c.JupyterHub.tornado_settings = merged_settings
|
||||
|
||||
# Single-user server configuration
|
||||
c.JupyterHub.spawner_class = DockerSpawner
|
||||
c.DockerSpawner.image = os.environ.get("DSP_JH_IMAGE", "jupyter/minimal-notebook:python-3.11")
|
||||
c.DockerSpawner.remove_containers = True
|
||||
c.DockerSpawner.cmd = ["start-singleuser.sh"]
|
||||
c.DockerSpawner.notebook_dir = "/home/jovyan/work"
|
||||
c.DockerSpawner.network_name = os.environ.get("DSP_JH_NETWORK", "dsp_default")
|
||||
|
||||
|
||||
def _workspace_volume(username: str) -> Dict[str, str]:
|
||||
safe = re.sub(r"[^a-zA-Z0-9._-]+", "-", username)
|
||||
host_root = os.environ.get("DSP_WORKSPACE_ROOT", "/var/www/html/uploads/jupyter_workspace")
|
||||
volumes: Dict[str, str] = {f"{host_root}/{safe}": "/home/jovyan/work"}
|
||||
|
||||
r_scripts_root = os.environ.get("DSP_R_SCRIPTS_ROOT", "/var/www/html/r_scripts")
|
||||
if os.path.isdir(r_scripts_root):
|
||||
volumes[r_scripts_root] = "/home/jovyan/work/r_scripts"
|
||||
|
||||
return volumes
|
||||
|
||||
|
||||
def _extract_person_id(username: str) -> Optional[str]:
|
||||
match = re.search(r"(\d+)$", username or "")
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def _run_sync(person_id: str) -> None:
|
||||
command = [
|
||||
"docker",
|
||||
"exec",
|
||||
os.environ.get("DSP_APP_CONTAINER", "dsp_app"),
|
||||
"php",
|
||||
"/var/www/html/scripts/trigger_workspace_sync.php",
|
||||
person_id,
|
||||
]
|
||||
subprocess.run(command, check=False)
|
||||
|
||||
|
||||
async def pre_spawn_hook(spawner):
|
||||
username = spawner.user.name
|
||||
spawner.volumes = _workspace_volume(username)
|
||||
person_id = _extract_person_id(username)
|
||||
if person_id:
|
||||
_run_sync(person_id)
|
||||
|
||||
|
||||
c.DockerSpawner.pre_spawn_hook = pre_spawn_hook
|
||||
|
||||
_cull_token = os.environ.get("JUPYTERHUB_CULL_API_TOKEN")
|
||||
|
||||
if _cull_token:
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
"name": "cull-idle",
|
||||
"command": [
|
||||
"python",
|
||||
"-m",
|
||||
"jupyterhub_idle_culler",
|
||||
"--timeout=3600",
|
||||
"--cull-every=600",
|
||||
"--concurrency=10",
|
||||
],
|
||||
"api_token": _cull_token,
|
||||
"admin": True,
|
||||
}
|
||||
]
|
||||
else:
|
||||
logging.warning("JUPYTERHUB_CULL_API_TOKEN not set; idle culler disabled.")
|
||||
88
includes/auth.php
Normal file
88
includes/auth.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
// includes/auth.php
|
||||
// Handles session management and basic authentication checks.
|
||||
|
||||
// Function to set a session message (for alerts)
|
||||
function set_message($message, $type = 'info') {
|
||||
$_SESSION['message'] = $message;
|
||||
$_SESSION['message_type'] = $type;
|
||||
}
|
||||
/**
|
||||
* Retrieves and clears a session message.
|
||||
* @return array|null The message array or null if no message exists.
|
||||
*/
|
||||
function get_message() {
|
||||
if (isset($_SESSION['message'])) {
|
||||
$message = $_SESSION['message'];
|
||||
unset($_SESSION['message']);
|
||||
return $message;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function to check if a user is logged in
|
||||
function is_logged_in() {
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
// Function to check if the logged-in user has a specific role
|
||||
function has_role($required_role) {
|
||||
if (!is_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
// For simplicity, this assumes a direct match.
|
||||
// In a real app, you might have an array of roles or more complex logic.
|
||||
return $_SESSION['user_status'] === $required_role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current user is allowed to run R/Jupyter integrations.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function has_r_access(): bool {
|
||||
return !empty($_SESSION['can_run_r']);
|
||||
}
|
||||
|
||||
// Function to redirect if not logged in
|
||||
function redirect_if_not_logged_in($redirect_path = '../index.php') {
|
||||
if (!is_logged_in()) {
|
||||
set_message("Please login to access this page.", "warning");
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||||
$scheme = $_SERVER['HTTP_X_FORWARDED_PROTO'];
|
||||
}
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
if ($host && str_starts_with($redirect_path, '../')) {
|
||||
$path = '/' . ltrim($redirect_path, './');
|
||||
header("Location: {$scheme}://{$host}{$path}");
|
||||
} else {
|
||||
header("Location: " . $redirect_path);
|
||||
}
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to redirect if user does not have required role
|
||||
function redirect_if_not_role($required_role, $redirect_path = '../index.php') {
|
||||
if (!has_role($required_role)) {
|
||||
set_message("You do not have permission to access this page.", "danger");
|
||||
header("Location: " . $redirect_path);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects away if the user lacks R/Jupyter access rights.
|
||||
*
|
||||
* @param string $redirect_path Where to redirect when access is denied.
|
||||
*/
|
||||
function redirect_if_no_r_access($redirect_path = '../index.php') {
|
||||
if (!has_r_access()) {
|
||||
set_message("You do not have R/Jupyter access. Please contact DAC Staff.", "danger");
|
||||
header("Location: " . $redirect_path);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
8
includes/footer_admin.php
Normal file
8
includes/footer_admin.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
document.querySelector('.sidebar').classList.toggle('show');
|
||||
document.getElementById('overlay').classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
8
includes/footer_contributor.php
Normal file
8
includes/footer_contributor.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
document.querySelector('.sidebar').classList.toggle('show');
|
||||
document.getElementById('overlay').classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
8
includes/footer_owner.php
Normal file
8
includes/footer_owner.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
document.querySelector('.sidebar').classList.toggle('show');
|
||||
document.getElementById('overlay').classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
67
includes/footer_public.php
Normal file
67
includes/footer_public.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<footer class="footer rounded-top">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<h5>Introduction to NIPH</h5>
|
||||
<p>The National Institute of Public Health (NIPH) is dedicated to advancing public health through research, education, laboratories, and services. Our data sharing platform aims to provide accessible and reliable health data for informed decision-making.</p>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<h5>Contact Us</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-map-marker-alt me-2"></i> St.(289), Phnom Penh, Cambodia</li>
|
||||
<li><i class="fas fa-phone me-2"></i> +855 12 345 678</li>
|
||||
<li><i class="fas fa-envelope me-2"></i> dac@niph.org.kh</li>
|
||||
</ul>
|
||||
<ul class="list-unstyled">
|
||||
<!-- Social Links -->
|
||||
<div class="social-icons ms-lg-3 d-flex align-items-center">
|
||||
<a href="#" class="text-decoration-none text-primary" aria-label="Facebook"><i class="fab fa-facebook-f" style="font-size:24px"></i></a>
|
||||
<a href="#" class="text-decoration-none text-danger" aria-label="YouTube"><i class="fab fa-youtube" style="font-size:24px"></i></a>
|
||||
<a href="#" class="text-decoration-none" aria-label="Telegram" style="color: #0088cc;font-size:24px"><i class="fab fa-telegram-plane"></i></a>
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<h5>Quick Links</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="index.php?page=home">Home</a></li>
|
||||
<li><a href="index.php?page=classifications">Dataset</a></li>
|
||||
<li><a href="index.php?page=announcements">Announcements</a></li>
|
||||
<li><a href="index.php?page=about">About Us</a></li>
|
||||
<li><a href="index.php?page=contact">Contact Us</a></li>
|
||||
<li><a href="index.php?page=faq">FAQ</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center pt-3 border-top border-light">
|
||||
<p class="mb-0">© <?php echo date('Y'); ?> NIPH Data Sharing Platform. All rights reserved.</p>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Navigation Bar for Mobile -->
|
||||
<br>
|
||||
<nav class="d-md-none fixed-bottom bg-white border-top bottom-nav">
|
||||
<div class="d-flex justify-content-around" style="height: 64px;">
|
||||
<!-- Home -->
|
||||
<a class="d-flex flex-column align-items-center justify-content-center text-center text-decoration-none flex-fill <?= ($page == 'home' ? 'active' : '') ?>" aria-current="page" href="index.php?page=home"><i class='fas fa-home' style='color:#28a745;'></i>
|
||||
<span class="small fw-medium">HOME</span></a>
|
||||
<!-- Classification -->
|
||||
<a class="d-flex flex-column align-items-center justify-content-center text-center text-decoration-none flex-fill <?= ($page == 'classifications' ? 'active' : '') ?>" href="index.php?page=classifications"><i class='fas fa-layer-group' style='color:#28a745;'></i>
|
||||
<span class="small fw-medium">DATASET</span></a>
|
||||
<!-- Announcement -->
|
||||
<a class="d-flex flex-column align-items-center justify-content-center text-center text-decoration-none flex-fill <?= ($page == 'announcements' ? 'active' : '') ?>" href="index.php?page=announcements"><i class='fas fa-volume-up' style='color:#28a745;'></i>
|
||||
<span class="small fw-medium">ANNOUNCE</span></a>
|
||||
<!-- About US -->
|
||||
<a class="d-flex flex-column align-items-center justify-content-center text-center text-decoration-none flex-fill <?= ($page == 'about' ? 'active' : '') ?>" href="index.php?page=about"><i class='fa fa-info-circle' style='color:#28a745;'></i>
|
||||
<span class="small fw-medium">ABOUT</span></a>
|
||||
<!-- Contact US -->
|
||||
<a class="d-flex flex-column align-items-center justify-content-center text-center text-decoration-none flex-fill <?= ($page == 'contact' ? 'active' : '') ?>" href="index.php?page=contact"><i class='fas fa-address-card' style='color:#28a745;'></i>
|
||||
<span class="small fw-medium">CONTACT</span></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
8
includes/footer_user.php
Normal file
8
includes/footer_user.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
document.querySelector('.sidebar').classList.toggle('show');
|
||||
document.getElementById('overlay').classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
163
includes/header_admin.php
Normal file
163
includes/header_admin.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<!-- Head tag -->
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NIPH Data Sharing Platform</title>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/images/niphlogo.ico">
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- FontAwesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />
|
||||
|
||||
<!-- Custom styles -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #0b4076ff;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1030;
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar .admin-header {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
padding: 16px 0;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid #157347;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #adb5bd;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i {
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: #495057;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: #1456b9ff;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 250px;
|
||||
padding: 1.5rem 2rem;
|
||||
flex-grow: 1;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1029;
|
||||
}
|
||||
|
||||
#mobile-topbar {
|
||||
height: 56px;
|
||||
background: #198754;
|
||||
color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
#mobile-topbar h4 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
#mobile-topbar button {
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.7);
|
||||
background: transparent;
|
||||
color: white;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#mobile-topbar button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
left: -250px;
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
left: 0;
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
#overlay.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.slide-img-thumbnail {
|
||||
max-width: 150px;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.announcement-img {
|
||||
max-width: 100px;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
150
includes/header_contributor.php
Normal file
150
includes/header_contributor.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<!-- Head tag -->
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NIPH Data Sharing Platform</title>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/images/niphlogo.ico">
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- FontAwesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />
|
||||
|
||||
<!-- Custom styles -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #0b4076ff;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1030;
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar .admin-header {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
padding: 16px 0;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid #157347;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #adb5bd;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i {
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: #495057;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: #1456b9ff;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 250px;
|
||||
padding: 1.5rem 2rem;
|
||||
flex-grow: 1;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1029;
|
||||
}
|
||||
|
||||
#mobile-topbar {
|
||||
height: 56px;
|
||||
background: #198754;
|
||||
color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
#mobile-topbar h4 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
#mobile-topbar button {
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.7);
|
||||
background: transparent;
|
||||
color: white;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#mobile-topbar button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
left: -250px;
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
left: 0;
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
#overlay.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
150
includes/header_owner.php
Normal file
150
includes/header_owner.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<!-- Head tag -->
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NIPH Data Sharing Platform</title>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/images/niphlogo.ico">
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- FontAwesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />
|
||||
|
||||
<!-- Custom styles -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #0b4076ff;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1030;
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar .admin-header {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
padding: 16px 0;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid #157347;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #adb5bd;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i {
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: #495057;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: #1456b9ff;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 250px;
|
||||
padding: 1.5rem 2rem;
|
||||
flex-grow: 1;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1029;
|
||||
}
|
||||
|
||||
#mobile-topbar {
|
||||
height: 56px;
|
||||
background: #198754;
|
||||
color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
#mobile-topbar h4 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
#mobile-topbar button {
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.7);
|
||||
background: transparent;
|
||||
color: white;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#mobile-topbar button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
left: -250px;
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
left: 0;
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
#overlay.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
107
includes/header_public.php
Normal file
107
includes/header_public.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NIPH Data Sharing Platform</title>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/images/niphlogo.ico">
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome for social icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<!-- Custom CSS for Inter font and additional styling -->
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #f8f9fa; /* Light background */
|
||||
padding-top: 100px; /* Adjust for fixed navbar */
|
||||
}
|
||||
.navbar {
|
||||
background-color: #ffffff; /* White background for navbar */
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.05); /* Subtle shadow */
|
||||
}
|
||||
.navbar-brand img {
|
||||
height: 40px; /* Adjust logo size */
|
||||
margin-right: 10px;
|
||||
}
|
||||
.nav-link {
|
||||
color: #0261bfff !important; /* Dark text for links */
|
||||
font-weight: 500;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.nav-link:hover {
|
||||
color: #007bff !important; /* Blue on hover */
|
||||
}
|
||||
|
||||
/* Let the search container in the mobile controls take up remaining space */
|
||||
.mobile-controls .search-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.btn-outline-primary {
|
||||
border-color: #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
.btn-outline-primary:hover {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
.social-icons a {
|
||||
color: #6c757d; /* Gray for social icons */
|
||||
margin-left: 15px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.social-icons a:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
.carousel-item img {
|
||||
max-height: 500px; /* Limit carousel image height */
|
||||
object-fit: cover;
|
||||
}
|
||||
.footer {
|
||||
background-color: #28a745; /* Dark background for footer */
|
||||
color: #f8f9fa; /* Light text */
|
||||
padding: 40px 0;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.footer a {
|
||||
color: #0261bfff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer a:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
.search-container {
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.search-input {
|
||||
border-radius: 20px;
|
||||
padding-right: 40px; /* Space for icon */
|
||||
}
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
}
|
||||
.cpanel-sidebar {
|
||||
background-color: #f1f1f1;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.cpanel-sidebar .nav-link {
|
||||
color: #333;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.cpanel-sidebar .nav-link.active,
|
||||
.cpanel-sidebar .nav-link:hover {
|
||||
background-color: #007bff;
|
||||
color: #fff !important;
|
||||
}
|
||||
.logotxt{
|
||||
color: #28a745;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
149
includes/header_user.php
Normal file
149
includes/header_user.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<!-- Head tag -->
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Data User Dashboard - NIPH DSP</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- FontAwesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />
|
||||
|
||||
<!-- Custom styles -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #0b4076ff;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1030;
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar .admin-header {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
padding: 16px 0;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid #157347;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #adb5bd;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i {
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: #495057;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: #1456b9ff;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 250px;
|
||||
padding: 1.5rem 2rem;
|
||||
flex-grow: 1;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1029;
|
||||
}
|
||||
|
||||
#mobile-topbar {
|
||||
height: 56px;
|
||||
background: #198754;
|
||||
color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
#mobile-topbar h4 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
#mobile-topbar button {
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.7);
|
||||
background: transparent;
|
||||
color: white;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#mobile-topbar button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
left: -250px;
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
left: 0;
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
#overlay.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
63
includes/jupyter_config_reference.php
Normal file
63
includes/jupyter_config_reference.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
// includes/jupyter_config_reference.php
|
||||
// Shared reference panel outlining Jupyter configuration values.
|
||||
|
||||
$jupyterDefaults = dsp_jupyter_defaults();
|
||||
$resolvedBaseUrl = dsp_jupyter_base_url();
|
||||
$resolvedToken = dsp_jupyter_token();
|
||||
$resolvedPort = dsp_jupyter_port();
|
||||
$envOverrides = dsp_jupyter_env_overrides();
|
||||
$workspaceRoot = $workspaceRelativeDir ?? $jupyterDefaults['workspace_root'];
|
||||
|
||||
$workspaceBaseMessage = $workspaceRoot;
|
||||
if (!empty($_SESSION['person_id'])) {
|
||||
$workspaceBaseMessage = sprintf('%s/user_%d', rtrim($jupyterDefaults['workspace_root'], '/'), (int) $_SESSION['person_id']);
|
||||
}
|
||||
// Determine relative path back to the application root for links.
|
||||
$currentScript = $_SERVER['PHP_SELF'] ?? '';
|
||||
$scriptDir = trim(dirname($currentScript), '/');
|
||||
$segmentCount = $scriptDir === '' ? 0 : substr_count($scriptDir, '/') + 1;
|
||||
$rootPrefix = $segmentCount ? str_repeat('../', $segmentCount) : '';
|
||||
$installConfigHref = $rootPrefix . 'install_config.php#r-in-jupyter-service';
|
||||
?>
|
||||
<section class="mb-4">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3 text-secondary">Jupyter Service Reference</h2>
|
||||
<p class="text-muted">
|
||||
Configuration guidance (defaults, overrides, and security notes) now lives on the
|
||||
<a href="<?= htmlspecialchars($installConfigHref, ENT_QUOTES, 'UTF-8') ?>">Install & Configuration</a>
|
||||
page under <em>R in JupyterHub Service</em>.
|
||||
</p>
|
||||
<p class="text-muted mb-4">
|
||||
Use the snapshot below to confirm how this environment is currently resolving the notebook endpoint.
|
||||
</p>
|
||||
<dl class="row mb-4">
|
||||
<dt class="col-sm-4 text-muted small text-uppercase">Notebook Base URL</dt>
|
||||
<dd class="col-sm-8 mb-3"><code><?= htmlspecialchars($resolvedBaseUrl) ?></code></dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted small text-uppercase">Published Port</dt>
|
||||
<dd class="col-sm-8 mb-3"><code><?= htmlspecialchars($resolvedPort) ?></code></dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted small text-uppercase">Authentication Token</dt>
|
||||
<dd class="col-sm-8 mb-3"><code><?= htmlspecialchars($resolvedToken) ?></code></dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted small text-uppercase">Workspace Mount</dt>
|
||||
<dd class="col-sm-8 mb-0"><code><?= htmlspecialchars($workspaceBaseMessage) ?></code></dd>
|
||||
</dl>
|
||||
<h3 class="h6 text-uppercase text-muted mb-3">Active Environment Overrides</h3>
|
||||
<?php
|
||||
$activeOverrides = array_filter($envOverrides, static fn($value) => $value !== null && $value !== '');
|
||||
?>
|
||||
<?php if ($activeOverrides): ?>
|
||||
<ul class="list-unstyled small text-muted mb-0">
|
||||
<?php foreach ($activeOverrides as $variable => $value): ?>
|
||||
<li><code><?= htmlspecialchars($variable) ?></code>: <code><?= htmlspecialchars($value) ?></code></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="text-muted small mb-0">No overrides detected; defaults from the Docker stack are in effect.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
203
includes/jupyter_helpers.php
Normal file
203
includes/jupyter_helpers.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
// includes/jupyter_helpers.php
|
||||
// Centralised helpers for Jupyter configuration and display logic.
|
||||
|
||||
/**
|
||||
* Returns the default configuration values documented on the Install & Configuration page.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
function dsp_jupyter_defaults(): array {
|
||||
return [
|
||||
'base_url' => 'https://localhost',
|
||||
'token' => 'dsp-token',
|
||||
'workspace_root' => 'datasources',
|
||||
'port' => '443',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the external base URL for the Jupyter service.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function dsp_jupyter_base_url(): string {
|
||||
$configured = getenv('JUPYTER_EXTERNAL_URL');
|
||||
if ($configured) {
|
||||
return rtrim($configured, '/');
|
||||
}
|
||||
|
||||
$defaults = dsp_jupyter_defaults();
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') ? 'https' : 'http';
|
||||
$hostHeader = $_SERVER['HTTP_HOST'] ?? '';
|
||||
|
||||
if ($hostHeader) {
|
||||
$hostname = explode(':', $hostHeader)[0];
|
||||
$port = dsp_jupyter_port();
|
||||
$authority = $hostname;
|
||||
|
||||
$portIsDefault = ($scheme === 'http' && $port === '80') || ($scheme === 'https' && $port === '443');
|
||||
if (!$portIsDefault) {
|
||||
$authority .= ':' . $port;
|
||||
}
|
||||
|
||||
return sprintf('%s://%s', $scheme, $authority);
|
||||
}
|
||||
|
||||
return rtrim($defaults['base_url'], '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the token used to authenticate with Jupyter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function dsp_jupyter_token(): string {
|
||||
$authStrategy = strtolower((string) getenv('JUPYTERHUB_AUTH_STRATEGY'));
|
||||
if ($authStrategy === 'oauth') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$defaults = dsp_jupyter_defaults();
|
||||
|
||||
$envToken = getenv('JUPYTER_TOKEN');
|
||||
if ($envToken !== false) {
|
||||
return $envToken;
|
||||
}
|
||||
|
||||
return $defaults['token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the username JupyterHub should use for the active user.
|
||||
*
|
||||
* The template can be customised via JUPYTERHUB_USERNAME_TEMPLATE.
|
||||
* Supported placeholders:
|
||||
* {person_id} - numeric person identifier from the session
|
||||
* {username} - DSP username from the session
|
||||
* {email} - Session email if available
|
||||
*
|
||||
* @param int|null $personId Optional explicit person ID
|
||||
* @return string|null Sanitised username or null when it cannot be determined.
|
||||
*/
|
||||
function dsp_resolve_jupyterhub_username(?int $personId, ?string $username = null, ?string $email = null): ?string {
|
||||
if ($personId === null || $personId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$template = getenv('JUPYTERHUB_USERNAME_TEMPLATE') ?: 'user_{person_id}';
|
||||
$usernameRaw = str_replace(
|
||||
['{person_id}', '{username}', '{email}'],
|
||||
[
|
||||
(string) $personId,
|
||||
(string) $username,
|
||||
(string) $email,
|
||||
],
|
||||
$template
|
||||
);
|
||||
|
||||
$usernameSanitised = preg_replace('/[^a-zA-Z0-9._-]+/', '-', $usernameRaw);
|
||||
$usernameSanitised = trim((string) $usernameSanitised, "-_.");
|
||||
|
||||
return $usernameSanitised !== '' ? $usernameSanitised : null;
|
||||
}
|
||||
|
||||
function dsp_jupyterhub_username(?int $personId = null): ?string {
|
||||
if ($personId === null) {
|
||||
$personId = isset($_SESSION['person_id']) ? (int) $_SESSION['person_id'] : null;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? null;
|
||||
$email = $_SESSION['email'] ?? null;
|
||||
|
||||
return dsp_resolve_jupyterhub_username($personId, $username, $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the per-user route served by JupyterHub.
|
||||
*
|
||||
* The path template can be overridden with JUPYTERHUB_USER_PATH (e.g. "user/{username}/lab").
|
||||
*
|
||||
* @param string $baseUrl Hub base URL.
|
||||
* @param int|null $personId Optional explicit person ID.
|
||||
* @return string Absolute path including the user segment, without trailing slash.
|
||||
*/
|
||||
function dsp_jupyterhub_user_route(string $baseUrl, ?int $personId = null): string {
|
||||
$baseUrl = rtrim($baseUrl, '/');
|
||||
|
||||
$pathTemplate = getenv('JUPYTERHUB_USER_PATH');
|
||||
if ($pathTemplate === false || $pathTemplate === '') {
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
$personId = $personId ?? (isset($_SESSION['person_id']) ? (int) $_SESSION['person_id'] : null);
|
||||
$username = dsp_jupyterhub_username($personId);
|
||||
|
||||
if ($username === null) {
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
$relativePath = str_replace(
|
||||
['{username}', '{person_id}'],
|
||||
[$username, (string) $personId],
|
||||
ltrim($pathTemplate, '/')
|
||||
);
|
||||
|
||||
return rtrim($baseUrl . '/' . $relativePath, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the iframe URL using the resolved base URL and token.
|
||||
*
|
||||
* @param string|null $baseUrl Optional override of the base URL.
|
||||
* @param string|null $token Optional override of the token.
|
||||
* @param int|null $personId Optional override of the person ID used for the route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function dsp_jupyter_iframe_url(?string $baseUrl = null, ?string $token = null, ?int $personId = null): string {
|
||||
$resolvedBase = rtrim($baseUrl ?: dsp_jupyter_base_url(), '/');
|
||||
$userRoute = dsp_jupyterhub_user_route($resolvedBase, $personId);
|
||||
$finalToken = $token ?: dsp_jupyter_token();
|
||||
|
||||
return $finalToken
|
||||
? sprintf('%s?token=%s', $userRoute, urlencode($finalToken))
|
||||
: $userRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the host port the Jupyter service is published on.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function dsp_jupyter_port(): string {
|
||||
$configured = getenv('JUPYTER_PORT');
|
||||
if ($configured === false || $configured === '') {
|
||||
$configured = getenv('JUPYTERHUB_PORT');
|
||||
}
|
||||
if ($configured) {
|
||||
return (string) $configured;
|
||||
}
|
||||
|
||||
$defaults = dsp_jupyter_defaults();
|
||||
|
||||
return $defaults['port'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the environment overrides impacting the embedded Jupyter configuration.
|
||||
*
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
function dsp_jupyter_env_overrides(): array {
|
||||
return [
|
||||
'JUPYTER_EXTERNAL_URL' => getenv('JUPYTER_EXTERNAL_URL') ?: null,
|
||||
'JUPYTER_TOKEN' => getenv('JUPYTER_TOKEN') ?: null,
|
||||
'JUPYTER_PORT' => getenv('JUPYTER_PORT') ?: null,
|
||||
'JUPYTERHUB_PORT' => getenv('JUPYTERHUB_PORT') ?: null,
|
||||
'DSP_APP_ORIGINS' => getenv('DSP_APP_ORIGINS') ?: null,
|
||||
'DSP_FRAME_ANCESTORS' => getenv('DSP_FRAME_ANCESTORS') ?: null,
|
||||
'JUPYTERHUB_USERNAME_TEMPLATE' => getenv('JUPYTERHUB_USERNAME_TEMPLATE') ?: null,
|
||||
'JUPYTERHUB_USER_PATH' => getenv('JUPYTERHUB_USER_PATH') ?: null,
|
||||
];
|
||||
}
|
||||
119
includes/nav_admin.php
Normal file
119
includes/nav_admin.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
$currentScript = $_SERVER['PHP_SELF'] ?? '';
|
||||
$currentPage = basename($currentScript);
|
||||
$scriptDir = trim(dirname($currentScript), '/');
|
||||
$segmentCount = $scriptDir === '' ? 0 : substr_count($scriptDir, '/') + 1;
|
||||
$rootPrefix = $segmentCount ? str_repeat('../', $segmentCount) : '';
|
||||
?>
|
||||
|
||||
<!-- Mobile Topbar -->
|
||||
<div id="mobile-topbar" class="d-lg-none d-flex justify-content-between align-items-center px-3 shadow-sm bg-success text-white">
|
||||
<h4 class="m-0 fw-bold">DSP ADMIN</h4>
|
||||
<button class="btn btn-outline-light rounded" onclick="toggleSidebar()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile overlay -->
|
||||
<div id="overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar d-lg-block shadow">
|
||||
<!-- Desktop only header -->
|
||||
<div class="admin-header d-none d-lg-block text-center">
|
||||
ADMIN-DAC STAFF
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column py-0">
|
||||
<li class="nav-item mt-0 border-top border-secondary pt-2">
|
||||
<?php
|
||||
$sections = [
|
||||
'Data Management' => [
|
||||
'admin/dashboard.php' => ['Dashboard', 'fa-tachometer-alt'],
|
||||
'admin/manage_users.php' => ['Manage Users', 'fa-users'],
|
||||
'admin/manage_datasources.php' => ['Manage Data Sources', 'fa-database'],
|
||||
'admin/manage_permissions_admin.php' => ['Data Permissions', 'fa-user-lock'],
|
||||
'admin/manage_classifications.php' => ['Classifications', 'fa-tags'],
|
||||
],
|
||||
'Content Management' => [
|
||||
'admin/manage_announcements.php' => ['Announcements', 'fa-bullhorn'],
|
||||
'admin/manage_faq.php' => ['FAQ', 'fa-question-circle'],
|
||||
'admin/manage_slides.php' => ['Slides', 'fa-images'],
|
||||
'admin/manage_aboutus.php' => ['About Us', 'fa-info-circle'],
|
||||
'admin/manage_contactus.php' => ['Contact Us', 'fa-envelope'],
|
||||
],
|
||||
'Resources' => [
|
||||
'user_guide.php' => ['User Guide', 'fa-book-open'],
|
||||
'install_config.php' => ['Install & Config', 'fa-tools'],
|
||||
'admin/app_log.php' => ['Application Log', 'fa-scroll'],
|
||||
],
|
||||
'Account' => [
|
||||
'profile.php' => ['My Profile', 'fa-id-card'],
|
||||
],
|
||||
];
|
||||
|
||||
if (!empty($_SESSION['can_run_r'])) {
|
||||
$sections['Data Management']['admin/r_in_jupyter.php'] = ['R in JupyterHub', 'fa-code'];
|
||||
}
|
||||
|
||||
echo '<style>
|
||||
.nav-section {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 0.75rem;
|
||||
margin: 0 0.75rem 1.2rem 0.75rem;
|
||||
}
|
||||
.nav-section legend {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.55);
|
||||
width: auto;
|
||||
padding: 0 0.4rem;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
</style>';
|
||||
|
||||
foreach ($sections as $sectionLabel => $items) {
|
||||
echo '<fieldset class="nav-section"><legend>' . htmlspecialchars($sectionLabel) . '</legend>';
|
||||
foreach ($items as $file => [$title, $icon]) {
|
||||
$targetPath = parse_url($file, PHP_URL_PATH);
|
||||
$targetPath = ($targetPath === null || $targetPath === false) ? $file : $targetPath;
|
||||
$active = ($currentPage === basename($targetPath)) ? 'active' : '';
|
||||
$isExternal = preg_match('#^https?://#', $file) === 1;
|
||||
if ($isExternal) {
|
||||
$href = $file;
|
||||
} else {
|
||||
$href = $rootPrefix . ltrim($targetPath, '/');
|
||||
$query = parse_url($file, PHP_URL_QUERY);
|
||||
if (!empty($query)) {
|
||||
$href .= '?' . $query;
|
||||
}
|
||||
$fragment = parse_url($file, PHP_URL_FRAGMENT);
|
||||
if (!empty($fragment)) {
|
||||
$href .= '#' . $fragment;
|
||||
}
|
||||
}
|
||||
$hrefEsc = htmlspecialchars($href, ENT_QUOTES, 'UTF-8');
|
||||
$titleEsc = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$iconEsc = htmlspecialchars($icon, ENT_QUOTES, 'UTF-8');
|
||||
echo <<<HTML
|
||||
<li class="nav-item">
|
||||
<a class="nav-link $active" href="$hrefEsc">
|
||||
<i class="fas $iconEsc me-2"></i> $titleEsc
|
||||
</a>
|
||||
</li>
|
||||
HTML;
|
||||
}
|
||||
echo '</fieldset>';
|
||||
}
|
||||
?>
|
||||
</li>
|
||||
<li class="nav-item mt-2 border-top border-secondary pt-2">
|
||||
<a class="nav-link text-danger" href="<?php echo htmlspecialchars($rootPrefix . 'logout.php', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<i class="fas fa-sign-out-alt me-2"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
109
includes/nav_contributor.php
Normal file
109
includes/nav_contributor.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
$currentScript = $_SERVER['PHP_SELF'] ?? '';
|
||||
$currentPage = basename($currentScript);
|
||||
$scriptDir = trim(dirname($currentScript), '/');
|
||||
$segmentCount = $scriptDir === '' ? 0 : substr_count($scriptDir, '/') + 1;
|
||||
$rootPrefix = $segmentCount ? str_repeat('../', $segmentCount) : '';
|
||||
?>
|
||||
|
||||
<!-- Mobile Topbar -->
|
||||
<div id="mobile-topbar" class="d-lg-none d-flex justify-content-between align-items-center px-3 shadow-sm bg-success text-white">
|
||||
<h4 class="m-0 fw-bold">DSP DATA Contributor</h4>
|
||||
<button class="btn btn-outline-light rounded" onclick="toggleSidebar()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile overlay -->
|
||||
<div id="overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar d-lg-block shadow">
|
||||
<!-- Desktop only header -->
|
||||
<div class="admin-header d-none d-lg-block text-center">
|
||||
DATA Contributor
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column py-0">
|
||||
<li class="nav-item mt-0 border-top border-secondary pt-2">
|
||||
<?php
|
||||
$sections = [
|
||||
'Data Management' => [
|
||||
'data_hybrid/dashboard.php' => ['Dashboard', 'fa-tachometer-alt'],
|
||||
'data_hybrid/manage_my_datasources.php' => ['Data Sources', 'fa-database'],
|
||||
'data_hybrid/manage_permissions.php' => ['Data Permissions', 'fa-user-shield'],
|
||||
'data_hybrid/my_permissions.php' => ['My data Request', 'fa-user-shield'],
|
||||
'data_hybrid/my_downloads.php' => ['My Downloads', 'fa-chart-bar'],
|
||||
'data_hybrid/browse_datasources.php' => ['Browse All Data', 'fa-database'],
|
||||
],
|
||||
'Resources' => [
|
||||
'user_guide.php' => ['User Guide', 'fa-book-open'],
|
||||
],
|
||||
'Account' => [
|
||||
'profile.php' => ['My Profile', 'fa-id-card'],
|
||||
],
|
||||
];
|
||||
if (!empty($_SESSION['can_run_r'])) {
|
||||
$sections['Data Management']['data_hybrid/r_in_jupyter.php'] = ['R in JupyterHub', 'fa-code'];
|
||||
}
|
||||
|
||||
echo '<style>
|
||||
.nav-section {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 0.75rem;
|
||||
margin: 0 0.75rem 1.2rem 0.75rem;
|
||||
}
|
||||
.nav-section legend {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.55);
|
||||
width: auto;
|
||||
padding: 0 0.4rem;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
</style>';
|
||||
|
||||
foreach ($sections as $sectionLabel => $items) {
|
||||
echo '<fieldset class="nav-section"><legend>' . htmlspecialchars($sectionLabel) . '</legend>';
|
||||
foreach ($items as $file => [$title, $icon]) {
|
||||
$targetPath = parse_url($file, PHP_URL_PATH);
|
||||
$targetPath = ($targetPath === null || $targetPath === false) ? $file : $targetPath;
|
||||
$active = ($currentPage === basename($targetPath)) ? 'active' : '';
|
||||
$isExternal = preg_match('#^https?://#', $file) === 1;
|
||||
if ($isExternal) {
|
||||
$href = $file;
|
||||
} else {
|
||||
$href = $rootPrefix . ltrim($targetPath, '/');
|
||||
$query = parse_url($file, PHP_URL_QUERY);
|
||||
if (!empty($query)) {
|
||||
$href .= '?' . $query;
|
||||
}
|
||||
if ($fragment = parse_url($file, PHP_URL_FRAGMENT)) {
|
||||
$href .= '#' . $fragment;
|
||||
}
|
||||
}
|
||||
$hrefEsc = htmlspecialchars($href, ENT_QUOTES, 'UTF-8');
|
||||
$titleEsc = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$iconEsc = htmlspecialchars($icon, ENT_QUOTES, 'UTF-8');
|
||||
echo <<<HTML
|
||||
<li class="nav-item">
|
||||
<a class="nav-link $active" href="$hrefEsc">
|
||||
<i class="fas $iconEsc me-2"></i> $titleEsc
|
||||
</a>
|
||||
</li>
|
||||
HTML;
|
||||
}
|
||||
echo '</fieldset>';
|
||||
}
|
||||
?>
|
||||
</li>
|
||||
<li class="nav-item mt-2 border-top border-secondary pt-2">
|
||||
<a class="nav-link text-danger" href="<?php echo htmlspecialchars($rootPrefix . 'logout.php', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<i class="fas fa-sign-out-alt me-2"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
108
includes/nav_owner.php
Normal file
108
includes/nav_owner.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
$currentScript = $_SERVER['PHP_SELF'] ?? '';
|
||||
$currentPage = basename($currentScript);
|
||||
$scriptDir = trim(dirname($currentScript), '/');
|
||||
$segmentCount = $scriptDir === '' ? 0 : substr_count($scriptDir, '/') + 1;
|
||||
$rootPrefix = $segmentCount ? str_repeat('../', $segmentCount) : '';
|
||||
?>
|
||||
|
||||
<!-- Mobile Topbar -->
|
||||
<div id="mobile-topbar" class="d-lg-none d-flex justify-content-between align-items-center px-3 shadow-sm bg-success text-white">
|
||||
<h4 class="m-0 fw-bold">DSP DATA OWNER</h4>
|
||||
<button class="btn btn-outline-light rounded" onclick="toggleSidebar()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile overlay -->
|
||||
<div id="overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar d-lg-block shadow">
|
||||
<!-- Desktop only header -->
|
||||
<div class="admin-header d-none d-lg-block text-center">
|
||||
DATA OWNER
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column py-0">
|
||||
<li class="nav-item mt-0 border-top border-secondary pt-2">
|
||||
<?php
|
||||
$sections = [
|
||||
'Data Management' => [
|
||||
'data_owner/dashboard.php' => ['Dashboard', 'fa-tachometer-alt'],
|
||||
'data_owner/manage_my_datasources.php' => ['My Data Sources', 'fa-database'],
|
||||
'data_owner/manage_permissions.php' => ['Data Permissions', 'fa-user-shield'],
|
||||
],
|
||||
'Resources' => [
|
||||
'user_guide.php' => ['User Guide', 'fa-book-open'],
|
||||
],
|
||||
'Account' => [
|
||||
'profile.php' => ['My Profile', 'fa-id-card'],
|
||||
],
|
||||
];
|
||||
|
||||
if (!empty($_SESSION['can_run_r'])) {
|
||||
$sections['Data Management']['data_owner/run_r_scripts.php'] = ['R in JupyterHub', 'fa-code'];
|
||||
}
|
||||
|
||||
echo '<style>
|
||||
.nav-section {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 0.75rem;
|
||||
margin: 0 0.75rem 1.2rem 0.75rem;
|
||||
}
|
||||
.nav-section legend {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.55);
|
||||
width: auto;
|
||||
padding: 0 0.4rem;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
</style>';
|
||||
|
||||
foreach ($sections as $sectionLabel => $items) {
|
||||
echo '<fieldset class="nav-section"><legend>' . htmlspecialchars($sectionLabel) . '</legend>';
|
||||
foreach ($items as $file => [$title, $icon]) {
|
||||
$targetPath = parse_url($file, PHP_URL_PATH);
|
||||
$targetPath = ($targetPath === null || $targetPath === false) ? $file : $targetPath;
|
||||
$active = ($currentPage === basename($targetPath)) ? 'active' : '';
|
||||
$isExternal = preg_match('#^https?://#', $file) === 1;
|
||||
if ($isExternal) {
|
||||
$href = $file;
|
||||
} else {
|
||||
$href = $rootPrefix . ltrim($targetPath, '/');
|
||||
$query = parse_url($file, PHP_URL_QUERY);
|
||||
if (!empty($query)) {
|
||||
$href .= '?' . $query;
|
||||
}
|
||||
$fragment = parse_url($file, PHP_URL_FRAGMENT);
|
||||
if (!empty($fragment)) {
|
||||
$href .= '#' . $fragment;
|
||||
}
|
||||
}
|
||||
$hrefEsc = htmlspecialchars($href, ENT_QUOTES, 'UTF-8');
|
||||
$titleEsc = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$iconEsc = htmlspecialchars($icon, ENT_QUOTES, 'UTF-8');
|
||||
echo <<<HTML
|
||||
<li class="nav-item">
|
||||
<a class="nav-link $active" href="$hrefEsc">
|
||||
<i class="fas $iconEsc me-2"></i> $titleEsc
|
||||
</a>
|
||||
</li>
|
||||
HTML;
|
||||
}
|
||||
echo '</fieldset>';
|
||||
}
|
||||
?>
|
||||
</li>
|
||||
<li class="nav-item mt-2 border-top border-secondary pt-2">
|
||||
<a class="nav-link text-danger" href="<?php echo htmlspecialchars($rootPrefix . 'logout.php', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<i class="fas fa-sign-out-alt me-2"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
85
includes/nav_public.php
Normal file
85
includes/nav_public.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
// includes/nav_public.php
|
||||
?>
|
||||
<!-- Public Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light fixed-top shadow-sm rounded-bottom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="index.php">
|
||||
<img src="assets/images/niphlogo.png" alt="NIPH Logo" class="rounded me-2" style="width:72px;height:72px;">
|
||||
<div class="lh-sm">
|
||||
<span class="d-block fw-bold text-success text-uppercase" style="letter-spacing:0.08em;">NIPH</span>
|
||||
<span class="d-block fw-semibold text-success" style="font-size:0.95rem;">Data Sharing Platform</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#publicNavbar" aria-controls="publicNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="publicNavbar">
|
||||
<ul class="navbar-nav ms-lg-4 me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($page == 'home' ? 'active' : '') ?>" href="index.php?page=home">
|
||||
<i class="fas fa-home text-success me-1"></i>HOME
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($page == 'classifications' ? 'active' : '') ?>" href="index.php?page=classifications">
|
||||
<i class="fas fa-layer-group text-success me-1"></i>DATASET
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($page == 'announcements' ? 'active' : '') ?>" href="index.php?page=announcements">
|
||||
<i class="fas fa-volume-up text-success me-1"></i>ANNOUNCE
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($page == 'about' ? 'active' : '') ?>" href="index.php?page=about">
|
||||
<i class="fa fa-info-circle text-success me-1"></i>ABOUT
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($page == 'contact' ? 'active' : '') ?>" href="index.php?page=contact">
|
||||
<i class="fas fa-address-card text-success me-1"></i>CONTACT
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="d-flex flex-column flex-lg-row align-items-lg-center gap-2">
|
||||
<a class="nav-link <?= ($page == 'faq' ? 'active' : '') ?>" href="index.php?page=faq">
|
||||
<i class="fas fa-question-circle text-success me-1"></i>FAQ
|
||||
</a>
|
||||
|
||||
<?php if (!$is_logged_in): ?>
|
||||
<div class="d-flex flex-column flex-sm-row gap-2">
|
||||
<button class="btn btn-outline-success rounded-pill" data-bs-toggle="modal" data-bs-target="#registerModal">
|
||||
<i class="fas fa-user-plus me-2"></i>Register
|
||||
</button>
|
||||
<button class="btn btn-success rounded-pill" data-bs-toggle="modal" data-bs-target="#loginModal">
|
||||
<i class="fas fa-lock me-2"></i>Login
|
||||
</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="d-flex flex-column flex-sm-row gap-2">
|
||||
<?php if ($user_role == 'DAC Staff'): ?>
|
||||
<a class="btn btn-outline-success rounded-pill" href="admin/dashboard.php">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
|
||||
</a>
|
||||
<?php elseif ($user_role == 'Data Owner'): ?>
|
||||
<a class="btn btn-outline-success rounded-pill" href="data_owner/dashboard.php">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
|
||||
</a>
|
||||
<?php elseif ($user_role == 'Data User'): ?>
|
||||
<a class="btn btn-outline-success rounded-pill" href="data_user/dashboard.php">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-outline-secondary rounded-pill" href="logout.php">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
108
includes/nav_user.php
Normal file
108
includes/nav_user.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
$currentScript = $_SERVER['PHP_SELF'] ?? '';
|
||||
$currentPage = basename($currentScript);
|
||||
$scriptDir = trim(dirname($currentScript), '/');
|
||||
$segmentCount = $scriptDir === '' ? 0 : substr_count($scriptDir, '/') + 1;
|
||||
$rootPrefix = $segmentCount ? str_repeat('../', $segmentCount) : '';
|
||||
?>
|
||||
|
||||
<!-- Mobile Topbar -->
|
||||
<div id="mobile-topbar" class="d-lg-none d-flex justify-content-between align-items-center px-3 shadow-sm bg-success text-white">
|
||||
<h4 class="m-0 fw-bold">DSP DATA USER</h4>
|
||||
<button class="btn btn-outline-light rounded" onclick="toggleSidebar()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile overlay -->
|
||||
<div id="overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar d-lg-block shadow">
|
||||
<!-- Desktop only header -->
|
||||
<div class="admin-header d-none d-lg-block text-center">
|
||||
DATA USER
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column py-0">
|
||||
<li class="nav-item mt-0 border-top border-secondary pt-2">
|
||||
<?php
|
||||
$sections = [
|
||||
'My Access' => [
|
||||
'data_user/dashboard.php' => ['Dashboard', 'fa-tachometer-alt'],
|
||||
'data_user/my_permissions.php' => ['My data Request', 'fa-user-shield'],
|
||||
'data_user/my_downloads.php' => ['My Downloads', 'fa-chart-bar'],
|
||||
'data_user/browse_datasources.php' => ['Browse All Data', 'fa-database'],
|
||||
],
|
||||
'Resources' => [
|
||||
'user_guide.php' => ['User Guide', 'fa-book-open'],
|
||||
],
|
||||
'Account' => [
|
||||
'profile.php' => ['My Profile', 'fa-id-card'],
|
||||
],
|
||||
];
|
||||
if (!empty($_SESSION['can_run_r'])) {
|
||||
$sections['My Access']['data_user/r_in_jupyter.php'] = ['R in JupyterHub', 'fa-code'];
|
||||
}
|
||||
|
||||
echo '<style>
|
||||
.nav-section {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 0.75rem;
|
||||
margin: 0 0.75rem 1.2rem 0.75rem;
|
||||
}
|
||||
.nav-section legend {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.55);
|
||||
width: auto;
|
||||
padding: 0 0.4rem;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
</style>';
|
||||
|
||||
foreach ($sections as $sectionLabel => $items) {
|
||||
echo '<fieldset class="nav-section"><legend>' . htmlspecialchars($sectionLabel) . '</legend>';
|
||||
foreach ($items as $file => [$title, $icon]) {
|
||||
$targetPath = parse_url($file, PHP_URL_PATH);
|
||||
$targetPath = ($targetPath === null || $targetPath === false) ? $file : $targetPath;
|
||||
$active = ($currentPage === basename($targetPath)) ? 'active' : '';
|
||||
$isExternal = preg_match('#^https?://#', $file) === 1;
|
||||
if ($isExternal) {
|
||||
$href = $file;
|
||||
} else {
|
||||
$href = $rootPrefix . ltrim($targetPath, '/');
|
||||
$query = parse_url($file, PHP_URL_QUERY);
|
||||
if (!empty($query)) {
|
||||
$href .= '?' . $query;
|
||||
}
|
||||
$fragment = parse_url($file, PHP_URL_FRAGMENT);
|
||||
if (!empty($fragment)) {
|
||||
$href .= '#' . $fragment;
|
||||
}
|
||||
}
|
||||
$hrefEsc = htmlspecialchars($href, ENT_QUOTES, 'UTF-8');
|
||||
$titleEsc = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$iconEsc = htmlspecialchars($icon, ENT_QUOTES, 'UTF-8');
|
||||
echo <<<HTML
|
||||
<li class="nav-item">
|
||||
<a class="nav-link $active" href="$hrefEsc">
|
||||
<i class="fas $iconEsc me-2"></i> $titleEsc
|
||||
</a>
|
||||
</li>
|
||||
HTML;
|
||||
}
|
||||
echo '</fieldset>';
|
||||
}
|
||||
?>
|
||||
</li>
|
||||
<li class="nav-item mt-2 border-top border-secondary pt-2">
|
||||
<a class="nav-link text-danger" href="<?php echo htmlspecialchars($rootPrefix . 'logout.php', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<i class="fas fa-sign-out-alt me-2"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
562
index.php
Normal file
562
index.php
Normal file
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
// index.php
|
||||
// This file will serve as the main entry point for your application.
|
||||
// It includes common elements like header, footer, and conditionally displays content.
|
||||
|
||||
session_start(); // Start the session at the very beginning
|
||||
|
||||
// Include necessary configuration and helper files
|
||||
require_once 'config.php';
|
||||
require_once 'includes/auth.php'; // Handles session checks and user roles
|
||||
require_once 'classes/User.php';
|
||||
require_once 'classes/Announcement.php';
|
||||
require_once 'classes/Classifications.php'; // For data types and categories
|
||||
require_once 'classes/Aboutus.php'; // For About Us content
|
||||
require_once 'classes/Contactus.php'; // For Contact Us (Feedback)
|
||||
require_once 'classes/Faq.php'; // For FAQ content
|
||||
require_once 'classes/Slide.php'; // NEW: For carousel slides
|
||||
require_once 'classes/DataSource.php'; // Still needed for actual data source browsing/permissions
|
||||
|
||||
// Instantiate classes
|
||||
$user_manager = new User($pdo);
|
||||
$announcement_manager = new Announcement($pdo);
|
||||
$classification_manager = new Classifications($pdo);
|
||||
$about_us_manager = new Aboutus($pdo);
|
||||
$contact_us_manager = new Contactus($pdo);
|
||||
$faq_manager = new Faq($pdo);
|
||||
$slide_manager = new Slide($pdo); // NEW: Instantiate Slide class
|
||||
$data_source_manager = new DataSource($pdo); // Keep DataSource for browsing actual data sources
|
||||
|
||||
// Determine which content to display based on URL or session
|
||||
$page = $_GET['page'] ?? 'home'; // Default to home page
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
$user_role = $_SESSION['user_status'] ?? 'Guest';
|
||||
|
||||
// Fetch data for the homepage (e.g., slides, announcements, classifications)
|
||||
$slides = $slide_manager->getAllSlides(); // NEW: Get slides from database
|
||||
// If no slides from DB, use placeholders
|
||||
if (empty($slides)) {
|
||||
$slides = [
|
||||
['dspsslide_title_en' => 'Welcome to NIPH DSPS', 'dspsslide_description' => 'Your gateway to public health data and insights.', 'dspsslide_photoname' => 'https://placehold.co/1200x400/007bff/ffffff?text=Slide+1+-+Welcome'],
|
||||
['dspsslide_title_en' => 'Discover Research Data', 'dspsslide_description' => 'Access a wide range of datasets for your studies.', 'dspsslide_photoname' => 'https://placehold.co/1200x400/28a745/ffffff?text=Slide+2+-+Data+Insights'],
|
||||
['dspsslide_title_en' => 'Stay Informed', 'dspsslide_description' => 'Read the latest announcements and updates from NIPH.', 'dspsslide_photoname' => 'https://placehold.co/1200x400/ffc107/ffffff?text=Slide+3+-+Announcements'],
|
||||
];
|
||||
} else {
|
||||
// Adjust photo path for display if coming from DB
|
||||
foreach ($slides as &$slide) {
|
||||
$slide['dspsslide_photoname'] = 'uploads/slides/' . $slide['dspsslide_photoname'];
|
||||
}
|
||||
unset($slide); // Unset reference
|
||||
}
|
||||
|
||||
$announcements = $announcement_manager->getAllAnnouncements('Published', 3); // Get 3 latest published announcements
|
||||
// If no announcements from DB, use placeholders
|
||||
if (empty($announcements)) {
|
||||
$announcements = [
|
||||
['dspsann_title' => 'Important Update on Data Policy', 'dspsann_description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 'dspsann_photopath' => 'https://placehold.co/400x250/6c757d/ffffff?text=Announcement+1'],
|
||||
['dspsann_title' => 'New Data Sources Available', 'dspsann_description' => 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', 'dspsann_photopath' => 'https://placehold.co/400x250/6c757d/ffffff?text=Announcement+2'],
|
||||
['dspsann_title' => 'Upcoming Maintenance Schedule', 'dspsann_description' => 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', 'dspsann_photopath' => 'https://placehold.co/400x250/6c757d/ffffff?text=Announcement+3'],
|
||||
];
|
||||
} else {
|
||||
// Adjust photo path for display if coming from DB
|
||||
foreach ($announcements as &$ann) {
|
||||
if (!empty($ann['dspsann_photopath'])) {
|
||||
$ann['dspsann_photopath'] = 'uploads/announcements/' . $ann['dspsann_photopath'];
|
||||
} else {
|
||||
$ann['dspsann_photopath'] = 'https://placehold.co/400x250/6c757d/ffffff?text=No+Image';
|
||||
}
|
||||
}
|
||||
unset($ann); // Unset reference
|
||||
}
|
||||
|
||||
|
||||
// Fetch data types for classifications section
|
||||
$data_types = $classification_manager->getAllDataTypes(); // Use Classifications manager
|
||||
if (empty($data_types)) {
|
||||
$data_types = [
|
||||
['dspstds_name_en' => 'CSV Data', 'icon' => 'fas fa-file-csv', 'color' => 'text-primary', 'description' => 'Explore tabular data in CSV format.'],
|
||||
['dspstds_name_en' => 'JSON Data', 'icon' => 'fas fa-file-code', 'color' => 'text-success', 'description' => 'Access structured data in JSON format.'],
|
||||
['dspstds_name_en' => 'API Endpoints', 'icon' => 'fas fa-code-branch', 'color' => 'text-warning', 'description' => 'Integrate with data via API interfaces.'],
|
||||
['dspstds_name_en' => 'PDF Documents', 'icon' => 'fas fa-file-pdf', 'color' => 'text-danger', 'description' => 'View reports and documents in PDF.'],
|
||||
];
|
||||
} else {
|
||||
// Map DB data to include placeholder icons/colors if not stored in DB
|
||||
foreach ($data_types as &$type) {
|
||||
switch ($type['dspstds_name_en']) {
|
||||
case 'CSV': $type['icon'] = 'fas fa-file-csv'; $type['color'] = 'text-primary'; break;
|
||||
case 'JSON': $type['icon'] = 'fas fa-file-code'; $type['color'] = 'text-success'; break;
|
||||
case 'API': $type['icon'] = 'fas fa-code-branch'; $type['color'] = 'text-warning'; break;
|
||||
case 'PDF': $type['icon'] = 'fas fa-file-pdf'; $type['color'] = 'text-danger'; break;
|
||||
default: $type['icon'] = 'fas fa-file'; $type['color'] = 'text-secondary'; break;
|
||||
}
|
||||
$type['description'] = "Explore data of type " . htmlspecialchars($type['dspstds_name_en']) . ".";
|
||||
}
|
||||
unset($type); // Unset reference
|
||||
}
|
||||
|
||||
// Fetch all categories for the filter dropdown
|
||||
$all_categories = $classification_manager->getAllCategories();
|
||||
|
||||
/**
|
||||
* Sanitises rich text content by allowing a limited subset of HTML tags.
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return string
|
||||
*/
|
||||
function dsp_render_rich_text(?string $value): string {
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$allowed = '<p><br><br/><strong><em><u><ol><ul><li><a><blockquote><table><thead><tbody><tr><td><th><span><div><h1><h2><h3><h4><h5><h6>';
|
||||
return strip_tags($value, $allowed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts rich text into a plain-text excerpt capped at the provided length.
|
||||
*
|
||||
* @param string|null $value
|
||||
* @param int $limit
|
||||
* @return string
|
||||
*/
|
||||
function dsp_plain_text_excerpt(?string $value, int $limit = 150): string {
|
||||
$text = trim(strip_tags((string) $value));
|
||||
if ($text === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (mb_strlen($text) > $limit) {
|
||||
return mb_substr($text, 0, $limit) . '...';
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Header -->
|
||||
<?php
|
||||
include_once("includes/header_public.php");
|
||||
?>
|
||||
<body>
|
||||
|
||||
<!-- Header (Sticky Navbar) -->
|
||||
<?php
|
||||
include_once("includes/nav_public.php");
|
||||
?>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header text-white rounded-top" style="background-color: #28a745;">
|
||||
<h5 class="modal-title" id="loginModalLabel">Login to Your Account</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="process_login.php" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="loginUsername" class="form-label">Username</label>
|
||||
<input type="text" class="form-control rounded" id="loginUsername" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="loginPassword" class="form-label">Password</label>
|
||||
<input type="password" class="form-control rounded" id="loginPassword" name="password" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center border-0">
|
||||
<p class="text-muted mb-0">Don't have an account? <a href="#" data-bs-toggle="modal" data-bs-target="#registerModal" data-bs-dismiss="modal">Register here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Modal -->
|
||||
<div class="modal fade" id="registerModal" tabindex="-1" aria-labelledby="registerModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded shadow-lg">
|
||||
<div class="modal-header text-white rounded-top" style="background-color: #28a745;">
|
||||
<h5 class="modal-title" id="registerModalLabel">Create New Account</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form action="process_register.php" method="POST">
|
||||
<div class="mb-3">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regFirstName" class="form-label">First Name (EN)</label>
|
||||
<input type="text" class="form-control rounded" id="regFirstName" name="first_name_en" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regLastName" class="form-label">Last Name (EN)</label>
|
||||
<input type="text" class="form-control rounded" id="regLastName" name="last_name_en" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regSex" class="form-label">Sex</label>
|
||||
<select class="form-select rounded" id="regSex" name="sex" required>
|
||||
<option value="">Select...</option>
|
||||
<option value="Male">Male</option>
|
||||
<option value="Female">Female</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regDob" class="form-label">Date of Birth</label>
|
||||
<input type="date" class="form-control rounded" id="regDob" name="dob" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regPhoneNumber" class="form-label">Phone Number</label>
|
||||
<input type="tel" class="form-control rounded" id="regPhoneNumber" name="phone_number">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regEmail" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control rounded" id="regEmail" name="email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regUsername" class="form-label">Username</label>
|
||||
<input type="text" class="form-control rounded" id="regUsername" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regPassword" class="form-label">Password</label>
|
||||
<input type="password" class="form-control rounded" id="regPassword" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="regConfirmPassword" class="form-label">Confirm Password</label>
|
||||
<input type="password" class="form-control rounded" id="regConfirmPassword" name="confirm_password" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center border-0">
|
||||
<p class="text-muted mb-0">Already have an account? <a href="#" data-bs-toggle="modal" data-bs-target="#loginModal" data-bs-dismiss="modal">Login here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="container mt-4">
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?= $_SESSION['message_type'] ?> alert-dismissible fade show rounded" role="alert">
|
||||
<?= $_SESSION['message'] ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message']);
|
||||
unset($_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// Display content based on the 'page' parameter
|
||||
switch ($page) {
|
||||
case 'home':
|
||||
// Home Page - Carousel Slide
|
||||
echo '<section id="home-carousel" class="mb-5">';
|
||||
echo '<div id="carouselExampleCaptions" class="carousel slide rounded shadow-sm" data-bs-ride="carousel">';
|
||||
echo '<div class="carousel-indicators">';
|
||||
foreach ($slides as $index => $slide) {
|
||||
echo '<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="' . $index . '" class="' . ($index == 0 ? 'active' : '') . '" aria-current="' . ($index == 0 ? 'true' : 'false') . '" aria-label="Slide ' . ($index + 1) . '"></button>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '<div class="carousel-inner rounded">';
|
||||
foreach ($slides as $index => $slide) {
|
||||
// Use dspsslide_photoname directly as it's already adjusted with 'uploads/slides/'
|
||||
echo '<div class="carousel-item ' . ($index == 0 ? 'active' : '') . '">';
|
||||
echo '<img src="' . htmlspecialchars($slide['dspsslide_photoname']) . '" class="d-block w-100 rounded" alt="' . htmlspecialchars($slide['dspsslide_title_en']) . '">';
|
||||
echo '<div class="carousel-caption d-none d-md-block bg-dark bg-opacity-50 rounded p-2">';
|
||||
echo '<h5>' . htmlspecialchars($slide['dspsslide_title_en']) . '</h5>';
|
||||
echo '<p>' . dsp_render_rich_text($slide['dspsslide_description']) . '</p>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '<button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="prev">';
|
||||
echo '<span class="carousel-control-prev-icon" aria-hidden="true"></span>';
|
||||
echo '<span class="visually-hidden">Previous</span>';
|
||||
echo '</button>';
|
||||
echo '<button class="carousel-control-next" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="next">';
|
||||
echo '<span class="carousel-control-next-icon" aria-hidden="true"></span>';
|
||||
echo '<span class="visually-hidden">Next</span>';
|
||||
echo '</button>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
|
||||
// Latest Announcements Section
|
||||
echo '<section class="my-5">';
|
||||
echo '<h2 class="text-center mb-4">Latest Announcements</h2>';
|
||||
echo '<div class="row row-cols-1 row-cols-md-3 g-4">';
|
||||
foreach ($announcements as $announcement) {
|
||||
echo '<div class="col">';
|
||||
echo '<div class="card h-100 shadow-sm rounded">';
|
||||
echo '<img src="' . htmlspecialchars($announcement['dspsann_photopath']) . '" class="card-img-top rounded-top" alt="' . htmlspecialchars($announcement['dspsann_title']) . '">';
|
||||
echo '<div class="card-body">';
|
||||
echo '<h5 class="card-title">' . htmlspecialchars($announcement['dspsann_title']) . '</h5>';
|
||||
echo '<p class="card-text">' . htmlspecialchars(dsp_plain_text_excerpt($announcement['dspsann_description'], 150)) . '</p>';
|
||||
echo '<a href="index.php?page=announcements&id=' . ($announcement['pkdspsann_id'] ?? '') . '" class="btn btn-primary rounded-pill">Read More</a>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
// Call-to-action cards under announcements
|
||||
echo '<div class="row g-4 mt-2">';
|
||||
echo ' <div class="col-12 col-md-6">';
|
||||
echo ' <div class="card h-100 shadow-sm rounded border-success">';
|
||||
echo ' <div class="card-body">';
|
||||
echo ' <p class="text-uppercase text-muted small mb-1"><i class="fas fa-user-plus me-1"></i> New here?</p>';
|
||||
echo ' <h5 class="card-title">Register a New Account</h5>';
|
||||
echo ' <p class="card-text">Create your DSP profile to request datasets, run analytics in JupyterHub, and stay informed about the latest releases.</p>';
|
||||
echo ' <a href="#" class="btn btn-success rounded-pill" data-bs-toggle="modal" data-bs-target="#registerModal">Start Registration</a>';
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
|
||||
$policyFile = 'uploads/policy/policy.pdf';
|
||||
$policyExists = is_file($policyFile);
|
||||
$policyLink = $policyExists ? $policyFile : '#';
|
||||
$policyAttrs = $policyExists ? 'target="_blank" rel="noopener"' : 'aria-disabled="true"';
|
||||
$policyBtnClass = $policyExists ? 'btn-primary' : 'btn-secondary disabled';
|
||||
|
||||
echo ' <div class="col-12 col-md-6">';
|
||||
echo ' <div class="card h-100 shadow-sm rounded border-primary">';
|
||||
echo ' <div class="card-body">';
|
||||
echo ' <p class="text-uppercase text-muted small mb-1"><i class="fas fa-file-alt me-1"></i> Policy & Governance</p>';
|
||||
echo ' <h5 class="card-title">Review the DSP Data Access Policy</h5>';
|
||||
echo ' <p class="card-text">Understand the legal, privacy, and usage conditions before requesting or sharing datasets on the platform.</p>';
|
||||
echo " <a href='" . htmlspecialchars($policyLink, ENT_QUOTES, 'UTF-8') . "' class='btn " . $policyBtnClass . " rounded-pill' $policyAttrs><i class='fas fa-file-pdf me-2'></i>Download Policy</a>";
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
|
||||
break;
|
||||
|
||||
case 'announcements':
|
||||
// Announcements Page (List all or show single)
|
||||
echo '<h2 class="text-center mb-4">All Announcements</h2>';
|
||||
if (isset($_GET['id'])) {
|
||||
$announcement_id = filter_var($_GET['id'], FILTER_SANITIZE_NUMBER_INT);
|
||||
$single_announcement = $announcement_manager->getAnnouncementById($announcement_id);
|
||||
if ($single_announcement) {
|
||||
// Adjust photo path for display
|
||||
$photo_path = !empty($single_announcement['dspsann_photopath']) ? 'uploads/announcements/' . $single_announcement['dspsann_photopath'] : 'https://placehold.co/800x400/6c757d/ffffff?text=No+Image';
|
||||
|
||||
echo '<div class="card shadow-sm rounded mb-4">';
|
||||
echo '<img src="' . htmlspecialchars($photo_path) . '" class="card-img-top rounded-top" alt="' . htmlspecialchars($single_announcement['dspsann_title']) . '">';
|
||||
echo '<div class="card-body">';
|
||||
echo '<h1 class="card-title">' . htmlspecialchars($single_announcement['dspsann_title']) . '</h1>';
|
||||
echo '<p class="card-text text-muted">Published: ' . date('F j, Y', strtotime($single_announcement['dspsann_reg_datetime'])) . '</p>';
|
||||
echo '<div class="card-text">' . dsp_render_rich_text($single_announcement['dspsann_description']) . '</div>';
|
||||
echo '<a href="index.php?page=announcements" class="btn btn-secondary mt-3 rounded-pill"><i class="fas fa-arrow-left me-2"></i>Back to Announcements</a>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="alert alert-warning rounded">Announcement not found.</div>';
|
||||
}
|
||||
} else {
|
||||
$all_announcements = $announcement_manager->getAllAnnouncements('Published');
|
||||
if (!empty($all_announcements)) {
|
||||
echo '<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">';
|
||||
foreach ($all_announcements as $announcement) {
|
||||
// Adjust photo path for display
|
||||
$photo_path = !empty($announcement['dspsann_photopath']) ? 'uploads/announcements/' . $announcement['dspsann_photopath'] : 'https://placehold.co/400x250/6c757d/ffffff?text=No+Image';
|
||||
|
||||
echo '<div class="col">';
|
||||
echo '<div class="card h-100 shadow-sm rounded">';
|
||||
echo '<img src="' . htmlspecialchars($photo_path) . '" class="card-img-top rounded-top" alt="' . htmlspecialchars($announcement['dspsann_title']) . '">';
|
||||
echo '<div class="card-body">';
|
||||
echo '<h5 class="card-title">' . htmlspecialchars($announcement['dspsann_title']) . '</h5>';
|
||||
echo '<p class="card-text text-muted small">Published: ' . date('M d, Y', strtotime($announcement['dspsann_reg_datetime'])) . '</p>';
|
||||
echo '<p class="card-text">' . htmlspecialchars(dsp_plain_text_excerpt($announcement['dspsann_description'], 100)) . '</p>';
|
||||
echo '<a href="index.php?page=announcements&id=' . htmlspecialchars($announcement['pkdspsann_id']) . '" class="btn btn-primary rounded-pill">Read More</a>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="alert alert-info rounded text-center">No announcements available at the moment.</div>';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'classifications':
|
||||
echo '<h2 class="text-center mb-4">Data Classifications</h2>';
|
||||
echo '<p class="text-center lead">Explore data categorized by type and subject matter.</p>';
|
||||
|
||||
// Filter to browse data sources by category and search query
|
||||
echo '<div class="card shadow-sm rounded mb-4 p-3">';
|
||||
echo '<div class="card-body">';
|
||||
echo '<form action="index.php" method="GET" class="row g-3 align-items-end">';
|
||||
echo '<input type="hidden" name="page" value="browse_datasources">';
|
||||
|
||||
// Category filter dropdown
|
||||
echo '<div class="col-md-4">';
|
||||
echo '<label for="categoryFilter" class="form-label">Filter by Category:</label>';
|
||||
echo '<select class="form-select rounded" id="categoryFilter" name="category_id">';
|
||||
echo '<option value="">All Categories</option>';
|
||||
foreach ($all_categories as $category) {
|
||||
echo '<option value="' . htmlspecialchars($category['pkdspscate_id']) . '">';
|
||||
echo htmlspecialchars($category['dspscate_title_en']);
|
||||
echo '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
echo '</div>';
|
||||
|
||||
// Search query input
|
||||
echo '<div class="col-md-5">';
|
||||
echo '<label for="searchQuery" class="form-label">Search by Title/Description:</label>';
|
||||
echo '<input type="text" class="form-control rounded" id="searchQuery" name="search" placeholder="Enter keywords...">';
|
||||
echo '</div>';
|
||||
|
||||
// Search button
|
||||
echo '<div class="col-md-3 d-grid">';
|
||||
echo '<button type="submit" class="btn btn-primary rounded"><i class="fas fa-search me-2"></i> Search Data</button>';
|
||||
echo '</div>';
|
||||
|
||||
echo '</form>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
|
||||
// Existing: List of Category cards
|
||||
echo '<h3 class="text-center mt-5 mb-4">Data Categories</h3>';
|
||||
echo '<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 mt-4">';
|
||||
$categories = $classification_manager->getAllCategories(); // Use Classifications manager
|
||||
if (!empty($categories)) {
|
||||
foreach ($categories as $category) {
|
||||
echo '<div class="col">';
|
||||
echo '<div class="card h-100 shadow-sm rounded p-3">';
|
||||
echo '<div class="card-body text-center">';
|
||||
echo '<i class="fas fa-folder fa-3x text-info mb-3"></i>'; // Generic icon for category
|
||||
echo '<h5 class="card-title">' . htmlspecialchars($category['dspscate_title_en']) . '</h5>';
|
||||
echo '<p class="card-text">' . htmlspecialchars($category['dspscate_details'] ?? 'No details provided.') . '</p>';
|
||||
echo '<a href="index.php?page=browse_datasources&category_id=' . htmlspecialchars($category['pkdspscate_id']) . '" class="btn btn-outline-info rounded-pill mt-2">View Data Sources</a>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
} else {
|
||||
echo '<div class="col-12"><div class="alert alert-info rounded text-center">No data categories defined yet.</div></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
/*
|
||||
echo '<h3 class="text-center mt-5 mb-4">Data Source Types</h3>';
|
||||
echo '<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4 text-center">';
|
||||
foreach ($data_types as $type) {
|
||||
echo '<div class="col">';
|
||||
echo '<div class="card h-100 shadow-sm rounded p-3">';
|
||||
echo '<div class="card-body">';
|
||||
echo '<i class="' . htmlspecialchars($type['icon']) . ' fa-3x ' . htmlspecialchars($type['color']) . ' mb-3"></i>';
|
||||
echo '<h5 class="card-title">' . htmlspecialchars($type['dspstds_name_en']) . '</h5>';
|
||||
echo '<p class="card-text">' . htmlspecialchars($type['description']) . '</p>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
*/
|
||||
break;
|
||||
case 'about':
|
||||
echo '<h2 class="text-center mb-4">About Us</h2>';
|
||||
$about_entries = $about_us_manager->getAllAboutUs();
|
||||
if (!empty($about_entries)) {
|
||||
foreach ($about_entries as $entry) {
|
||||
echo '<div class="card shadow-sm rounded mb-3">';
|
||||
echo '<div class="card-body">';
|
||||
echo '<h3 class="card-title">' . htmlspecialchars($entry['dspsabout_title_en']) . '</h3>';
|
||||
echo '<p class="card-text">' . dsp_render_rich_text($entry['dspsabout_description']) . '</p>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
} else {
|
||||
echo '<div class="alert alert-info rounded text-center">No "About Us" content available yet.</div>';
|
||||
}
|
||||
break;
|
||||
case 'contact':
|
||||
echo '<h2 class="text-center mb-4">Contact Us</h2>';
|
||||
?>
|
||||
<div class="row">
|
||||
<div class="accordion">
|
||||
<div class="card shadow-sm rounded p-4">
|
||||
<h4 class="mb-3">Send us a message</h4>
|
||||
<form action="process_contact.php" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="contactName" class="form-label">Your Name</label>
|
||||
<input type="text" class="form-control rounded" id="contactName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="contactEmail" class="form-label">Your Email (Optional)</label>
|
||||
<input type="email" class="form-control rounded" id="contactEmail" name="email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="contactMessage" class="form-label">Message</label>
|
||||
<textarea class="form-control rounded-3" id="contactMessage" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded">Send Message</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr class="my-4">
|
||||
<h4 class="mb-3">Our Contact Information</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-map-marker-alt me-2"></i> St.(289), Phnom Penh, Cambodia</li>
|
||||
<li><i class="fas fa-phone me-2"></i> +855 12 345 678</li>
|
||||
<li><i class="fas fa-envelope me-2"></i> dac@niph.org.kh</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
break;
|
||||
case 'faq':
|
||||
echo '<h2 class="text-center mb-4">Frequently Asked Questions</h2>';
|
||||
$faq_entries = $faq_manager->getAllFaqs();
|
||||
if (!empty($faq_entries)) {
|
||||
echo '<div class="accordion" id="faqAccordion">';
|
||||
foreach ($faq_entries as $index => $entry) {
|
||||
echo '<div class="accordion-item rounded shadow-sm mb-3">';
|
||||
echo '<h2 class="accordion-header" id="heading' . $index . '">';
|
||||
echo '<button class="accordion-button ' . ($index == 0 ? '' : 'collapsed') . ' rounded-top" type="button" data-bs-toggle="collapse" data-bs-target="#collapse' . $index . '" aria-expanded="' . ($index == 0 ? 'true' : 'false') . '" aria-controls="collapse' . $index . '">';
|
||||
echo htmlspecialchars($entry['dspsfaq_title_en']);
|
||||
echo '</button>';
|
||||
echo '</h2>';
|
||||
echo '<div id="collapse' . $index . '" class="accordion-collapse collapse ' . ($index == 0 ? 'show' : '') . '" aria-labelledby="heading' . $index . '" data-bs-parent="#faqAccordion">';
|
||||
echo '<div class="accordion-body">';
|
||||
echo dsp_render_rich_text($entry['dspsfaq_description']);
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="alert alert-info rounded text-center">No FAQ entries available yet.</div>';
|
||||
}
|
||||
break;
|
||||
case 'browse_datasources':
|
||||
// This will be a public page for browsing data sources
|
||||
include 'browse_datasources.php'; // Include the content from the data_user folder
|
||||
break;
|
||||
default:
|
||||
// Fallback to home if page is not recognized
|
||||
header("Location: index.php?page=home");
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<?php
|
||||
include_once("includes/footer_public.php");
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
467
install_config.php
Normal file
467
install_config.php
Normal file
@@ -0,0 +1,467 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
require_once __DIR__ . '/includes/jupyter_helpers.php';
|
||||
|
||||
redirect_if_not_logged_in('index.php');
|
||||
redirect_if_not_role('DAC Staff', 'index.php');
|
||||
|
||||
$jupyterDefaults = dsp_jupyter_defaults();
|
||||
$resolvedBaseUrl = dsp_jupyter_base_url();
|
||||
$resolvedToken = dsp_jupyter_token();
|
||||
$resolvedPort = dsp_jupyter_port();
|
||||
$envOverrides = dsp_jupyter_env_overrides();
|
||||
$workspaceBaseMessage = $jupyterDefaults['workspace_root'];
|
||||
|
||||
if (!empty($_SESSION['person_id'])) {
|
||||
$workspaceBaseMessage = sprintf(
|
||||
'%s/user_%d',
|
||||
rtrim($jupyterDefaults['workspace_root'], '/'),
|
||||
(int) $_SESSION['person_id']
|
||||
);
|
||||
}
|
||||
|
||||
$activeJupyterOverrides = array_filter(
|
||||
$envOverrides,
|
||||
static fn($value) => $value !== null && $value !== ''
|
||||
);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php include_once __DIR__ . '/includes/header_admin.php'; ?>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include_once __DIR__ . '/includes/nav_admin.php'; ?>
|
||||
<div class="main-content">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Install & Configuration</a>
|
||||
<div class="d-flex">
|
||||
<span class="navbar-text text-muted">
|
||||
Signed in as <?php echo htmlspecialchars($_SESSION['username'] ?? ''); ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-<?php echo htmlspecialchars($_SESSION['message_type'] ?? 'info'); ?> alert-dismissible fade show rounded-pill" role="alert">
|
||||
<?php echo htmlspecialchars($_SESSION['message']); ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php
|
||||
unset($_SESSION['message'], $_SESSION['message_type']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-primary">Before You Start</h2>
|
||||
<ul class="mb-0 text-muted">
|
||||
<li>Install the latest <strong>Docker Desktop</strong> (or Docker Engine with the Compose plugin) and reboot if prompted.</li>
|
||||
<li>Ensure at least <strong>2 GB</strong> of free disk space for base images and seed data.</li>
|
||||
<li>Confirm your account has access to the repository in the organisation’s source control system.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-success">Quick Setup</h2>
|
||||
<ol class="mb-0 text-muted">
|
||||
<li class="mb-2">Copy and unzip: <code><dsp></code> and <code>cd dsp</code>.</li>
|
||||
<li class="mb-2">Copy any provided <code>.env</code> templates if custom credentials are required.</li>
|
||||
<li class="mb-2">Bring the stack online with <code>docker-compose up --build</code>.</li>
|
||||
<li class="mb-2">Wait for the first run to import <code>db/niph_dsps.sql</code> into MySQL automatically.</li>
|
||||
<li class="mb-0">Visit each service to confirm connectivity (see table below).</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4" id="data-ecosystem">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-secondary">Understanding the Data Ecosystem</h2>
|
||||
<p class="text-muted">
|
||||
The Docker stack wires the PHP/Apache portal, MySQL database, phpMyAdmin console, and R-enabled Jupyter service together.
|
||||
Shared bind mounts ensure uploaded datasets and vetted R scripts stay identical across runtime services.
|
||||
</p>
|
||||
<div class="row row-cols-1 row-cols-lg-2 g-4">
|
||||
<div class="col">
|
||||
<div class="border rounded h-100 p-3 bg-light-subtle">
|
||||
<h3 class="h6 text-uppercase text-muted">Platform Architecture</h3>
|
||||
<ul class="mb-0 text-muted small">
|
||||
<li><code>dsp_app</code> serves dashboards on port 8082 and talks to MySQL over the internal network.</li>
|
||||
<li><code>dsp_db</code> seeds from <code>db/niph_dsps.sql</code> on first boot and exposes port 3307 for host access.</li>
|
||||
<li><code>dsp_phpmyadmin</code> and <code>dsp_jupyter</code> connect to the same database/files so admins can troubleshoot or run reproducible analytics.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="border rounded h-100 p-3 bg-light-subtle">
|
||||
<h3 class="h6 text-uppercase text-muted">Data Lifecycle & Governance</h3>
|
||||
<ul class="mb-0 text-muted small">
|
||||
<li><code>dsps_tbl_datasource</code> binds each dataset to its owner (<code>fkisp_id_of</code>), type, category, filenames, and publication state.</li>
|
||||
<li><code>dsps_tbl_datasource_permission</code> logs read/download/analyze approvals while <code>dsps_tbl_datasource_used</code> tracks downstream usage.</li>
|
||||
<li><code>ist_tbl_people</code> and <code>ist_tbl_users</code> provide identity, roles, and the <code>isu_can_run_r</code> capability flag for analytics features.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="h6 text-uppercase text-muted">Diagram Reference</h3>
|
||||
<div class="text-center">
|
||||
<img src="assets/images/niph_dsp_data_ecosystem.png" alt="NIPH DSP data ecosystem overview" class="img-fluid rounded border shadow-sm">
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#ecosystemDiagramModal">
|
||||
<i class="fas fa-search-plus me-1"></i>Expand diagram
|
||||
</button>
|
||||
<a class="btn btn-sm btn-outline-primary ms-2" href="assets/images/niph_dsp_data_ecosystem.png" download>
|
||||
<i class="fas fa-download me-1"></i>Download PNG
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-info">Service Endpoints</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">Service</th>
|
||||
<th scope="col">URL</th>
|
||||
<th scope="col">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PHP Application</td>
|
||||
<td><code>http://localhost:8082</code></td>
|
||||
<td>Uses credentials defined in <code>docker-compose.yml</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>phpMyAdmin</td>
|
||||
<td><code>http://localhost:8081</code></td>
|
||||
<td>Login with <code>dsp_user/dsp_pass</code> or MySQL root credentials.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jupyter (R)</td>
|
||||
<td><code>http://localhost:8888</code></td>
|
||||
<td>Authenticate with token <code>dsp-token</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MySQL (host access)</td>
|
||||
<td><code>localhost:3307</code></td>
|
||||
<td>Database <code>niph_dsps</code>, user <code>dsp_user/dsp_pass</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-warning">Configuration Checklist</h2>
|
||||
<p class="text-muted">Update the following environment variables in <code>docker-compose.yml</code> (or host environment) to match your deployment:</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">Variable</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>DB_HOST</code></td>
|
||||
<td>Database hostname reachable from the PHP container.</td>
|
||||
<td><code>mysql</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DB_PORT</code></td>
|
||||
<td>MySQL port exposed inside the container.</td>
|
||||
<td><code>3306</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DB_NAME</code></td>
|
||||
<td>Name of the primary DSP database.</td>
|
||||
<td><code>niph_dsps</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DB_USER</code></td>
|
||||
<td>Application database username.</td>
|
||||
<td><code>dsp_user</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DB_PASS</code></td>
|
||||
<td>Application database password.</td>
|
||||
<td><code>dsp_pass</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>RSCRIPT_PATH</code></td>
|
||||
<td>Override if <code>Rscript</code> is not located at the system default.</td>
|
||||
<td><code>/usr/bin/Rscript</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>JUPYTER_EXTERNAL_URL</code></td>
|
||||
<td>Base URL embedded in the portal when exposing Jupyter on a custom host or domain.</td>
|
||||
<td><code>https://localhost</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>JUPYTERHUB_PORT</code></td>
|
||||
<td>Published host port for the JupyterHub ingress.</td>
|
||||
<td><code>443</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>JUPYTER_TOKEN</code></td>
|
||||
<td>Legacy token for standalone Jupyter deployments.</td>
|
||||
<td><code>(empty)</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_APP_ORIGINS</code></td>
|
||||
<td>Space-separated origins allowed to call Jupyter APIs (CORS).</td>
|
||||
<td><code>http://localhost:8082 http://127.0.0.1:8082</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_FRAME_ANCESTORS</code></td>
|
||||
<td>Space-separated origins permitted to embed Jupyter in an iframe.</td>
|
||||
<td><code>http://localhost:8082 http://127.0.0.1:8082</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4" id="r-in-jupyter-service">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-info">R in JupyterHub Service</h2>
|
||||
<p class="text-muted">
|
||||
The <code>dsp_jupyterhub</code> service launches per-user JupyterLab containers through JupyterHub.
|
||||
These defaults mirror the values exposed in the in-app JupyterHub Service Reference so you can verify overrides in <code>.env</code> or infrastructure tooling.
|
||||
</p>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">Setting</th>
|
||||
<th scope="col">Default</th>
|
||||
<th scope="col">How to Override</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Notebook Base URL</td>
|
||||
<td><code>https://localhost</code></td>
|
||||
<td>Set <code>JUPYTER_EXTERNAL_URL</code> (or configure your reverse proxy) when publishing under a different host or scheme.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Published Port</td>
|
||||
<td><code>443</code></td>
|
||||
<td>Update the <code>${JUPYTERHUB_PORT:-443}:8000</code> mapping in <code>docker-compose.yml</code> or export <code>JUPYTERHUB_PORT</code> in your <code>.env</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Authentication Token</td>
|
||||
<td><code>(managed by OAuth)</code></td>
|
||||
<td>OAuth replaces static tokens; keep <code>JUPYTER_TOKEN</code> empty when using JupyterHub.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Workspace Mount</td>
|
||||
<td><code>datasources/user_{person_id}</code></td>
|
||||
<td>Mounted from <code>./uploads/jupyter_workspace</code>; adjust the volume in <code>docker-compose.yml</code> if you relocate storage.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h3 class="h6 text-uppercase text-muted mb-3">Environment Override Guidance</h3>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-sm table-striped align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">Variable</th>
|
||||
<th scope="col">Purpose</th>
|
||||
<th scope="col">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>JUPYTER_EXTERNAL_URL</code></td>
|
||||
<td>Overrides the base URL surfaced in the portal and iframe embeds.</td>
|
||||
<td>Leave empty to fall back to the detected hostname and <code>JUPYTERHUB_PORT</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>JUPYTERHUB_PORT</code></td>
|
||||
<td>Sets the host port used when constructing external URLs.</td>
|
||||
<td>Must align with the published port mapping in <code>docker-compose.yml</code> (fallback to <code>JUPYTER_PORT</code> for legacy deployments).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_APP_ORIGINS</code></td>
|
||||
<td>Extends the CORS allow list for DSP web origins calling notebook APIs.</td>
|
||||
<td>Include every host that loads the embedded notebook via XHR.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_FRAME_ANCESTORS</code></td>
|
||||
<td>Controls the Content-Security-Policy <code>frame-ancestors</code> directive for notebook iframes.</td>
|
||||
<td>Match the origins defined in <code>DSP_APP_ORIGINS</code> (and any admin hostnames).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>JUPYTERHUB_USERNAME_TEMPLATE</code></td>
|
||||
<td>Pattern used to map DSP accounts to JupyterHub usernames.</td>
|
||||
<td>Defaults to <code>user_{person_id}</code>. Placeholders: <code>{person_id}</code>, <code>{username}</code>, <code>{email}</code>. Only used when <code>JUPYTERHUB_USER_PATH</code> is set.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>JUPYTERHUB_USER_PATH</code></td>
|
||||
<td>Template for the per-user notebook route served by JupyterHub.</td>
|
||||
<td>When empty, DSP assumes a single JupyterLab instance and embeds the base URL. Set (for example) to <code>user/{username}/lab</code> in JupyterHub deployments.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_OAUTH_CLIENT_ID</code></td>
|
||||
<td>Client identifier issued to JupyterHub.</td>
|
||||
<td>Matches the record created in <code>dsp_oauth_clients</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_OAUTH_CLIENT_SECRET</code></td>
|
||||
<td>Confidential secret shared with JupyterHub.</td>
|
||||
<td>Store securely (prefer Docker secrets or vault tooling).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_OAUTH_AUTHORIZE_URL</code></td>
|
||||
<td>Endpoint JupyterHub uses for the OAuth authorization code flow.</td>
|
||||
<td>Typically <code>https://portal.example.com/oauth/authorize</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_OAUTH_TOKEN_URL</code></td>
|
||||
<td>Token exchange endpoint exposed by DSP.</td>
|
||||
<td>Typically <code>https://portal.example.com/oauth/token</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DSP_OAUTH_USERINFO_URL</code></td>
|
||||
<td>Endpoint returning user profile details for JupyterHub.</td>
|
||||
<td>Typically <code>https://portal.example.com/oauth/userinfo</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>JUPYTERHUB_OAUTH_CALLBACK</code></td>
|
||||
<td>Callback URL the hub advertises to DSP.</td>
|
||||
<td>Should match the hub’s public URL (e.g., <code>https://hub.example.com/hub/oauth_callback</code>).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ul class="mb-0 text-muted">
|
||||
<li>Seeded R scripts live under <code>r_scripts/</code> and appear in the notebook at <code>work/r_scripts</code> for quick demos.</li>
|
||||
<li>When tightening security headers, keep <code>DSP_APP_ORIGINS</code> and <code>DSP_FRAME_ANCESTORS</code> aligned with your published host so the embedded notebook continues to load.</li>
|
||||
</ul>
|
||||
<h3 class="h6 text-uppercase text-muted mt-4 mb-3">Current Runtime Snapshot</h3>
|
||||
<div class="table-responsive mb-3">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">Setting</th>
|
||||
<th scope="col">Value (detected)</th>
|
||||
<th scope="col">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Notebook Base URL</td>
|
||||
<td><code><?= htmlspecialchars($resolvedBaseUrl, ENT_QUOTES, 'UTF-8') ?></code></td>
|
||||
<td>Derived from <code>JUPYTER_EXTERNAL_URL</code> or the active host/port.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Published Port</td>
|
||||
<td><code><?= htmlspecialchars($resolvedPort, ENT_QUOTES, 'UTF-8') ?></code></td>
|
||||
<td>Matches <code>JUPYTERHUB_PORT</code> (or legacy <code>JUPYTER_PORT</code>) when set, otherwise the compose default.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Authentication Token</td>
|
||||
<td><code><?= htmlspecialchars($resolvedToken, ENT_QUOTES, 'UTF-8') ?></code></td>
|
||||
<td>Leave blank when OAuth is enabled via JupyterHub.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Workspace Mount</td>
|
||||
<td><code><?= htmlspecialchars($workspaceBaseMessage, ENT_QUOTES, 'UTF-8') ?></code></td>
|
||||
<td>Reflects the root folder synced into notebooks for the active account.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h4 class="h6 text-uppercase text-muted mb-2">Active Environment Overrides</h4>
|
||||
<?php if ($activeJupyterOverrides): ?>
|
||||
<ul class="small text-muted mb-0">
|
||||
<?php foreach ($activeJupyterOverrides as $variable => $value): ?>
|
||||
<li><code><?= htmlspecialchars($variable, ENT_QUOTES, 'UTF-8') ?></code>: <code><?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?></code></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="small text-muted mb-0">No overrides detected; defaults from <code>docker-compose.yml</code> are currently active.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-secondary">Maintenance Commands</h2>
|
||||
<pre class="bg-light border rounded p-3 mb-0 text-muted"><code># Stop and remove containers, keep database volume
|
||||
docker-compose down
|
||||
|
||||
# Stop containers and remove database volume (fresh start)
|
||||
docker-compose down -v
|
||||
|
||||
# Tail logs from all services
|
||||
docker-compose logs -f</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3 text-danger">Troubleshooting</h2>
|
||||
<ul class="mb-0 text-muted">
|
||||
<li class="mb-2"><strong>Database state stuck?</strong> Remove the <code>mysql_data</code> volume with <code>docker-compose down -v</code> to trigger a fresh import.</li>
|
||||
<li class="mb-2"><strong>Rscript not found?</strong> Rebuild the PHP image (<code>docker-compose build</code>) or set <code>RSCRIPT_PATH</code> to the correct binary.</li>
|
||||
<li class="mb-2"><strong>Port already in use?</strong> Adjust published ports (<code>8081</code>, <code>8082</code>, <code>8888</code>, <code>3307</code>) in <code>docker-compose.yml</code>.</li>
|
||||
<li class="mb-0"><strong>Permission denied on uploads?</strong> Run <code>chmod -R 775 uploads</code> (or update group ownership) on the host machine.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="ecosystemDiagramModal" tabindex="-1" aria-labelledby="ecosystemDiagramLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ecosystemDiagramLabel">NIPH DSP Data Ecosystem</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img src="assets/images/niph_dsp_data_ecosystem.png" alt="NIPH DSP data ecosystem expanded view" class="img-fluid rounded border shadow-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include_once __DIR__ . '/includes/footer_admin.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
25
logout.php
Normal file
25
logout.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// logout.php
|
||||
session_start();
|
||||
session_unset(); // Unset all session variables
|
||||
session_destroy(); // Destroy the session
|
||||
|
||||
require_once 'includes/auth.php'; // For set_message function
|
||||
|
||||
set_message("You have been logged out.", "info");
|
||||
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
$homeUrl = $scheme . '://' . $host . '/index.php';
|
||||
|
||||
$redirectUrl = $homeUrl;
|
||||
|
||||
$jupyterBase = getenv('JUPYTER_EXTERNAL_URL');
|
||||
if (!empty($jupyterBase)) {
|
||||
$hubLogout = rtrim($jupyterBase, '/') . '/hub/logout';
|
||||
$redirectUrl = $hubLogout . '?next=' . urlencode($homeUrl);
|
||||
}
|
||||
|
||||
header("Location: " . $redirectUrl);
|
||||
exit();
|
||||
?>
|
||||
1
logs/.gitkeep
Executable file
1
logs/.gitkeep
Executable file
@@ -0,0 +1 @@
|
||||
|
||||
282
logs/app.log
Executable file
282
logs/app.log
Executable file
@@ -0,0 +1,282 @@
|
||||
[08-Dec-2025 07:40:24 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection refused
|
||||
[08-Dec-2025 08:02:33 UTC] [DataSource] Upload requested: name=demo.csv size=5 mime=text/csv rules=CSV files (.csv)
|
||||
[08-Dec-2025 08:02:33 UTC] [DataSource] File stored as datasource_6936861905595_demo.csv
|
||||
[08-Dec-2025 08:21:22 UTC] [DataSource] Upload requested: name=individual.dta size=5464990 mime=application/octet-stream rules=Stata dataset (.dta)
|
||||
[08-Dec-2025 08:21:22 UTC] [DataSource] File stored as datasource_69368a828843c_individual.dta
|
||||
[08-Dec-2025 08:46:36 UTC] Error retrieving file info: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'dspsds_file_name' in 'field list'
|
||||
[08-Dec-2025 08:46:40 UTC] Error retrieving file info: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'dspsds_file_name' in 'field list'
|
||||
[08-Dec-2025 08:46:43 UTC] Error retrieving file info: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'dspsds_file_name' in 'field list'
|
||||
[08-Dec-2025 08:47:50 UTC] Error retrieving file info: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'dspsds_file_name' in 'field list'
|
||||
[08-Dec-2025 08:48:35 UTC] Error retrieving file info: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'dspsds_file_name' in 'field list'
|
||||
[08-Dec-2025 08:48:46 UTC] Error retrieving file info: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'dspsds_file_name' in 'field list'
|
||||
[08-Dec-2025 08:49:07 UTC] Error retrieving file info: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'dspsds_file_name' in 'field list'
|
||||
[08-Dec-2025 08:51:06 UTC] [DataSource] Upload requested: name=household.dta size=4207218 mime=application/octet-stream rules=Stata dataset (.dta)
|
||||
[08-Dec-2025 08:51:06 UTC] [DataSource] File stored as datasource_6936917a7503c_household.dta
|
||||
[08-Dec-2025 08:53:48 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:53:50 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:53:52 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:53:53 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:01 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:02 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:03 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:04 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:04 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:04 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:04 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:54:46 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:46 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:47 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:47 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:47 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:47 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:47 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:47 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:48 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:54:48 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 08:55:16 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:17 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:18 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:18 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:23 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:23 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:24 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:25 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:25 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:26 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:55:26 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 08:57:36 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:135) in /var/www/html/index.php on line 551
|
||||
[08-Dec-2025 08:57:38 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:135) in /var/www/html/index.php on line 551
|
||||
[08-Dec-2025 09:00:35 UTC] PHP Fatal error: Uncaught Error: Call to undefined function redirect() in /var/www/html/data_hybrid/download.php:13
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/data_hybrid/download.php on line 13
|
||||
[08-Dec-2025 09:01:45 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:46 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:46 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:47 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:47 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:47 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:48 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:48 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:49 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:01:54 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:00 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:01 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:01 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:01 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:02 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:02 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:03 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:03 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:03 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:03 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:04 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:04 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:02:04 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:03:45 UTC] PHP Fatal error: Uncaught Error: Call to undefined function redirect() in /var/www/html/data_hybrid/download.php:13
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/data_hybrid/download.php on line 13
|
||||
[08-Dec-2025 09:03:46 UTC] PHP Fatal error: Uncaught Error: Call to undefined function redirect() in /var/www/html/data_hybrid/download.php:13
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/data_hybrid/download.php on line 13
|
||||
[08-Dec-2025 09:03:46 UTC] PHP Fatal error: Uncaught Error: Call to undefined function redirect() in /var/www/html/data_hybrid/download.php:13
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/data_hybrid/download.php on line 13
|
||||
[08-Dec-2025 09:03:46 UTC] PHP Fatal error: Uncaught Error: Call to undefined function redirect() in /var/www/html/data_hybrid/download.php:13
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/data_hybrid/download.php on line 13
|
||||
[08-Dec-2025 09:03:47 UTC] PHP Fatal error: Uncaught Error: Call to undefined function redirect() in /var/www/html/data_hybrid/download.php:13
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/data_hybrid/download.php on line 13
|
||||
[08-Dec-2025 09:03:50 UTC] PHP Fatal error: Uncaught Error: Call to undefined function redirect() in /var/www/html/data_hybrid/download.php:13
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/data_hybrid/download.php on line 13
|
||||
[08-Dec-2025 09:04:07 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:29 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:30 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:30 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:31 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:31 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:31 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:31 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:32 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:32 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:04:32 UTC] PHP Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/html/data_hybrid/download.php on line 101
|
||||
[08-Dec-2025 09:05:01 UTC] Error submitting permission request: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '16-112' for key 'dsps_tbl_datasource_permission.fkdspsds_id'
|
||||
[08-Dec-2025 09:05:28 UTC] Error submitting permission request: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '16-112' for key 'dsps_tbl_datasource_permission.fkdspsds_id'
|
||||
[08-Dec-2025 09:07:28 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:28 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:29 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:29 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:29 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:29 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:29 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:29 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:30 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:30 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:30 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:07:30 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:15 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection refused
|
||||
[08-Dec-2025 09:09:15 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection refused
|
||||
[08-Dec-2025 09:09:15 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection refused
|
||||
[08-Dec-2025 09:09:15 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection refused
|
||||
[08-Dec-2025 09:09:16 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:16 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:16 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:16 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:16 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:16 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:17 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:17 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:17 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:17 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:17 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:09:17 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:34 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:34 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:23:35 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:24:40 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:24:40 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:24:40 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:24:40 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:24:40 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:24:40 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:25:54 UTC] Error submitting permission request: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '16-112' for key 'dsps_tbl_datasource_permission.fkdspsds_id'
|
||||
[08-Dec-2025 09:27:46 UTC] Error submitting permission request: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '16-112' for key 'dsps_tbl_datasource_permission.fkdspsds_id'
|
||||
[08-Dec-2025 09:27:58 UTC] Error submitting permission request: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '16-112' for key 'dsps_tbl_datasource_permission.fkdspsds_id'
|
||||
[08-Dec-2025 09:28:29 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:28:29 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:28:30 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:28:30 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:28:30 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:28:30 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:30:24 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_user/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:30:24 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_user/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:48 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:49 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:49 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:49 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:49 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:49 UTC] PHP Warning: Undefined array key "username" in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:39:49 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/data_hybrid/browse_datasources.php on line 96
|
||||
[08-Dec-2025 09:45:57 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/data_hybrid/download.php:5) in /var/www/html/data_hybrid/download.php on line 89
|
||||
[08-Dec-2025 09:45:57 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/data_hybrid/download.php:5) in /var/www/html/data_hybrid/download.php on line 90
|
||||
[08-Dec-2025 09:45:57 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/data_hybrid/download.php:5) in /var/www/html/data_hybrid/download.php on line 91
|
||||
[08-Dec-2025 09:45:57 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/data_hybrid/download.php:5) in /var/www/html/data_hybrid/download.php on line 92
|
||||
[08-Dec-2025 09:45:57 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/data_hybrid/download.php:5) in /var/www/html/data_hybrid/download.php on line 93
|
||||
[08-Dec-2025 09:45:57 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/data_hybrid/download.php:5) in /var/www/html/data_hybrid/download.php on line 94
|
||||
[08-Dec-2025 09:45:57 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/data_hybrid/download.php:5) in /var/www/html/data_hybrid/download.php on line 95
|
||||
[08-Dec-2025 09:50:25 UTC] [DataSource] Upload requested: name=Household_wide_3.dta size=4031848 mime=application/octet-stream rules=Stata dataset (.dta)
|
||||
[08-Dec-2025 09:50:25 UTC] [DataSource] File stored as datasource_69369f61b1b4d_household-wide-3.dta
|
||||
[08-Dec-2025 12:53:51 UTC] Registration failed: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'isp_idcard' cannot be null
|
||||
[08-Dec-2025 12:55:49 UTC] Registration failed: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'isp_idcard' cannot be null
|
||||
[08-Dec-2025 12:56:30 UTC] Registration failed: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'isp_idcard' cannot be null
|
||||
[08-Dec-2025 13:05:49 UTC] Error submitting permission request: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '16-2' for key 'dsps_tbl_datasource_permission.fkdspsds_id'
|
||||
[09-Dec-2025 01:32:52 UTC] Database connection failed: SQLSTATE[HY000] [2002] Operation not permitted
|
||||
[09-Dec-2025 01:44:18 UTC] Database connection failed: SQLSTATE[HY000] [2002] Unknown error while connecting
|
||||
[09-Dec-2025 14:40:52 UTC] [DataSource] Upload requested: name=Individual Health, Lifestyle, and Care-Seeking Dataset – Population-Based Survey on Diabetes and Hypertension in Cambodia, 2024_datasource_69368a828843c_individual.dta size=5464990 mime=application/octet-stream rules=Stata dataset (.dta)
|
||||
[09-Dec-2025 14:40:52 UTC] [DataSource] File stored as datasource_693834f41d365_individual-health-lifestyle-and-care-seeking-datas.dta
|
||||
[09-Dec-2025 14:40:52 UTC] [DataSource] Upload requested: name=Individual Health, Lifestyle, and Care-Seeking Dataset – Population-Based Survey on Diabetes and Hypertension in Cambodia, 2024_datasource_69368a828843c_individual.dta size=5464990 mime=application/octet-stream rules=CSV, JSON, PDF, XLS, XLSX
|
||||
[09-Dec-2025 14:40:52 UTC] [DataSource] Upload requested: name=Individual Health, Lifestyle, and Care-Seeking Dataset – Population-Based Survey on Diabetes and Hypertension in Cambodia, 2024_datasource_69368a828843c_individual.dta size=5464990 mime=application/octet-stream rules=CSV, JSON, PDF, XLS, XLSX
|
||||
[09-Dec-2025 14:40:52 UTC] [DataSource] Upload requested: name=Individual Health, Lifestyle, and Care-Seeking Dataset – Population-Based Survey on Diabetes and Hypertension in Cambodia, 2024_datasource_69368a828843c_individual.dta size=5464990 mime=application/octet-stream rules=CSV, JSON, PDF, XLS, XLSX
|
||||
[17-Dec-2025 09:57:47 UTC] [DataSource] Upload requested: name=Questionnaire.pdf size=366062 mime=application/pdf rules=CSV, JSON, PDF, XLS, XLSX
|
||||
[17-Dec-2025 09:57:47 UTC] [DataSource] File stored as datasource_69427e9be4fe3_questionnaire.pdf
|
||||
[24-Dec-2025 06:16:00 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:135) in /var/www/html/index.php on line 551
|
||||
[16-Jan-2026 04:28:14 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 05:24:46 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 12:46:03 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 12:46:07 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 12:46:12 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 12:46:16 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 12:46:21 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 17:36:49 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 21:27:02 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[16-Jan-2026 22:55:11 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[17-Jan-2026 01:14:58 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[17-Jan-2026 10:21:31 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[17-Jan-2026 13:01:48 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[17-Jan-2026 22:22:15 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[18-Jan-2026 04:39:25 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[18-Jan-2026 14:17:08 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[18-Jan-2026 15:07:08 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[18-Jan-2026 17:06:34 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[18-Jan-2026 17:08:29 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[19-Jan-2026 20:39:00 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[19-Jan-2026 20:39:32 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 01:31:40 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 01:32:41 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 04:28:52 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 05:13:03 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 05:13:56 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 07:10:47 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 11:42:59 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 21:20:25 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 21:57:23 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[20-Jan-2026 22:14:07 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 01:06:38 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 01:07:39 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 03:52:35 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 03:57:02 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 03:57:35 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 03:58:36 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 14:02:01 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[21-Jan-2026 19:41:05 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 03:33:31 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 03:33:45 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 03:34:27 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 03:34:31 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 08:00:16 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 11:24:50 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 12:27:55 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 13:28:39 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 14:30:22 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 14:32:09 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 15:32:14 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 16:33:32 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 17:33:50 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 18:34:17 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 19:34:37 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 20:35:05 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 21:02:25 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 21:35:46 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 22:04:31 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 22:36:44 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 23:02:02 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[22-Jan-2026 23:38:03 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 00:39:54 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 01:41:03 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:41:49 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:44:15 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:44:24 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:52:47 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:53:09 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:53:22 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:54:14 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:56:17 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 02:58:50 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 03:02:24 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
[23-Jan-2026 03:07:59 UTC] Database connection failed: SQLSTATE[HY000] [2002] Connection timed out
|
||||
64
nginx.conf
Normal file
64
nginx.conf
Normal file
@@ -0,0 +1,64 @@
|
||||
# /etc/nginx/sites-available/dsp
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name dsp.niph.org.kh;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/dsp.niph.org.kh/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/dsp.niph.org.kh/privkey.pem;
|
||||
|
||||
# DSP app
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:4010;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
|
||||
# JupyterHub
|
||||
location /hub/ {
|
||||
proxy_pass http://127.0.0.1:443/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name hub.niph.org.kh;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/dsp.niph.org.kh/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/dsp.niph.org.kh/privkey.pem;
|
||||
|
||||
# Force bare-domain hits to /hub/ so JupyterHub routes exist
|
||||
location = / {
|
||||
return 302 https://hub.niph.org.kh/hub/;
|
||||
}
|
||||
|
||||
location /hub/ {
|
||||
proxy_pass http://127.0.0.1:443/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name dsp.niph.org.kh hub.niph.org.kh;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
110
oauth/authorize.php
Normal file
110
oauth/authorize.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
// oauth/authorize.php
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../classes/OAuth.php';
|
||||
|
||||
$requestParams = [
|
||||
'response_type' => $_GET['response_type'] ?? '',
|
||||
'client_id' => $_GET['client_id'] ?? '',
|
||||
'redirect_uri' => $_GET['redirect_uri'] ?? '',
|
||||
'scope' => $_GET['scope'] ?? '',
|
||||
'state' => $_GET['state'] ?? '',
|
||||
];
|
||||
|
||||
$responseType = $requestParams['response_type'];
|
||||
$clientId = trim($requestParams['client_id']);
|
||||
$redirectUri = trim($requestParams['redirect_uri']);
|
||||
$scope = trim($requestParams['scope']);
|
||||
$state = $requestParams['state'];
|
||||
|
||||
function oauth_bad_request(string $message): void {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'invalid_request', 'error_description' => $message], JSON_UNESCAPED_SLASHES);
|
||||
exit();
|
||||
}
|
||||
|
||||
function oauth_redirect_with_error(string $redirectUri, string $error, ?string $description = null, ?string $state = null): void {
|
||||
$fragment = '';
|
||||
if (($hashPos = strpos($redirectUri, '#')) !== false) {
|
||||
$fragment = substr($redirectUri, $hashPos);
|
||||
$redirectUri = substr($redirectUri, 0, $hashPos);
|
||||
}
|
||||
$separator = (strpos($redirectUri, '?') === false) ? '?' : '&';
|
||||
$payload = ['error' => $error];
|
||||
if ($description) {
|
||||
$payload['error_description'] = $description;
|
||||
}
|
||||
if ($state !== null && $state !== '') {
|
||||
$payload['state'] = $state;
|
||||
}
|
||||
$location = $redirectUri . $separator . http_build_query($payload) . $fragment;
|
||||
header('Location: ' . $location);
|
||||
exit();
|
||||
}
|
||||
|
||||
function oauth_redirect_with_params(string $redirectUri, array $params): void {
|
||||
$fragment = '';
|
||||
if (($hashPos = strpos($redirectUri, '#')) !== false) {
|
||||
$fragment = substr($redirectUri, $hashPos);
|
||||
$redirectUri = substr($redirectUri, 0, $hashPos);
|
||||
}
|
||||
$separator = (strpos($redirectUri, '?') === false) ? '?' : '&';
|
||||
$location = $redirectUri . $separator . http_build_query($params) . $fragment;
|
||||
header('Location: ' . $location);
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($responseType !== 'code') {
|
||||
oauth_bad_request('Unsupported response_type. Only "code" is supported.');
|
||||
}
|
||||
|
||||
if ($clientId === '' || $redirectUri === '') {
|
||||
oauth_bad_request('Missing client_id or redirect_uri.');
|
||||
}
|
||||
|
||||
if (!is_logged_in()) {
|
||||
$_SESSION['oauth_pending_request'] = [
|
||||
'params' => $requestParams,
|
||||
'created_at' => time(),
|
||||
];
|
||||
set_message("Please login to continue with the requested integration.", "warning");
|
||||
header('Location: ../index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['person_id']) || (int) $_SESSION['person_id'] <= 0) {
|
||||
oauth_bad_request('Your session is missing required profile information.');
|
||||
}
|
||||
|
||||
$oauthService = new OAuthService($pdo);
|
||||
$client = $oauthService->getClient($clientId);
|
||||
|
||||
if (!$client) {
|
||||
oauth_bad_request('Unknown or revoked client.');
|
||||
}
|
||||
|
||||
if (!$oauthService->isRedirectUriAllowed($client, $redirectUri)) {
|
||||
oauth_bad_request('The provided redirect_uri is not registered for this client.');
|
||||
}
|
||||
|
||||
if (!$oauthService->isScopeAllowed($client, $scope)) {
|
||||
oauth_redirect_with_error($redirectUri, 'invalid_scope', 'Requested scope is not permitted.', $state);
|
||||
}
|
||||
|
||||
$codeData = $oauthService->issueAuthorizationCode(
|
||||
$clientId,
|
||||
(int) $_SESSION['person_id'],
|
||||
$redirectUri,
|
||||
$scope !== '' ? $scope : null
|
||||
);
|
||||
|
||||
$payload = ['code' => $codeData['code']];
|
||||
if ($state !== '') {
|
||||
$payload['state'] = $state;
|
||||
}
|
||||
|
||||
oauth_redirect_with_params($redirectUri, $payload);
|
||||
125
oauth/token.php
Normal file
125
oauth/token.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
// oauth/token.php
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../classes/OAuth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-store');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'invalid_request', 'error_description' => 'POST required.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
function respond_with_error(string $error, string $description, int $status = 400): void {
|
||||
http_response_code($status);
|
||||
echo json_encode(['error' => $error, 'error_description' => $description], JSON_UNESCAPED_SLASHES);
|
||||
exit();
|
||||
}
|
||||
|
||||
function extract_client_credentials(): array {
|
||||
$clientId = null;
|
||||
$clientSecret = null;
|
||||
|
||||
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && stripos($_SERVER['HTTP_AUTHORIZATION'], 'basic ') === 0) {
|
||||
$encoded = substr($_SERVER['HTTP_AUTHORIZATION'], 6);
|
||||
$decoded = base64_decode($encoded, true);
|
||||
if ($decoded !== false && strpos($decoded, ':') !== false) {
|
||||
[$clientId, $clientSecret] = explode(':', $decoded, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if ($clientId === null && isset($_POST['client_id'])) {
|
||||
$clientId = $_POST['client_id'];
|
||||
$clientSecret = $_POST['client_secret'] ?? '';
|
||||
}
|
||||
|
||||
return [trim((string) $clientId), (string) $clientSecret];
|
||||
}
|
||||
|
||||
[$clientId, $clientSecret] = extract_client_credentials();
|
||||
$grantType = $_POST['grant_type'] ?? '';
|
||||
|
||||
if ($clientId === '' || $grantType === '') {
|
||||
respond_with_error('invalid_request', 'client_id and grant_type are required.');
|
||||
}
|
||||
|
||||
$oauthService = new OAuthService($pdo);
|
||||
$client = $oauthService->getClient($clientId);
|
||||
|
||||
if (!$client) {
|
||||
respond_with_error('unauthorized_client', 'Unknown or revoked client.', 401);
|
||||
}
|
||||
|
||||
$requiresSecret = (int) $client['is_confidential'] === 1;
|
||||
if ($requiresSecret && $clientSecret === '') {
|
||||
respond_with_error('invalid_client', 'Client credentials required.', 401);
|
||||
}
|
||||
|
||||
if ($requiresSecret && !$oauthService->verifyClientSecret($client, $clientSecret)) {
|
||||
respond_with_error('invalid_client', 'Client authentication failed.', 401);
|
||||
}
|
||||
|
||||
switch ($grantType) {
|
||||
case 'authorization_code':
|
||||
$code = $_POST['code'] ?? '';
|
||||
$redirectUri = $_POST['redirect_uri'] ?? '';
|
||||
|
||||
if ($code === '') {
|
||||
respond_with_error('invalid_request', 'Authorization code is required.');
|
||||
}
|
||||
|
||||
$authRecord = $oauthService->consumeAuthorizationCode($code, $clientId);
|
||||
if (!$authRecord) {
|
||||
respond_with_error('invalid_grant', 'Authorization code is invalid or expired.');
|
||||
}
|
||||
|
||||
if ($redirectUri !== '' && !hash_equals($authRecord['redirect_uri'], $redirectUri)) {
|
||||
respond_with_error('invalid_grant', 'redirect_uri mismatch.');
|
||||
}
|
||||
|
||||
$tokens = $oauthService->issueTokens(
|
||||
$clientId,
|
||||
(int) $authRecord['person_id'],
|
||||
$authRecord['scope'] ?? null,
|
||||
true
|
||||
);
|
||||
break;
|
||||
|
||||
case 'refresh_token':
|
||||
$refreshToken = $_POST['refresh_token'] ?? '';
|
||||
if ($refreshToken === '') {
|
||||
respond_with_error('invalid_request', 'refresh_token is required.');
|
||||
}
|
||||
|
||||
$tokens = $oauthService->exchangeRefreshToken($clientId, $refreshToken);
|
||||
if (!$tokens) {
|
||||
respond_with_error('invalid_grant', 'Refresh token is invalid or expired.');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
respond_with_error('unsupported_grant_type', 'The grant_type is not supported.');
|
||||
}
|
||||
|
||||
$response = [
|
||||
'access_token' => $tokens['access_token'],
|
||||
'token_type' => $tokens['token_type'],
|
||||
'expires_in' => max(0, $tokens['access_expires_at'] - time()),
|
||||
];
|
||||
|
||||
if (!empty($tokens['scope'])) {
|
||||
$response['scope'] = $tokens['scope'];
|
||||
}
|
||||
|
||||
if (!empty($tokens['refresh_token'])) {
|
||||
$response['refresh_token'] = $tokens['refresh_token'];
|
||||
if (!empty($tokens['refresh_expires_at'])) {
|
||||
$response['refresh_expires_in'] = max(0, $tokens['refresh_expires_at'] - time());
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response, JSON_UNESCAPED_SLASHES);
|
||||
86
oauth/userinfo.php
Normal file
86
oauth/userinfo.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
// oauth/userinfo.php
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../classes/OAuth.php';
|
||||
require_once __DIR__ . '/../includes/jupyter_helpers.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-store');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
function unauthorized(string $message = 'Unauthorized'): void {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'invalid_token', 'error_description' => $message], JSON_UNESCAPED_SLASHES);
|
||||
exit();
|
||||
}
|
||||
|
||||
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
if ($authorization === '' && function_exists('apache_request_headers')) {
|
||||
$headers = apache_request_headers();
|
||||
if (isset($headers['Authorization'])) {
|
||||
$authorization = $headers['Authorization'];
|
||||
}
|
||||
}
|
||||
|
||||
if (stripos($authorization, 'bearer ') !== 0) {
|
||||
unauthorized('Bearer token required.');
|
||||
}
|
||||
|
||||
$token = trim(substr($authorization, 7));
|
||||
if ($token === '') {
|
||||
unauthorized('Bearer token required.');
|
||||
}
|
||||
|
||||
$oauthService = new OAuthService($pdo);
|
||||
$tokenRecord = $oauthService->getAccessToken($token);
|
||||
|
||||
if (!$tokenRecord) {
|
||||
unauthorized('Access token is invalid or expired.');
|
||||
}
|
||||
|
||||
$oauthService->recordTokenUsage($tokenRecord['token_hash']);
|
||||
|
||||
$personId = (int) $tokenRecord['person_id'];
|
||||
|
||||
$sql = "SELECT p.pkisp_id AS person_id,
|
||||
p.isp_firstname_en,
|
||||
p.isp_lastname_en,
|
||||
p.isp_email,
|
||||
u.isu_name,
|
||||
u.isu_status
|
||||
FROM ist_tbl_people p
|
||||
JOIN ist_tbl_users u ON u.fkisp_id_of = p.pkisp_id
|
||||
WHERE p.pkisp_id = :person_id
|
||||
LIMIT 1";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([':person_id' => $personId]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
unauthorized('Associated user account not found.');
|
||||
}
|
||||
|
||||
$hubUsername = dsp_resolve_jupyterhub_username(
|
||||
$personId,
|
||||
$user['isu_name'] ?? null,
|
||||
$user['isp_email'] ?? null
|
||||
);
|
||||
|
||||
$response = [
|
||||
'sub' => (string) $personId,
|
||||
'person_id' => $personId,
|
||||
'hub_username' => $hubUsername,
|
||||
'username' => $user['isu_name'] ?? null,
|
||||
'email' => $user['isp_email'] ?? null,
|
||||
'first_name' => $user['isp_firstname_en'] ?? null,
|
||||
'last_name' => $user['isp_lastname_en'] ?? null,
|
||||
'role' => $user['isu_status'] ?? null,
|
||||
];
|
||||
|
||||
if (!empty($tokenRecord['scope'])) {
|
||||
$response['scope'] = $tokenRecord['scope'];
|
||||
}
|
||||
|
||||
echo json_encode($response, JSON_UNESCAPED_SLASHES);
|
||||
18
phpunit.xml.dist
Normal file
18
phpunit.xml.dist
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
cacheResult="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="DSP Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<server name="DSP_TESTING" value="1"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
43
process_contact.php
Normal file
43
process_contact.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
// process_contact.php
|
||||
// Handles the submission of the contact form.
|
||||
|
||||
session_start();
|
||||
require_once 'config.php';
|
||||
require_once 'includes/auth.php'; // For set_message function
|
||||
require_once 'classes/Contactus.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? null);
|
||||
$message = trim($_POST['message'] ?? '');
|
||||
$client_ip = $_SERVER['REMOTE_ADDR'] ?? null;
|
||||
|
||||
if (empty($name) || empty($message)) {
|
||||
set_message('Please fill in all required fields (Name and Message).', 'danger');
|
||||
header('Location: index.php?page=contact');
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
set_message('Invalid email format.', 'danger');
|
||||
header('Location: index.php?page=contact');
|
||||
exit();
|
||||
}
|
||||
|
||||
$contactUs = new Contactus($pdo);
|
||||
|
||||
try {
|
||||
$contactUs->submitFeedback($name, $email, $message, $client_ip);
|
||||
set_message('Your message has been sent successfully! We will get back to you soon.', 'success');
|
||||
} catch (Exception $e) {
|
||||
set_message('Failed to send your message: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
|
||||
header('Location: index.php?page=contact');
|
||||
exit();
|
||||
} else {
|
||||
// If accessed directly without POST, redirect to contact page
|
||||
header('Location: index.php?page=contact');
|
||||
exit();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user