prepare intranet for first Dokploy deployment

This commit is contained in:
2026-08-26 12:59:51 +02:00
parent 5cfe688bdc
commit 29dfb85fef
1321 changed files with 227234 additions and 0 deletions
@@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/runtime_env.php';
$host = intranet_env('DB_HOST');
$port = intranet_env('DB_PORT', '5432');
$dbname = intranet_env('DB_NAME');
$username = intranet_env('DB_USER');
$password = intranet_env('DB_PASSWORD');
try {
$pdo = new PDO(
"pgsql:host={$host};port={$port};dbname={$dbname}",
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
error_log('Database connection failed: ' . $e->getMessage());
http_response_code(500);
exit('Database connection failed.');
}
return $pdo;
@@ -0,0 +1,124 @@
<?php /* Name: functions.php
Beschreibung: Generelle Funktionen
- Verwaltung von Schaltflächen
- Info-Panels (Boxen und Tabellen)
- Markdown-Formatierung
- Eigene Prüffunktionen */
// Schaltfläche: Ausgabe eines beliebigen Buttons
function display_button($p_key,$p_text,$align) {
global $p_buttons;
if (array_key_exists($p_key, $p_buttons)) {
$p_file = $p_buttons[$p_key];
echo "<div class='button_picto' style='justify-content:" . $align . ";'>";
echo file_get_contents($p_file);
if ($p_text) echo $p_text;
echo "</div>";
}
}
/** Infopanel mit einer zweispaltigen Tabelle
* @param string $title Überschrift des Panels
* @param array $data_array Strukturdaten mit Schlüssel und Wert
* @return string Die formatierte Zeichenkette.
*/
function infopanel_table($title, $data_array) {
// Ausgabe des Panels mit ZWEI Spalten
echo '<div class="info_panel_table">';
echo ' <table>';
echo ' <thead>';
echo ' <tr>';
echo ' <th colspan="2">';
echo ' <label>' . $title . '</label>';
echo ' </th>';
echo ' </tr>';
echo '</thead>';
echo ' <tbody id="tableBody">';
foreach ($data_array as $key => $value) {
echo ' <tr>';
echo ' <td>' . $key . '</td>';
echo ' <td>' . nvl($value, '---') . '</td>';
echo ' </tr>';
}
echo ' </tbody>';
echo ' </table>';
echo '</div>';
}
/** Markdown-Formatierung für Schnelleingaben
* @param string $text Die zu formatierende Zeichenkette.
* @return string Die formatierte Zeichenkette.
*/
function markdownFormat($text) {
$text = nl2br(rtrim($text, "\n")); // Umbrüche formatieren (<BR>)
$pattern = "/(\::::)( .+)/";
$replacement = "<li style='margin-left:40px; list-style: none;'>$2</li>"; // :::: {Einrücken 2. Ebene}
$text = preg_replace($pattern, $replacement, $text);
$pattern = "/(\::)( .+)/";
$replacement = "<li style='margin-left:20px; list-style: none;'>$2</li>"; // :: {Einrücken 1. Ebene}
$text = preg_replace($pattern, $replacement, $text);
$pattern = "/(\-\-)( .+)/";
$replacement = "<li style='margin-left:40px;'>$2</li>"; // -- {Listenelement 2. Ebene}
$text = preg_replace($pattern, $replacement, $text);
$pattern = "/(\-)( .+)/";
$replacement = "<li>$2</li>"; // - {Listenelement 1. Ebene}
$text = preg_replace($pattern, $replacement, $text);
$text = preg_replace("/\*\*(.*?)\*\*/s", "<b>$1</b>", $text); // **{Fett}**
$text = preg_replace("/\*(.*?)\*/s", "<i>$1</i>", $text); // *{Kursiv}*
$text = "<ul>" . $text . "</ul>"; // Listenumschluss
return $text;
}
/////////////////////////////////////////
// EIGENE PRÜFFUNKTIONEN //
/////////////////////////////////////////
/** Checken einer Zeichenkette auf das Vorhandensein.
* @param string|null $value Die zu überprüfende Zeichenkette.
* @param mixed $default Der Ersatzwert, der zurückgegeben wird, wenn die Zeichenkette leer ist.
* @return mixed Der ursprüngliche Wert oder der Ersatzwert.
*/
function nvl(string $value = null, $default) {
return isset($value) && $value !== '' ? $value : $default;
}
/** Gegenderte Anzeige eines Rollennamens (Datenbankwert → Anzeige).
* @param string $role Der Rollenname aus der Datenbank
* @return string Der gegenderte Anzeigename
*/
function genderRole(string $role): string {
static $map = [
'Admin' => 'Admin',
'Technik' => 'Technik',
'Entwickler' => 'Entwickler*in',
'Amtsgeschwister' => 'Amtsgeschwister',
'Diakon' => 'Diakon*in',
'DiakonIn' => 'Diakon*in',
'Priester' => 'Priester*in',
'PriesterIn' => 'Priester*in',
'Brandschutz' => 'Brandschutz',
'Brandschutzbeauftragter' => 'Brandschutz­beauftrage*r',
];
return $map[$role] ?? $role;
}
?>
@@ -0,0 +1,80 @@
<?php /* Name: parameters.php
Beschreibung: Generelle Parameter
- inkl. Pfade und Standardbuttons */
require_once __DIR__ . '/runtime_env.php';
$publicHost = $_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN');
$p_version = "v0.6.0"; // {Release}.{Update}.{Hotfix/Commit}
$p_nac_base = "bln-rd"; // Gemeinde
$p_project = "Intranet"; // Systemname des Projekts
$p_status = "#dev"; // Entwicklungsstatus: #dev #test #live
// Lokalisierung
date_default_timezone_set('Europe/Berlin'); //Mitteleuropäische Zeitzone
$g_locale = setlocale(LC_TIME, 'de_DE', 'de_DE.UTF-8'); //Deutsche UTF-8 Kodierung
// Zusammenfassungen von Informationen für Administratoren und Entwickler
$array_admin = [
'Projekt' => $p_project,
'Gemeinde' => $p_nac_base,
'Version' => $p_version,
'Status' => $p_status,
];
$array_dev = [
'Host' => $_SERVER['HTTP_HOST'],
'IPv4-Adresse' => $_SERVER['SERVER_ADDR'],
'PHP-Version' => phpversion(),
'Server-Software' => $_SERVER['SERVER_SOFTWARE'],
'Betriebssystem' => php_uname('s'),
'Zeitzone' => date_default_timezone_get(),
'Locale-Einstellung' => $g_locale,
'Aktivierte PHP-Erweiterungen' => implode(', ', get_loaded_extensions()),
'Maximale Upload-Dateigröße' => ini_get('upload_max_filesize'),
'Speicherlimit für PHP-Skripte' => ini_get('memory_limit'),
];
// Prefixe
$isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https');
$g_httpPrefix = ($isHttps ? 'https' : 'http') . '://';
// Absolute Pfade
$g_generalFilePath = $_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/";
$g_projectsPath = $_SERVER['DOCUMENT_ROOT']."/intranet_repo/projects/";
$g_httpCurrentUrl = $g_httpPrefix.$publicHost.$_SERVER['REQUEST_URI'];
$g_httpCurrentCleanUrl = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$g_httpCurrentUrl = $g_httpPrefix.$publicHost.$_SERVER['REQUEST_URI'];
$g_httpHomePath = $g_httpPrefix.$publicHost."/intranet_repo/projects/intranet/index.php";
$g_httpLoginPath = $g_httpPrefix.$publicHost."/intranet_repo/projects/authorization/login.php";
$g_httpLogoutPath = $g_httpPrefix.$publicHost."/intranet_repo/projects/authorization/logout.php";
$g_imagePath = $g_projectsPath."_central/img/";
$g_imageButtonPath = $g_projectsPath."_central/img/picto/";
$g_phpSnippetsPath = $g_projectsPath."_central/php/snippets/";
$g_profilePicturePath = $g_generalFilePath."profile_pictures/";
// Relative Pfade
$g_imageRelativePath = "/intranet_repo/projects/_central/img/";
$g_imageButtonRelativePath = "/intranet_repo/projects/_central/img/picto/";
$g_profilePictureRelativePath = "/intranet_repo/general_files/profile_pictures/";
// Buttons verwalten (SVG-Grafiken)
$p_buttons = array(
'add' => $g_imageButtonPath.'add.svg',
'arrow_left' => $g_imageButtonPath.'arrow_left.svg',
'arrow_right' => $g_imageButtonPath.'arrow_right.svg',
'arrow_up' => $g_imageButtonPath.'arrow_up.svg',
'cancel' => $g_imageButtonPath.'cancel.svg',
'edit' => $g_imageButtonPath.'edit.svg',
'calendar' => $g_imageButtonPath.'calendar.svg'
);
?>
@@ -0,0 +1,14 @@
<?php
if (!function_exists('intranet_env')) {
function intranet_env(string $name, ?string $default = null): string
{
$value = getenv($name);
if ($value === false || $value === '') {
if ($default !== null) {
return $default;
}
throw new RuntimeException("Missing required environment variable: {$name}");
}
return $value;
}
}
@@ -0,0 +1,323 @@
<?php
/* Name: send_mail.php
Beschreibung: Zentrale Mail-Funktion für das Intranet
Sendet direkt über SMTP (smtp.strato.de:587 mit STARTTLS)
HTML-Template für moderne Darstellung
*/
require_once __DIR__ . '/runtime_env.php';
define('SMTP_HOST', intranet_env('SMTP_HOST', 'smtp.strato.de'));
define('SMTP_PORT', (int) intranet_env('SMTP_PORT', '587'));
define('SMTP_USER', intranet_env('SMTP_USER'));
define('SMTP_PASS', intranet_env('SMTP_PASSWORD'));
define('ADMIN_EMAIL', intranet_env('ADMIN_EMAIL', 'technik@nak-reinickendorf.de'));
/**
* Erzeugt das HTML-Template für E-Mails im NAK-Design.
*/
function build_html_mail(string $betreff, string $inhalt, ?string $buttonText = null, ?string $buttonUrl = null): string {
$buttonHtml = '';
if ($buttonText && $buttonUrl) {
$buttonHtml = '
<tr><td style="padding:24px 0 8px 0;" align="center">
<a href="' . htmlspecialchars($buttonUrl) . '" style="display:inline-block;padding:14px 32px;background:#2786cf;color:#ffffff;text-decoration:none;font-weight:700;font-size:15px;letter-spacing:0.02em;">
' . htmlspecialchars($buttonText) . '
</a>
</td></tr>';
}
// Inhalt: Zeilenumbrüche in <br> umwandeln, aber vorhandenes HTML beibehalten
$inhaltHtml = nl2br(htmlspecialchars($inhalt));
$datum = date('d.m.Y, H:i') . ' Uhr';
// Logo-URL dynamisch ermitteln
$host = $_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN');
$logoUrl = 'https://' . $host . '/intranet_repo/projects/_central/img/nak-logo.svg';
return '<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
<body style="margin:0;padding:0;background-color:#f0f2f5;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,Helvetica,Arial,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0f2f5;padding:32px 16px;">
<tr><td align="center">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;overflow:hidden;">
<!-- Header with Logo + Wordmark -->
<tr><td style="background:#ffffff;padding:28px 32px;border-bottom:3px solid #2786cf;" align="left">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td style="vertical-align:bottom;padding-right:14px;">
<img src="' . htmlspecialchars($logoUrl) . '" alt="NAK" width="48" height="48" style="display:block;width:48px;height:48px;">
</td>
<td style="vertical-align:bottom;line-height:1.2;">
<div style="font-size:15px;font-weight:500;color:#1a1a1a;white-space:nowrap;">Neuapostolische Kirche</div>
<div style="font-size:12px;font-weight:300;color:#6b7280;white-space:nowrap;">Gemeinde Reinickendorf</div>
</td>
</tr></table>
</td></tr>
<!-- Blue title bar -->
<tr><td style="background:linear-gradient(135deg,#2786cf,#1e6fad);padding:20px 32px;">
<div style="font-size:20px;font-weight:700;color:#ffffff;letter-spacing:-0.01em;">' . htmlspecialchars($betreff) . '</div>
</td></tr>
<!-- Content -->
<tr><td style="background:#ffffff;padding:28px 32px 12px 32px;">
<div style="font-size:15px;line-height:1.7;color:#374151;">' . $inhaltHtml . '</div>
</td></tr>
' . $buttonHtml . '
<!-- Footer -->
<tr><td style="background:#ffffff;padding:20px 32px 24px 32px;border-top:1px solid #e5e7eb;">
<div style="font-size:12px;color:#9ca3af;text-align:center;line-height:1.5;">
Gesendet am ' . $datum . '<br>
Intranet NAK Reinickendorf &middot; Automatische Benachrichtigung
</div>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>';
}
/**
* Sendet eine HTML-E-Mail über SMTP.
*
* @param string $empfaenger Empfänger
* @param string $betreff Betreff
* @param string $nachricht Klartext-Nachricht (wird ins HTML-Template eingebettet)
* @param string|null $cc CC-Adresse
* @param string $absender Absender
* @param string|null $buttonText Optionaler Button-Text
* @param string|null $buttonUrl Optionale Button-URL
* @return bool
*/
function send_intranet_mail(
string $empfaenger,
string $betreff,
string $nachricht,
?string $cc = null,
string $absender = "intranet@nak-reinickendorf.de",
?string $buttonText = null,
?string $buttonUrl = null
): bool {
$htmlBody = build_html_mail($betreff, $nachricht, $buttonText, $buttonUrl);
// MIME-Header
$boundary = 'boundary_' . bin2hex(random_bytes(16));
$msgId = '<' . bin2hex(random_bytes(16)) . '@nak-reinickendorf.de>';
$headers = "From: Intranet NAK Reinickendorf <{$absender}>\r\n";
$headers .= "Reply-To: {$absender}\r\n";
$headers .= "To: {$empfaenger}\r\n";
if (!empty($cc)) {
$headers .= "Cc: {$cc}\r\n";
}
$headers .= "Subject: =?UTF-8?B?" . base64_encode($betreff) . "?=\r\n";
$headers .= "Message-ID: {$msgId}\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";
$headers .= "X-Mailer: Intranet-NAK-Reinickendorf/1.0\r\n";
$headers .= "Date: " . date('r') . "\r\n";
// Plaintext-Fallback
$plainBody = strip_tags($nachricht);
// Multipart Body
$body = "--{$boundary}\r\n";
$body .= "Content-Type: text/plain; charset=utf-8\r\n";
$body .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$body .= $plainBody . "\r\n\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Type: text/html; charset=utf-8\r\n";
$body .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$body .= $htmlBody . "\r\n\r\n";
$body .= "--{$boundary}--\r\n";
$message = $headers . "\r\n" . $body;
$recipients = [$empfaenger];
if (!empty($cc)) {
$recipients[] = $cc;
}
return smtp_send(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, $absender, $recipients, $message);
}
/**
* Direkter SMTP-Versand mit STARTTLS und AUTH LOGIN.
*/
function smtp_send(string $host, int $port, string $user, string $pass, string $from, array $to, string $data): bool {
$errno = 0;
$errstr = '';
$socket = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, 30);
if (!$socket) {
error_log("SMTP connect failed: {$errstr} ({$errno})");
return false;
}
stream_set_timeout($socket, 30);
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '220') { fclose($socket); return false; }
smtp_write($socket, "EHLO " . gethostname());
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
smtp_write($socket, "STARTTLS");
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '220') { fclose($socket); return false; }
$crypto = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT);
if (!$crypto) {
error_log("SMTP STARTTLS failed");
fclose($socket);
return false;
}
smtp_write($socket, "EHLO " . gethostname());
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
smtp_write($socket, "AUTH LOGIN");
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '334') { fclose($socket); return false; }
smtp_write($socket, base64_encode($user));
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '334') { fclose($socket); return false; }
smtp_write($socket, base64_encode($pass));
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '235') {
error_log("SMTP auth failed: {$response}");
fclose($socket);
return false;
}
smtp_write($socket, "MAIL FROM:<{$from}>");
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
foreach ($to as $recipient) {
smtp_write($socket, "RCPT TO:<{$recipient}>");
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
}
smtp_write($socket, "DATA");
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '354') { fclose($socket); return false; }
$data = str_replace("\r\n.", "\r\n..", $data);
fwrite($socket, $data . "\r\n.\r\n");
$response = smtp_read($socket);
if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
smtp_write($socket, "QUIT");
smtp_read($socket);
fclose($socket);
return true;
}
function smtp_write($socket, string $cmd): void {
fwrite($socket, $cmd . "\r\n");
}
function smtp_read($socket): string {
$response = '';
while ($line = fgets($socket, 512)) {
$response .= $line;
if (isset($line[3]) && $line[3] === ' ') break;
if (strlen($line) < 4) break;
}
return $response;
}
/**
* Ermittelt alle E-Mail-Adressen von Nutzern, die BEIDE angegebenen Rollen besitzen.
* Benötigt eine bestehende $pdo-Verbindung.
*
* @param PDO $pdo Datenbankverbindung
* @param array $roles Array von Rollennamen, die alle erfüllt sein müssen
* @return array Liste der E-Mail-Adressen
*/
function get_emails_by_roles(PDO $pdo, array $roles): array {
if (empty($roles)) return [];
$conditions = [];
foreach ($roles as $role) {
$conditions[] = '"' . str_replace('"', '""', $role) . '" = 1';
}
$where = implode(' AND ', $conditions);
$sql = "SELECT ugi.mail FROM user_permissions up
INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id
WHERE {$where} AND ugi.mail IS NOT NULL AND ugi.mail != ''";
$result = $pdo->query($sql);
$emails = [];
if ($result) {
while ($row = $result->fetch()) {
$emails[] = $row['mail'];
}
}
return $emails;
}
/**
* Sendet eine Mail an alle Nutzer mit den Rollen Admin UND Technik (Sammelmail).
*
* @param PDO $pdo Datenbankverbindung
* @param string $betreff Betreff
* @param string $nachricht Nachricht
* @param string|null $buttonText Optionaler Button-Text
* @param string|null $buttonUrl Optionale Button-URL
* @return bool True wenn mindestens eine Mail gesendet wurde
*/
function send_admin_team_mail(
PDO $pdo,
string $betreff,
string $nachricht,
?string $buttonText = null,
?string $buttonUrl = null
): bool {
$emails = get_emails_by_roles($pdo, ['Admin', 'Technik']);
if (empty($emails)) {
// Fallback auf ADMIN_EMAIL
return send_intranet_mail(ADMIN_EMAIL, $betreff, $nachricht, null, "intranet@nak-reinickendorf.de", $buttonText, $buttonUrl);
}
$success = false;
foreach ($emails as $email) {
if (send_intranet_mail($email, $betreff, $nachricht, null, "intranet@nak-reinickendorf.de", $buttonText, $buttonUrl)) {
$success = true;
}
}
return $success;
}
/**
* Sendet eine Mail an alle Nutzer mit der Rolle Brandschutzbeauftragter.
*
* @param PDO $pdo Datenbankverbindung
* @param string $betreff Betreff
* @param string $nachricht Nachricht
* @param string|null $buttonText Optionaler Button-Text
* @param string|null $buttonUrl Optionale Button-URL
* @return bool True wenn mindestens eine Mail gesendet wurde
*/
function send_brandschutzbeauftragter_mail(
PDO $pdo,
string $betreff,
string $nachricht,
?string $buttonText = null,
?string $buttonUrl = null
): bool {
$emails = get_emails_by_roles($pdo, ['Brandschutzbeauftragter']);
if (empty($emails)) return false;
$success = false;
foreach ($emails as $email) {
if (send_intranet_mail($email, $betreff, $nachricht, null, "intranet@nak-reinickendorf.de", $buttonText, $buttonUrl)) {
$success = true;
}
}
return $success;
}
?>