Initial commit

This commit is contained in:
Cedric Abonnel
2026-05-08 12:55:46 +02:00
commit 700329f156
46 changed files with 8495 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
<?php // includes/ConfigRepo.php
declare(strict_types=1);
function config_repo_get(): array {
$pdo = db();
$row = $pdo->query("SELECT * FROM app_config WHERE id=1")->fetch(PDO::FETCH_ASSOC);
if (!$row) { return [
'allow_password'=>true,'allow_oidc'=>false,'registrations_open'=>true,
'oidc_issuer'=>null,'oidc_name'=>null,'oidc_client_id'=>null,'oidc_client_secret'=>null,'oidc_redirect_uri'=>null
]; }
return $row;
}
function config_repo_save(array $in): void {
$pdo = db();
$sql = "INSERT INTO app_config
(id, allow_password, allow_oidc, registrations_open, oidc_issuer, oidc_name, oidc_client_id, oidc_client_secret, oidc_redirect_uri, updated_at)
VALUES (1,:pw,:oidc,:open,:iss,:name,:cid,:sec,:redir, now())
ON CONFLICT (id) DO UPDATE SET
allow_password=:pw, allow_oidc=:oidc, registrations_open=:open,
oidc_issuer=:iss, oidc_name=:name, oidc_client_id=:cid, oidc_client_secret=:sec, oidc_redirect_uri=:redir,
updated_at=now()";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':pw' => (bool)$in['allow_password'],
':oidc' => (bool)$in['allow_oidc'],
':open' => (bool)$in['registrations_open'],
':iss' => trim((string)($in['oidc_issuer'] ?? '')) ?: null,
':name' => trim((string)($in['oidc_name'] ?? '')) ?: null,
':cid' => trim((string)($in['oidc_client_id'] ?? '')) ?: null,
':sec' => trim((string)($in['oidc_client_secret'] ?? '')) ?: null,
':redir'=> trim((string)($in['oidc_redirect_uri'] ?? '')) ?: null,
]);
}
/**
* Met à jour le fichier .env en conservant les autres lignes.
* $pairs = ['KEY'=>'value', ...] ; value null => supprime la clé.
*/
function env_set_pairs(string $envPath, array $pairs): void {
if (!is_file($envPath)) { file_put_contents($envPath, ""); }
$lines = file($envPath, FILE_IGNORE_NEW_LINES);
$map = [];
foreach ($lines as $i => $line) {
if (preg_match('/^\s*#/', $line) || trim($line)==='') { $map[$i] = $line; continue; }
if (!str_contains($line, '=')) { $map[$i] = $line; continue; }
[$k,$v] = explode('=', $line, 2);
$k = trim($k);
if ($k==='') { $map[$i] = $line; continue; }
if (array_key_exists($k, $pairs)) {
if ($pairs[$k] === null) { $map[$i] = null; } // supprimé
else { $map[$i] = $k.'='.env_quote((string)$pairs[$k]); }
unset($pairs[$k]);
} else {
$map[$i] = $line;
}
}
// append keys restantes
foreach ($pairs as $k=>$v) {
if ($v === null) continue;
$map[] = $k.'='.env_quote((string)$v);
}
// re-écriture
$out = [];
foreach ($map as $line) { if ($line === null) continue; $out[] = $line; }
file_put_contents($envPath, implode(PHP_EOL, $out).PHP_EOL);
}
function env_quote(string $v): string {
if ($v === '' || preg_match('/\s|[#"\'=]/', $v)) {
// met entre guillemets et échappe
$v = str_replace(['\\','"'], ['\\\\','\\"'], $v);
return "\"$v\"";
}
return $v;
}
function ensure_admin(): void {
// adapte à ton système
if (empty($_SESSION['user']['is_admin'])) {
http_response_code(403);
exit('Forbidden');
}
}
+10
View File
@@ -0,0 +1,10 @@
<div class="container">
<footer class="py-3 my-4">
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
<li class="nav-item"><a href="https://alpinux.org/mentions-legales" class="nav-link px-2 text-body-secondary">Mentions légales</a></li>
<li class="nav-item"><a href="/index/a-propos" class="nav-link px-2 text-body-secondary">A propos</a></li>
</ul>
<p class="text-center text-body-secondary">Association 1901 - <a href="https://alpinux.org/">Alpinux, le LUG de Savoie</a></p>
</footer>
</div>
+49
View File
@@ -0,0 +1,49 @@
<div class="container">
<header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom">
<a href="/" class="d-flex align-items-center text-body-emphasis text-decoration-none">
<img width="32" src="/img/logo-mail.svg" class="bi me-2" >
<span class="fs-4">Mug ALPINUX</span>
</a>
<ul class="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0">
</ul>
<?php
// Créer une instance de MessageManager avec le fichier de base de données SQLite
$messageManager = new ace\MessageManager('database.db');
if ($messageManager->sessionAlready()) {
?>
<div class="dropdown text-end">
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo $messageManager->getUsername($_SESSION['user_id']); ?>
</a>
<ul class="dropdown-menu text-small">
<li><a class="dropdown-item" href="/user/parametres">Paramètres <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sliders" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z"/>
</svg></a></li>
<li><a class="dropdown-item" href="/user/profil">Profil <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-vcard" viewBox="0 0 16 16">
<path d="M5 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm4-2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5ZM9 8a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 9 8Zm1 2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5Z"/>
<path d="M2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2ZM1 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H8.96c.026-.163.04-.33.04-.5C9 10.567 7.21 9 5 9c-2.086 0-3.8 1.398-3.984 3.181A1.006 1.006 0 0 1 1 12V4Z"/>
</svg></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/user/disconnect">Déconnexion <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg></a></li>
</ul>
</div>
<?php
}
?>
</header>
</div>
+149
View File
@@ -0,0 +1,149 @@
<?php
// projet : mug.a5l.fr
// fichier : includes/mailer.php
// version : 20251011
declare(strict_types=1);
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require_once dirname(__DIR__) . '/vendor/autoload.php';
if (!function_exists('env')) {
function env(string $key, ?string $default = null): ?string {
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') return (string)$_ENV[$key];
$v = getenv($key);
if ($v !== false && $v !== '') return (string)$v;
return $default;
}
}
if (!function_exists('db')) {
function db(): \PDO { return \App\Infrastructure\Database::get(); }
}
/**
* Anti-abus simple : 1 envoi / 5 min et 5 en 12 h par destinataire (status in ('sent','queued')).
*/
function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): array {
// bypass complet si désactivé
$enabled = (int) (env('SMTP_RATE_LIMIT_ENABLE', '1'));
if ($enabled === 0) return [true, ''];
$pdo = db();
// Cooldown (actif seulement si >0)
if ($coolMin > 0) {
$q1 = "SELECT 1 FROM journal_smtp
WHERE to_email = :e AND created_at >= NOW() - INTERVAL :cool
AND status IN ('sent','queued')
LIMIT 1";
$stmt = $pdo->prepare($q1);
$stmt->execute([':e'=>$email, ':cool'=>sprintf('%d minutes', $coolMin)]);
if ($stmt->fetchColumn()) return [false, "Un email vient d’être envoyé. Réessayez dans {$coolMin} min."];
}
// Plafond 12h (actif seulement si >0)
if ($maxPer12h > 0) {
$q2 = "SELECT COUNT(*) FROM journal_smtp
WHERE to_email = :e AND created_at >= NOW() - INTERVAL '12 hours'
AND status IN ('sent','queued')";
$stmt = $pdo->prepare($q2);
$stmt->execute([':e'=>$email]);
if ((int)$stmt->fetchColumn() >= $maxPer12h) return [false, 'Quota atteint. Réessayez plus tard.'];
}
return [true, ''];
}
/**
* Envoi immédiat SMTP avec PHPMailer + journalisation.
* @param string $to destinataire
* @param string $subject objet
* @param string $html corps HTML
* @param string|null $text corps texte brut (optionnel, auto-généré si null)
* @param array $opts ['reply_to'=>['email','name']]
*/
function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $text = null, array $opts = []): bool {
[$ok, $msg] = mailer_can_send($to, (int)env('SMTP_COOLDOWN_MINUTES', '5'), (int)env('SMTP_MAX_PER_12H', '5'));
if (!$ok) throw new RuntimeException($msg);
$pdo = db();
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("INSERT INTO journal_smtp
(created_at, script_path, to_email, subject, content_html, content_text, status, ip, user_agent)
VALUES (NOW(), :script, :to, :subj, :html, :text, 'queued', :ip, :ua)
RETURNING id");
$stmt->execute([
':script' => ($_SERVER['SCRIPT_NAME'] ?? ''),
':to' => $to,
':subj' => $subject,
':html' => $html,
':text' => $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES)),
':ip' => ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''),
':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512),
]);
$rowId = (int)$stmt->fetchColumn();
$pdo->commit();
} catch (\Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
throw $e;
}
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = (string)env('SMTP_HOST', 'localhost');
$mail->Port = (int)env('SMTP_PORT', '587');
$mail->SMTPAuth = (env('SMTP_USER') || env('SMTP_PASS')) ? true : false;
$mail->Username = (string)env('SMTP_USER', '');
$mail->Password = (string)env('SMTP_PASS', '');
$secure = strtolower((string)env('SMTP_SECURE', 'tls'));
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->SMTPKeepAlive = true; // réutilise la connexion
$mail->Timeout = 30; // évite les blocages longs
$mail->SMTPOptions = ['ssl'=>['verify_peer'=>true,'verify_peer_name'=>true,'allow_self_signed'=>false]];
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
// Expéditeur
$from = (string)env('SMTP_FROM', 'no-reply@mug.a5l.fr');
$fromName = (string)env('SMTP_FROM_NAME', 'MUG');
$mail->setFrom($from, $fromName);
// Reply-To
if (!empty($opts['reply_to']) && is_array($opts['reply_to']) && filter_var($opts['reply_to'][0] ?? '', FILTER_VALIDATE_EMAIL)) {
$mail->addReplyTo($opts['reply_to'][0], $opts['reply_to'][1] ?? '');
} elseif ($rt = env('SMTP_REPLY_TO')) {
$mail->addReplyTo($rt, (string)env('SMTP_REPLY_TO_NAME', 'Support'));
}
// DKIM optionnel
if ($d = env('DKIM_DOMAIN')) {
$mail->DKIM_domain = $d;
$mail->DKIM_selector = (string)env('DKIM_SELECTOR', 'default');
$mail->DKIM_private = (string)env('DKIM_PRIVATE_KEY_PATH', '');
$mail->DKIM_passphrase = (string)env('DKIM_PASSPHRASE', '');
$mail->DKIM_identity = $from;
}
$mail->addAddress($to);
$mail->Subject = $subject;
$mail->Body = $html;
$mail->AltBody = $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES));
$mail->send();
$pdo->prepare("UPDATE journal_smtp SET status='sent', sent_at=NOW() WHERE id=:id")->execute([':id'=>$rowId]);
return true;
} catch (Exception $e) {
$pdo->prepare("UPDATE journal_smtp SET status='error', error_message=:err, sent_at=NOW() WHERE id=:id")
->execute([':id'=>$rowId, ':err'=>substr($e->getMessage(),0,1000)]);
throw new RuntimeException('Envoi email impossible: '.$e->getMessage());
}
}