mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Remove required from all admin add/edit form inputs
- Skip required-field validation for orientation/ap/finality/licence/jury in admin add+edit
This commit is contained in:
21
TODO.md
21
TODO.md
@@ -50,3 +50,24 @@
|
|||||||
- [x] `search.php`: show cover thumbnail on result cards
|
- [x] `search.php`: show cover thumbnail on result cards
|
||||||
- [x] `student-preview.php`: use `$coverMap` instead of `banner_path`
|
- [x] `student-preview.php`: use `$coverMap` instead of `banner_path`
|
||||||
- [x] Migration applied and file moved to `applied/`
|
- [x] Migration applied and file moved to `applied/`
|
||||||
|
|
||||||
|
- [x] Remove `required` from all form inputs in admin add/edit
|
||||||
|
- [x] Introduced `$adminMode` flag in `form.php` (true when `$mode` is `'add'` or `'edit'`)
|
||||||
|
- [x] Hidden "champs obligatoires" note in admin mode
|
||||||
|
- [x] All `$required = true` callers in `form.php`, `fieldset-tfe-info.php`, `fieldset-academic.php`, `fieldset-licence-explanation.php`, `fieldset-files.php` changed to `!$adminMode`
|
||||||
|
- [x] Hardcoded `required` HTML attributes in `fieldset-tfe-info.php` (synopsis, objet radios), `fieldset-licence-explanation.php` (access type radios), `jury-fieldset.php` (promoteur, lecteurs interne/externe) gated on `!$adminMode`
|
||||||
|
- [x] Dynamic JS `ulbInput.required` in jury fieldset also gated
|
||||||
|
- [x] Remove server-side validation for orientation, ap, finality, licence, jury roles in `ThesisEditController::save()` — admins can save partial records
|
||||||
|
- [x] Same for `ThesisCreateController::submit()`: added `$adminMode` param, pass `true` from `admin/actions/formulaire.php`
|
||||||
|
|
||||||
|
- [x] Encrypt SMTP password at rest (AES-256-GCM)
|
||||||
|
- [x] `app/.env` — holds `APP_KEY` (base64, 32 bytes); added to `.gitignore`
|
||||||
|
- [x] `src/Crypto.php` — `encrypt()` / `decrypt()` / `isEncrypted()` via OpenSSL AES-256-GCM
|
||||||
|
- [x] `SmtpRelay::getSettings()` — decrypts password after DB fetch
|
||||||
|
- [x] `SmtpRelay::updateSettings()` — encrypts password before DB write
|
||||||
|
- [x] `parametres.php` template — password field no longer pre-filled (ciphertext never sent to browser)
|
||||||
|
- [x] Migration `018_encrypt_smtp_password.php` — encrypted existing plaintext in DB; moved to applied/
|
||||||
|
- [x] `justfile` — `deploy` calls `deploy-env` (uploads `.env` only if remote doesn't exist yet)
|
||||||
|
- [x] `justfile` — `deploy-env` recipe: safe upload with guards
|
||||||
|
- [x] `justfile` — `reencrypt-password` recipe: rotates APP_KEY on remote DB
|
||||||
|
- [x] `scripts/reencrypt-smtp-password.php` — decrypts with old key, re-encrypts with new key, updates `.env`
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ $authorName = $_POST['auteurice'] ?? 'unknown';
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$ctrl = ThesisCreateController::make();
|
$ctrl = ThesisCreateController::make();
|
||||||
$thesisId = $ctrl->submit($_POST, $_FILES);
|
$thesisId = $ctrl->submit($_POST, $_FILES, true);
|
||||||
|
|
||||||
$identifier = $ctrl->getIdentifier($thesisId);
|
$identifier = $ctrl->getIdentifier($thesisId);
|
||||||
$logger->logSubmission('admin', $thesisId, $identifier, $authorName);
|
$logger->logSubmission('admin', $thesisId, $identifier, $authorName);
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ class ThesisCreateController
|
|||||||
* @return int The newly created thesis ID.
|
* @return int The newly created thesis ID.
|
||||||
* @throws Exception On validation or DB error.
|
* @throws Exception On validation or DB error.
|
||||||
*/
|
*/
|
||||||
public function submit(array $post, array $files): int
|
public function submit(array $post, array $files, bool $adminMode = false): int
|
||||||
{
|
{
|
||||||
// ── 1. Validate + sanitise ────────────────────────────────────────────
|
// ── 1. Validate + sanitise ────────────────────────────────────────────
|
||||||
$data = $this->validateAndSanitise($post);
|
$data = $this->validateAndSanitise($post, $adminMode);
|
||||||
|
|
||||||
// ── 1b. Duplicate detection ───────────────────────────────────────────
|
// ── 1b. Duplicate detection ───────────────────────────────────────────
|
||||||
require_once APP_ROOT . '/src/DuplicateThesisException.php';
|
require_once APP_ROOT . '/src/DuplicateThesisException.php';
|
||||||
@@ -293,7 +293,7 @@ class ThesisCreateController
|
|||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
* @throws Exception on validation failure.
|
* @throws Exception on validation failure.
|
||||||
*/
|
*/
|
||||||
private function validateAndSanitise(array $post): array
|
private function validateAndSanitise(array $post, bool $adminMode = false): array
|
||||||
{
|
{
|
||||||
// Split authors by comma, trim, filter empty, sort alphabetically.
|
// Split authors by comma, trim, filter empty, sort alphabetically.
|
||||||
$authorRaw = $this->sanitiseString($post['auteurice'] ?? '');
|
$authorRaw = $this->sanitiseString($post['auteurice'] ?? '');
|
||||||
@@ -321,18 +321,18 @@ class ThesisCreateController
|
|||||||
throw new Exception('Année invalide. Veuillez entrer une année valide.');
|
throw new Exception('Année invalide. Veuillez entrer une année valide.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$orientationId = filter_var($post['orientation'] ?? '', FILTER_VALIDATE_INT);
|
$orientationId = filter_var($post['orientation'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||||
if ($orientationId === false) {
|
if (!$adminMode && !$orientationId) {
|
||||||
throw new Exception('Veuillez sélectionner une orientation.');
|
throw new Exception('Veuillez sélectionner une orientation.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$apProgramId = filter_var($post['ap'] ?? '', FILTER_VALIDATE_INT);
|
$apProgramId = filter_var($post['ap'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||||
if ($apProgramId === false) {
|
if (!$adminMode && !$apProgramId) {
|
||||||
throw new Exception('Veuillez sélectionner un Atelier Pratique.');
|
throw new Exception('Veuillez sélectionner un Atelier Pratique.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$finalityId = filter_var($post['finality'] ?? '', FILTER_VALIDATE_INT);
|
$finalityId = filter_var($post['finality'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||||
if ($finalityId === false) {
|
if (!$adminMode && !$finalityId) {
|
||||||
throw new Exception('Veuillez sélectionner une finalité.');
|
throw new Exception('Veuillez sélectionner une finalité.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,13 +412,13 @@ class ThesisCreateController
|
|||||||
$juryMembers[] = ['name' => trim($post['jury_president']), 'role' => 'president', 'is_external' => 0];
|
$juryMembers[] = ['name' => trim($post['jury_president']), 'role' => 'president', 'is_external' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$hasPromoteur) {
|
if (!$adminMode && !$hasPromoteur) {
|
||||||
throw new Exception('Veuillez indiquer au moins un·e promoteur·ice interne.');
|
throw new Exception('Veuillez indiquer au moins un·e promoteur·ice interne.');
|
||||||
}
|
}
|
||||||
if (!$hasLecteurInt) {
|
if (!$adminMode && !$hasLecteurInt) {
|
||||||
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice interne.');
|
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice interne.');
|
||||||
}
|
}
|
||||||
if (!$hasLecteurExt) {
|
if (!$adminMode && !$hasLecteurExt) {
|
||||||
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice externe.');
|
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice externe.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +441,7 @@ class ThesisCreateController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (empty($languageIds)) {
|
if (!$adminMode && empty($languageIds)) {
|
||||||
throw new Exception('Veuillez sélectionner au moins une langue.');
|
throw new Exception('Veuillez sélectionner au moins une langue.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,13 +449,13 @@ class ThesisCreateController
|
|||||||
$formatIds = isset($post['formats']) && is_array($post['formats'])
|
$formatIds = isset($post['formats']) && is_array($post['formats'])
|
||||||
? array_map('intval', $post['formats'])
|
? array_map('intval', $post['formats'])
|
||||||
: [];
|
: [];
|
||||||
if (empty($formatIds)) {
|
if (!$adminMode && empty($formatIds)) {
|
||||||
throw new Exception('Veuillez sélectionner au moins un format.');
|
throw new Exception('Veuillez sélectionner au moins un format.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||||
$licenseCustom = trim($post['license_custom'] ?? '');
|
$licenseCustom = trim($post['license_custom'] ?? '');
|
||||||
if (!$licenseId && $licenseCustom === '') {
|
if (!$adminMode && !$licenseId && $licenseCustom === '') {
|
||||||
throw new Exception('Veuillez sélectionner une licence ou en préciser une.');
|
throw new Exception('Veuillez sélectionner une licence ou en préciser une.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,62 +181,8 @@ class ThesisEditController
|
|||||||
$errors[] = "L'année est invalide.";
|
$errors[] = "L'année est invalide.";
|
||||||
}
|
}
|
||||||
$orientationId = intval($post['orientation'] ?? 0);
|
$orientationId = intval($post['orientation'] ?? 0);
|
||||||
if ($orientationId <= 0) {
|
|
||||||
$errors[] = "L'orientation est requise.";
|
|
||||||
}
|
|
||||||
$apProgramId = intval($post['ap'] ?? 0);
|
$apProgramId = intval($post['ap'] ?? 0);
|
||||||
if ($apProgramId <= 0) {
|
|
||||||
$errors[] = "L'atelier pluridisciplinaire est requis.";
|
|
||||||
}
|
|
||||||
$finalityId = intval($post['finality'] ?? 0);
|
$finalityId = intval($post['finality'] ?? 0);
|
||||||
if ($finalityId <= 0) {
|
|
||||||
$errors[] = 'La finalité est requise.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Languages
|
|
||||||
$langIds = isset($post['languages']) && is_array($post['languages']) ? $post['languages'] : [];
|
|
||||||
if (empty($langIds)) {
|
|
||||||
$errors[] = 'Au moins une langue est requise.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formats
|
|
||||||
$fmtIds = isset($post['formats']) && is_array($post['formats']) ? $post['formats'] : [];
|
|
||||||
if (empty($fmtIds)) {
|
|
||||||
$errors[] = 'Au moins un format est requis.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Licence
|
|
||||||
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
|
||||||
$licenseCustom = trim($post['license_custom'] ?? '');
|
|
||||||
if (!$licenseId && $licenseCustom === '') {
|
|
||||||
$errors[] = 'Une licence est requise.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jury
|
|
||||||
$hasPromoteur = !empty(trim($post['jury_promoteur'] ?? ''));
|
|
||||||
$hasLecteurInt = false;
|
|
||||||
$hasLecteurExt = false;
|
|
||||||
foreach ($post['jury_lecteur_interne'] ?? [] as $n) {
|
|
||||||
if (trim((string)$n) !== '') {
|
|
||||||
$hasLecteurInt = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($post['jury_lecteur_externe'] ?? [] as $n) {
|
|
||||||
if (trim((string)$n) !== '') {
|
|
||||||
$hasLecteurExt = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$hasPromoteur) {
|
|
||||||
$errors[] = 'Un·e promoteur·ice interne est requis.';
|
|
||||||
}
|
|
||||||
if (!$hasLecteurInt) {
|
|
||||||
$errors[] = 'Au moins un·e lecteur·ice interne est requis.';
|
|
||||||
}
|
|
||||||
if (!$hasLecteurExt) {
|
|
||||||
$errors[] = 'Au moins un·e lecteur·ice externe est requis.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
throw new RuntimeException(implode(' ', $errors));
|
throw new RuntimeException(implode(' ', $errors));
|
||||||
|
|||||||
@@ -14,12 +14,13 @@
|
|||||||
$oldFn = $oldFn ?? (function_exists('old') ? 'old' : fn($k, $d = '') => $d);
|
$oldFn = $oldFn ?? (function_exists('old') ? 'old' : fn($k, $d = '') => $d);
|
||||||
$withAutofocusFn = $withAutofocusFn ?? fn($field, $attrs = []) => $attrs;
|
$withAutofocusFn = $withAutofocusFn ?? fn($field, $attrs = []) => $attrs;
|
||||||
$formData = $formData ?? [];
|
$formData = $formData ?? [];
|
||||||
|
$adminMode = $adminMode ?? false;
|
||||||
?>
|
?>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Cadre académique</legend>
|
<legend>Cadre académique</legend>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$name = 'année'; $label = 'Année :'; $value = $oldFn('année'); $required = true;
|
$name = 'année'; $label = 'Année :'; $value = $oldFn('année'); $required = !$adminMode;
|
||||||
$type = 'number';
|
$type = 'number';
|
||||||
$placeholder = date('Y');
|
$placeholder = date('Y');
|
||||||
$attrs = $withAutofocusFn('année', ['min' => 2000, 'max' => date('Y') + 1]);
|
$attrs = $withAutofocusFn('année', ['min' => 2000, 'max' => date('Y') + 1]);
|
||||||
@@ -28,19 +29,19 @@ $formData = $formData ?? [];
|
|||||||
|
|
||||||
<?php
|
<?php
|
||||||
$name = 'orientation'; $label = 'Orientation :'; $options = $orientations;
|
$name = 'orientation'; $label = 'Orientation :'; $options = $orientations;
|
||||||
$selected = $formData['orientation'] ?? ''; $required = true; $placeholder = '';
|
$selected = $formData['orientation'] ?? ''; $required = !$adminMode; $placeholder = '';
|
||||||
$attrs = $withAutofocusFn('orientation');
|
$attrs = $withAutofocusFn('orientation');
|
||||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||||
?>
|
?>
|
||||||
<?php
|
<?php
|
||||||
$name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms;
|
$name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms;
|
||||||
$selected = $formData['ap'] ?? ''; $required = true; $placeholder = '';
|
$selected = $formData['ap'] ?? ''; $required = !$adminMode; $placeholder = '';
|
||||||
$attrs = $withAutofocusFn('ap');
|
$attrs = $withAutofocusFn('ap');
|
||||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||||
?>
|
?>
|
||||||
<?php
|
<?php
|
||||||
$name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes;
|
$name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes;
|
||||||
$selected = $formData['finality'] ?? ''; $required = true; $placeholder = '';
|
$selected = $formData['finality'] ?? ''; $required = !$adminMode; $placeholder = '';
|
||||||
$attrs = $withAutofocusFn('finality');
|
$attrs = $withAutofocusFn('finality');
|
||||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
* 3. TFE (obligatoire)
|
* 3. TFE (obligatoire)
|
||||||
* 4. Annexes éventuelles (optionnel)
|
* 4. Annexes éventuelles (optionnel)
|
||||||
*
|
*
|
||||||
* Variables consumed: none beyond APP_ROOT (always available).
|
* Variables consumed:
|
||||||
|
* bool $adminMode — when true, no field is required (admin add/edit mode).
|
||||||
*/
|
*/
|
||||||
|
$adminMode = $adminMode ?? false;
|
||||||
?>
|
?>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Fichiers</legend>
|
<legend>Fichiers</legend>
|
||||||
@@ -27,7 +29,7 @@
|
|||||||
$label = 'Note d\'intention :';
|
$label = 'Note d\'intention :';
|
||||||
$accept ='.pdf';
|
$accept ='.pdf';
|
||||||
$hint = 'Format PDF uniquement.';
|
$hint = 'Format PDF uniquement.';
|
||||||
$required = true;
|
$required = !$adminMode;
|
||||||
include APP_ROOT . '/templates/partials/form/file-field.php';
|
include APP_ROOT . '/templates/partials/form/file-field.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ $interneEnabled = $interneEnabled ?? true;
|
|||||||
$interditEnabled = $interditEnabled ?? true;
|
$interditEnabled = $interditEnabled ?? true;
|
||||||
$generalitiesHtml = $generalitiesHtml ?? '';
|
$generalitiesHtml = $generalitiesHtml ?? '';
|
||||||
$defaultAccessTypeId = $defaultAccessTypeId ?? 2;
|
$defaultAccessTypeId = $defaultAccessTypeId ?? 2;
|
||||||
|
$adminMode = $adminMode ?? false;
|
||||||
?>
|
?>
|
||||||
<fieldset class="licence-explanation">
|
<fieldset class="licence-explanation">
|
||||||
<legend>Degrés d'ouverture et licences</legend>
|
<legend>Degrés d'ouverture et licences</legend>
|
||||||
@@ -55,7 +56,7 @@ $defaultAccessTypeId = $defaultAccessTypeId ?? 2;
|
|||||||
<div class="licence-degree">
|
<div class="licence-degree">
|
||||||
<label class="admin-checkbox-label">
|
<label class="admin-checkbox-label">
|
||||||
<input type="radio" name="access_type_id" value="1"
|
<input type="radio" name="access_type_id" value="1"
|
||||||
<?= $selectedAccess === '1' ? 'checked' : '' ?> required>
|
<?= $selectedAccess === '1' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
|
||||||
<strong>🔓 Libre</strong> — Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg.
|
<strong>🔓 Libre</strong> — Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg.
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +66,7 @@ $defaultAccessTypeId = $defaultAccessTypeId ?? 2;
|
|||||||
<div class="licence-degree">
|
<div class="licence-degree">
|
||||||
<label class="admin-checkbox-label">
|
<label class="admin-checkbox-label">
|
||||||
<input type="radio" name="access_type_id" value="2"
|
<input type="radio" name="access_type_id" value="2"
|
||||||
<?= $selectedAccess === '2' ? 'checked' : '' ?> required>
|
<?= $selectedAccess === '2' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
|
||||||
<strong>🔒 Interne</strong> — Mon TFE n'est accessible que sur place en physique. Une note descriptive est disponible sur le site.
|
<strong>🔒 Interne</strong> — Mon TFE n'est accessible que sur place en physique. Une note descriptive est disponible sur le site.
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +76,7 @@ $defaultAccessTypeId = $defaultAccessTypeId ?? 2;
|
|||||||
<div class="licence-degree">
|
<div class="licence-degree">
|
||||||
<label class="admin-checkbox-label">
|
<label class="admin-checkbox-label">
|
||||||
<input type="radio" name="access_type_id" value="3"
|
<input type="radio" name="access_type_id" value="3"
|
||||||
<?= $selectedAccess === '3' ? 'checked' : '' ?> required>
|
<?= $selectedAccess === '3' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
|
||||||
<strong>🚫 Interdit</strong> — Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.
|
<strong>🚫 Interdit</strong> — Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +89,7 @@ $defaultAccessTypeId = $defaultAccessTypeId ?? 2;
|
|||||||
<h3>Licence du TFE</h3>
|
<h3>Licence du TFE</h3>
|
||||||
<?php
|
<?php
|
||||||
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes;
|
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes;
|
||||||
$selected = $formData['license_id'] ?? ''; $placeholder = '— Sélectionner —'; $required = true;
|
$selected = $formData['license_id'] ?? ''; $placeholder = '— Sélectionner —'; $required = !$adminMode;
|
||||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ $withAutofocusFn = $withAutofocusFn ?? fn($field, $attrs = []) => $attrs;
|
|||||||
$allowedObjet = $allowedObjet ?? [];
|
$allowedObjet = $allowedObjet ?? [];
|
||||||
$formData = $formData ?? [];
|
$formData = $formData ?? [];
|
||||||
$synopsisExtra = $synopsisExtra ?? '';
|
$synopsisExtra = $synopsisExtra ?? '';
|
||||||
|
$adminMode = $adminMode ?? false;
|
||||||
?>
|
?>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Informations du TFE</legend>
|
<legend>Informations du TFE</legend>
|
||||||
@@ -30,7 +31,8 @@ $synopsisExtra = $synopsisExtra ?? '';
|
|||||||
<?php foreach ($allowedObjet as $objetVal): ?>
|
<?php foreach ($allowedObjet as $objetVal): ?>
|
||||||
<label class="admin-checkbox-label">
|
<label class="admin-checkbox-label">
|
||||||
<input type="radio" name="objet" value="<?= htmlspecialchars($objetVal) ?>"
|
<input type="radio" name="objet" value="<?= htmlspecialchars($objetVal) ?>"
|
||||||
<?= ($oldFn('objet') ?: $allowedObjet[0]) === $objetVal ? 'checked' : '' ?> required>
|
<?= ($oldFn('objet') ?: $allowedObjet[0]) === $objetVal ? 'checked' : '' ?>
|
||||||
|
<?= $adminMode ? '' : 'required' ?>>
|
||||||
<?= htmlspecialchars(ucfirst($objetVal)) ?>
|
<?= htmlspecialchars(ucfirst($objetVal)) ?>
|
||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -42,7 +44,7 @@ $synopsisExtra = $synopsisExtra ?? '';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$name = 'titre'; $label = 'Titre du TFE :'; $value = $oldFn('titre'); $required = true;
|
$name = 'titre'; $label = 'Titre du TFE :'; $value = $oldFn('titre'); $required = !$adminMode;
|
||||||
$attrs = $withAutofocusFn('titre');
|
$attrs = $withAutofocusFn('titre');
|
||||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||||
?>
|
?>
|
||||||
@@ -51,7 +53,7 @@ $synopsisExtra = $synopsisExtra ?? '';
|
|||||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||||
?>
|
?>
|
||||||
<?php
|
<?php
|
||||||
$name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = $oldFn('auteurice'); $required = true;
|
$name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = $oldFn('auteurice'); $required = !$adminMode;
|
||||||
$attrs = $withAutofocusFn('auteurice', ['autocomplete' => 'name']);
|
$attrs = $withAutofocusFn('auteurice', ['autocomplete' => 'name']);
|
||||||
$hint = 'Séparez les auteur·ices par des virgules.';
|
$hint = 'Séparez les auteur·ices par des virgules.';
|
||||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||||
@@ -64,9 +66,9 @@ $synopsisExtra = $synopsisExtra ?? '';
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="synopsis">Synopsis : <span class="asterisk">*</span></label>
|
<label for="synopsis">Synopsis :<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></label>
|
||||||
<?= $synopsisExtra ?>
|
<?= $synopsisExtra ?>
|
||||||
<textarea id="synopsis" name="synopsis" rows="7" required
|
<textarea id="synopsis" name="synopsis" rows="7" <?= $adminMode ? '' : 'required' ?>
|
||||||
<?= ($withAutofocusFn('synopsis')['autofocus'] ?? false) ? 'autofocus' : '' ?>><?= $oldFn('synopsis') ?></textarea>
|
<?= ($withAutofocusFn('synopsis')['autofocus'] ?? false) ? 'autofocus' : '' ?>><?= $oldFn('synopsis') ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -56,6 +56,8 @@
|
|||||||
|
|
||||||
// ── Defaults ──────────────────────────────────────────────────────────────────
|
// ── Defaults ──────────────────────────────────────────────────────────────────
|
||||||
$mode = $mode ?? 'add';
|
$mode = $mode ?? 'add';
|
||||||
|
// In admin add/edit, no field is required (admins can save partial records)
|
||||||
|
$adminMode = ($mode === 'add' || $mode === 'edit');
|
||||||
$formAction = $formAction ?? '';
|
$formAction = $formAction ?? '';
|
||||||
$hiddenFields = $hiddenFields ?? '';
|
$hiddenFields = $hiddenFields ?? '';
|
||||||
$formData = $formData ?? [];
|
$formData = $formData ?? [];
|
||||||
@@ -135,7 +137,9 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
|||||||
<form action="<?= $formAction ?>" method="post" enctype="multipart/form-data" class="admin-form" data-beforeunload-guard>
|
<form action="<?= $formAction ?>" method="post" enctype="multipart/form-data" class="admin-form" data-beforeunload-guard>
|
||||||
<?= $hiddenFields ?>
|
<?= $hiddenFields ?>
|
||||||
|
|
||||||
|
<?php if (!$adminMode): ?>
|
||||||
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
|
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
|
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
|
||||||
<?php
|
<?php
|
||||||
@@ -172,7 +176,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
|||||||
$label = "Langue(s) du TFE :";
|
$label = "Langue(s) du TFE :";
|
||||||
$options = $languages;
|
$options = $languages;
|
||||||
$checked = $formData["languages"] ?? [];
|
$checked = $formData["languages"] ?? [];
|
||||||
$required = true;
|
$required = !$adminMode;
|
||||||
$hxPost = $mode === 'partage' ? "/partage/language-autre-fragment" : "/admin/language-autre-fragment.php";
|
$hxPost = $mode === 'partage' ? "/partage/language-autre-fragment" : "/admin/language-autre-fragment.php";
|
||||||
$hxTarget = "#language-autre-row";
|
$hxTarget = "#language-autre-row";
|
||||||
$hxSwap = "outerHTML";
|
$hxSwap = "outerHTML";
|
||||||
@@ -191,7 +195,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
|||||||
id="language_autre"
|
id="language_autre"
|
||||||
name="language_autre"
|
name="language_autre"
|
||||||
value="<?= $_langAutreValue ?>"
|
value="<?= $_langAutreValue ?>"
|
||||||
<?= $_langAutreRequired ? 'required' : '' ?>>
|
<?= (!$adminMode && $_langAutreRequired) ? 'required' : '' ?>>
|
||||||
<small>Si votre TFE contient une langue absente de la liste, précisez-la ici.</small>
|
<small>Si votre TFE contient une langue absente de la liste, précisez-la ici.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,7 +238,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
|||||||
$label = "Format(s) du TFE :";
|
$label = "Format(s) du TFE :";
|
||||||
$options = $formatTypes;
|
$options = $formatTypes;
|
||||||
$checked = $formData["formats"] ?? [];
|
$checked = $formData["formats"] ?? [];
|
||||||
$required = true;
|
$required = !$adminMode;
|
||||||
$hxPost = "/partage/format-website-fragment";
|
$hxPost = "/partage/format-website-fragment";
|
||||||
$hxTarget = "#website-url-fieldset";
|
$hxTarget = "#website-url-fieldset";
|
||||||
include APP_ROOT . "/templates/partials/form/checkbox-list.php";
|
include APP_ROOT . "/templates/partials/form/checkbox-list.php";
|
||||||
@@ -560,7 +564,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
|||||||
$label = "Adresse e-mail :";
|
$label = "Adresse e-mail :";
|
||||||
$value = $oldFn("confirmation_email");
|
$value = $oldFn("confirmation_email");
|
||||||
$type = "email";
|
$type = "email";
|
||||||
$required = true;
|
$required = !$adminMode;
|
||||||
$placeholder = "ton.email@exemple.be";
|
$placeholder = "ton.email@exemple.be";
|
||||||
$hint =
|
$hint =
|
||||||
"Nécessaire pour recevoir le récapitulatif de ta soumission.";
|
"Nécessaire pour recevoir le récapitulatif de ta soumission.";
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ $juryPresident = $juryPresident ?? null;
|
|||||||
$showPresident = $showPresident ?? false;
|
$showPresident = $showPresident ?? false;
|
||||||
$showPromoteurUlb = $showPromoteurUlb ?? true;
|
$showPromoteurUlb = $showPromoteurUlb ?? true;
|
||||||
$promoteurUlbConditional = $promoteurUlbConditional ?? false;
|
$promoteurUlbConditional = $promoteurUlbConditional ?? false;
|
||||||
|
$adminMode = $adminMode ?? false;
|
||||||
|
|
||||||
// Add-mode repopulation from flash data
|
// Add-mode repopulation from flash data
|
||||||
$addMode = ($juryPromoteur === null && $juryPromoteurUlb === null && empty($lecteursInternes) && empty($lecteursExternes) && $juryPresident === null);
|
$addMode = ($juryPromoteur === null && $juryPromoteurUlb === null && empty($lecteursInternes) && empty($lecteursExternes) && $juryPresident === null);
|
||||||
@@ -45,9 +46,9 @@ if ($addMode && function_exists('old')) {
|
|||||||
|
|
||||||
<!-- Promoteur·ice interne -->
|
<!-- Promoteur·ice interne -->
|
||||||
<div>
|
<div>
|
||||||
<label for="jury_promoteur">Promoteur·ice interne : <span class="asterisk">*</span></label>
|
<label for="jury_promoteur">Promoteur·ice interne :<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></label>
|
||||||
<input type="text" id="jury_promoteur" name="jury_promoteur"
|
<input type="text" id="jury_promoteur" name="jury_promoteur"
|
||||||
value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom" required>
|
value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom" <?= $adminMode ? '' : 'required' ?>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($showPromoteurUlb): ?>
|
<?php if ($showPromoteurUlb): ?>
|
||||||
@@ -70,7 +71,7 @@ if ($addMode && function_exists('old')) {
|
|||||||
var isApprofondi = text.includes('approfondi');
|
var isApprofondi = text.includes('approfondi');
|
||||||
ulbRow.style.display = isApprofondi ? '' : 'none';
|
ulbRow.style.display = isApprofondi ? '' : 'none';
|
||||||
if (ulbInput) {
|
if (ulbInput) {
|
||||||
ulbInput.required = isApprofondi;
|
ulbInput.required = <?= $adminMode ? 'false' : 'isApprofondi' ?>;
|
||||||
ulbInput.disabled = !isApprofondi;
|
ulbInput.disabled = !isApprofondi;
|
||||||
if (!isApprofondi) ulbInput.value = '';
|
if (!isApprofondi) ulbInput.value = '';
|
||||||
}
|
}
|
||||||
@@ -86,11 +87,11 @@ if ($addMode && function_exists('old')) {
|
|||||||
|
|
||||||
<!-- Lecteur·ice(s) interne -->
|
<!-- Lecteur·ice(s) interne -->
|
||||||
<fieldset class="admin-jury-lecteurs">
|
<fieldset class="admin-jury-lecteurs">
|
||||||
<legend>Lecteur·ice(s) interne <span class="asterisk">*</span></legend>
|
<legend>Lecteur·ice(s) interne<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend>
|
||||||
<div id="jury-lecteurs-internes-list" class="admin-jury-list">
|
<div id="jury-lecteurs-internes-list" class="admin-jury-list">
|
||||||
<?php if (empty($lecteursInternes)): ?>
|
<?php if (empty($lecteursInternes)): ?>
|
||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_interne[]" placeholder="Nom" required
|
<input type="text" name="jury_lecteur_interne[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?>
|
||||||
aria-label="Lecteur·ice interne 1 — nom">
|
aria-label="Lecteur·ice interne 1 — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
@@ -100,7 +101,7 @@ if ($addMode && function_exists('old')) {
|
|||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_interne[]"
|
<input type="text" name="jury_lecteur_interne[]"
|
||||||
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
||||||
<?= $li === 0 ? 'required' : '' ?>
|
<?= (!$adminMode && $li === 0) ? 'required' : '' ?>
|
||||||
aria-label="Lecteur·ice interne <?= $li + 1 ?> — nom">
|
aria-label="Lecteur·ice interne <?= $li + 1 ?> — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
@@ -116,11 +117,11 @@ if ($addMode && function_exists('old')) {
|
|||||||
|
|
||||||
<!-- Lecteur·ice(s) externe -->
|
<!-- Lecteur·ice(s) externe -->
|
||||||
<fieldset class="admin-jury-lecteurs">
|
<fieldset class="admin-jury-lecteurs">
|
||||||
<legend>Lecteur·ice(s) externe <span class="asterisk">*</span></legend>
|
<legend>Lecteur·ice(s) externe<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend>
|
||||||
<div id="jury-lecteurs-externes-list" class="admin-jury-list">
|
<div id="jury-lecteurs-externes-list" class="admin-jury-list">
|
||||||
<?php if (empty($lecteursExternes)): ?>
|
<?php if (empty($lecteursExternes)): ?>
|
||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_externe[]" placeholder="Nom" required
|
<input type="text" name="jury_lecteur_externe[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?>
|
||||||
aria-label="Lecteur·ice externe 1 — nom">
|
aria-label="Lecteur·ice externe 1 — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
@@ -130,7 +131,7 @@ if ($addMode && function_exists('old')) {
|
|||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_externe[]"
|
<input type="text" name="jury_lecteur_externe[]"
|
||||||
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
||||||
<?= $li === 0 ? 'required' : '' ?>
|
<?= (!$adminMode && $li === 0) ? 'required' : '' ?>
|
||||||
aria-label="Lecteur·ice externe <?= $li + 1 ?> — nom">
|
aria-label="Lecteur·ice externe <?= $li + 1 ?> — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
|
|||||||
39
justfile
39
justfile
@@ -49,6 +49,7 @@ deploy:
|
|||||||
--exclude '.claude' \
|
--exclude '.claude' \
|
||||||
--exclude '.pi' \
|
--exclude '.pi' \
|
||||||
--exclude '.DS_Store' \
|
--exclude '.DS_Store' \
|
||||||
|
--exclude '.env' \
|
||||||
--exclude 'storage/xamxam.db' \
|
--exclude 'storage/xamxam.db' \
|
||||||
--exclude 'storage/theses' \
|
--exclude 'storage/theses' \
|
||||||
--exclude 'storage/covers' \
|
--exclude 'storage/covers' \
|
||||||
@@ -62,6 +63,44 @@ deploy:
|
|||||||
app/ xamxam:/var/www/xamxam/
|
app/ xamxam:/var/www/xamxam/
|
||||||
ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}"
|
ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}"
|
||||||
ssh xamxam "cd /var/www/xamxam && php migrations/run.php /var/www/xamxam/storage/xamxam.db"
|
ssh xamxam "cd /var/www/xamxam && php migrations/run.php /var/www/xamxam/storage/xamxam.db"
|
||||||
|
# Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys)
|
||||||
|
@just deploy-env
|
||||||
|
|
||||||
|
[group('deploy')]
|
||||||
|
deploy-env:
|
||||||
|
# Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY.
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [ ! -f app/.env ]; then
|
||||||
|
echo "WARNING: app/.env not found locally — skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then
|
||||||
|
echo "Remote .env already exists — skipping to avoid overwriting key."
|
||||||
|
echo "Run 'just reencrypt-password' if you rotated APP_KEY."
|
||||||
|
else
|
||||||
|
rsync -v --progress app/.env xamxam:/var/www/xamxam/.env
|
||||||
|
ssh xamxam "chmod 640 /var/www/xamxam/.env && chown www-data:xamxam /var/www/xamxam/.env"
|
||||||
|
echo ".env uploaded."
|
||||||
|
fi
|
||||||
|
|
||||||
|
[group('deploy')]
|
||||||
|
reencrypt-password new_key_b64="":
|
||||||
|
# Re-encrypt the SMTP password in the remote DB after rotating APP_KEY.
|
||||||
|
# Usage:
|
||||||
|
# 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));"
|
||||||
|
# 2. Run: just reencrypt-password <new_base64_key>
|
||||||
|
# 3. Update app/.env locally with the new key, then run: just deploy-env
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "{{new_key_b64}}" ]; then
|
||||||
|
echo "Usage: just reencrypt-password <new_base64_key>"
|
||||||
|
echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Run the re-encryption script on the server using the current key (from remote .env)
|
||||||
|
# and the supplied new key.
|
||||||
|
ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db"
|
||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
deploy-db:
|
deploy-db:
|
||||||
|
|||||||
101
scripts/reencrypt-smtp-password.php
Normal file
101
scripts/reencrypt-smtp-password.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Re-encrypt the SMTP password after rotating APP_KEY.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* php scripts/reencrypt-smtp-password.php <new_base64_key> [DB_PATH]
|
||||||
|
*
|
||||||
|
* Steps performed:
|
||||||
|
* 1. Read old APP_KEY from the .env file adjacent to this script's app root.
|
||||||
|
* 2. Decrypt the current password from smtp_settings using the old key.
|
||||||
|
* 3. Encrypt it with the new key.
|
||||||
|
* 4. Write the new ciphertext back to the DB.
|
||||||
|
* 5. Update app/.env with the new APP_KEY.
|
||||||
|
*
|
||||||
|
* After running this script, redeploy or manually update app/.env on every
|
||||||
|
* environment that shares the same database.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$newKeyB64 = $argv[1] ?? '';
|
||||||
|
$dbPath = $argv[2] ?? null;
|
||||||
|
|
||||||
|
if ($newKeyB64 === '') {
|
||||||
|
fwrite(STDERR, "Usage: php reencrypt-smtp-password.php <new_base64_key> [DB_PATH]\n");
|
||||||
|
fwrite(STDERR, "Generate a key: php -r \"echo base64_encode(random_bytes(32));\"\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newKeyRaw = base64_decode($newKeyB64, strict: true);
|
||||||
|
if ($newKeyRaw === false || strlen($newKeyRaw) !== 32) {
|
||||||
|
fwrite(STDERR, "ERROR: new key must be a base64-encoded 32-byte value.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate app root (script lives in app/scripts/ or scripts/ next to app/)
|
||||||
|
$candidates = [
|
||||||
|
__DIR__ . '/../app', // repo root / scripts/
|
||||||
|
__DIR__ . '/..', // app / scripts/
|
||||||
|
];
|
||||||
|
$appRoot = null;
|
||||||
|
foreach ($candidates as $c) {
|
||||||
|
if (file_exists(realpath($c) . '/src/Crypto.php')) {
|
||||||
|
$appRoot = realpath($c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($appRoot === null) {
|
||||||
|
fwrite(STDERR, "ERROR: could not locate app root (looking for src/Crypto.php).\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
define('APP_ROOT', $appRoot);
|
||||||
|
require_once $appRoot . '/src/Crypto.php';
|
||||||
|
|
||||||
|
$dbPath = $dbPath ?? $appRoot . '/storage/xamxam.db';
|
||||||
|
if (!file_exists($dbPath)) {
|
||||||
|
fwrite(STDERR, "ERROR: database not found: $dbPath\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = new PDO('sqlite:' . $dbPath);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
$row = $pdo->query("SELECT password FROM smtp_settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$row) {
|
||||||
|
fwrite(STDERR, "ERROR: no smtp_settings row found.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt with the old key (read from existing .env via Crypto::decrypt)
|
||||||
|
$plaintext = Crypto::decrypt($row['password']);
|
||||||
|
if ($plaintext === '') {
|
||||||
|
echo "Password is empty — nothing to re-encrypt.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt with the new key directly (bypass the singleton so we can supply a different key)
|
||||||
|
$iv = random_bytes(12);
|
||||||
|
$tag = '';
|
||||||
|
$cipher = openssl_encrypt($plaintext, 'aes-256-gcm', $newKeyRaw, OPENSSL_RAW_DATA, $iv, $tag, '', 16);
|
||||||
|
if ($cipher === false) {
|
||||||
|
fwrite(STDERR, "ERROR: encryption failed: " . openssl_error_string() . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$newBlob = base64_encode($iv . $tag . $cipher);
|
||||||
|
|
||||||
|
// Write new ciphertext to DB
|
||||||
|
$pdo->prepare("UPDATE smtp_settings SET password = ? WHERE id = 1")->execute([$newBlob]);
|
||||||
|
echo "DB updated with new ciphertext.\n";
|
||||||
|
|
||||||
|
// Update .env
|
||||||
|
$envFile = $appRoot . '/.env';
|
||||||
|
$envContent = file_exists($envFile) ? file_get_contents($envFile) : '';
|
||||||
|
if (preg_match('/^APP_KEY=.*/m', $envContent)) {
|
||||||
|
$envContent = preg_replace('/^APP_KEY=.*/m', 'APP_KEY=' . $newKeyB64, $envContent);
|
||||||
|
} else {
|
||||||
|
$envContent .= "APP_KEY={$newKeyB64}\n";
|
||||||
|
}
|
||||||
|
file_put_contents($envFile, $envContent);
|
||||||
|
echo ".env updated with new APP_KEY.\n";
|
||||||
|
echo "Done. Redeploy app/.env to all environments: just deploy-env\n";
|
||||||
Reference in New Issue
Block a user