diff --git a/.env.example b/.env.example
index 95d1fcf..4e1554d 100644
--- a/.env.example
+++ b/.env.example
@@ -15,3 +15,7 @@ TV_SEASON_ID=your_season_id_here
# Application Settings
APP_ENV=production
APP_DEBUG=false
+
+# Authentication Settings
+# Session timeout in seconds (default: 7200 = 2 hours)
+AUTH_SESSION_TIMEOUT=7200
diff --git a/.gitignore b/.gitignore
index 887a2c2..f786d2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
*.sln.docstates
.env
.DS_Store
+.auth_setup_done
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
diff --git a/INSTALLATION.md b/INSTALLATION.md
index e6363b0..7eff7e5 100644
--- a/INSTALLATION.md
+++ b/INSTALLATION.md
@@ -61,9 +61,11 @@ EXIT;
### Stap 4: Database Migratie
-**4.1 Voer migratie uit:**
+**4.1 Voer migraties uit:**
```bash
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:**
@@ -73,9 +75,12 @@ mysql -u talpa_user -p talpa_planning -e "SHOW TABLES;"
Je zou moeten zien:
- block_templates
-- infomercials
- daily_blocks
+- infomercials
+- login_attempts
+- sessions
- transmissions
+- users
### Stap 5: Environment Configuratie
@@ -189,9 +194,34 @@ server {
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/
```
@@ -349,13 +379,20 @@ Bij problemen:
- [ ] PHP 7.4+ geïnstalleerd
- [ ] MySQL database aangemaakt
- [ ] Composer dependencies geïnstalleerd
-- [ ] Database migratie uitgevoerd
+- [ ] Database migraties uitgevoerd (001, 002, 003)
- [ ] .env file geconfigureerd
- [ ] Bestandspermissies ingesteld
- [ ] 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
-- [ ] Test infomercial aangemaakt
+- [ ] Test infomercial aangemaakt (admin)
- [ ] Kalender laadt correct
+- [ ] Gebruikersbeheer bereikbaar (admin)
+- [ ] Standaard wachtwoorden gewijzigd
Als alle items zijn afgevinkt, is de installatie succesvol! 🎉
diff --git a/admin/users.php b/admin/users.php
new file mode 100644
index 0000000..982d21f
--- /dev/null
+++ b/admin/users.php
@@ -0,0 +1,420 @@
+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';
+?>
+
+
+
+
+
+ Gebruikersbeheer - Telvero Talpa
+
+
+
+
+
+
+
+
+
+
Gebruikersbeheer
+
+
+
+
+
+ = htmlspecialchars($success) ?>
+
+
+
+
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+
+
+
+
+
+
= count($users) ?>
+
Totaal Gebruikers
+
+
+
+
+
+
+
= $loginStats['successful_logins'] ?? 0 ?>
+
Succesvolle Logins (24u)
+
+
+
+
+
+
+
= $loginStats['failed_logins'] ?? 0 ?>
+
Mislukte Logins (24u)
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Gebruikersnaam |
+ Email |
+ Rol |
+ Status |
+ Laatste Login |
+ Aangemaakt |
+ Acties |
+
+
+
+
+
+ | = $user['id'] ?> |
+
+
+ = htmlspecialchars($user['username']) ?>
+
+ Jij
+
+ |
+ = htmlspecialchars($user['email']) ?> |
+
+
+
+ = ucfirst($user['role']) ?>
+
+ |
+
+
+ = $user['is_active'] ? 'Actief' : 'Inactief' ?>
+
+ |
+
+ = $user['last_login']
+ ? date('d-m-Y H:i', strtotime($user['last_login']))
+ : 'Nooit' ?>
+ |
+
+ = date('d-m-Y', strtotime($user['created_at'])) ?>
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/api/assign_color.php b/api/assign_color.php
index 918ea18..60041ad 100644
--- a/api/assign_color.php
+++ b/api/assign_color.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,20 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/copy_block.php b/api/copy_block.php
index 2e7f919..62a92b3 100644
--- a/api/copy_block.php
+++ b/api/copy_block.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,20 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/create_transmission.php b/api/create_transmission.php
index de436bb..3b57689 100644
--- a/api/create_transmission.php
+++ b/api/create_transmission.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,20 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/delete_transmission.php b/api/delete_transmission.php
index 7048147..60f83c3 100644
--- a/api/delete_transmission.php
+++ b/api/delete_transmission.php
@@ -7,6 +7,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../TalpaAPI.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -15,6 +16,20 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/get_available_source_blocks.php b/api/get_available_source_blocks.php
index 43964e0..ca0172f 100644
--- a/api/get_available_source_blocks.php
+++ b/api/get_available_source_blocks.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json');
+if (!isLoggedIn()) {
+ http_response_code(401);
+ echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
+ exit;
+}
+
try {
$db = getDbConnection();
diff --git a/api/get_block_details.php b/api/get_block_details.php
index 085fea7..70f26cc 100644
--- a/api/get_block_details.php
+++ b/api/get_block_details.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json');
+if (!isLoggedIn()) {
+ http_response_code(401);
+ echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
+ exit;
+}
+
try {
$db = getDbConnection();
diff --git a/api/get_block_time.php b/api/get_block_time.php
index fa8ce72..1447a91 100644
--- a/api/get_block_time.php
+++ b/api/get_block_time.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json');
+if (!isLoggedIn()) {
+ http_response_code(401);
+ echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
+ exit;
+}
+
try {
$db = getDbConnection();
diff --git a/api/get_daily_blocks.php b/api/get_daily_blocks.php
index cd72f72..1bff893 100644
--- a/api/get_daily_blocks.php
+++ b/api/get_daily_blocks.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json');
+if (!isLoggedIn()) {
+ http_response_code(401);
+ echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
+ exit;
+}
+
try {
$db = getDbConnection();
diff --git a/api/get_next_start_time.php b/api/get_next_start_time.php
index 3c47c9f..c900a16 100644
--- a/api/get_next_start_time.php
+++ b/api/get_next_start_time.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json');
+if (!isLoggedIn()) {
+ http_response_code(401);
+ echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
+ exit;
+}
+
try {
$db = getDbConnection();
diff --git a/api/get_transmissions.php b/api/get_transmissions.php
index e2867ca..dc3419f 100644
--- a/api/get_transmissions.php
+++ b/api/get_transmissions.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,13 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/insert_transmission_at_position.php b/api/insert_transmission_at_position.php
index c46a3ec..4a3845c 100644
--- a/api/insert_transmission_at_position.php
+++ b/api/insert_transmission_at_position.php
@@ -7,6 +7,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -15,6 +16,20 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/sync_block.php b/api/sync_block.php
index d387a32..2e0843e 100644
--- a/api/sync_block.php
+++ b/api/sync_block.php
@@ -11,6 +11,7 @@ error_reporting(E_ALL);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
@@ -18,6 +19,20 @@ $dotenv->load();
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();
$db = getDbConnection();
diff --git a/api/update_block_time.php b/api/update_block_time.php
index 437e515..01c6007 100644
--- a/api/update_block_time.php
+++ b/api/update_block_time.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,20 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/update_transmission.php b/api/update_transmission.php
index 5fb3111..f1da1a7 100644
--- a/api/update_transmission.php
+++ b/api/update_transmission.php
@@ -7,6 +7,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -15,6 +16,20 @@ $dotenv->load();
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 {
$db = getDbConnection();
diff --git a/api/validate_transmission_time.php b/api/validate_transmission_time.php
index 234991f..23b8f20 100644
--- a/api/validate_transmission_time.php
+++ b/api/validate_transmission_time.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
+require_once __DIR__ . '/../auth/auth_functions.php';
use Dotenv\Dotenv;
@@ -14,6 +15,12 @@ $dotenv->load();
header('Content-Type: application/json');
+if (!isLoggedIn()) {
+ http_response_code(401);
+ echo json_encode(['success' => false, 'error' => 'Niet geautoriseerd.']);
+ exit;
+}
+
try {
$db = getDbConnection();
diff --git a/auth/403.php b/auth/403.php
new file mode 100644
index 0000000..3e7f318
--- /dev/null
+++ b/auth/403.php
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Geen Toegang - Telvero Talpa
+
+
+
+
+
+
+
+
+
+
+
Geen Toegang
+
Je hebt geen rechten om deze pagina te bekijken.
+
Alleen admins hebben toegang tot deze functionaliteit.
+
+ Terug naar Dashboard
+
+
+
+
+
+
+
+
diff --git a/auth/auth_functions.php b/auth/auth_functions.php
new file mode 100644
index 0000000..fdc95a1
--- /dev/null
+++ b/auth/auth_functions.php
@@ -0,0 +1,305 @@
+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();
diff --git a/auth/login.php b/auth/login.php
new file mode 100644
index 0000000..3893567
--- /dev/null
+++ b/auth/login.php
@@ -0,0 +1,244 @@
+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'];
+ }
+ }
+}
+?>
+
+
+
+
+
+ Inloggen - Telvero Talpa
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Je sessie is verlopen. Log opnieuw in.
+
+
+
+
+
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/auth/logout.php b/auth/logout.php
new file mode 100644
index 0000000..5811869
--- /dev/null
+++ b/auth/logout.php
@@ -0,0 +1,18 @@
+load();
+
+require_once __DIR__ . '/auth_functions.php';
+
+logout();
+
+header("Location: /auth/login.php");
+exit;
diff --git a/blocks.php b/blocks.php
index 50f0da0..42334c3 100644
--- a/blocks.php
+++ b/blocks.php
@@ -10,15 +10,19 @@ error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/helpers.php';
+require_once __DIR__ . '/auth/auth_functions.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
+// Require login
+requireLogin();
+
$db = getDbConnection();
-// Handle form submissions
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+// Handle form submissions - Admin only
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && canEdit()) {
if (isset($_POST['create_template'])) {
$stmt = $db->prepare("
INSERT INTO block_templates
@@ -104,20 +108,7 @@ $dayNames = [
-
+
@@ -142,6 +133,7 @@ $dayNames = [