Compare commits

..

6 Commits

Author SHA1 Message Date
Flatlogic Bot
851fd0211a beta.0002 2025-09-28 22:08:52 +00:00
Flatlogic Bot
95de2da48e beta.0001 2025-09-28 22:00:58 +00:00
Flatlogic Bot
0f1653e913 FlexPass_v.0005 2025-09-26 19:57:10 +00:00
Flatlogic Bot
3d274404c6 0004 2025-09-25 21:18:43 +00:00
Flatlogic Bot
b5c30c0773 0001 2025-09-25 20:21:56 +00:00
Flatlogic Bot
a096f91b5c FlexPass v.001 2025-09-25 18:33:53 +00:00
21 changed files with 1955 additions and 122 deletions

136
add-client.php Normal file
View File

@ -0,0 +1,136 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$displayName = $_SESSION['user_display_name'] ?? 'User';
$errors = [];
$successMessage = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$clientId = $_POST['client_id'] ?? '';
$clientName = $_POST['name'] ?? '';
$status = $_POST['status'] ?? 'active';
// Validation
if (empty($clientId) || !preg_match('/^\d{4}$/', $clientId)) {
$errors[] = "Client ID must be exactly 4 digits.";
}
if (empty($clientName)) {
$errors[] = "Client Name is required.";
}
if (empty($errors)) {
$pdo = db();
// Check for uniqueness
$stmt = $pdo->prepare("SELECT COUNT(*) FROM clients WHERE client_id = ?");
$stmt->execute([$clientId]);
if ($stmt->fetchColumn() > 0) {
$errors[] = "Client ID already exists.";
} else {
// Insert into database
$stmt = $pdo->prepare("INSERT INTO clients (client_id, name, status, user_id) VALUES (?, ?, ?, ?)");
if ($stmt->execute([$clientId, $clientName, $status, $_SESSION['user_id']])) {
log_audit_event('client_create', $_SESSION['user_id'], 'client', $clientId);
header("Location: dashboard.php?status=client_added");
exit;
} else {
$errors[] = "Failed to create the client. Please try again.";
}
}
}
}
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add New Client - FlexPass</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">
<i class="bi bi-shield-lock"></i> FlexPass
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="dashboard.php">Clients</a>
</li>
</ul>
<div class="d-flex">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars($displayName); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="mb-0">Add New Client</h1>
<a href="dashboard.php" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i> Back to Client List</a>
</div>
<div class="card">
<div class="card-body">
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<ul class="mb-0">
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="add-client.php" method="POST">
<div class="mb-3">
<label for="client_id" class="form-label">Client ID</label>
<input type="text" class="form-control" id="client_id" name="client_id" required maxlength="4" pattern="\d{4}" title="Client ID must be exactly 4 digits.">
<div class="form-text">A unique 4-digit identifier for the client.</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Client Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="active" selected>Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Save Client</button>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

152
add-credential.php Normal file
View File

@ -0,0 +1,152 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$displayName = $_SESSION['user_display_name'] ?? 'User';
$pdo = db();
// Ensure a client_id is provided
if (!isset($_GET['client_id']) || empty($_GET['client_id'])) {
header('Location: dashboard.php?error=noclient');
exit;
}
$client_id = $_GET['client_id'];
// Fetch client details to display
$stmt = $pdo->prepare("SELECT name FROM clients WHERE client_id = ? AND user_id = ?");
$stmt->execute([$client_id, $_SESSION['user_id']]);
$client = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$client) {
header('Location: dashboard.php?error=invalidclient');
exit;
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? ''; // Do not trim password
$url = trim($_POST['url'] ?? '');
$notes = trim($_POST['notes'] ?? '');
if (empty($name)) {
$errors[] = 'Credential name is required.';
}
if (empty($password)) {
$errors[] = 'Password is required.';
}
if (empty($errors)) {
// For now, we will store passwords in plain text. This is NOT secure and will be updated.
// In a real-world scenario, use a proper encryption method.
$insertStmt = $pdo->prepare(
"INSERT INTO credentials (client_id, name, username, password, url, notes, created_by, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
$user_id = $_SESSION['user_id'];
try {
$insertStmt->execute([$client_id, $name, $username, $password, $url, $notes, $user_id, $user_id]);
$newCredentialId = $pdo->lastInsertId();
log_audit_event('credential_create', $user_id, 'credential', $newCredentialId);
header("Location: dashboard.php?client_id=$client_id&status=credential_added");
exit;
} catch (PDOException $e) {
$errors[] = "Database error: " . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Credential - FlexPass</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">
<i class="bi bi-shield-lock"></i> FlexPass
</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="dashboard.php">Clients</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h1 class="h4 mb-0">Add New Credential for <?php echo htmlspecialchars($client['name']); ?></h1>
</div>
<div class="card-body">
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<ul class="mb-0">
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="add-credential.php?client_id=<?php echo htmlspecialchars($client_id); ?>" method="POST">
<div class="mb-3">
<label for="name" class="form-label">Credential Name</label>
<input type="text" class="form-control" id="name" name="name" required>
<div class="form-text">A descriptive name, e.g., "Primary Admin Login" or "Database Access".</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username">
</div>
<div class="col-md-6 mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
</div>
<div class="mb-3">
<label for="url" class="form-label">URL</label>
<input type="url" class="form-control" id="url" name="url" placeholder="https://example.com/login">
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
</div>
<div class="d-flex justify-content-end">
<a href="dashboard.php?client_id=<?php echo htmlspecialchars($client_id); ?>" class="btn btn-secondary me-2">Cancel</a>
<button type="submit" class="btn btn-primary">Add Credential</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

51
add-note.php Normal file
View File

@ -0,0 +1,51 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
header('Content-Type: application/json');
// If user is not logged in, return error
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'message' => 'User not logged in.']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$clientId = $_POST['client_id'] ?? null;
$note = $_POST['note'] ?? null;
$userId = $_SESSION['user_id'];
if ($clientId && $note) {
try {
$pdo = db();
$stmt = $pdo->prepare("INSERT INTO notes (client_id, user_id, note) VALUES (?, ?, ?)");
$stmt->execute([$clientId, $userId, $note]);
$newNoteId = $pdo->lastInsertId();
log_audit_event('note_create', $userId, 'note', $newNoteId);
// Fetch the new note to return to the client
$stmt = $pdo->prepare(
"SELECT n.*, u.display_name FROM notes n " .
"JOIN users u ON n.user_id = u.user_id " .
"WHERE n.note_id = ?"
);
$stmt->execute([$newNoteId]);
$newNote = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'note' => $newNote]);
exit;
} catch (PDOException $e) {
// Optional: Log error
echo json_encode(['success' => false, 'message' => 'Database error.']);
exit;
}
} else {
echo json_encode(['success' => false, 'message' => 'Client ID and note are required.']);
exit;
}
} else {
echo json_encode(['success' => false, 'message' => 'Invalid request method.']);
exit;
}
?>

82
assets/css/custom.css Normal file
View File

@ -0,0 +1,82 @@
:root {
--primary-color: #4A90E2;
--background-color: #F4F7FA;
--surface-color: #FFFFFF;
--text-color: #333333;
--error-color: #D0021B;
--border-radius: 8px;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
}
.login-container {
width: 100%;
max-width: 400px;
padding: 2rem;
}
.login-card {
background-color: var(--surface-color);
padding: 2.5rem;
border-radius: var(--border-radius);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
text-align: center;
}
.login-card h1 {
font-weight: 700;
font-size: 1.75rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.login-card .tagline {
margin-bottom: 2rem;
color: #6c757d;
}
.form-control {
border-radius: var(--border-radius);
padding: 0.75rem 1rem;
border: 1px solid #ced4da;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(74, 144, 226, 0.25);
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
border-radius: var(--border-radius);
padding: 0.75rem;
font-weight: 600;
transition: background-color 0.2s ease-in-out;
}
.btn-primary:hover {
background-color: #357ABD;
border-color: #357ABD;
}
.phi-banner {
margin-top: 2rem;
padding: 1rem;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: var(--border-radius);
font-size: 0.875rem;
text-align: left;
}

166
assets/js/main.js Normal file
View File

@ -0,0 +1,166 @@
document.addEventListener('DOMContentLoaded', function () {
// Password visibility toggle
const togglePasswordButtons = document.querySelectorAll('.btn-toggle-password');
togglePasswordButtons.forEach(button => {
button.addEventListener('click', function () {
const targetInput = document.querySelector(this.dataset.target);
const icon = this.querySelector('i');
if (targetInput.type === 'password') {
targetInput.type = 'text';
icon.classList.remove('bi-eye-slash');
icon.classList.add('bi-eye');
} else {
targetInput.type = 'password';
icon.classList.remove('bi-eye');
icon.classList.add('bi-eye-slash');
}
});
});
// Copy password to clipboard
const copyPasswordButtons = document.querySelectorAll('.btn-copy-password');
copyPasswordButtons.forEach(button => {
button.addEventListener('click', function () {
const targetInput = document.querySelector(this.dataset.target);
navigator.clipboard.writeText(targetInput.value).then(() => {
// Visual feedback
const originalIcon = 'bi-clipboard';
const successIcon = 'bi-check-lg';
const icon = this.querySelector('i');
icon.classList.remove(originalIcon);
icon.classList.add(successIcon);
setTimeout(() => {
icon.classList.remove(successIcon);
icon.classList.add(originalIcon);
}, 1500);
}).catch(err => {
console.error('Failed to copy password: ', err);
alert('Failed to copy password.');
});
});
});
// Client search filtering
const clientSearchInput = document.getElementById('client-search');
if (clientSearchInput) {
clientSearchInput.addEventListener('keyup', function () {
const searchTerm = this.value.toLowerCase();
const clientList = document.getElementById('client-list');
const clients = clientList.getElementsByTagName('tr');
for (let i = 0; i < clients.length; i++) {
const client = clients[i];
const clientId = client.getElementsByTagName('td')[0].textContent.toLowerCase();
const clientName = client.getElementsByTagName('td')[1].textContent.toLowerCase();
if (clientId.includes(searchTerm) || clientName.includes(searchTerm)) {
client.style.display = '';
} else {
client.style.display = 'none';
}
}
});
}
// AJAX for adding a new note
const addNoteForm = document.getElementById('add-note-form');
if (addNoteForm) {
addNoteForm.addEventListener('submit', function (e) {
e.preventDefault();
const formData = new FormData(this);
const noteTextarea = this.querySelector('textarea[name="note"]');
fetch('add-note.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Create the new note element
const newNote = data.note;
const noteElement = document.createElement('div');
noteElement.classList.add('card', 'mb-2');
noteElement.innerHTML = `
<div class="card-body p-2">
<p class="card-text mb-1">"${newNote.note.replace(/\n/g, '<br>')}"</p>
<small class="text-muted">
Added by ${newNote.display_name} on ${new Date(newNote.created_at).toLocaleString()}
</small>
</div>
<div class="card-footer bg-transparent border-top-0 text-end p-1">
<a href="delete-note.php?note_id=${newNote.note_id}" class="btn btn-sm btn-outline-danger delete-note-btn" data-note-id="${newNote.note_id}"><i class="bi bi-trash"></i></a>
</div>
`;
// Add the new note to the list
const notesList = document.getElementById('notes-list');
notesList.insertBefore(noteElement, notesList.firstChild);
// Hide the "no notes" message if it's visible
const noNotesMessage = document.getElementById('no-notes-message');
if (noNotesMessage) {
noNotesMessage.classList.add('d-none');
}
// Clear the textarea
noteTextarea.value = '';
} else {
alert('Error adding note: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An unexpected error occurred.');
});
});
}
// AJAX for deleting a new note
const notesList = document.getElementById('notes-list');
if (notesList) {
notesList.addEventListener('click', function (e) {
const deleteButton = e.target.closest('.delete-note-btn');
if (deleteButton) {
e.preventDefault();
if (confirm('Are you sure you want to delete this note?')) {
const noteId = deleteButton.dataset.noteId;
const formData = new FormData();
formData.append('note_id', noteId);
fetch('delete-note.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the note element from the DOM
deleteButton.closest('.card').remove();
// Show the "no notes" message if the list is empty
if (notesList.children.length === 0) {
const noNotesMessage = document.getElementById('no-notes-message');
if (noNotesMessage) {
noNotesMessage.classList.remove('d-none');
}
}
} else {
alert('Error deleting note: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An unexpected error occurred.');
});
}
}
});
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

114
audit-log.php Normal file
View File

@ -0,0 +1,114 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
// Optional: Add role-based access control here if needed
$displayName = $_SESSION['user_display_name'] ?? 'User';
$pdo = db();
// Fetch audit events
$stmt = $pdo->query(
"SELECT ae.*, u.display_name " .
"FROM audit_events ae " .
"LEFT JOIN users u ON ae.user_id = u.user_id " .
"ORDER BY ae.created_at DESC LIMIT 200" // Limit to recent 200 events for performance
);
$events = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audit Log - FlexPass</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">
<i class="bi bi-shield-lock"></i> FlexPass
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="dashboard.php">Clients</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="audit-log.php">Audit Log</a>
</li>
</ul>
<div class="d-flex">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars($displayName); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<h1 class="mb-3">Audit Log</h1>
<div class="card">
<div class="card-body">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Timestamp</th>
<th>User</th>
<th>Action</th>
<th>Target</th>
</tr>
</thead>
<tbody>
<?php foreach ($events as $event): ?>
<tr>
<td><?php echo htmlspecialchars(date('Y-m-d H:i:s', strtotime($event['created_at']))); ?></td>
<td><?php echo htmlspecialchars($event['display_name'] ?? 'System/Unknown'); ?></td>
<td><span class="badge bg-info text-dark"><?php echo htmlspecialchars($event['action']); ?></span></td>
<td>
<?php if ($event['target_type'] && $event['target_id']): ?>
<?php echo htmlspecialchars($event['target_type'] . ' #' . $event['target_id']); ?>
<?php else: ?>
N/A
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($events)): ?>
<tr>
<td colspan="4" class="text-center text-muted">No audit events found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

279
dashboard.php Normal file
View File

@ -0,0 +1,279 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$displayName = $_SESSION['user_display_name'] ?? 'User';
$pdo = db();
$viewingClient = null;
$clients = [];
if (isset($_GET['client_id'])) {
$stmt = $pdo->prepare("SELECT * FROM clients WHERE client_id = ? AND user_id = ?");
$stmt->execute([$_GET['client_id'], $_SESSION['user_id']]);
$viewingClient = $stmt->fetch(PDO::FETCH_ASSOC);
$credentials = [];
$notes = [];
if ($viewingClient) {
$credStmt = $pdo->prepare("SELECT * FROM credentials WHERE client_id = ? ORDER BY name ASC");
$credStmt->execute([$viewingClient['client_id']]);
$credentials = $credStmt->fetchAll(PDO::FETCH_ASSOC);
$noteStmt = $pdo->prepare(
"SELECT n.*, u.display_name FROM notes n " .
"JOIN users u ON n.user_id = u.user_id " .
"WHERE n.client_id = ? ORDER BY n.created_at DESC"
);
$noteStmt->execute([$viewingClient['client_id']]);
$notes = $noteStmt->fetchAll(PDO::FETCH_ASSOC);
}
} else {
$stmt = $pdo->prepare("SELECT * FROM clients WHERE user_id = ? ORDER BY name ASC");
$stmt->execute([$_SESSION['user_id']]);
$clients = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - FlexPass</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">
<i class="bi bi-shield-lock"></i> ClientManager
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="dashboard.php">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="audit-log.php">Audit Log</a>
</li>
</ul>
<div class="d-flex">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars($displayName); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<?php if ($viewingClient): ?>
<!-- Client Detail View -->
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="dashboard.php" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i> Back to Client List</a>
<a href="add-credential.php?client_id=<?php echo htmlspecialchars($viewingClient['client_id']); ?>" class="btn btn-sm btn-primary"><i class="bi bi-plus-circle"></i> Add Credential</a>
</div>
<?php if (isset($_GET['status']) && $_GET['status'] === 'credential_added'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
New credential has been added successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['status']) && $_GET['status'] === 'credential_updated'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
Credential has been updated successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['status']) && $_GET['status'] === 'credential_deleted'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
Credential has been deleted successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['status']) && $_GET['status'] === 'note_added'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
New note has been added successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['status']) && $_GET['status'] === 'note_deleted'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
Note has been deleted successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card">
<div class="card-header">
<h2 class="mb-0">
<?php echo htmlspecialchars($viewingClient['name']); ?>
<span class="badge bg-secondary ms-2"><?php echo htmlspecialchars($viewingClient['client_id']); ?></span>
</h2>
</div>
<div class="card-body">
<p><strong>Status:</strong> <span class="badge bg-<?php echo $viewingClient['status'] === 'active' ? 'success' : 'danger'; ?>"><?php echo htmlspecialchars($viewingClient['status']); ?></span></p>
<hr>
<h4>Credentials</h4>
<table class="table table-sm table-vcenter">
<thead>
<tr>
<th>Name</th>
<th>Username</th>
<th>Password</th>
<th>Last Modified</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($credentials as $cred): ?>
<tr>
<td><?php echo htmlspecialchars($cred['name']); ?></td>
<td><?php echo htmlspecialchars($cred['username']); ?></td>
<td>
<div class="input-group input-group-sm">
<input type="password" class="form-control form-control-sm" readonly value="<?php echo htmlspecialchars($cred['password']); ?>" id="cred-pass-<?php echo $cred['credential_id']; ?>">
<button class="btn btn-outline-secondary btn-toggle-password" type="button" data-target="#cred-pass-<?php echo $cred['credential_id']; ?>"><i class="bi bi-eye-slash"></i></button>
<button class="btn btn-outline-secondary btn-copy-password" type="button" data-target="#cred-pass-<?php echo $cred['credential_id']; ?>"><i class="bi bi-clipboard"></i></button>
</div>
</td>
<td><?php echo htmlspecialchars(date('Y-m-d H:i', strtotime($cred['updated_at']))); ?></td>
<td>
<a href="edit-credential.php?credential_id=<?php echo $cred['credential_id']; ?>" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil-square"></i> Edit</a>
<a href="delete-credential.php?credential_id=<?php echo $cred['credential_id']; ?>" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i> Delete</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($credentials)): ?>
<tr>
<td colspan="5" class="text-center text-muted">No credentials found for this client.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<hr>
<h4>Notes</h4>
<div id="notes-section">
<div class="card mb-3">
<div class="card-body">
<form id="add-note-form" action="add-note.php" method="POST">
<input type="hidden" name="client_id" value="<?php echo htmlspecialchars($viewingClient['client_id']); ?>">
<div class="mb-3">
<label for="note" class="form-label">Add a new note</label>
<textarea class="form-control" id="note" name="note" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-sm btn-primary">Add Note</button>
</form>
</div>
</div>
<div id="notes-list">
<?php foreach ($notes as $note): ?>
<div class="card mb-2">
<div class="card-body p-2">
<p class="card-text mb-1">"<?php echo nl2br(htmlspecialchars($note['note'])); ?>"</p>
<small class="text-muted">
Added by <?php echo htmlspecialchars($note['display_name']); ?> on <?php echo htmlspecialchars(date('Y-m-d H:i', strtotime($note['created_at']))); ?>
</small>
</div>
<div class="card-footer bg-transparent border-top-0 text-end p-1">
<a href="delete-note.php?note_id=<?php echo $note['note_id']; ?>" class="btn btn-sm btn-outline-danger delete-note-btn" data-note-id="<?php echo $note['note_id']; ?>"><i class="bi bi-trash"></i></a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (empty($notes)): ?>
<p id="no-notes-message" class="text-center text-muted">No notes found for this client.</p>
<?php else: ?>
<p id="no-notes-message" class="text-center text-muted d-none">No notes found for this client.</p>
<?php endif; ?>
</div>
</div>
</div>
<?php else: ?>
<!-- Client List View -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="mb-0">Clients</h1>
<a href="add-client.php" class="btn btn-primary"><i class="bi bi-plus-circle"></i> Add New Client</a>
</div>
<?php if (isset($_GET['status']) && $_GET['status'] === 'client_added'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
New client has been added successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="mb-3">
<input type="text" id="client-search" class="form-control" placeholder="Search clients...">
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Client ID</th>
<th>Name</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="client-list">
<?php foreach ($clients as $client): ?>
<tr>
<td><?php echo htmlspecialchars($client['client_id']); ?></td>
<td><?php echo htmlspecialchars($client['name']); ?></td>
<td><span class="badge bg-<?php echo $client['status'] === 'active' ? 'success' : 'danger'; ?>"><?php echo htmlspecialchars($client['status']); ?></span></td>
<td>
<a href="dashboard.php?client_id=<?php echo htmlspecialchars($client['client_id']); ?>" class="btn btn-sm btn-outline-primary">View</a>
<a href="edit-client.php?client_id=<?php echo htmlspecialchars($client['client_id']); ?>" class="btn btn-sm btn-outline-secondary">Edit</a>
<a href="delete-client.php?client_id=<?php echo htmlspecialchars($client['client_id']); ?>" class="btn btn-sm btn-outline-danger">Delete</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($clients)): ?>
<tr>
<td colspan="4" class="text-center text-muted">No clients found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

63
db/migrate.php Normal file
View File

@ -0,0 +1,63 @@
<?php
require_once __DIR__ . '/config.php';
function run_migrations() {
$pdo = db();
$migrationsDir = __DIR__ . '/migrations';
// 1. Create migrations table if it doesn't exist
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS `migrations` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`migration` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
} catch (PDOException $e) {
echo "Error creating migrations table: " . $e->getMessage() . "\n";
return false;
}
// 2. Get all migration files and executed migrations
$migrationFiles = glob($migrationsDir . '/*.sql');
sort($migrationFiles);
$stmt = $pdo->query("SELECT migration FROM migrations");
$executedMigrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
// 3. Run migrations that have not been executed yet
foreach ($migrationFiles as $file) {
$migrationName = basename($file);
if (in_array($migrationName, $executedMigrations)) {
echo "Skipping already executed migration: {$migrationName}\n";
continue;
}
echo "Running migration: {$migrationName}...\n";
$sql = file_get_contents($file);
if ($sql === false) {
echo "Error: Could not read file {$migrationName}.\n";
continue;
}
try {
// DDL statements often have implicit commits, so transactions are not reliable.
$pdo->exec($sql);
$insertStmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
$insertStmt->execute([$migrationName]);
echo "Success.\n";
} catch (PDOException $e) {
echo "Error executing migration {$migrationName}: " . $e->getMessage() . "\n";
// Exit on first error
return false;
}
}
echo "All new migrations have been executed.\n";
return true;
}
if (php_sapi_name() === 'cli') {
run_migrations();
}
?>

View File

@ -0,0 +1,69 @@
CREATE TABLE IF NOT EXISTS `users` (
`id` char(36) NOT NULL DEFAULT (uuid()),
`email` varchar(255) NOT NULL,
`display_name` varchar(255) DEFAULT NULL,
`role` enum('Admin','TechTeam') NOT NULL,
`status` enum('active','disabled') NOT NULL DEFAULT 'active',
`password_enc` varchar(255) NOT NULL,
`mfa_enabled` tinyint(1) NOT NULL DEFAULT '0',
`last_login_at` datetime DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `clients` (
`client_id` varchar(4) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`status` enum('active','inactive') NOT NULL DEFAULT 'active',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `credentials` (
`id` char(36) NOT NULL DEFAULT (uuid()),
`client_id` varchar(4) NOT NULL,
`system_name` varchar(255) NOT NULL,
`username` varchar(255) NOT NULL,
`password_enc` text NOT NULL,
`additional_fields` json DEFAULT NULL,
`tags` json DEFAULT NULL,
`rotation_due_at` datetime DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`),
CONSTRAINT `credentials_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`client_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `notes` (
`id` char(36) NOT NULL DEFAULT (uuid()),
`client_id` varchar(4) NOT NULL,
`user_id` char(36) NOT NULL,
`text` text NOT NULL,
`pinned` tinyint(1) NOT NULL DEFAULT '0',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `notes_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`client_id`) ON DELETE CASCADE,
CONSTRAINT `notes_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `audit_events` (
`id` char(36) NOT NULL DEFAULT (uuid()),
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`actor_user_id` char(36) DEFAULT NULL,
`action` varchar(255) NOT NULL,
`entity_type` varchar(255) DEFAULT NULL,
`entity_id` varchar(255) DEFAULT NULL,
`client_id` varchar(4) DEFAULT NULL,
`metadata` json DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `actor_user_id` (`actor_user_id`),
KEY `client_id_idx` (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -0,0 +1 @@
ALTER TABLE `clients` ADD COLUMN `user_id` CHAR(36) NULL AFTER `status`;

View File

@ -0,0 +1,2 @@
ALTER TABLE `clients` MODIFY COLUMN `user_id` CHAR(36) NOT NULL;
ALTER TABLE `clients` ADD CONSTRAINT `fk_clients_user_id` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE;

78
db/seed.php Normal file
View File

@ -0,0 +1,78 @@
<?php
require_once __DIR__ . '/config.php';
function seed_database() {
$pdo = db();
// Seed admin user
$email = 'admin@flexpass.local';
$password = 'password';
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
try {
// Check if user already exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);
$adminUserId = $stmt->fetchColumn();
if ($adminUserId) {
echo "Admin user already exists with ID: $adminUserId.\n";
} else {
$stmt = $pdo->prepare("INSERT INTO users (email, password_enc, role, display_name) VALUES (?, ?, 'Admin', 'Admin User')");
$stmt->execute([$email, $hashedPassword]);
$adminUserId = $pdo->lastInsertId();
echo "Admin user created successfully with ID: $adminUserId.\n";
echo "Email: " . $email . "\n";
echo "Password: " . $password . "\n";
}
} catch (PDOException $e) {
echo "Error seeding database: " . $e->getMessage() . "\n";
return false;
}
// Seed clients
try {
$stmt = $pdo->query("SELECT count(*) FROM clients");
if ($stmt->fetchColumn() > 0) {
echo "Clients table already has data. Checking for user_id assignment.\n";
// Check if clients are unassigned
$stmt = $pdo->prepare("SELECT client_id FROM clients WHERE user_id IS NULL OR user_id = 0");
$stmt->execute();
$unassigned_clients = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (count($unassigned_clients) > 0) {
echo "Found " . count($unassigned_clients) . " unassigned clients. Assigning to admin user.\n";
$updateStmt = $pdo->prepare("UPDATE clients SET user_id = ? WHERE client_id = ?");
foreach ($unassigned_clients as $client_id) {
$updateStmt->execute([$adminUserId, $client_id]);
}
echo "Assigned " . count($unassigned_clients) . " clients to admin user.\n";
} else {
echo "All clients are already assigned to a user.\n";
}
} else {
echo "Clients table is empty. Seeding new clients.\n";
$clients = [
['1001', 'Stark Industries', 'active', $adminUserId],
['1002', 'Wayne Enterprises', 'active', $adminUserId],
['1003', 'Cyberdyne Systems', 'inactive', $adminUserId],
];
$stmt = $pdo->prepare("INSERT INTO clients (client_id, name, status, user_id) VALUES (?, ?, ?, ?)");
foreach ($clients as $client) {
$stmt->execute($client);
}
echo "Seeded " . count($clients) . " clients for admin user.\n";
}
} catch (PDOException $e) {
echo "Error seeding clients: " . $e->getMessage() . "\n";
return false;
}
return true;
}
if (php_sapi_name() === 'cli') {
seed_database();
}
?>

127
delete-client.php Normal file
View File

@ -0,0 +1,127 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$displayName = $_SESSION['user_display_name'] ?? 'User';
$clientId = $_GET['client_id'] ?? null;
$client = null;
$error = '';
if (!$clientId) {
header('Location: dashboard.php');
exit;
}
$pdo = db();
// Fetch client data to display its name in the confirmation message
try {
$stmt = $pdo->prepare("SELECT name FROM clients WHERE client_id = ? AND user_id = ?");
$stmt->execute([$clientId, $_SESSION['user_id']]);
$client = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$client) {
// If client not found or doesn't belong to the user, redirect.
header('Location: dashboard.php');
exit;
}
} catch (PDOException $e) {
$error = "Error fetching client data: " . $e->getMessage();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Check for confirmation
if (isset($_POST['confirm_delete'])) {
try {
// The ON DELETE CASCADE constraint will handle associated credentials and notes.
$stmt = $pdo->prepare("DELETE FROM clients WHERE client_id = ? AND user_id = ?");
$stmt->execute([$clientId, $_SESSION['user_id']]);
log_audit_event('client_delete', $_SESSION['user_id'], 'client', $clientId);
// Using session to pass success message
$_SESSION['success_message'] = "Client '" . htmlspecialchars($client['name']) . "' and all associated data have been deleted.";
header('Location: dashboard.php?status=client_deleted');
exit;
} catch (PDOException $e) {
$error = "Error deleting client: " . $e->getMessage();
}
} else {
// If not confirmed, just redirect
header('Location: dashboard.php?client_id=' . $clientId);
exit;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Delete Client - ClientManager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">ClientManager</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="dashboard.php">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="audit-log.php">Audit Log</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars($displayName); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<h2>Delete Client</h2>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if ($client): ?>
<div class="alert alert-warning">
<h4>Are you sure?</h4>
<p>This action will permanently delete the client <strong><?php echo htmlspecialchars($client['name']); ?></strong> and all of their associated credentials and notes. This cannot be undone.</p>
</div>
<form action="delete-client.php?client_id=<?php echo htmlspecialchars($clientId); ?>" method="post">
<button type="submit" name="confirm_delete" class="btn btn-danger">Yes, Delete Everything</button>
<a href="dashboard.php?client_id=<?php echo htmlspecialchars($clientId); ?>" class="btn btn-secondary">Cancel</a>
</form>
<?php else: ?>
<div class="alert alert-info">Client not found or you do not have permission to delete it.</div>
<a href="dashboard.php" class="btn btn-primary">Back to Dashboard</a>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

104
delete-credential.php Normal file
View File

@ -0,0 +1,104 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$displayName = $_SESSION['user_display_name'] ?? 'User';
$credentialId = $_GET['credential_id'] ?? null;
$credential = null;
$error = '';
if (!$credentialId) {
header('Location: dashboard.php');
exit;
}
$pdo = db();
// Fetch credential data to display its name and ensure it belongs to the user.
try {
$stmt = $pdo->prepare(
"SELECT c.credential_id, c.name, c.client_id FROM credentials c " .
"JOIN clients cl ON c.client_id = cl.client_id " .
"WHERE c.credential_id = ? AND cl.user_id = ?"
);
$stmt->execute([$credentialId, $_SESSION['user_id']]);
$credential = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$credential) {
// If credential not found or doesn't belong to the user, redirect.
header('Location: dashboard.php');
exit;
}
} catch (PDOException $e) {
$error = "Error fetching credential data: " . $e->getMessage();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['confirm_delete'])) {
try {
$stmt = $pdo->prepare(
"DELETE c FROM credentials c " .
"JOIN clients cl ON c.client_id = cl.client_id " .
"WHERE c.credential_id = ? AND cl.user_id = ?"
);
$stmt->execute([$credentialId, $_SESSION['user_id']]);
log_audit_event('credential_delete', $_SESSION['user_id'], 'credential', $credentialId);
header('Location: dashboard.php?client_id=' . $credential['client_id'] . '&status=credential_deleted');
exit;
} catch (PDOException $e) {
$error = "Error deleting credential: " . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Delete Credential - ClientManager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">ClientManager</a>
</div>
</nav>
<div class="container mt-4">
<h2>Delete Credential</h2>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if ($credential): ?>
<div class="alert alert-warning">
<h4>Are you sure?</h4>
<p>This action will permanently delete the credential <strong><?php echo htmlspecialchars($credential['name']); ?></strong>.</p>
</div>
<form action="delete-credential.php?credential_id=<?php echo htmlspecialchars($credentialId); ?>" method="post">
<button type="submit" name="confirm_delete" class="btn btn-danger">Yes, Delete</button>
<a href="dashboard.php?client_id=<?php echo htmlspecialchars($credential['client_id']); ?>" class="btn btn-secondary">Cancel</a>
</form>
<?php else: ?>
<div class="alert alert-info">Credential not found or you do not have permission to delete it.</div>
<a href="dashboard.php" class="btn btn-primary">Back to Dashboard</a>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

57
delete-note.php Normal file
View File

@ -0,0 +1,57 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method.']);
exit;
}
// If user is not logged in, return error
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'message' => 'User not logged in.']);
exit;
}
$noteId = $_POST['note_id'] ?? null;
if ($noteId) {
try {
$pdo = db();
// Verify that the note belongs to a client of the logged-in user
$stmt = $pdo->prepare(
"SELECT n.note_id FROM notes n " .
"JOIN clients c ON n.client_id = c.client_id " .
"WHERE n.note_id = ? AND c.user_id = ?"
);
$stmt->execute([$noteId, $_SESSION['user_id']]);
$note = $stmt->fetch(PDO::FETCH_ASSOC);
if ($note) {
log_audit_event('note_delete', $_SESSION['user_id'], 'note', $noteId);
// Now, delete the note
$deleteStmt = $pdo->prepare("DELETE FROM notes WHERE note_id = ?");
$deleteStmt->execute([$noteId]);
echo json_encode(['success' => true]);
exit;
} else {
echo json_encode(['success' => false, 'message' => 'Note not found or you do not have permission to delete it.']);
exit;
}
} catch (PDOException $e) {
// Optional: Log error
error_log("Error deleting note: " . $e->getMessage());
echo json_encode(['success' => false, 'message' => 'Database error.']);
exit;
}
} else {
echo json_encode(['success' => false, 'message' => 'Note ID is required.']);
exit;
}
?>

140
edit-client.php Normal file
View File

@ -0,0 +1,140 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$displayName = $_SESSION['user_display_name'] ?? 'User';
$clientId = $_GET['client_id'] ?? null;
$client = null;
$errors = [];
if (!$clientId) {
header('Location: dashboard.php');
exit;
}
$pdo = db();
// Fetch client data
try {
$stmt = $pdo->prepare("SELECT * FROM clients WHERE client_id = ? AND user_id = ?");
$stmt->execute([$clientId, $_SESSION['user_id']]);
$client = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$client) {
// If client not found or doesn't belong to the user, redirect.
header('Location: dashboard.php');
exit;
}
} catch (PDOException $e) {
$errors[] = "Error fetching client data: " . $e->getMessage();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$status = trim($_POST['status'] ?? '');
if (empty($name)) {
$errors[] = 'Client name is required.';
}
if (!in_array($status, ['active', 'inactive'])) {
$errors[] = 'Invalid status value.';
}
if (empty($errors)) {
try {
$stmt = $pdo->prepare("UPDATE clients SET name = ?, status = ? WHERE client_id = ? AND user_id = ?");
$stmt->execute([$name, $status, $clientId, $_SESSION['user_id']]);
log_audit_event('client_edit', $_SESSION['user_id'], 'client', $clientId);
header('Location: dashboard.php?client_id=' . $clientId . '&status=client_updated');
exit;
} catch (PDOException $e) {
$errors[] = "Error updating client: " . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Client - ClientManager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">ClientManager</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="dashboard.php">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="audit-log.php">Audit Log</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars($displayName); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<h2>Edit Client</h2>
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<ul class="mb-0">
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($client): ?>
<form action="edit-client.php?client_id=<?php echo htmlspecialchars($clientId); ?>" method="post">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($client['name']); ?>" required>
</div>
<div class="mb-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="active" <?php echo ($client['status'] === 'active') ? 'selected' : ''; ?>>Active</option>
<option value="inactive" <?php echo ($client['status'] === 'inactive') ? 'selected' : ''; ?>>Inactive</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="dashboard.php?client_id=<?php echo htmlspecialchars($clientId); ?>" class="btn btn-secondary">Cancel</a>
</form>
<?php else: ?>
<p>Client not found or you do not have permission to view it.</p>
<a href="dashboard.php" class="btn btn-primary">Back to Dashboard</a>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

168
edit-credential.php Normal file
View File

@ -0,0 +1,168 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/audit.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
// Check if credential_id is provided
if (!isset($_GET['credential_id'])) {
header('Location: dashboard.php');
exit;
}
$pdo = db();
$credential_id = $_GET['credential_id'];
// Fetch the credential
$stmt = $pdo->prepare(
"SELECT c.* FROM credentials c " .
"JOIN clients cl ON c.client_id = cl.client_id " .
"WHERE c.credential_id = ? AND cl.user_id = ?"
);
$stmt->execute([$credential_id, $_SESSION['user_id']]);
$credential = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$credential) {
// Credential not found
header('Location: dashboard.php');
exit;
}
$client_id = $credential['client_id']; // For redirecting back
// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name']);
$username = trim($_POST['username']);
$password = $_POST['password']; // Don't trim password
$url = trim($_POST['url']);
$notes = trim($_POST['notes']);
// Basic validation
if (empty($name) || empty($username)) {
$error = "Credential Name and Username are required.";
} else {
// To securely update, we must join against the clients table to check the user_id.
$sql = "UPDATE credentials c " .
"JOIN clients cl ON c.client_id = cl.client_id " .
"SET c.name = ?, c.username = ?, c.url = ?, c.notes = ?, c.updated_at = NOW()";
$params = [$name, $username, $url, $notes];
if (!empty($password)) {
$sql .= ", c.password = ?";
$params[] = $password;
}
$sql .= " WHERE c.credential_id = ? AND cl.user_id = ?";
$params[] = $credential_id;
$params[] = $_SESSION['user_id'];
$updateStmt = $pdo->prepare($sql);
$updateStmt->execute($params);
log_audit_event('credential_edit', $_SESSION['user_id'], 'credential', $credential_id);
// Redirect back to the client detail page with a success message
header("Location: dashboard.php?client_id=$client_id&status=credential_updated");
exit;
}
}
$displayName = $_SESSION['user_display_name'] ?? 'User';
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Credential - FlexPass</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">
<i class="bi bi-shield-lock"></i> FlexPass
</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="dashboard.php">Clients</a>
</li>
</ul>
<div class="d-flex">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars($displayName); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h1 class="h4 mb-0">Edit Credential</h1>
</div>
<div class="card-body">
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<form action="edit-credential.php?credential_id=<?php echo htmlspecialchars($credential_id); ?>" method="POST">
<div class="mb-3">
<label for="name" class="form-label">Credential Name</label>
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($credential['name']); ?>" required>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" value="<?php echo htmlspecialchars($credential['username']); ?>" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Leave blank to keep current password">
<small class="form-text text-muted">Enter a new password only if you want to change it.</small>
</div>
<div class="mb-3">
<label for="url" class="form-label">URL</label>
<input type="url" class="form-control" id="url" name="url" value="<?php echo htmlspecialchars($credential['url']); ?>">
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3"><?php echo htmlspecialchars($credential['notes']); ?></textarea>
</div>
<div class="d-flex justify-content-end">
<a href="dashboard.php?client_id=<?php echo htmlspecialchars($client_id); ?>" class="btn btn-secondary me-2">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

26
includes/audit.php Normal file
View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../db/config.php';
/**
* Logs an audit event.
*
* @param string $action The action performed (e.g., 'login_success', 'credential_create').
* @param int|null $userId The ID of the user who performed the action. Can be null.
* @param string|null $targetType The type of object the action was performed on (e.g., 'client', 'credential').
* @param int|null $targetId The ID of the object.
*/
function log_audit_event(string $action, ?int $userId, ?string $targetType = null, ?int $targetId = null)
{
try {
$pdo = db();
$stmt = $pdo->prepare(
"INSERT INTO audit_events (user_id, action, target_type, target_id) VALUES (?, ?, ?, ?)"
);
$stmt->execute([$userId, $action, $targetType, $targetId]);
} catch (PDOException $e) {
// In a real application, you would log this error to a file or monitoring service.
// For this example, we'll fail silently to not disrupt the user experience.
error_log('Audit log failed: ' . $e->getMessage());
}
}
?>

224
index.php
View File

@ -1,131 +1,121 @@
<?php <?php
declare(strict_types=1); session_start();
@ini_set('display_errors', '1'); require_once __DIR__ . '/db/config.php';
@error_reporting(E_ALL); require_once __DIR__ . '/includes/audit.php';
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; // If user is already logged in, redirect to dashboard
$now = date('Y-m-d H:i:s'); if (isset($_SESSION['user_id'])) {
header('Location: dashboard.php');
exit;
}
$error_message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password)) {
$error_message = 'Please enter both email and password.';
} else {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_enc'])) {
if ($user['status'] === 'active') {
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_email'] = $user['email'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_display_name'] = $user['display_name'];
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// Update last login timestamp
$updateStmt = $pdo->prepare("UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?");
$updateStmt->execute([$user['id']]);
log_audit_event('login_success', $user['id']);
header('Location: dashboard.php');
exit;
} else {
$error_message = 'Your account is disabled. Please contact an administrator.';
log_audit_event('login_failed_disabled', $user['id']);
}
} else {
$error_message = 'Invalid email or password.';
log_audit_event('login_failed_credentials', $user['id'] ?? null);
}
} catch (PDOException $e) {
// In a real app, you would log this error.
$error_message = 'A database error occurred. Please try again later.';
}
}
}
?> ?>
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Style</title> <title>FlexPass Secure Login</title>
<!-- SEO & Meta Tags -->
<meta name="description" content="Secure login for FlexPass, the HIPAA-ready credentials vault.">
<meta name="robots" content="noindex, nofollow"> <!-- Internal tool, no need to index -->
<!-- Open Graph -->
<meta property="og:title" content="FlexPass Secure Login">
<meta property="og:description" content="A secure, multi-tenant internal web app to store and manage client credentials.">
<meta property="og:type" content="website">
<meta property="og:url" content="">
<!-- Stylesheets -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style> <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main>
<div class="card"> <main class="login-container">
<h1>Analyzing your requirements and generating your website…</h1> <div class="login-card">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <h1>FlexPass</h1>
<span class="sr-only">Loading…</span> <p class="tagline">HIPAA-Ready Credential Vault</p>
<?php if (!empty($error_message)): ?>
<div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($error_message); ?>
</div>
<?php endif; ?>
<form id="loginForm" method="POST">
<div class="mb-3 text-start">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-4 text-start">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Sign In</button>
</form>
<div class="phi-banner">
<strong>Reminder:</strong> Do not store Protected Health Information (PHI) in notes or other non-encrypted fields.
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWiZZy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> </main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <!-- Scripts -->
</footer> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body> </body>
</html> </html>

28
logout.php Normal file
View File

@ -0,0 +1,28 @@
<?php
session_start();
require_once __DIR__ . '/includes/audit.php';
if (isset($_SESSION['user_id'])) {
log_audit_event('logout', $_SESSION['user_id']);
}
// Unset all of the session variables
$_SESSION = [];
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Finally, destroy the session.
session_destroy();
// Redirect to login page
header('Location: index.php');
exit;
?>