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';
- Avatar + Avatar +
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";
- +
@@ -206,6 +206,7 @@ $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}
`; 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';
- Avatar + Avatar +
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';
- Avatar + Avatar +
@@ -1071,16 +1052,17 @@ require_once __DIR__ . '/includes/header.php';
- + +

Minimaal 12 tekens (incl. hoofdletter, kleine letter, cijfer en speciaal teken).

- +
@@ -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';
- Avatar + Avatar +