Login added

This commit is contained in:
Mark Pinkster 2026-02-19 15:58:15 +01:00
parent 0b0396af48
commit 341a113463
33 changed files with 3012 additions and 170 deletions

View File

@ -15,3 +15,7 @@ TV_SEASON_ID=your_season_id_here
# Application Settings # Application Settings
APP_ENV=production APP_ENV=production
APP_DEBUG=false APP_DEBUG=false
# Authentication Settings
# Session timeout in seconds (default: 7200 = 2 hours)
AUTH_SESSION_TIMEOUT=7200

1
.gitignore vendored
View File

@ -12,6 +12,7 @@
*.sln.docstates *.sln.docstates
.env .env
.DS_Store .DS_Store
.auth_setup_done
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs

View File

@ -61,9 +61,11 @@ EXIT;
### Stap 4: Database Migratie ### Stap 4: Database Migratie
**4.1 Voer migratie uit:** **4.1 Voer migraties uit:**
```bash ```bash
mysql -u talpa_user -p talpa_planning < migrations/001_add_blocks_and_colors.sql mysql -u talpa_user -p talpa_planning < migrations/001_add_blocks_and_colors.sql
mysql -u talpa_user -p talpa_planning < migrations/002_add_talpa_transmission_id.sql
mysql -u talpa_user -p talpa_planning < migrations/003_add_authentication.sql
``` ```
**4.2 Controleer of tabellen zijn aangemaakt:** **4.2 Controleer of tabellen zijn aangemaakt:**
@ -73,9 +75,12 @@ mysql -u talpa_user -p talpa_planning -e "SHOW TABLES;"
Je zou moeten zien: Je zou moeten zien:
- block_templates - block_templates
- infomercials
- daily_blocks - daily_blocks
- infomercials
- login_attempts
- sessions
- transmissions - transmissions
- users
### Stap 5: Environment Configuratie ### Stap 5: Environment Configuratie
@ -189,9 +194,34 @@ server {
sudo systemctl restart nginx sudo systemctl restart nginx
``` ```
### Stap 8: Test de Installatie ### Stap 8: Authenticatie Setup
**8.1 Open in browser:** **8.1 Voer de setup script uit:**
Via browser:
```
http://localhost/telvero_whatson_talpa/setup_auth.php
```
Of via command line:
```bash
php setup_auth.php
```
Dit maakt de standaard gebruikers aan:
- **Admin**: gebruikersnaam `admin`, wachtwoord `Admin@2026!`
- **Guest**: gebruikersnaam `guest`, wachtwoord `Guest@2026!`
⚠️ **Wijzig deze wachtwoorden na de eerste login!**
**8.2 Verwijder de setup script na gebruik:**
```bash
rm setup_auth.php
```
### Stap 9: Test de Installatie
**9.1 Open in browser:**
``` ```
http://localhost/telvero_whatson_talpa/ http://localhost/telvero_whatson_talpa/
``` ```
@ -349,13 +379,20 @@ Bij problemen:
- [ ] PHP 7.4+ geïnstalleerd - [ ] PHP 7.4+ geïnstalleerd
- [ ] MySQL database aangemaakt - [ ] MySQL database aangemaakt
- [ ] Composer dependencies geïnstalleerd - [ ] Composer dependencies geïnstalleerd
- [ ] Database migratie uitgevoerd - [ ] Database migraties uitgevoerd (001, 002, 003)
- [ ] .env file geconfigureerd - [ ] .env file geconfigureerd
- [ ] Bestandspermissies ingesteld - [ ] Bestandspermissies ingesteld
- [ ] Webserver geconfigureerd - [ ] Webserver geconfigureerd
- [ ] Dashboard bereikbaar in browser - [ ] setup_auth.php uitgevoerd
- [ ] setup_auth.php verwijderd
- [ ] Login pagina bereikbaar
- [ ] Admin login werkt
- [ ] Guest login werkt
- [ ] Dashboard bereikbaar na login
- [ ] Blok templates zichtbaar - [ ] Blok templates zichtbaar
- [ ] Test infomercial aangemaakt - [ ] Test infomercial aangemaakt (admin)
- [ ] Kalender laadt correct - [ ] Kalender laadt correct
- [ ] Gebruikersbeheer bereikbaar (admin)
- [ ] Standaard wachtwoorden gewijzigd
Als alle items zijn afgevinkt, is de installatie succesvol! 🎉 Als alle items zijn afgevinkt, is de installatie succesvol! 🎉

420
admin/users.php Normal file
View File

@ -0,0 +1,420 @@
<?php
/**
* User Management - Admin Only
* Telvero Talpa Planning System
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
require_once __DIR__ . '/../auth/auth_functions.php';
// Only admins can access this page
requireAdmin();
$db = getAuthDb();
$success = '';
$error = '';
// Handle create user
if (isset($_POST['create_user'])) {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$role = $_POST['role'] ?? 'guest';
if (empty($username) || empty($email) || empty($password)) {
$error = 'Vul alle verplichte velden in.';
} elseif (strlen($password) < 8) {
$error = 'Wachtwoord moet minimaal 8 tekens lang zijn.';
} elseif (!in_array($role, ['admin', 'guest'])) {
$error = 'Ongeldige rol geselecteerd.';
} else {
try {
$passwordHash = password_hash($password, PASSWORD_BCRYPT);
$stmt = $db->prepare("
INSERT INTO users (username, email, password_hash, role, is_active)
VALUES (?, ?, ?, ?, 1)
");
$stmt->execute([$username, $email, $passwordHash, $role]);
$success = "Gebruiker '{$username}' succesvol aangemaakt.";
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$error = 'Gebruikersnaam of email is al in gebruik.';
} else {
$error = 'Er is een fout opgetreden bij het aanmaken van de gebruiker.';
}
}
}
}
// Handle update user
if (isset($_POST['update_user'])) {
$userId = (int)($_POST['user_id'] ?? 0);
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$role = $_POST['role'] ?? 'guest';
$isActive = isset($_POST['is_active']) ? 1 : 0;
$newPassword = $_POST['new_password'] ?? '';
if (empty($username) || empty($email)) {
$error = 'Vul alle verplichte velden in.';
} elseif (!in_array($role, ['admin', 'guest'])) {
$error = 'Ongeldige rol geselecteerd.';
} else {
try {
if (!empty($newPassword)) {
if (strlen($newPassword) < 8) {
$error = 'Nieuw wachtwoord moet minimaal 8 tekens lang zijn.';
} else {
$passwordHash = password_hash($newPassword, PASSWORD_BCRYPT);
$stmt = $db->prepare("
UPDATE users
SET username = ?, email = ?, password_hash = ?, role = ?, is_active = ?
WHERE id = ?
");
$stmt->execute([$username, $email, $passwordHash, $role, $isActive, $userId]);
$success = "Gebruiker succesvol bijgewerkt (inclusief wachtwoord).";
}
} else {
$stmt = $db->prepare("
UPDATE users
SET username = ?, email = ?, role = ?, is_active = ?
WHERE id = ?
");
$stmt->execute([$username, $email, $role, $isActive, $userId]);
$success = "Gebruiker succesvol bijgewerkt.";
}
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$error = 'Gebruikersnaam of email is al in gebruik.';
} else {
$error = 'Er is een fout opgetreden bij het bijwerken van de gebruiker.';
}
}
}
}
// Handle delete user
if (isset($_POST['delete_user'])) {
$userId = (int)($_POST['user_id'] ?? 0);
$currentUserId = getCurrentUser()['id'];
if ($userId === $currentUserId) {
$error = 'Je kunt je eigen account niet verwijderen.';
} else {
try {
$stmt = $db->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$userId]);
$success = "Gebruiker succesvol verwijderd.";
} catch (PDOException $e) {
$error = 'Er is een fout opgetreden bij het verwijderen van de gebruiker.';
}
}
}
// Get all users
$users = $db->query("
SELECT id, username, email, role, is_active, last_login, created_at
FROM users
ORDER BY created_at DESC
")->fetchAll();
// Get login stats
$loginStats = $db->query("
SELECT
COUNT(*) as total_attempts,
SUM(success) as successful_logins,
COUNT(*) - SUM(success) as failed_logins
FROM login_attempts
WHERE attempted_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
")->fetch();
$activePage = 'users';
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gebruikersbeheer - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/assets/css/custom.css">
</head>
<body class="bg-light">
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-people"></i> Gebruikersbeheer</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i class="bi bi-plus-circle"></i> Nieuwe Gebruiker
</button>
</div>
<?php if (!empty($success)): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> <?= htmlspecialchars($success) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle"></i> <?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<!-- Login Stats -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card shadow-sm text-center">
<div class="card-body">
<div class="fs-2 fw-bold text-primary"><?= count($users) ?></div>
<div class="text-muted small">Totaal Gebruikers</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm text-center">
<div class="card-body">
<div class="fs-2 fw-bold text-success"><?= $loginStats['successful_logins'] ?? 0 ?></div>
<div class="text-muted small">Succesvolle Logins (24u)</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm text-center">
<div class="card-body">
<div class="fs-2 fw-bold text-danger"><?= $loginStats['failed_logins'] ?? 0 ?></div>
<div class="text-muted small">Mislukte Logins (24u)</div>
</div>
</div>
</div>
</div>
<!-- Users Table -->
<div class="card shadow-sm">
<div class="card-header bg-dark text-white">
<h5 class="mb-0"><i class="bi bi-table"></i> Gebruikers</h5>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-secondary">
<tr>
<th>ID</th>
<th>Gebruikersnaam</th>
<th>Email</th>
<th>Rol</th>
<th>Status</th>
<th>Laatste Login</th>
<th>Aangemaakt</th>
<th class="text-center">Acties</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td class="text-muted small"><?= $user['id'] ?></td>
<td>
<i class="bi bi-person-circle me-1"></i>
<strong><?= htmlspecialchars($user['username']) ?></strong>
<?php if ($user['id'] === getCurrentUser()['id']): ?>
<span class="badge bg-info ms-1">Jij</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($user['email']) ?></td>
<td>
<span class="badge <?= $user['role'] === 'admin' ? 'bg-danger' : 'bg-secondary' ?>">
<i class="bi <?= $user['role'] === 'admin' ? 'bi-shield-fill' : 'bi-eye' ?>"></i>
<?= ucfirst($user['role']) ?>
</span>
</td>
<td>
<span class="badge <?= $user['is_active'] ? 'bg-success' : 'bg-warning text-dark' ?>">
<?= $user['is_active'] ? 'Actief' : 'Inactief' ?>
</span>
</td>
<td class="small text-muted">
<?= $user['last_login']
? date('d-m-Y H:i', strtotime($user['last_login']))
: '<em>Nooit</em>' ?>
</td>
<td class="small text-muted">
<?= date('d-m-Y', strtotime($user['created_at'])) ?>
</td>
<td class="text-center">
<button type="button"
class="btn btn-sm btn-outline-primary"
onclick="editUser(<?= htmlspecialchars(json_encode($user)) ?>)"
title="Bewerken">
<i class="bi bi-pencil"></i>
</button>
<?php if ($user['id'] !== getCurrentUser()['id']): ?>
<button type="button"
class="btn btn-sm btn-outline-danger ms-1"
onclick="confirmDelete(<?= $user['id'] ?>, '<?= htmlspecialchars($user['username']) ?>')"
title="Verwijderen">
<i class="bi bi-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create User Modal -->
<div class="modal fade" id="createUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="bi bi-person-plus"></i> Nieuwe Gebruiker Aanmaken
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">Gebruikersnaam <span class="text-danger">*</span></label>
<input type="text" name="username" class="form-control" required
pattern="[a-zA-Z0-9_-]{3,50}"
title="3-50 tekens, alleen letters, cijfers, _ en -">
<div class="form-text">3-50 tekens, alleen letters, cijfers, _ en -</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Email <span class="text-danger">*</span></label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Wachtwoord <span class="text-danger">*</span></label>
<input type="password" name="password" class="form-control" required minlength="8">
<div class="form-text">Minimaal 8 tekens</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Rol <span class="text-danger">*</span></label>
<select name="role" class="form-select" required>
<option value="guest">Guest (alleen lezen)</option>
<option value="admin">Admin (volledige toegang)</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" name="create_user" class="btn btn-primary">
<i class="bi bi-person-plus"></i> Aanmaken
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST">
<input type="hidden" name="user_id" id="editUserId">
<div class="modal-header bg-warning">
<h5 class="modal-title">
<i class="bi bi-pencil"></i> Gebruiker Bewerken
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">Gebruikersnaam <span class="text-danger">*</span></label>
<input type="text" name="username" id="editUsername" class="form-control" required
pattern="[a-zA-Z0-9_-]{3,50}">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Email <span class="text-danger">*</span></label>
<input type="email" name="email" id="editEmail" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Rol <span class="text-danger">*</span></label>
<select name="role" id="editRole" class="form-select" required>
<option value="guest">Guest (alleen lezen)</option>
<option value="admin">Admin (volledige toegang)</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_active" id="editIsActive" value="1">
<label class="form-check-label" for="editIsActive">
Account actief
</label>
</div>
</div>
<hr>
<div class="mb-3">
<label class="form-label fw-semibold">Nieuw Wachtwoord</label>
<input type="password" name="new_password" id="editPassword" class="form-control" minlength="8">
<div class="form-text text-muted">
<i class="bi bi-info-circle"></i>
Laat leeg om het huidige wachtwoord te behouden.
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" name="update_user" class="btn btn-warning">
<i class="bi bi-check-circle"></i> Opslaan
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete User Form (hidden) -->
<form method="POST" id="deleteUserForm" style="display: none;">
<input type="hidden" name="user_id" id="deleteUserId">
<input type="hidden" name="delete_user" value="1">
</form>
<footer class="mt-5 py-4 bg-dark text-white text-center">
<div class="container">
<p class="mb-0">
<i class="bi bi-tv"></i> Telvero Talpa Planning System &copy; <?= date('Y') ?>
</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function editUser(user) {
document.getElementById('editUserId').value = user.id;
document.getElementById('editUsername').value = user.username;
document.getElementById('editEmail').value = user.email;
document.getElementById('editRole').value = user.role;
document.getElementById('editIsActive').checked = user.is_active == 1;
document.getElementById('editPassword').value = '';
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
}
function confirmDelete(userId, username) {
if (confirm(`Weet je zeker dat je gebruiker "${username}" wilt verwijderen?\n\nDeze actie kan niet ongedaan worden gemaakt.`)) {
document.getElementById('deleteUserId').value = userId;
document.getElementById('deleteUserForm').submit();
}
}
</script>
</body>
</html>

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only
if (!canEdit()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen kleuren toewijzen.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only
if (!canEdit()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen blokken kopiëren.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only for write operations
if (!canCreate()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen uitzendingen aanmaken.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -7,6 +7,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../TalpaAPI.php'; require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -15,6 +16,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only
if (!canDelete()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen uitzendingen verwijderen.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,13 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check - all logged in users can read
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -7,6 +7,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../TalpaAPI.php'; require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -15,6 +16,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only
if (!canCreate()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen uitzendingen invoegen.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -11,6 +11,7 @@ error_reporting(E_ALL);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../TalpaAPI.php'; require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..'); $dotenv = Dotenv::createImmutable(__DIR__ . '/..');
@ -18,6 +19,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only
if (!canSync()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen synchroniseren.']);
exit;
}
$api = new TalpaApi(); $api = new TalpaApi();
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only
if (!canEdit()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen bloktijden bijwerken.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -7,6 +7,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../TalpaAPI.php'; require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -15,6 +16,20 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Authentication check
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd. Log eerst in.']);
exit;
}
// Authorization check - admin only
if (!canEdit()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang. Alleen admins kunnen uitzendingen bijwerken.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

View File

@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php'; require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json'); header('Content-Type: application/json');
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
exit;
}
try { try {
$db = getDbConnection(); $db = getDbConnection();

29
auth/403.php Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Geen Toegang - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body text-center p-5">
<i class="bi bi-shield-lock text-danger" style="font-size: 4rem;"></i>
<h2 class="mt-3 text-danger">Geen Toegang</h2>
<p class="text-muted">Je hebt geen rechten om deze pagina te bekijken.</p>
<p class="text-muted small">Alleen admins hebben toegang tot deze functionaliteit.</p>
<a href="/index.php" class="btn btn-primary mt-3">
<i class="bi bi-house"></i> Terug naar Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

305
auth/auth_functions.php Normal file
View File

@ -0,0 +1,305 @@
<?php
/**
* Authentication Helper Functions
* Telvero Talpa Planning System
*/
if (session_status() === PHP_SESSION_NONE) {
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_samesite', 'Lax');
session_start();
}
/**
* Get database connection for auth
*/
function getAuthDb(): PDO {
static $db = null;
if ($db === null) {
// Load env if not already loaded
if (!isset($_ENV['DB_HOST'])) {
require_once __DIR__ . '/../vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
}
$db = new PDO(
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4",
$_ENV['DB_USER'],
$_ENV['DB_PASS'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
}
return $db;
}
/**
* Check if user is logged in
*/
function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && isset($_SESSION['user']) && !empty($_SESSION['user']);
}
/**
* Get current logged in user
*/
function getCurrentUser(): ?array {
return $_SESSION['user'] ?? null;
}
/**
* Check if user has specific role
*/
function hasRole(string $role): bool {
if (!isLoggedIn()) {
return false;
}
return ($_SESSION['user']['role'] ?? '') === $role;
}
/**
* Check if user is admin
*/
function isAdmin(): bool {
return hasRole('admin');
}
/**
* Check if user is guest
*/
function isGuest(): bool {
return hasRole('guest');
}
/**
* Check if user can perform write operations
*/
function canWrite(): bool {
return isLoggedIn() && isAdmin();
}
/**
* Check if user can create
*/
function canCreate(): bool {
return isAdmin();
}
/**
* Check if user can edit
*/
function canEdit(): bool {
return isAdmin();
}
/**
* Check if user can delete
*/
function canDelete(): bool {
return isAdmin();
}
/**
* Check if user can sync with Talpa
*/
function canSync(): bool {
return isAdmin();
}
/**
* Require user to be logged in, redirect to login if not
*/
function requireLogin(): void {
if (!isLoggedIn()) {
$currentUrl = $_SERVER['REQUEST_URI'] ?? '/';
$redirect = urlencode(ltrim($currentUrl, '/'));
header("Location: /auth/login.php?redirect=$redirect");
exit;
}
}
/**
* Require user to have specific role
*/
function requireRole(string $role): void {
requireLogin();
if (!hasRole($role)) {
http_response_code(403);
include __DIR__ . '/403.php';
exit;
}
}
/**
* Require admin role
*/
function requireAdmin(): void {
requireRole('admin');
}
/**
* Check brute force protection
* Returns true if user is blocked
*/
function isBlockedByBruteForce(string $username, string $ip): bool {
try {
$db = getAuthDb();
// Check by username
$stmt = $db->prepare("
SELECT COUNT(*) FROM login_attempts
WHERE username = ? AND success = 0
AND attempted_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)
");
$stmt->execute([$username]);
if ($stmt->fetchColumn() >= 5) {
return true;
}
// Check by IP
$stmt = $db->prepare("
SELECT COUNT(*) FROM login_attempts
WHERE ip_address = ? AND success = 0
AND attempted_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)
");
$stmt->execute([$ip]);
if ($stmt->fetchColumn() >= 10) {
return true;
}
} catch (Exception $e) {
// If we can't check, allow the attempt
}
return false;
}
/**
* Log a login attempt
*/
function logLoginAttempt(string $username, string $ip, bool $success): void {
try {
$db = getAuthDb();
$stmt = $db->prepare("
INSERT INTO login_attempts (username, ip_address, success)
VALUES (?, ?, ?)
");
$stmt->execute([$username, $ip, $success ? 1 : 0]);
} catch (Exception $e) {
// Silently fail
}
}
/**
* Attempt to login a user
* Returns array with 'success', 'user', 'error' keys
*/
function attemptLogin(string $usernameOrEmail, string $password): array {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
// Check brute force
if (isBlockedByBruteForce($usernameOrEmail, $ip)) {
return [
'success' => false,
'error' => 'Te veel mislukte inlogpogingen. Probeer het over 15 minuten opnieuw.'
];
}
try {
$db = getAuthDb();
// Find user by username or email
$stmt = $db->prepare("
SELECT id, username, email, password_hash, role, is_active
FROM users
WHERE (username = ? OR email = ?)
LIMIT 1
");
$stmt->execute([$usernameOrEmail, $usernameOrEmail]);
$user = $stmt->fetch();
if (!$user) {
logLoginAttempt($usernameOrEmail, $ip, false);
return ['success' => false, 'error' => 'Ongeldige gebruikersnaam of wachtwoord.'];
}
if (!$user['is_active']) {
logLoginAttempt($usernameOrEmail, $ip, false);
return ['success' => false, 'error' => 'Dit account is gedeactiveerd. Neem contact op met de beheerder.'];
}
if (!password_verify($password, $user['password_hash'])) {
logLoginAttempt($usernameOrEmail, $ip, false);
return ['success' => false, 'error' => 'Ongeldige gebruikersnaam of wachtwoord.'];
}
// Success - create session
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['user'] = [
'id' => $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role'],
];
$_SESSION['login_time'] = time();
// Log success
logLoginAttempt($usernameOrEmail, $ip, true);
// Update last login
$stmt = $db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
$stmt->execute([$user['id']]);
return ['success' => true, 'user' => $_SESSION['user']];
} catch (Exception $e) {
return ['success' => false, 'error' => 'Er is een fout opgetreden. Probeer het opnieuw.'];
}
}
/**
* Logout the current user
*/
function logout(): void {
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
session_destroy();
}
/**
* Check session timeout (2 hours of inactivity)
*/
function checkSessionTimeout(): void {
$timeout = 7200; // 2 hours
if (isLoggedIn()) {
if (isset($_SESSION['login_time']) && (time() - $_SESSION['login_time']) > $timeout) {
logout();
header("Location: /auth/login.php?reason=timeout");
exit;
}
// Update last activity
$_SESSION['login_time'] = time();
}
}
// Check session timeout on every request
checkSessionTimeout();

244
auth/login.php Normal file
View File

@ -0,0 +1,244 @@
<?php
/**
* Login Page
* Telvero Talpa Planning System
*/
require_once __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
require_once __DIR__ . '/auth_functions.php';
// If already logged in, redirect to dashboard
if (isLoggedIn()) {
header("Location: /index.php");
exit;
}
$error = '';
$redirect = $_GET['redirect'] ?? 'index.php';
$reason = $_GET['reason'] ?? '';
// Handle login form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = 'Vul je gebruikersnaam en wachtwoord in.';
} else {
$result = attemptLogin($username, $password);
if ($result['success']) {
// Redirect to original page or dashboard
$redirectUrl = '/' . ltrim(urldecode($redirect), '/');
// Security: only allow relative redirects
if (strpos($redirectUrl, '//') !== false || strpos($redirectUrl, 'http') === 0) {
$redirectUrl = '/index.php';
}
header("Location: $redirectUrl");
exit;
} else {
$error = $result['error'];
}
}
}
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inloggen - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-wrapper {
width: 100%;
max-width: 420px;
padding: 1rem;
}
.login-card {
border: none;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px 16px 0 0;
padding: 2rem;
text-align: center;
color: white;
}
.login-header .brand-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.login-header h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.login-header p {
font-size: 0.875rem;
opacity: 0.85;
margin-bottom: 0;
}
.login-body {
padding: 2rem;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-login {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
padding: 0.75rem;
font-weight: 600;
letter-spacing: 0.5px;
}
.btn-login:hover {
background: linear-gradient(135deg, #5a6fd6 0%, #6a3f96 100%);
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.input-group-text {
background-color: #f8f9fa;
border-right: none;
}
.input-group .form-control {
border-left: none;
}
.input-group .form-control:focus {
border-left: none;
}
.login-footer {
text-align: center;
padding: 1rem 2rem 1.5rem;
border-top: 1px solid #f0f0f0;
}
</style>
</head>
<body>
<div class="login-wrapper">
<div class="card login-card">
<!-- Header -->
<div class="login-header">
<div class="brand-icon">
<i class="bi bi-tv"></i>
</div>
<h1>Telvero Talpa</h1>
<p>Planning System</p>
</div>
<!-- Body -->
<div class="login-body">
<?php if ($reason === 'timeout'): ?>
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-clock"></i>
Je sessie is verlopen. Log opnieuw in.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" autocomplete="on">
<input type="hidden" name="redirect" value="<?= htmlspecialchars($redirect) ?>">
<div class="mb-3">
<label for="username" class="form-label fw-semibold">
Gebruikersnaam of Email
</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-person text-muted"></i>
</span>
<input type="text"
id="username"
name="username"
class="form-control"
placeholder="Gebruikersnaam of email"
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
required
autofocus
autocomplete="username">
</div>
</div>
<div class="mb-4">
<label for="password" class="form-label fw-semibold">
Wachtwoord
</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-lock text-muted"></i>
</span>
<input type="password"
id="password"
name="password"
class="form-control"
placeholder="Wachtwoord"
required
autocomplete="current-password">
<button class="btn btn-outline-secondary"
type="button"
id="togglePassword"
title="Wachtwoord tonen/verbergen">
<i class="bi bi-eye" id="toggleIcon"></i>
</button>
</div>
</div>
<button type="submit" class="btn btn-primary btn-login w-100">
<i class="bi bi-box-arrow-in-right me-2"></i>
Inloggen
</button>
</form>
</div>
<!-- Footer -->
<div class="login-footer">
<small class="text-muted">
<i class="bi bi-shield-check text-success"></i>
Beveiligde verbinding &bull; Telvero &copy; <?= date('Y') ?>
</small>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Toggle password visibility
document.getElementById('togglePassword').addEventListener('click', function() {
const passwordInput = document.getElementById('password');
const toggleIcon = document.getElementById('toggleIcon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.className = 'bi bi-eye-slash';
} else {
passwordInput.type = 'password';
toggleIcon.className = 'bi bi-eye';
}
});
</script>
</body>
</html>

18
auth/logout.php Normal file
View File

@ -0,0 +1,18 @@
<?php
/**
* Logout Handler
* Telvero Talpa Planning System
*/
require_once __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
require_once __DIR__ . '/auth_functions.php';
logout();
header("Location: /auth/login.php");
exit;

View File

@ -10,15 +10,19 @@ error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/helpers.php'; require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__); $dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load(); $dotenv->load();
// Require login
requireLogin();
$db = getDbConnection(); $db = getDbConnection();
// Handle form submissions // Handle form submissions - Admin only
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST' && canEdit()) {
if (isset($_POST['create_template'])) { if (isset($_POST['create_template'])) {
$stmt = $db->prepare(" $stmt = $db->prepare("
INSERT INTO block_templates INSERT INTO block_templates
@ -104,20 +108,7 @@ $dayNames = [
<link rel="stylesheet" href="assets/css/custom.css"> <link rel="stylesheet" href="assets/css/custom.css">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <?php $activePage = 'blocks'; include __DIR__ . '/includes/nav.php'; ?>
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link active" href="blocks.php">Blokken</a>
<a class="nav-link" href="infomercials.php">Infomercials</a>
</div>
</div>
</nav>
<div class="container mt-4"> <div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
@ -142,6 +133,7 @@ $dayNames = [
<?php endif; ?> <?php endif; ?>
<div class="row"> <div class="row">
<?php if (canEdit()): ?>
<div class="col-md-4"> <div class="col-md-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">
@ -212,8 +204,9 @@ $dayNames = [
</div> </div>
</div> </div>
</div> </div>
<?php endif; ?>
<div class="col-md-8"> <div class="<?= canEdit() ? 'col-md-8' : 'col-md-12' ?>">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-secondary text-white"> <div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Bestaande Templates</h5> <h5 class="mb-0"><i class="bi bi-list-ul"></i> Bestaande Templates</h5>
@ -257,6 +250,7 @@ $dayNames = [
</td> </td>
<td> <td>
<div class="action-buttons"> <div class="action-buttons">
<?php if (canEdit()): ?>
<a href="?edit=<?= $template['id'] ?>" <a href="?edit=<?= $template['id'] ?>"
class="btn-icon btn-icon-xs btn-icon-primary"> class="btn-icon btn-icon-xs btn-icon-primary">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
@ -268,6 +262,8 @@ $dayNames = [
<i class="bi bi-<?= $template['is_active'] ? 'pause' : 'play' ?>-circle"></i> <i class="bi bi-<?= $template['is_active'] ? 'pause' : 'play' ?>-circle"></i>
</button> </button>
</form> </form>
<?php endif; ?>
<?php if (canDelete()): ?>
<form method="POST" style="display:inline;" <form method="POST" style="display:inline;"
onsubmit="return confirm('Weet je zeker dat je dit template wilt verwijderen?');"> onsubmit="return confirm('Weet je zeker dat je dit template wilt verwijderen?');">
<input type="hidden" name="template_id" value="<?= $template['id'] ?>"> <input type="hidden" name="template_id" value="<?= $template['id'] ?>">
@ -276,6 +272,10 @@ $dayNames = [
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</form> </form>
<?php endif; ?>
<?php if (isGuest()): ?>
<span class="text-muted small"><i class="bi bi-eye"></i></span>
<?php endif; ?>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -10,11 +10,15 @@ error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/helpers.php'; require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__); $dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load(); $dotenv->load();
// Require login
requireLogin();
$db = getDbConnection(); $db = getDbConnection();
// Get all infomercials with colors for sidebar // Get all infomercials with colors for sidebar
@ -64,20 +68,7 @@ $infomercials = $db->query("
<link rel="stylesheet" href="assets/css/custom.css"> <link rel="stylesheet" href="assets/css/custom.css">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <?php $activePage = 'calendar'; include __DIR__ . '/includes/nav.php'; ?>
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Planner</a>
<a class="nav-link active" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link" href="infomercials.php">Infomercials</a>
</div>
</div>
</nav>
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<!-- Alert Container --> <!-- Alert Container -->
@ -332,6 +323,13 @@ $infomercials = $db->query("
<!-- Scripts --> <!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src='https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@6.1.10/index.global.min.js'></script> <script src='https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@6.1.10/index.global.min.js'></script>
<script>
// Pass user role to JavaScript
const userIsAdmin = <?= json_encode(isAdmin()) ?>;
const userCanEdit = <?= json_encode(canEdit()) ?>;
const userCanSync = <?= json_encode(canSync()) ?>;
const userCanDelete = <?= json_encode(canDelete()) ?>;
</script>
<script src="assets/js/calendar-init.js"></script> <script src="assets/js/calendar-init.js"></script>
</body> </body>
</html> </html>

83
includes/nav.php Normal file
View File

@ -0,0 +1,83 @@
<?php
/**
* Shared Navigation Component
* Telvero Talpa Planning System
*
* Usage: include __DIR__ . '/includes/nav.php';
* Requires: $activePage variable to be set (e.g., 'dashboard', 'planner', 'calendar', 'blocks', 'infomercials', 'users')
*/
// Ensure auth functions are available
if (!function_exists('isLoggedIn')) {
require_once __DIR__ . '/../auth/auth_functions.php';
}
$activePage = $activePage ?? '';
$currentUser = getCurrentUser();
?>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<!-- Main Navigation -->
<div class="navbar-nav">
<a class="nav-link <?= $activePage === 'dashboard' ? 'active' : '' ?>" href="/index.php">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<a class="nav-link <?= $activePage === 'planner' ? 'active' : '' ?>" href="/planner.php">
<i class="bi bi-table"></i> Planner
</a>
<a class="nav-link <?= $activePage === 'calendar' ? 'active' : '' ?>" href="/calendar.php">
<i class="bi bi-calendar-week"></i> Kalender
</a>
<a class="nav-link <?= $activePage === 'blocks' ? 'active' : '' ?>" href="/blocks.php">
<i class="bi bi-grid-3x3"></i> Blokken
</a>
<a class="nav-link <?= $activePage === 'infomercials' ? 'active' : '' ?>" href="/infomercials.php">
<i class="bi bi-collection-play"></i> Infomercials
</a>
<?php if (isAdmin()): ?>
<a class="nav-link <?= $activePage === 'users' ? 'active' : '' ?>" href="/admin/users.php">
<i class="bi bi-people"></i> Gebruikers
</a>
<?php endif; ?>
</div>
<!-- User Info & Logout -->
<div class="navbar-nav ms-auto align-items-center">
<?php if ($currentUser): ?>
<span class="nav-link pe-2">
<i class="bi bi-person-circle"></i>
<span class="ms-1"><?= htmlspecialchars($currentUser['username']) ?></span>
<span class="badge ms-1 <?= isAdmin() ? 'bg-danger' : 'bg-secondary' ?>">
<?= isAdmin() ? 'Admin' : 'Guest' ?>
</span>
</span>
<a class="nav-link" href="/auth/logout.php" title="Uitloggen">
<i class="bi bi-box-arrow-right"></i>
<span class="d-lg-none ms-1">Uitloggen</span>
</a>
<?php endif; ?>
</div>
</div>
</div>
</nav>
<?php if (isGuest()): ?>
<div class="alert alert-info alert-dismissible fade show mb-0 rounded-0 border-0" role="alert"
style="background-color: #cff4fc; border-bottom: 1px solid #b6effb !important;">
<div class="container-fluid">
<i class="bi bi-eye"></i>
<strong>Alleen lezen modus:</strong> Je bent ingelogd als <strong>Guest</strong>.
Je kunt informatie bekijken maar geen wijzigingen aanbrengen.
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>

View File

@ -5,19 +5,23 @@ error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/TalpaAPI.php'; require_once __DIR__ . '/TalpaAPI.php';
require_once __DIR__ . '/auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__); $dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load(); $dotenv->load();
// Require login
requireLogin();
$api = new TalpaApi(); $api = new TalpaApi();
$db = new PDO("mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']}", $_ENV['DB_USER'], $_ENV['DB_PASS']); $db = new PDO("mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']}", $_ENV['DB_USER'], $_ENV['DB_PASS']);
// Array om logs voor console te verzamelen // Array om logs voor console te verzamelen
$apiLogs = []; $apiLogs = [];
// 1. Registratie Infomercial (Stap 1, 2, 4) // 1. Registratie Infomercial (Stap 1, 2, 4) - Admin only
if (isset($_POST['add_commercial'])) { if (isset($_POST['add_commercial']) && canCreate()) {
$ep = $api->createEpisode($_POST['title'], $_POST['duration'], $_POST['season_id']); $ep = $api->createEpisode($_POST['title'], $_POST['duration'], $_POST['season_id']);
$apiLogs[] = ['call' => 'Create Episode', 'response' => $api->lastResponse]; $apiLogs[] = ['call' => 'Create Episode', 'response' => $api->lastResponse];
@ -36,8 +40,8 @@ if (isset($_POST['add_commercial'])) {
} }
} }
// 2. LOKALE Planning opslaan of bewerken // 2. LOKALE Planning opslaan of bewerken - Admin only
if (isset($_POST['schedule_transmission'])) { if (isset($_POST['schedule_transmission']) && canCreate()) {
$stmt = $db->prepare("SELECT duration FROM infomercials WHERE id = ?"); $stmt = $db->prepare("SELECT duration FROM infomercials WHERE id = ?");
$stmt->execute([$_POST['infomercial_id']]); $stmt->execute([$_POST['infomercial_id']]);
$commDuration = $stmt->fetchColumn(); $commDuration = $stmt->fetchColumn();
@ -53,8 +57,8 @@ if (isset($_POST['schedule_transmission'])) {
exit; exit;
} }
// 3. Handmatige Sync naar Talpa (Stap 3 op verzoek) // 3. Handmatige Sync naar Talpa (Stap 3 op verzoek) - Admin only
if (isset($_POST['sync_item'])) { if (isset($_POST['sync_item']) && canSync()) {
$stmt = $db->prepare("SELECT t.*, c.content_id FROM transmissions t JOIN infomercials c ON t.infomercial_id = c.id WHERE t.id = ?"); $stmt = $db->prepare("SELECT t.*, c.content_id FROM transmissions t JOIN infomercials c ON t.infomercial_id = c.id WHERE t.id = ?");
$stmt->execute([$_POST['sync_id']]); $stmt->execute([$_POST['sync_id']]);
$tx = $stmt->fetch(); $tx = $stmt->fetch();
@ -74,8 +78,8 @@ if (isset($_POST['sync_item'])) {
->execute([$status, json_encode($res), $_POST['sync_id']]); ->execute([$status, json_encode($res), $_POST['sync_id']]);
} }
// 4. Media Asset Label en Status bijwerken // 4. Media Asset Label en Status bijwerken - Admin only
if (isset($_POST['update_media_asset'])) { if (isset($_POST['update_media_asset']) && canEdit()) {
$stmt = $db->prepare("UPDATE infomercials SET media_asset_label = ?, upload_status = ? WHERE id = ?"); $stmt = $db->prepare("UPDATE infomercials SET media_asset_label = ?, upload_status = ? WHERE id = ?");
$stmt->execute([$_POST['media_asset_label'], $_POST['upload_status'], $_POST['infomercial_id']]); $stmt->execute([$_POST['media_asset_label'], $_POST['upload_status'], $_POST['infomercial_id']]);
header("Location: index.php?view_date=" . $selectedDate); header("Location: index.php?view_date=" . $selectedDate);
@ -108,20 +112,7 @@ if (isset($_GET['edit'])) {
<link rel="stylesheet" href="assets/css/custom.css"> <link rel="stylesheet" href="assets/css/custom.css">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <?php $activePage = 'dashboard'; include __DIR__ . '/includes/nav.php'; ?>
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link active" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link" href="infomercials.php">Infomercials</a>
</div>
</div>
</nav>
<div class="container mt-4"> <div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
@ -227,6 +218,7 @@ if (isset($_GET['edit'])) {
<h2 class="mb-3"><i class="bi bi-clock-history"></i> Recente Activiteit</h2> <h2 class="mb-3"><i class="bi bi-clock-history"></i> Recente Activiteit</h2>
<?php if (canCreate()): ?>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<div class="card p-3 shadow-sm"> <div class="card p-3 shadow-sm">
@ -279,6 +271,7 @@ if (isset($_GET['edit'])) {
</div> </div>
</div> </div>
</div> </div>
<?php endif; ?>
<hr class="my-4"> <hr class="my-4">
@ -319,10 +312,12 @@ if (isset($_GET['edit'])) {
</td> </td>
<td> <td>
<div class="action-buttons justify-content-center"> <div class="action-buttons justify-content-center">
<?php if (canEdit()): ?>
<a href="?edit=<?= $tx['id'] ?>&view_date=<?= $selectedDate ?>" class="btn-icon btn-icon-xs btn-icon-primary"> <a href="?edit=<?= $tx['id'] ?>&view_date=<?= $selectedDate ?>" class="btn-icon btn-icon-xs btn-icon-primary">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</a> </a>
<?php if($tx['api_status'] !== 'synced'): ?> <?php endif; ?>
<?php if(canSync() && $tx['api_status'] !== 'synced'): ?>
<form method="POST" style="display:inline;"> <form method="POST" style="display:inline;">
<input type="hidden" name="sync_id" value="<?= $tx['id'] ?>"> <input type="hidden" name="sync_id" value="<?= $tx['id'] ?>">
<button type="submit" name="sync_item" class="btn-icon btn-icon-xs btn-icon-success"> <button type="submit" name="sync_item" class="btn-icon btn-icon-xs btn-icon-success">
@ -363,16 +358,20 @@ if (isset($_GET['edit'])) {
<td><span class="badge bg-info text-dark"><?= $c['duration'] ?></span></td> <td><span class="badge bg-info text-dark"><?= $c['duration'] ?></span></td>
<td><code><?= htmlspecialchars($c['content_id']) ?></code></td> <td><code><?= htmlspecialchars($c['content_id']) ?></code></td>
<td> <td>
<input type="text" name="media_asset_label" class="form-control form-control-sm" value="<?= htmlspecialchars($c['media_asset_label']) ?>"> <input type="text" name="media_asset_label" class="form-control form-control-sm" value="<?= htmlspecialchars($c['media_asset_label']) ?>" <?= canEdit() ? '' : 'readonly' ?>>
</td> </td>
<td> <td>
<select name="upload_status" class="form-select form-select-sm"> <select name="upload_status" class="form-select form-select-sm" <?= canEdit() ? '' : 'disabled' ?>>
<option value="pending" <?= ($c['upload_status'] ?? 'pending') == 'pending' ? 'selected' : '' ?>>Pending</option> <option value="pending" <?= ($c['upload_status'] ?? 'pending') == 'pending' ? 'selected' : '' ?>>Pending</option>
<option value="uploaded" <?= ($c['upload_status'] ?? '') == 'uploaded' ? 'selected' : '' ?>>Uploaded</option> <option value="uploaded" <?= ($c['upload_status'] ?? '') == 'uploaded' ? 'selected' : '' ?>>Uploaded</option>
</select> </select>
</td> </td>
<td> <td>
<?php if (canEdit()): ?>
<button type="submit" name="update_media_asset" class="btn btn-sm btn-primary">Opslaan</button> <button type="submit" name="update_media_asset" class="btn btn-sm btn-primary">Opslaan</button>
<?php else: ?>
<span class="text-muted small"><i class="bi bi-eye"></i> Alleen lezen</span>
<?php endif; ?>
</td> </td>
</form> </form>
</tr> </tr>
@ -386,12 +385,14 @@ if (isset($_GET['edit'])) {
<footer class="mt-5 py-4 bg-dark text-white text-center"> <footer class="mt-5 py-4 bg-dark text-white text-center">
<div class="container"> <div class="container">
<p class="mb-0"> <p class="mb-0">
<i class="bi bi-tv"></i> Telvero Talpa Planning System &copy; 2026 <i class="bi bi-tv"></i> Telvero Talpa Planning System &copy; <?= date('Y') ?>
</p> </p>
</div> </div>
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
<?php if (canCreate()): ?>
// Bestaande JS voor duur selectie // Bestaande JS voor duur selectie
const select = document.getElementById('commercial_select'); const select = document.getElementById('commercial_select');
const durationInput = document.getElementById('display_duration'); const durationInput = document.getElementById('display_duration');
@ -400,6 +401,7 @@ if (isset($_GET['edit'])) {
const duration = selectedOption.getAttribute('data-duration'); const duration = selectedOption.getAttribute('data-duration');
durationInput.value = duration || ''; durationInput.value = duration || '';
}); });
<?php endif; ?>
// LOGGING NAAR CONSOLE // LOGGING NAAR CONSOLE
const apiLogs = <?= json_encode($apiLogs) ?>; const apiLogs = <?= json_encode($apiLogs) ?>;

View File

@ -11,24 +11,27 @@ error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/TalpaAPI.php'; require_once __DIR__ . '/TalpaAPI.php';
require_once __DIR__ . '/helpers.php'; require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__); $dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load(); $dotenv->load();
// Require login
requireLogin();
$api = new TalpaApi(); $api = new TalpaApi();
$db = getDbConnection(); $db = getDbConnection();
$apiLogs = []; $apiLogs = [];
// Retrieve and clear refresh logs from session // Retrieve and clear refresh logs from session (session already started in auth_functions.php)
session_start();
$refreshLogs = $_SESSION['refresh_logs'] ?? null; $refreshLogs = $_SESSION['refresh_logs'] ?? null;
if ($refreshLogs) { if ($refreshLogs) {
unset($_SESSION['refresh_logs']); unset($_SESSION['refresh_logs']);
} }
// Handle infomercial registration // Handle infomercial registration - Admin only
if (isset($_POST['add_commercial'])) { if (isset($_POST['add_commercial']) && canCreate()) {
$apiLogs[] = ['step' => 'Start registration', 'input' => $_POST]; $apiLogs[] = ['step' => 'Start registration', 'input' => $_POST];
$ep = $api->createEpisode($_POST['title'], $_POST['duration'], $_POST['season_id']); $ep = $api->createEpisode($_POST['title'], $_POST['duration'], $_POST['season_id']);
@ -91,8 +94,8 @@ if (isset($_POST['add_commercial'])) {
} }
} }
// Handle infomercial update (sync to Talpa) // Handle infomercial update (sync to Talpa) - Admin only
if (isset($_POST['update_infomercial'])) { if (isset($_POST['update_infomercial']) && canEdit()) {
// Get current infomercial data // Get current infomercial data
$stmt = $db->prepare("SELECT content_id, title, duration FROM infomercials WHERE id = ?"); $stmt = $db->prepare("SELECT content_id, title, duration FROM infomercials WHERE id = ?");
$stmt->execute([$_POST['infomercial_id']]); $stmt->execute([$_POST['infomercial_id']]);
@ -136,8 +139,8 @@ if (isset($_POST['update_infomercial'])) {
} }
} }
// Handle refresh single infomercial from Talpa // Handle refresh single infomercial from Talpa - Admin only
if (isset($_POST['refresh_infomercial'])) { if (isset($_POST['refresh_infomercial']) && canEdit()) {
$stmt = $db->prepare("SELECT title, media_asset_id FROM infomercials WHERE id = ?"); $stmt = $db->prepare("SELECT title, media_asset_id FROM infomercials WHERE id = ?");
$stmt->execute([$_POST['infomercial_id']]); $stmt->execute([$_POST['infomercial_id']]);
$infomercial = $stmt->fetch(); $infomercial = $stmt->fetch();
@ -162,8 +165,7 @@ if (isset($_POST['refresh_infomercial'])) {
header('Location: infomercials.php?success=refreshed'); header('Location: infomercials.php?success=refreshed');
exit; exit;
} else { } else {
// Store failed refresh log // Store failed refresh log (session already started in auth_functions.php)
session_start();
$_SESSION['refresh_logs'] = [[ $_SESSION['refresh_logs'] = [[
'status' => 'failed', 'status' => 'failed',
'title' => $infomercial['title'], 'title' => $infomercial['title'],
@ -176,8 +178,8 @@ if (isset($_POST['refresh_infomercial'])) {
exit; exit;
} }
// Handle refresh all infomercials from Talpa // Handle refresh all infomercials from Talpa - Admin only
if (isset($_POST['refresh_all'])) { if (isset($_POST['refresh_all']) && canEdit()) {
$stmt = $db->query("SELECT id, title, media_asset_id FROM infomercials WHERE media_asset_id IS NOT NULL"); $stmt = $db->query("SELECT id, title, media_asset_id FROM infomercials WHERE media_asset_id IS NOT NULL");
$infomercials_to_refresh = $stmt->fetchAll(); $infomercials_to_refresh = $stmt->fetchAll();
@ -214,16 +216,15 @@ if (isset($_POST['refresh_all'])) {
} }
} }
// Store logs in session for display // Store logs in session for display (session already started in auth_functions.php)
session_start();
$_SESSION['refresh_logs'] = $refreshLogs; $_SESSION['refresh_logs'] = $refreshLogs;
header("Location: infomercials.php?success=refreshed_all&count=$refreshed&failed=$failed"); header("Location: infomercials.php?success=refreshed_all&count=$refreshed&failed=$failed");
exit; exit;
} }
// Handle delete // Handle delete - Admin only
if (isset($_POST['delete_commercial'])) { if (isset($_POST['delete_commercial']) && canDelete()) {
// Check if infomercial is used in transmissions // Check if infomercial is used in transmissions
$stmt = $db->prepare("SELECT COUNT(*) FROM transmissions WHERE infomercial_id = ?"); $stmt = $db->prepare("SELECT COUNT(*) FROM transmissions WHERE infomercial_id = ?");
$stmt->execute([$_POST['infomercial_id']]); $stmt->execute([$_POST['infomercial_id']]);
@ -277,20 +278,7 @@ $infomercials = $db->query("
<link rel="stylesheet" href="assets/css/custom.css"> <link rel="stylesheet" href="assets/css/custom.css">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <?php $activePage = 'infomercials'; include __DIR__ . '/includes/nav.php'; ?>
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link active" href="infomercials.php">Infomercials</a>
</div>
</div>
</nav>
<div class="container mt-4"> <div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
@ -376,6 +364,7 @@ $infomercials = $db->query("
<?php endif; ?> <?php endif; ?>
<div class="row"> <div class="row">
<?php if (canCreate()): ?>
<!-- Registration Form --> <!-- Registration Form -->
<div class="col-md-4"> <div class="col-md-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
@ -418,13 +407,14 @@ $infomercials = $db->query("
</div> </div>
</div> </div>
</div> </div>
<?php endif; ?>
<!-- Infomercial List --> <!-- Infomercial List -->
<div class="col-md-8"> <div class="<?= canCreate() ? 'col-md-8' : 'col-md-12' ?>">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center"> <div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Geregistreerde Infomercials</h5> <h5 class="mb-0"><i class="bi bi-list-ul"></i> Geregistreerde Infomercials</h5>
<?php if (!empty($infomercials)): ?> <?php if (!empty($infomercials) && canEdit()): ?>
<form method="POST" style="display:inline;" <form method="POST" style="display:inline;"
onsubmit="return confirm('Weet je zeker dat je alle infomercials wilt verversen vanuit Talpa?');"> onsubmit="return confirm('Weet je zeker dat je alle infomercials wilt verversen vanuit Talpa?');">
<button type="submit" name="refresh_all" class="btn btn-sm btn-light" title="Ververs alle infomercials vanuit Talpa"> <button type="submit" name="refresh_all" class="btn btn-sm btn-light" title="Ververs alle infomercials vanuit Talpa">
@ -487,29 +477,37 @@ $infomercials = $db->query("
</td> </td>
<td> <td>
<div class="action-buttons"> <div class="action-buttons">
<?php if (canEdit()): ?>
<form method="POST" style="display:inline;"> <form method="POST" style="display:inline;">
<input type="hidden" name="infomercial_id" value="<?= $c['id'] ?>"> <input type="hidden" name="infomercial_id" value="<?= $c['id'] ?>">
<button type="submit" name="refresh_infomercial" <button type="submit" name="refresh_infomercial"
class="btn-icon btn-icon-xs btn-icon-info"> class="btn-icon btn-icon-xs btn-icon-info"
title="Verversen vanuit Talpa">
<i class="bi bi-arrow-clockwise"></i> <i class="bi bi-arrow-clockwise"></i>
</button> </button>
</form> </form>
<button type="button" <button type="button"
class="btn-icon btn-icon-xs btn-icon-primary" class="btn-icon btn-icon-xs btn-icon-primary"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#editModal<?= $c['id'] ?>"> data-bs-target="#editModal<?= $c['id'] ?>"
title="Bewerken">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</button> </button>
<?php if ($c['usage_count'] == 0): ?> <?php endif; ?>
<?php if (canDelete() && $c['usage_count'] == 0): ?>
<form method="POST" style="display:inline;" <form method="POST" style="display:inline;"
onsubmit="return confirm('Weet je zeker dat je deze infomercial wilt verwijderen?');"> onsubmit="return confirm('Weet je zeker dat je deze infomercial wilt verwijderen?');">
<input type="hidden" name="infomercial_id" value="<?= $c['id'] ?>"> <input type="hidden" name="infomercial_id" value="<?= $c['id'] ?>">
<button type="submit" name="delete_commercial" <button type="submit" name="delete_commercial"
class="btn-icon btn-icon-xs btn-icon-danger"> class="btn-icon btn-icon-xs btn-icon-danger"
title="Verwijderen">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</form> </form>
<?php endif; ?> <?php endif; ?>
<?php if (isGuest()): ?>
<span class="text-muted small"><i class="bi bi-eye" title="Alleen lezen"></i></span>
<?php endif; ?>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,54 @@
-- Migration: Add authentication tables
-- Date: 2026-02-19
-- Description: Adds users, sessions, and login_attempts tables for authentication
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin', 'guest') DEFAULT 'guest',
is_active BOOLEAN DEFAULT 1,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_role (role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create sessions table
CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(128) PRIMARY KEY,
user_id INT NOT NULL,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_last_activity (last_activity)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create login_attempts table
CREATE TABLE IF NOT EXISTS login_attempts (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
ip_address VARCHAR(45),
success BOOLEAN DEFAULT 0,
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_ip_address (ip_address),
INDEX idx_attempted_at (attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default admin user (password: Admin@2026!)
-- Password hash generated with: password_hash('Admin@2026!', PASSWORD_BCRYPT)
INSERT INTO users (username, email, password_hash, role, is_active) VALUES
('admin', 'admin@telvero.nl', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 1);
-- Insert default guest user (password: Guest@2026!)
-- Password hash generated with: password_hash('Guest@2026!', PASSWORD_BCRYPT)
INSERT INTO users (username, email, password_hash, role, is_active) VALUES
('guest', 'guest@telvero.nl', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'guest', 1);

View File

@ -10,15 +10,19 @@ error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/helpers.php'; require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/auth/auth_functions.php';
use Dotenv\Dotenv; use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__); $dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load(); $dotenv->load();
// Require login
requireLogin();
$db = getDbConnection(); $db = getDbConnection();
// Handle adding transmission to block // Handle adding transmission to block - Admin only
if (isset($_POST['add_to_block'])) { if (isset($_POST['add_to_block']) && canCreate()) {
$commercialId = $_POST['infomercial_id']; $commercialId = $_POST['infomercial_id'];
$channel = $_POST['channel']; $channel = $_POST['channel'];
$date = $_POST['date']; $date = $_POST['date'];
@ -47,8 +51,8 @@ if (isset($_POST['add_to_block'])) {
// Handle remove transmission - REMOVED: Now handled via AJAX to delete_transmission.php API // Handle remove transmission - REMOVED: Now handled via AJAX to delete_transmission.php API
// This ensures proper Talpa API deletion // This ensures proper Talpa API deletion
// Handle reorder (move up/down) // Handle reorder (move up/down) - Admin only
if (isset($_POST['reorder'])) { if (isset($_POST['reorder']) && canEdit()) {
$transmissionId = $_POST['transmission_id']; $transmissionId = $_POST['transmission_id'];
$direction = $_POST['direction']; // 'up' or 'down' $direction = $_POST['direction']; // 'up' or 'down'
$date = $_POST['date']; $date = $_POST['date'];
@ -105,8 +109,8 @@ if (isset($_POST['reorder'])) {
exit; exit;
} }
// Handle block time update // Handle block time update - Admin only
if (isset($_POST['update_block_time'])) { if (isset($_POST['update_block_time']) && canEdit()) {
$blockId = $_POST['block_id']; $blockId = $_POST['block_id'];
$newStartTime = $_POST['new_start_time']; $newStartTime = $_POST['new_start_time'];
$recalculate = isset($_POST['recalculate']); $recalculate = isset($_POST['recalculate']);
@ -255,20 +259,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
</style> </style>
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <?php $activePage = 'planner'; include __DIR__ . '/includes/nav.php'; ?>
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link active" href="planner.php">Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link" href="infomercials.php">Infomercials</a>
</div>
</div>
</nav>
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<?php if (isset($_GET['success'])): ?> <?php if (isset($_GET['success'])): ?>
@ -474,6 +465,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
</div> </div>
<div class="block-header-actions"> <div class="block-header-actions">
<?php if (canEdit()): ?>
<button type="button" class="btn-icon btn-icon-sm btn-icon-secondary" <button type="button" class="btn-icon btn-icon-sm btn-icon-secondary"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#blockTimeModal<?= $block['id'] ?>"> data-bs-target="#blockTimeModal<?= $block['id'] ?>">
@ -484,10 +476,13 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
data-bs-target="#copyBlockModal<?= $block['id'] ?>"> data-bs-target="#copyBlockModal<?= $block['id'] ?>">
<i class="bi bi-files"></i> <i class="bi bi-files"></i>
</button> </button>
<?php endif; ?>
<?php if (canSync()): ?>
<button type="button" class="btn-icon btn-icon-sm btn-icon-success" <button type="button" class="btn-icon btn-icon-sm btn-icon-success"
onclick="syncBlockPlanner('<?= $selectedDate ?>', '<?= $channel ?>')"> onclick="syncBlockPlanner('<?= $selectedDate ?>', '<?= $channel ?>')">
<i class="bi bi-cloud-upload"></i> <i class="bi bi-cloud-upload"></i>
</button> </button>
<?php endif; ?>
</div> </div>
<div class="block-header-time"> <div class="block-header-time">
@ -568,7 +563,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
</td> </td>
<td> <td>
<div class="action-buttons"> <div class="action-buttons">
<?php if ($index > 0): ?> <?php if (canEdit() && $index > 0): ?>
<form method="POST" style="display:inline;"> <form method="POST" style="display:inline;">
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>"> <input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
<input type="hidden" name="direction" value="up"> <input type="hidden" name="direction" value="up">
@ -581,7 +576,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
</form> </form>
<?php endif; ?> <?php endif; ?>
<?php if ($index < count($blockTransmissions) - 1): ?> <?php if (canEdit() && $index < count($blockTransmissions) - 1): ?>
<form method="POST" style="display:inline;"> <form method="POST" style="display:inline;">
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>"> <input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
<input type="hidden" name="direction" value="down"> <input type="hidden" name="direction" value="down">
@ -594,17 +589,20 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
</form> </form>
<?php endif; ?> <?php endif; ?>
<?php if (canDelete()): ?>
<button type="button" <button type="button"
class="btn-icon btn-icon-xs btn-icon-danger" class="btn-icon btn-icon-xs btn-icon-danger"
onclick="deleteTransmissionPlanner(<?= $tx['id'] ?>, '<?= $selectedDate ?>')"> onclick="deleteTransmissionPlanner(<?= $tx['id'] ?>, '<?= $selectedDate ?>')">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
<?php endif; ?>
</div> </div>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
<?php if (canCreate()): ?>
<!-- Add Infomercial Row (also a drop zone) --> <!-- Add Infomercial Row (also a drop zone) -->
<tr class="table-success drop-zone-end" <tr class="table-success drop-zone-end"
data-block-id="<?= $block['id'] ?>" data-block-id="<?= $block['id'] ?>"
@ -635,6 +633,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
</form> </form>
</td> </td>
</tr> </tr>
<?php endif; ?>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -0,0 +1,740 @@
# Authentication System - Implementatie Voorbeelden
## Code Voorbeelden
### 1. Login Pagina (auth/login.php)
```php
<?php
session_start();
require_once __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
$db = new PDO(
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']}",
$_ENV['DB_USER'],
$_ENV['DB_PASS']
);
$error = '';
$redirect = $_GET['redirect'] ?? 'index.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
// Check brute force
$stmt = $db->prepare("
SELECT COUNT(*) FROM login_attempts
WHERE username = ? AND success = 0
AND attempted_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)
");
$stmt->execute([$username]);
$failedAttempts = $stmt->fetchColumn();
if ($failedAttempts >= 5) {
$error = 'Te veel mislukte pogingen. Probeer het over 15 minuten opnieuw.';
} else {
// Verify credentials
$stmt = $db->prepare("
SELECT id, username, email, password_hash, role, is_active
FROM users
WHERE (username = ? OR email = ?) AND is_active = 1
");
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
// Success - create session
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['user'] = [
'id' => $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role']
];
// Log success
$stmt = $db->prepare("
INSERT INTO login_attempts (username, ip_address, success)
VALUES (?, ?, 1)
");
$stmt->execute([$username, $_SERVER['REMOTE_ADDR']]);
// Update last login
$stmt = $db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
$stmt->execute([$user['id']]);
header("Location: ../$redirect");
exit;
} else {
// Failed login
$error = 'Ongeldige gebruikersnaam of wachtwoord';
$stmt = $db->prepare("
INSERT INTO login_attempts (username, ip_address, success)
VALUES (?, ?, 0)
");
$stmt->execute([$username, $_SERVER['REMOTE_ADDR']]);
}
}
}
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
max-width: 400px;
width: 100%;
}
</style>
</head>
<body>
<div class="login-card">
<div class="card shadow-lg">
<div class="card-body p-5">
<div class="text-center mb-4">
<i class="bi bi-tv" style="font-size: 3rem; color: #667eea;"></i>
<h3 class="mt-3">Telvero Talpa</h3>
<p class="text-muted">Planning System</p>
</div>
<?php if ($error): ?>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i>
<?= htmlspecialchars($error) ?>
</div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label">Gebruikersnaam of Email</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-person"></i>
</span>
<input type="text"
name="username"
class="form-control"
required
autofocus
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label">Wachtwoord</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-lock"></i>
</span>
<input type="password"
name="password"
class="form-control"
required>
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">
Onthoud mij
</label>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-box-arrow-in-right"></i> Inloggen
</button>
</form>
<div class="mt-4 text-center">
<small class="text-muted">
Telvero © <?= date('Y') ?>
</small>
</div>
</div>
</div>
</div>
</body>
</html>
```
### 2. Authenticatie Functies (auth/auth_functions.php)
```php
<?php
/**
* Authentication Helper Functions
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
/**
* Get database connection
*/
function getAuthDb() {
static $db = null;
if ($db === null) {
$db = new PDO(
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']}",
$_ENV['DB_USER'],
$_ENV['DB_PASS']
);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return $db;
}
/**
* Check if user is logged in
*/
function isLoggedIn() {
return isset($_SESSION['user_id']) && isset($_SESSION['user']);
}
/**
* Get current logged in user
*/
function getCurrentUser() {
return $_SESSION['user'] ?? null;
}
/**
* Check if user has specific role
*/
function hasRole($role) {
if (!isLoggedIn()) {
return false;
}
return $_SESSION['user']['role'] === $role;
}
/**
* Check if user is admin
*/
function isAdmin() {
return hasRole('admin');
}
/**
* Check if user is guest
*/
function isGuest() {
return hasRole('guest');
}
/**
* Require user to be logged in
*/
function requireLogin() {
if (!isLoggedIn()) {
$currentUrl = $_SERVER['REQUEST_URI'];
$redirect = urlencode($currentUrl);
header("Location: /auth/login.php?redirect=$redirect");
exit;
}
}
/**
* Require user to have specific role
*/
function requireRole($role) {
requireLogin();
if (!hasRole($role)) {
http_response_code(403);
echo '<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Geen Toegang</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="alert alert-danger">
<h4><i class="bi bi-exclamation-triangle"></i> Geen Toegang</h4>
<p>Je hebt geen toegang tot deze pagina.</p>
<a href="/index.php" class="btn btn-primary">Terug naar Dashboard</a>
</div>
</div>
</body>
</html>';
exit;
}
}
/**
* Require admin role
*/
function requireAdmin() {
requireRole('admin');
}
/**
* Logout user
*/
function logout() {
$_SESSION = [];
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time() - 3600, '/');
}
session_destroy();
}
/**
* Check if user can perform write operations
*/
function canWrite() {
return isLoggedIn() && isAdmin();
}
/**
* Check if user can only read
*/
function canOnlyRead() {
return isLoggedIn() && isGuest();
}
```
### 3. Middleware (auth/middleware.php)
```php
<?php
/**
* Authorization Middleware
*/
require_once __DIR__ . '/auth_functions.php';
/**
* Check if user can create
*/
function canCreate() {
return isAdmin();
}
/**
* Check if user can edit
*/
function canEdit() {
return isAdmin();
}
/**
* Check if user can delete
*/
function canDelete() {
return isAdmin();
}
/**
* Check if user can sync with Talpa
*/
function canSync() {
return isAdmin();
}
/**
* Render button only if user has permission
*/
function renderButton($permission, $html) {
if ($permission) {
echo $html;
}
}
/**
* Render form only if user has permission
*/
function renderForm($permission, $html) {
if ($permission) {
echo $html;
}
}
/**
* Get disabled attribute for guest users
*/
function getDisabledAttr() {
return isGuest() ? 'disabled' : '';
}
/**
* Get readonly attribute for guest users
*/
function getReadonlyAttr() {
return isGuest() ? 'readonly' : '';
}
```
### 4. Aangepaste Navigatie
```php
<!-- In alle pagina's: index.php, planner.php, calendar.php, etc. -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link" href="infomercials.php">Infomercials</a>
<?php if (isAdmin()): ?>
<a class="nav-link" href="admin/users.php">Gebruikers</a>
<?php endif; ?>
</div>
<!-- User Info & Logout -->
<div class="navbar-nav ms-auto">
<span class="nav-link">
<i class="bi bi-person-circle"></i>
<?= htmlspecialchars(getCurrentUser()['username']) ?>
<span class="badge bg-<?= isAdmin() ? 'danger' : 'secondary' ?>">
<?= isAdmin() ? 'Admin' : 'Guest' ?>
</span>
</span>
<a class="nav-link" href="auth/logout.php">
<i class="bi bi-box-arrow-right"></i> Uitloggen
</a>
</div>
</div>
</nav>
```
### 5. Voorbeeld: Dashboard met Role-based UI
```php
<?php
require_once __DIR__ . '/auth/auth_functions.php';
require_once __DIR__ . '/auth/middleware.php';
// Require login
requireLogin();
// Rest of existing code...
?>
<!-- In de HTML -->
<div class="container mt-4">
<?php if (isGuest()): ?>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Je bent ingelogd als <strong>Guest</strong>. Je kunt alleen informatie bekijken.
</div>
<?php endif; ?>
<!-- Existing content -->
<!-- Infomercial Registration Form - Only for Admins -->
<?php if (canCreate()): ?>
<div class="card p-3 shadow-sm">
<h5>1. Infomercial Registreren</h5>
<form method="POST">
<input type="text" name="title" class="form-control mb-2" placeholder="Product Naam" required>
<input type="text" name="duration" class="form-control mb-2" placeholder="Duur (HH:MM:SS)" required>
<input type="hidden" name="season_id" value="<?= $_ENV['TV_SEASON_ID'] ?>">
<button type="submit" name="add_commercial" class="btn btn-primary w-100">
Registreren bij Talpa
</button>
</form>
</div>
<?php endif; ?>
<!-- Sync buttons - Only for Admins -->
<?php if (canSync()): ?>
<form method="POST" style="display:inline;">
<input type="hidden" name="sync_id" value="<?= $tx['id'] ?>">
<button type="submit" name="sync_item" class="btn-icon btn-icon-xs btn-icon-success">
<i class="bi bi-cloud-upload"></i>
</button>
</form>
<?php else: ?>
<button type="button"
class="btn-icon btn-icon-xs btn-icon-secondary"
disabled
title="Alleen admins kunnen synchroniseren">
<i class="bi bi-cloud-upload"></i>
</button>
<?php endif; ?>
</div>
```
### 6. API Beveiliging Voorbeeld
```php
<?php
// api/create_transmission.php
require_once __DIR__ . '/../auth/auth_functions.php';
// Check authentication
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode([
'success' => false,
'error' => 'Niet geautoriseerd. Log eerst in.'
]);
exit;
}
// Check authorization (admin only for write operations)
if (!isAdmin()) {
http_response_code(403);
echo json_encode([
'success' => false,
'error' => 'Geen toegang. Alleen admins kunnen uitzendingen aanmaken.'
]);
exit;
}
// Continue with existing logic...
```
### 7. Gebruikersbeheer Interface (admin/users.php)
```php
<?php
require_once __DIR__ . '/../auth/auth_functions.php';
require_once __DIR__ . '/../auth/middleware.php';
// Only admins can access
requireAdmin();
$db = getAuthDb();
// Handle user creation
if (isset($_POST['create_user'])) {
$username = $_POST['username'];
$email = $_POST['email'];
$password = $_POST['password'];
$role = $_POST['role'];
$passwordHash = password_hash($password, PASSWORD_BCRYPT);
$stmt = $db->prepare("
INSERT INTO users (username, email, password_hash, role)
VALUES (?, ?, ?, ?)
");
$stmt->execute([$username, $email, $passwordHash, $role]);
header("Location: users.php?success=created");
exit;
}
// Handle user update
if (isset($_POST['update_user'])) {
$userId = $_POST['user_id'];
$username = $_POST['username'];
$email = $_POST['email'];
$role = $_POST['role'];
$isActive = isset($_POST['is_active']) ? 1 : 0;
$stmt = $db->prepare("
UPDATE users
SET username = ?, email = ?, role = ?, is_active = ?
WHERE id = ?
");
$stmt->execute([$username, $email, $role, $isActive, $userId]);
header("Location: users.php?success=updated");
exit;
}
// Get all users
$users = $db->query("
SELECT id, username, email, role, is_active, last_login, created_at
FROM users
ORDER BY created_at DESC
")->fetchAll();
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Gebruikersbeheer - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body class="bg-light">
<!-- Navigation -->
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container mt-4">
<h1><i class="bi bi-people"></i> Gebruikersbeheer</h1>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show">
Gebruiker succesvol <?= $_GET['success'] === 'created' ? 'aangemaakt' : 'bijgewerkt' ?>!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<!-- Create User Button -->
<button type="button" class="btn btn-primary mb-3" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i class="bi bi-plus-circle"></i> Nieuwe Gebruiker
</button>
<!-- Users Table -->
<div class="card shadow-sm">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Gebruikersnaam</th>
<th>Email</th>
<th>Rol</th>
<th>Status</th>
<th>Laatste Login</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?= htmlspecialchars($user['username']) ?></td>
<td><?= htmlspecialchars($user['email']) ?></td>
<td>
<span class="badge bg-<?= $user['role'] === 'admin' ? 'danger' : 'secondary' ?>">
<?= ucfirst($user['role']) ?>
</span>
</td>
<td>
<span class="badge bg-<?= $user['is_active'] ? 'success' : 'warning' ?>">
<?= $user['is_active'] ? 'Actief' : 'Inactief' ?>
</span>
</td>
<td>
<?= $user['last_login'] ? date('d-m-Y H:i', strtotime($user['last_login'])) : 'Nooit' ?>
</td>
<td>
<button type="button"
class="btn btn-sm btn-primary"
onclick="editUser(<?= htmlspecialchars(json_encode($user)) ?>)">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create User Modal -->
<div class="modal fade" id="createUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST">
<div class="modal-header">
<h5 class="modal-title">Nieuwe Gebruiker</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Gebruikersnaam</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Wachtwoord</label>
<input type="password" name="password" class="form-control" required minlength="8">
</div>
<div class="mb-3">
<label class="form-label">Rol</label>
<select name="role" class="form-select" required>
<option value="guest">Guest</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" name="create_user" class="btn btn-primary">Aanmaken</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
```
### 8. JavaScript voor Guest Mode (calendar-init.js aanpassing)
```javascript
// In assets/js/calendar-init.js
// Check if user is guest
const isGuest = <?= json_encode(isGuest()) ?>;
// Disable drag and drop for guests
if (isGuest) {
// Disable external events
document.querySelectorAll('.infomercial-item').forEach(item => {
item.style.cursor = 'not-allowed';
item.style.opacity = '0.6';
item.removeAttribute('draggable');
});
// Make calendar read-only
calendar = new FullCalendar.Calendar(calendarEl, {
// ... existing config
editable: false,
droppable: false,
eventStartEditable: false,
eventDurationEditable: false,
eventResourceEditable: false
});
// Show tooltip on hover
document.querySelectorAll('.infomercial-item').forEach(item => {
item.title = 'Alleen admins kunnen infomercials slepen';
});
}
```
## Visuele Voorbeelden
### Login Pagina
```
┌─────────────────────────────────────┐
│ │
│ 📺 Telvero Talpa │
│ Planning System

View File

@ -0,0 +1,538 @@
# Authentication & Authorization System Plan
## Overzicht
Dit plan beschrijft de implementatie van een login systeem met rollen en rechten voor de Telvero Talpa Planning applicatie.
## Rollen & Rechten
### Admin Rol
- **Volledige toegang** tot alle functionaliteiten
- Kan infomercials toevoegen, bewerken en verwijderen
- Kan uitzendingen plannen, bewerken en verwijderen
- Kan synchroniseren met Talpa API
- Kan blokken beheren
- Kan gebruikers beheren (toevoegen, bewerken, verwijderen)
### Guest Rol
- **Alleen lezen** toegang
- Kan dashboards bekijken
- Kan planning bekijken (kalender, planner)
- Kan infomercials bekijken
- Kan blokken bekijken
- **Geen** toevoeg-, bewerk- of verwijderfunctionaliteit
- **Geen** sync functionaliteit
- **Geen** gebruikersbeheer
## Database Schema
### Tabel: `users`
```sql
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin', 'guest') DEFAULT 'guest',
is_active BOOLEAN DEFAULT 1,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_role (role)
);
```
### Tabel: `sessions`
```sql
CREATE TABLE sessions (
id VARCHAR(128) PRIMARY KEY,
user_id INT NOT NULL,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_last_activity (last_activity)
);
```
### Tabel: `login_attempts`
```sql
CREATE TABLE login_attempts (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
ip_address VARCHAR(45),
success BOOLEAN DEFAULT 0,
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_ip_address (ip_address),
INDEX idx_attempted_at (attempted_at)
);
```
## Bestandsstructuur
```
/
├── auth/
│ ├── login.php # Login pagina
│ ├── logout.php # Logout handler
│ ├── auth_functions.php # Authenticatie functies
│ └── middleware.php # Access control middleware
├── admin/
│ └── users.php # Gebruikersbeheer (admin only)
├── migrations/
│ └── 003_add_authentication.sql
└── (bestaande bestanden worden aangepast)
```
## Implementatie Details
### 1. Authenticatie Functies (`auth/auth_functions.php`)
```php
// Kernfuncties:
- login($username, $password) // Inloggen
- logout() // Uitloggen
- isLoggedIn() // Check of gebruiker ingelogd is
- getCurrentUser() // Haal huidige gebruiker op
- hasRole($role) // Check of gebruiker rol heeft
- requireLogin() // Forceer login (redirect)
- requireRole($role) // Forceer specifieke rol
- hashPassword($password) // Wachtwoord hashen
- verifyPassword($password, $hash) // Wachtwoord verifiëren
- createSession($userId) // Sessie aanmaken
- destroySession() // Sessie vernietigen
- logLoginAttempt($username, $success) // Log inlogpoging
- checkBruteForce($username, $ip) // Brute force bescherming
```
### 2. Middleware (`auth/middleware.php`)
```php
// Access control functies:
- checkAuthentication() // Check authenticatie
- checkAuthorization($requiredRole) // Check autorisatie
- canCreate() // Kan aanmaken?
- canEdit() // Kan bewerken?
- canDelete() // Kan verwijderen?
- canSync() // Kan synchroniseren?
```
### 3. Login Pagina (`auth/login.php`)
**Features:**
- Username/email + wachtwoord formulier
- "Onthoud mij" functionaliteit
- Foutmeldingen bij ongeldige credentials
- Brute force bescherming (max 5 pogingen per 15 minuten)
- Redirect naar oorspronkelijke pagina na login
- Responsive design (Bootstrap)
### 4. Gebruikersbeheer (`admin/users.php`)
**Features (alleen voor admins):**
- Lijst van alle gebruikers
- Gebruiker toevoegen
- Gebruiker bewerken (username, email, rol)
- Gebruiker deactiveren/activeren
- Wachtwoord resetten
- Laatste login datum tonen
- Zoek- en filterfunctionaliteit
### 5. Bestaande Pagina's Aanpassen
Alle bestaande pagina's moeten worden aangepast:
#### Bovenaan elke pagina toevoegen:
```php
<?php
require_once __DIR__ . '/auth/auth_functions.php';
require_once __DIR__ . '/auth/middleware.php';
// Forceer login
requireLogin();
// Voor admin-only pagina's:
// requireRole('admin');
?>
```
#### Navigatie aanpassen:
- Gebruikersnaam tonen
- Logout knop toevoegen
- Rol badge tonen (Admin/Guest)
#### UI Aanpassingen voor Guest rol:
**Dashboard (`index.php`):**
- Verberg "Infomercial Registreren" formulier
- Verberg "Tijdblok Reserveren" formulier
- Verberg "Sync" knoppen
- Verberg "Opslaan" knoppen in Media Asset Management
**Planner (`planner.php`):**
- Verberg "Toevoegen" formulieren
- Verberg "Verwijderen" knoppen
- Verberg "Reorder" knoppen (pijltjes)
- Verberg "Sync" knoppen
- Verberg "Blok tijd aanpassen" knoppen
- Verberg "Kopieer blok" knoppen
- Disable drag-and-drop functionaliteit
**Kalender (`calendar.php`):**
- Disable drag-and-drop functionaliteit
- Verberg "Sync" knoppen
- Verberg "Verwijderen" knoppen in event modal
- Maak kalender read-only (geen event creation/editing)
**Blokken (`blocks.php`):**
- Verberg "Nieuw Template" knop
- Verberg "Bewerken" knoppen
- Verberg "Verwijderen" knoppen
- Verberg formulieren
**Infomercials (`infomercials.php`):**
- Verberg "Registreren" formulier
- Verberg "Bewerken" knoppen
- Verberg "Verwijderen" knoppen
- Verberg "Kleur aanpassen" functionaliteit
### 6. API Endpoints Beveiligen
Alle API endpoints in `/api/` moeten worden beveiligd:
```php
<?php
require_once __DIR__ . '/../auth/auth_functions.php';
require_once __DIR__ . '/../auth/middleware.php';
// Check authenticatie
if (!isLoggedIn()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd']);
exit;
}
// Voor write operations (POST/PUT/DELETE):
if (!hasRole('admin')) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Geen toegang']);
exit;
}
?>
```
**API endpoints die bescherming nodig hebben:**
- `create_transmission.php` - Admin only
- `update_transmission.php` - Admin only
- `delete_transmission.php` - Admin only
- `sync_block.php` - Admin only
- `copy_block.php` - Admin only
- `update_block_time.php` - Admin only
- `assign_color.php` - Admin only
- Read-only endpoints (`get_*.php`) - Alle ingelogde gebruikers
## Beveiliging
### Wachtwoord Beveiliging
- Gebruik `password_hash()` met `PASSWORD_BCRYPT` of `PASSWORD_ARGON2ID`
- Minimum wachtwoordlengte: 8 karakters
- Wachtwoord sterkte validatie (optioneel: hoofdletters, cijfers, speciale tekens)
### Sessie Beveiliging
- Gebruik PHP native sessions met secure settings
- Session timeout: 2 uur inactiviteit
- Regenerate session ID na login
- HttpOnly en Secure flags voor cookies (in productie)
- Session fixation bescherming
### Brute Force Bescherming
- Maximum 5 mislukte inlogpogingen per 15 minuten per username
- Maximum 10 mislukte inlogpogingen per 15 minuten per IP
- Lockout periode: 15 minuten
- Log alle inlogpogingen
### CSRF Bescherming
- CSRF tokens voor alle formulieren
- Token validatie bij POST requests
- Token regeneratie na gebruik
### SQL Injection Bescherming
- Gebruik prepared statements (reeds geïmplementeerd)
- Input validatie en sanitization
### XSS Bescherming
- Output escaping met `htmlspecialchars()` (reeds geïmplementeerd)
- Content Security Policy headers (optioneel)
## Environment Variabelen
Toevoegen aan `.env`:
```env
# Authentication Settings
AUTH_SESSION_TIMEOUT=7200
AUTH_MAX_LOGIN_ATTEMPTS=5
AUTH_LOCKOUT_DURATION=900
AUTH_REMEMBER_ME_DURATION=2592000
```
## Standaard Gebruikers
Bij installatie worden de volgende gebruikers aangemaakt:
```sql
-- Admin gebruiker
INSERT INTO users (username, email, password_hash, role) VALUES
('admin', 'admin@telvero.nl', '$2y$10$...', 'admin');
-- Guest gebruiker (voor demo)
INSERT INTO users (username, email, password_hash, role) VALUES
('guest', 'guest@telvero.nl', '$2y$10$...', 'guest');
```
**Standaard wachtwoorden:**
- Admin: `admin123` (moet bij eerste login worden gewijzigd)
- Guest: `guest123`
## UI/UX Overwegingen
### Login Pagina Design
- Centraal gepositioneerd formulier
- Telvero branding (logo, kleuren)
- Duidelijke foutmeldingen
- Loading indicator tijdens login
- Responsive design voor mobiel
### Navigatie Updates
```html
<div class="navbar-nav ms-auto">
<span class="nav-link">
<i class="bi bi-person-circle"></i>
<?= htmlspecialchars($_SESSION['user']['username']) ?>
<span class="badge bg-<?= $_SESSION['user']['role'] === 'admin' ? 'danger' : 'secondary' ?>">
<?= ucfirst($_SESSION['user']['role']) ?>
</span>
</span>
<a class="nav-link" href="auth/logout.php">
<i class="bi bi-box-arrow-right"></i> Uitloggen
</a>
</div>
```
### Guest Mode Indicators
- Toon duidelijk dat gebruiker in "alleen lezen" modus is
- Tooltip bij disabled knoppen: "Alleen admins kunnen deze actie uitvoeren"
- Optioneel: banner bovenaan pagina voor guest gebruikers
## Testing Checklist
### Authenticatie Tests
- [ ] Login met geldige credentials (admin)
- [ ] Login met geldige credentials (guest)
- [ ] Login met ongeldige credentials
- [ ] Logout functionaliteit
- [ ] Session timeout na inactiviteit
- [ ] Brute force bescherming
- [ ] "Onthoud mij" functionaliteit
- [ ] Redirect naar oorspronkelijke pagina na login
### Autorisatie Tests
- [ ] Admin kan alle pagina's bezoeken
- [ ] Guest kan alleen lezen
- [ ] Guest kan geen create/update/delete acties uitvoeren
- [ ] Guest kan niet synchroniseren met Talpa
- [ ] API endpoints zijn beveiligd
- [ ] Direct URL access wordt geblokkeerd voor ongeautoriseerde acties
### UI Tests
- [ ] Knoppen zijn verborgen/disabled voor guests
- [ ] Formulieren zijn verborgen voor guests
- [ ] Drag-and-drop is disabled voor guests
- [ ] Navigatie toont correcte gebruikersinformatie
- [ ] Rol badge wordt correct getoond
### Gebruikersbeheer Tests (Admin only)
- [ ] Admin kan gebruikers toevoegen
- [ ] Admin kan gebruikers bewerken
- [ ] Admin kan gebruikers deactiveren
- [ ] Admin kan wachtwoorden resetten
- [ ] Guest kan gebruikersbeheer niet benaderen
## Migratie Strategie
### Stap 1: Database Migratie
```bash
mysql -u username -p talpa_planning < migrations/003_add_authentication.sql
```
### Stap 2: Standaard Gebruikers Aanmaken
Via SQL of via setup script
### Stap 3: Bestaande Pagina's Updaten
Alle PHP bestanden updaten met authenticatie checks
### Stap 4: Testing
Uitgebreid testen van alle functionaliteiten
### Stap 5: Deployment
- Backup maken van database
- Code deployen
- Migratie uitvoeren
- Testen in productie
## Toekomstige Uitbreidingen
Mogelijke uitbreidingen voor de toekomst:
1. **Meer Rollen:**
- Editor: Kan plannen maar niet synchroniseren
- Viewer: Alleen lezen (zoals guest)
- Manager: Kan alles behalve gebruikersbeheer
2. **Granulaire Rechten:**
- Per module rechten toewijzen
- Custom permission sets
3. **Two-Factor Authentication (2FA):**
- TOTP via Google Authenticator
- SMS verificatie
4. **Audit Log:**
- Log alle acties van gebruikers
- Wie heeft wat wanneer gedaan
5. **Wachtwoord Beleid:**
- Wachtwoord expiratie
- Wachtwoord geschiedenis
- Complexiteitseisen
6. **Single Sign-On (SSO):**
- LDAP/Active Directory integratie
- OAuth2 providers (Google, Microsoft)
7. **API Keys:**
- Voor externe integraties
- Rate limiting
## Documentatie Updates
Na implementatie moeten de volgende documenten worden bijgewerkt:
1. **README.md:**
- Sectie over authenticatie toevoegen
- Standaard credentials documenteren
- Gebruikersbeheer uitleggen
2. **INSTALLATION.md:**
- Authenticatie migratie stappen
- Eerste admin gebruiker aanmaken
3. **Nieuwe documentatie:**
- `docs/AUTHENTICATION.md` - Uitgebreide authenticatie documentatie
- `docs/USER_MANAGEMENT.md` - Gebruikersbeheer handleiding
## Mermaid Diagram: Authenticatie Flow
```mermaid
graph TD
A[Gebruiker bezoekt pagina] --> B{Ingelogd?}
B -->|Nee| C[Redirect naar login.php]
C --> D[Gebruiker vult credentials in]
D --> E{Credentials geldig?}
E -->|Nee| F[Toon foutmelding]
F --> G{Max pogingen bereikt?}
G -->|Ja| H[Account tijdelijk geblokkeerd]
G -->|Nee| D
E -->|Ja| I[Maak sessie aan]
I --> J[Redirect naar oorspronkelijke pagina]
B -->|Ja| K{Juiste rol?}
K -->|Nee| L[Toon 403 Forbidden]
K -->|Ja| M{Admin of Guest?}
M -->|Admin| N[Volledige toegang]
M -->|Guest| O[Alleen lezen toegang]
N --> P[Toon alle functionaliteit]
O --> Q[Verberg write acties]
```
## Mermaid Diagram: Database Schema
```mermaid
erDiagram
users ||--o{ sessions : has
users ||--o{ login_attempts : has
users {
int id PK
varchar username UK
varchar email UK
varchar password_hash
enum role
boolean is_active
timestamp last_login
timestamp created_at
timestamp updated_at
}
sessions {
varchar id PK
int user_id FK
varchar ip_address
varchar user_agent
timestamp last_activity
timestamp created_at
}
login_attempts {
int id PK
varchar username
varchar ip_address
boolean success
timestamp attempted_at
}
```
## Prioriteit & Volgorde
### Fase 1: Basis Authenticatie (Hoge Prioriteit)
1. Database migratie
2. Authenticatie functies
3. Login pagina
4. Logout functionaliteit
5. Sessie management
### Fase 2: Autorisatie (Hoge Prioriteit)
1. Middleware implementatie
2. Rol checks in bestaande pagina's
3. API beveiliging
### Fase 3: UI Aanpassingen (Gemiddelde Prioriteit)
1. Navigatie updates
2. Guest mode UI restrictions
3. Tooltips en indicators
### Fase 4: Gebruikersbeheer (Gemiddelde Prioriteit)
1. Gebruikersbeheer interface
2. CRUD operaties voor gebruikers
3. Wachtwoord reset functionaliteit
### Fase 5: Extra Beveiliging (Lage Prioriteit)
1. Brute force bescherming
2. CSRF tokens
3. Audit logging
## Conclusie
Dit plan biedt een complete oplossing voor authenticatie en autorisatie in de Telvero Talpa Planning applicatie. De implementatie zorgt voor:
- **Veilige authenticatie** met moderne best practices
- **Duidelijke rolscheiding** tussen admin en guest
- **Gebruiksvriendelijke interface** met duidelijke feedback
- **Uitbreidbaarheid** voor toekomstige requirements
- **Minimale impact** op bestaande functionaliteit
De implementatie kan gefaseerd worden uitgevoerd, waarbij eerst de basis authenticatie wordt geïmplementeerd en daarna stapsgewijs de autorisatie en UI aanpassingen worden toegevoegd.

202
setup_auth.php Normal file
View File

@ -0,0 +1,202 @@
<?php
/**
* Authentication Setup Script
* Run this script once to set up the authentication system
*
* Usage: php setup_auth.php
* Or via browser: http://your-domain/setup_auth.php
*
* IMPORTANT: Delete this file after running it!
*/
// Security check - only allow running once
$lockFile = __DIR__ . '/.auth_setup_done';
if (file_exists($lockFile)) {
die('Setup has already been completed. Delete .auth_setup_done to run again.');
}
require_once __DIR__ . '/vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
$db = new PDO(
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4",
$_ENV['DB_USER'],
$_ENV['DB_PASS'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$errors = [];
$success = [];
// Run migration
try {
$sql = file_get_contents(__DIR__ . '/migrations/003_add_authentication.sql');
// Split by semicolons and execute each statement
$statements = array_filter(array_map('trim', explode(';', $sql)));
foreach ($statements as $statement) {
if (!empty($statement)) {
try {
$db->exec($statement);
} catch (PDOException $e) {
// Ignore duplicate entry errors (tables already exist)
if ($e->getCode() != '42S01' && $e->getCode() != 23000) {
$errors[] = "SQL Error: " . $e->getMessage();
}
}
}
}
$success[] = "Database migration completed";
} catch (Exception $e) {
$errors[] = "Migration failed: " . $e->getMessage();
}
// Create admin user with proper password hash
$adminPassword = 'Admin@2026!';
$adminHash = password_hash($adminPassword, PASSWORD_BCRYPT);
try {
$stmt = $db->prepare("
INSERT INTO users (username, email, password_hash, role, is_active)
VALUES ('admin', 'admin@telvero.nl', ?, 'admin', 1)
ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash)
");
$stmt->execute([$adminHash]);
$success[] = "Admin user created/updated (username: admin, password: {$adminPassword})";
} catch (Exception $e) {
$errors[] = "Failed to create admin user: " . $e->getMessage();
}
// Create guest user with proper password hash
$guestPassword = 'Guest@2026!';
$guestHash = password_hash($guestPassword, PASSWORD_BCRYPT);
try {
$stmt = $db->prepare("
INSERT INTO users (username, email, password_hash, role, is_active)
VALUES ('guest', 'guest@telvero.nl', ?, 'guest', 1)
ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash)
");
$stmt->execute([$guestHash]);
$success[] = "Guest user created/updated (username: guest, password: {$guestPassword})";
} catch (Exception $e) {
$errors[] = "Failed to create guest user: " . $e->getMessage();
}
// Create lock file
if (empty($errors)) {
file_put_contents($lockFile, date('Y-m-d H:i:s'));
}
// Output results
$isCli = php_sapi_name() === 'cli';
if ($isCli) {
echo "\n=== Authentication Setup ===\n\n";
if (!empty($success)) {
echo "✓ SUCCESS:\n";
foreach ($success as $msg) {
echo " - {$msg}\n";
}
}
if (!empty($errors)) {
echo "\n✗ ERRORS:\n";
foreach ($errors as $msg) {
echo " - {$msg}\n";
}
}
echo "\n=== Default Credentials ===\n";
echo "Admin: admin / Admin@2026!\n";
echo "Guest: guest / Guest@2026!\n";
echo "\n⚠️ IMPORTANT: Change these passwords after first login!\n";
echo "\n✓ Setup complete. This script can now be deleted.\n\n";
} else {
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Auth Setup - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1><i class="bi bi-shield-check"></i> Authentication Setup</h1>
<?php if (!empty($success)): ?>
<div class="card mb-3 border-success">
<div class="card-header bg-success text-white"> Succesvol</div>
<div class="card-body">
<ul class="mb-0">
<?php foreach ($success as $msg): ?>
<li><?= htmlspecialchars($msg) ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="card mb-3 border-danger">
<div class="card-header bg-danger text-white"> Fouten</div>
<div class="card-body">
<ul class="mb-0">
<?php foreach ($errors as $msg): ?>
<li><?= htmlspecialchars($msg) ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
<div class="card mb-3 border-warning">
<div class="card-header bg-warning">⚠️ Standaard Inloggegevens</div>
<div class="card-body">
<table class="table table-sm mb-0">
<thead>
<tr><th>Gebruiker</th><th>Wachtwoord</th><th>Rol</th></tr>
</thead>
<tbody>
<tr>
<td><strong>admin</strong></td>
<td><code>Admin@2026!</code></td>
<td><span class="badge bg-danger">Admin</span></td>
</tr>
<tr>
<td><strong>guest</strong></td>
<td><code>Guest@2026!</code></td>
<td><span class="badge bg-secondary">Guest</span></td>
</tr>
</tbody>
</table>
<div class="alert alert-warning mt-3 mb-0">
<strong>Belangrijk:</strong> Wijzig deze wachtwoorden na de eerste login!
</div>
</div>
</div>
<div class="d-flex gap-2">
<a href="/auth/login.php" class="btn btn-primary">
Ga naar Login
</a>
<a href="/admin/users.php" class="btn btn-secondary">
Gebruikersbeheer
</a>
</div>
</div>
</div>
</div>
</body>
</html>
<?php
}