diff --git a/.gitignore b/.gitignore
index 0169e6d..ae7ee76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,7 @@ Thumbs.db
.idea/
.continue/
.continueignore
+
+# Uploads
+/assets/uploads/*
+!/assets/uploads/**/.gitkeep
diff --git a/admin.php b/admin.php
index f615c1b..65bb048 100644
--- a/admin.php
+++ b/admin.php
@@ -783,7 +783,10 @@ require_once __DIR__ . '/includes/header.php';
-  ?>)
+  ?>)
+
+ = strtoupper(substr($usr['name'], 0, 1)) ?>
+
= strtoupper(substr($usr['name'], 0, 1)) ?>
diff --git a/auth/invite.php b/auth/invite.php
index d4a348f..728991b 100644
--- a/auth/invite.php
+++ b/auth/invite.php
@@ -174,12 +174,12 @@ $pageTitle = "Fociz — Uitnodiging Accepteren";
diff --git a/auth/reset-password.php b/auth/reset-password.php
index a07c0d2..b1ea258 100644
--- a/auth/reset-password.php
+++ b/auth/reset-password.php
@@ -185,16 +185,16 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
-
+
+ Minimaal 12 tekens (incl. hoofdletter, kleine letter, cijfer en speciaal teken).
@@ -215,7 +216,8 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
type="password"
name="confirm_password"
required
- placeholder="Herhaal wachtwoord"
+ minlength="12"
+ placeholder="Herhaal wachtwoord (min. 12 tekens)"
class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition"
>
@@ -257,9 +259,9 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
if (!val) return;
var score = 0;
- if (val.length >= 8) score++;
- if (/[A-Z]/.test(val) && /[0-9]/.test(val)) score++;
- if (/[^A-Za-z0-9]/.test(val) && val.length >= 10) score++;
+ if (val.length >= 12) score++;
+ if (/[A-Z]/.test(val) && /[a-z]/.test(val) && /[0-9]/.test(val)) score++;
+ if (/[^A-Za-z0-9]/.test(val) && val.length >= 12) score++;
if (score === 1) {
b1.className = 'h-1 flex-1 rounded-full bg-amber-500';
diff --git a/auth/setup.php b/auth/setup.php
index 993ba6a..c0d6daf 100644
--- a/auth/setup.php
+++ b/auth/setup.php
@@ -195,12 +195,12 @@ $pageTitle = "Fociz — Eerste Initialisatie";
diff --git a/board.php b/board.php
index 2d0ccb9..7002232 100644
--- a/board.php
+++ b/board.php
@@ -678,7 +678,7 @@ function renderCards() {
if (card.assignee_name) {
const initial = card.assignee_name.charAt(0).toUpperCase();
const avatarMarkup = card.assignee_avatar
- ? ` }) `
+ ? ` }) ${initial} `
: ` ${initial} `;
assigneeHtml = `
diff --git a/config/database.php b/config/database.php
index ae28134..f26f03a 100644
--- a/config/database.php
+++ b/config/database.php
@@ -146,6 +146,7 @@ function ensure_tables_exist(PDO $pdo): void {
"ALTER TABLE users ADD COLUMN notification_settings TEXT NULL AFTER remember_token_expires_at",
"ALTER TABLE users ADD COLUMN theme VARCHAR(20) NOT NULL DEFAULT 'dark' AFTER notification_settings",
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme",
+ "ALTER TABLE users MODIFY COLUMN avatar_url MEDIUMTEXT NULL",
"ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled",
"ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url",
"ALTER TABLE users ADD COLUMN fociz_widget_layout TEXT NULL AFTER default_jira_filter_code",
diff --git a/config/schema.sql b/config/schema.sql
index c7a3ed7..00d67f8 100644
--- a/config/schema.sql
+++ b/config/schema.sql
@@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`remember_token` VARCHAR(64) NULL,
`remember_token_expires_at` DATETIME NULL,
`notification_settings` TEXT NULL,
- `avatar_url` VARCHAR(255) NULL,
+ `avatar_url` MEDIUMTEXT NULL,
`default_jira_filter_code` VARCHAR(50) NULL,
`fociz_widget_layout` TEXT NULL,
`last_mail_check_at` DATETIME NULL,
diff --git a/includes/auth.php b/includes/auth.php
index 2fa3a8a..47d6f04 100644
--- a/includes/auth.php
+++ b/includes/auth.php
@@ -384,6 +384,141 @@ function ensure_theme_column(PDO $db): void {
}
}
+/**
+ * Valideert en normaliseert een avatar URL.
+ * Controleert of geüploade bestanden nog fysiek aanwezig zijn op de server (bijv. na container herstart).
+ * Indien een bestand ontbreekt, wordt de database automatisch hersteld (self-healing) en null geretourneerd.
+ */
+function fociz_get_valid_avatar_url(?string $avatarUrl, ?int $userId = null): ?string {
+ if (empty($avatarUrl)) {
+ return null;
+ }
+
+ // 1. Data-URI (base64) of meegeleverde repository preset SVG: altijd persistent en geldig
+ if (str_starts_with($avatarUrl, 'data:image/') || str_starts_with($avatarUrl, '/assets/avatars/')) {
+ return $avatarUrl;
+ }
+
+ // 2. Geüpload bestand (/assets/uploads/...): controleer fysieke aanwezigheid op server
+ if (str_starts_with($avatarUrl, '/assets/uploads/')) {
+ $localPath = dirname(__DIR__) . '/' . ltrim($avatarUrl, '/');
+ if (file_exists($localPath)) {
+ return $avatarUrl;
+ }
+
+ // Fysiek bestand ontbreekt (bijv. door container herstart of git deploy)
+ if ($userId) {
+ try {
+ $db = get_db_connection();
+ $stmt = $db->prepare("UPDATE users SET avatar_url = NULL WHERE id = ? AND avatar_url = ?");
+ $stmt->execute([$userId, $avatarUrl]);
+ if (isset($_SESSION['user_avatar_url']) && $_SESSION['user_avatar_url'] === $avatarUrl) {
+ unset($_SESSION['user_avatar_url']);
+ }
+ } catch (Throwable $e) {
+ error_log("Self-healing avatar cleanup notice: " . $e->getMessage());
+ }
+ }
+ return null;
+ }
+
+ return $avatarUrl;
+}
+
+/**
+ * Verwerkt een geüploade avatar-afbeelding.
+ * - Knipt vierkant uit en comprimeert naar max. 256x256 via GD
+ * - Converteert naar Base64 Data URI voor 100% gegarandeerde persistentie in MariaDB
+ * - Slaat tevens optioneel een backup op in assets/uploads/avatars/
+ */
+function fociz_process_avatar_upload(array $file, int $userId): array {
+ $allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mime = finfo_file($finfo, $file['tmp_name']);
+ finfo_close($finfo);
+
+ if (!in_array($mime, $allowedTypes)) {
+ return ['success' => false, 'error' => 'Alleen JPG, PNG, WebP of GIF afbeeldingen zijn toegestaan.'];
+ }
+
+ $ext = match($mime) {
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/webp' => 'webp',
+ 'image/gif' => 'gif',
+ default => 'jpg'
+ };
+
+ $rawContent = file_get_contents($file['tmp_name']);
+ if ($rawContent === false || strlen($rawContent) === 0) {
+ return ['success' => false, 'error' => 'Kon geüploade afbeelding niet inlezen.'];
+ }
+
+ $finalMime = $mime;
+ $finalData = $rawContent;
+
+ // Verklein en comprimeer naar max 256x256 vierkant via GD
+ if (extension_loaded('gd') && function_exists('imagecreatefromstring')) {
+ try {
+ $srcImg = @imagecreatefromstring($rawContent);
+ if ($srcImg !== false) {
+ $w = imagesx($srcImg);
+ $h = imagesy($srcImg);
+ $maxDim = 256;
+
+ $minDim = min($w, $h);
+ $cropX = (int)(($w - $minDim) / 2);
+ $cropY = (int)(($h - $minDim) / 2);
+
+ $targetSize = min($maxDim, $minDim);
+ $dstImg = imagecreatetruecolor($targetSize, $targetSize);
+
+ imagealphablending($dstImg, false);
+ imagesavealpha($dstImg, true);
+
+ imagecopyresampled($dstImg, $srcImg, 0, 0, $cropX, $cropY, $targetSize, $targetSize, $minDim, $minDim);
+
+ ob_start();
+ if ($mime === 'image/png') {
+ imagepng($dstImg, null, 8);
+ $finalMime = 'image/png';
+ } elseif (function_exists('imagewebp') && ($mime === 'image/webp' || $mime === 'image/jpeg')) {
+ imagewebp($dstImg, null, 85);
+ $finalMime = 'image/webp';
+ } else {
+ imagejpeg($dstImg, null, 85);
+ $finalMime = 'image/jpeg';
+ }
+ $compressed = ob_get_clean();
+ if (!empty($compressed)) {
+ $finalData = $compressed;
+ }
+ imagedestroy($dstImg);
+ imagedestroy($srcImg);
+ }
+ } catch (Throwable $e) {
+ error_log("fociz avatar resize notice: " . $e->getMessage());
+ }
+ }
+
+ // Bouw data URI voor permanente opslag in MariaDB
+ $dataUri = 'data:' . $finalMime . ';base64,' . base64_encode($finalData);
+
+ // Sla bestand ook lokaal op als backup / snelle cache
+ $uploadDir = dirname(__DIR__) . '/assets/uploads/avatars';
+ if (!is_dir($uploadDir)) {
+ @mkdir($uploadDir, 0755, true);
+ }
+ $filename = "avatar_{$userId}_" . time() . "_" . bin2hex(random_bytes(4)) . ".{$ext}";
+ @file_put_contents($uploadDir . '/' . $filename, $finalData);
+
+ return [
+ 'success' => true,
+ 'data_uri' => $dataUri,
+ 'web_path' => "/assets/uploads/avatars/{$filename}"
+ ];
+}
+
/**
* Genereert een cryptografisch veilig dagsessie-token dat strikt geldig is tot 23:59:59 van vandaag.
* Voorkomt dat gebruikers tussentijds worden uitgelogd bij idle timeouts of browserherstart.
@@ -534,6 +669,8 @@ function get_logged_in_user(): ?array {
$_SESSION['auth_fingerprint'] = $clientFingerprint;
$_SESSION['session_expires_at'] = strtotime('today 23:59:59');
$_SESSION['last_activity'] = $now;
+ $user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
+ $_SESSION['user_avatar_url'] = $user['avatar_url'];
return $user;
}
// Gebruiker bestaat niet meer
@@ -568,6 +705,8 @@ function get_logged_in_user(): ?array {
$_SESSION['last_activity'] = $now;
$_SESSION['login_time'] = $now;
$_SESSION['last_rotated'] = $now;
+ $user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
+ $_SESSION['user_avatar_url'] = $user['avatar_url'];
return $user;
}
// Token is ongeldig of van gisteren
diff --git a/includes/header.php b/includes/header.php
index a52c0f6..81a1ccd 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -253,7 +253,10 @@ require_once __DIR__ . '/env_badge.php';
-  ?>)
+  ?>)
+
+ = strtoupper(substr($currentUser['name'], 0, 1)) ?>
+
= strtoupper(substr($currentUser['name'], 0, 1)) ?>
diff --git a/profile.php b/profile.php
index f3cc028..78b0a82 100644
--- a/profile.php
+++ b/profile.php
@@ -219,43 +219,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = "De afbeelding is te groot (maximaal 4MB toegestaan).";
$messageType = 'error';
} else {
- $allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
- $finfo = finfo_open(FILEINFO_MIME_TYPE);
- $mime = finfo_file($finfo, $file['tmp_name']);
- finfo_close($finfo);
-
- if (!in_array($mime, $allowedTypes)) {
- $message = "Alleen JPG, PNG, WebP of GIF afbeeldingen zijn toegestaan.";
+ $processed = fociz_process_avatar_upload($file, $userId);
+ if (!$processed['success']) {
+ $message = $processed['error'] ?? "Fout bij verwerken van de afbeelding.";
$messageType = 'error';
} else {
- $ext = match($mime) {
- 'image/jpeg' => 'jpg',
- 'image/png' => 'png',
- 'image/webp' => 'webp',
- 'image/gif' => 'gif',
- default => 'jpg'
- };
-
- $uploadDir = __DIR__ . '/assets/uploads/avatars';
- if (!is_dir($uploadDir)) {
- @mkdir($uploadDir, 0755, true);
- }
-
- $filename = "avatar_{$userId}_" . time() . "_" . bin2hex(random_bytes(4)) . ".{$ext}";
- $targetPath = $uploadDir . '/' . $filename;
-
- if (move_uploaded_file($file['tmp_name'], $targetPath)) {
- $webPath = "/assets/uploads/avatars/{$filename}";
- $stmtUp = $db->prepare("UPDATE users SET avatar_url = ? WHERE id = ?");
- $stmtUp->execute([$webPath, $userId]);
- $_SESSION['user_avatar_url'] = $webPath;
- $user['avatar_url'] = $webPath;
- $message = "Uw profielfoto is succesvol geüpload en ingesteld!";
- $messageType = 'success';
- } else {
- $message = "Er is een fout opgetreden bij het verplaatsen van het geüploade bestand.";
- $messageType = 'error';
- }
+ $avatarUri = $processed['data_uri'];
+ $stmtUp = $db->prepare("UPDATE users SET avatar_url = ? WHERE id = ?");
+ $stmtUp->execute([$avatarUri, $userId]);
+ $_SESSION['user_avatar_url'] = $avatarUri;
+ $user['avatar_url'] = $avatarUri;
+ $message = "Uw profielfoto is succesvol geüpload en permanent bewaard!";
+ $messageType = 'success';
}
}
}
@@ -277,6 +252,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmtUser = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmtUser->execute([$userId]);
$userProfile = $stmtUser->fetch();
+if ($userProfile) {
+ $userProfile['avatar_url'] = fociz_get_valid_avatar_url($userProfile['avatar_url'] ?? null, $userId);
+}
// Outlook status ophalen
$stmtOutlook = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
@@ -316,7 +294,10 @@ require_once __DIR__ . '/includes/header.php';
-  ?>)
+  ?>)
+
+ = strtoupper(substr($userProfile['name'], 0, 1)) ?>
+
= strtoupper(substr($userProfile['name'], 0, 1)) ?>
@@ -1071,16 +1052,17 @@ require_once __DIR__ . '/includes/header.php';
-
+
@@ -1151,6 +1133,22 @@ function previewUploadAvatar(input) {
nameEl.textContent = '📄 ' + file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
nameEl.classList.remove('hidden');
}
+ // Live thumbnail weergave
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ let prevImg = document.getElementById('uploadAvatarLivePreview');
+ if (!prevImg && nameEl && nameEl.parentNode) {
+ prevImg = document.createElement('img');
+ prevImg.id = 'uploadAvatarLivePreview';
+ prevImg.className = 'w-16 h-16 rounded-full mx-auto my-2 object-cover border-2 border-indigo-500 shadow-sm';
+ nameEl.parentNode.insertBefore(prevImg, nameEl);
+ }
+ if (prevImg) {
+ prevImg.src = e.target.result;
+ prevImg.classList.remove('hidden');
+ }
+ };
+ reader.readAsDataURL(file);
}
}
diff --git a/users.php b/users.php
index 3f00294..ce868b7 100644
--- a/users.php
+++ b/users.php
@@ -216,7 +216,10 @@ require_once __DIR__ . '/includes/header.php';
-  ?>)
+  ?>)
+
+ = strtoupper(substr($u['name'], 0, 1)) ?>
+
= strtoupper(substr($u['name'], 0, 1)) ?>
|