feat(deploy): add deploy-verify-permissions recipe + upload/run deploy-server.sh before verification + run migrations in deploy

This commit is contained in:
Pontoporeia
2026-05-11 02:28:11 +02:00
parent 3136fa7113
commit 72f7192156
8 changed files with 5290 additions and 444 deletions

View File

@@ -6,6 +6,12 @@
- [x] WAL mode already active (`PRAGMA journal_mode``wal`) — set in Database constructor - [x] WAL mode already active (`PRAGMA journal_mode``wal`) — set in Database constructor
- [ ] Verify `-wal` and `-shm` sidecar files exist after writes - [ ] Verify `-wal` and `-shm` sidecar files exist after writes
- [ ] Verify nginx/PHP write access to sidecar files on server - [ ] Verify nginx/PHP write access to sidecar files on server
- [x] Add deploy-verify-permissions recipe that checks ownership, directory perms, file perms, and writability after rsync
- [x] deploy recipe now uploads and runs deploy-server.sh to fix permissions, then verifies them
- [x] deploy recipe now runs migrations (scripts/migrate.sh) after ensuring DB exists
- [x] fix migrate.sh to detect server vs local layout (no app/ subdir on server)
- [x] regenerate schema.sql from local DB via generate-schema.py (includes v_smtp_active, all 28 migrations)
- [x] fix generate-schema.py to include v_smtp_active (was explicitly excluded)
### Phase 2 — Audit Log ### Phase 2 — Audit Log
- [x] `admin_audit_log` table already exists (migration 009), `AdminLogger` already writes to it - [x] `admin_audit_log` table already exists (migration 009), `AdminLogger` already writes to it

View File

@@ -20,9 +20,12 @@ $logger = AdminLogger::make();
$isHxRequest = (isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true'); $isHxRequest = (isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true');
$section = $_POST['section'] ?? ''; $section = $_POST['section'] ?? '';
error_log('[settings.php] PROCESS | section=' . $section . ' | post_keys=' . implode(',', array_keys($_POST)));
if ($section === 'formulaire_restrictions') { if ($section === 'formulaire_restrictions') {
$newValues = ['restricted_files_enabled' => ($_POST['restricted_files_enabled'] ?? '0') === '1' ? '1' : '0']; // HTMX may not send unchecked checkboxes even with hidden 0-value inputs;
// missing key means unchecked → treat as '0'.
$newValues = ['restricted_files_enabled' => empty($_POST['restricted_files_enabled']) ? '0' : '1'];
$db->setSetting('restricted_files_enabled', $newValues['restricted_files_enabled']); $db->setSetting('restricted_files_enabled', $newValues['restricted_files_enabled']);
$logger->logFormSettingsUpdate($newValues); $logger->logFormSettingsUpdate($newValues);
if (!$isHxRequest) { if (!$isHxRequest) {
@@ -36,7 +39,7 @@ if ($section === 'formulaire_restrictions') {
]; ];
$newValues = []; $newValues = [];
foreach ($allowed as $key) { foreach ($allowed as $key) {
$value = ($_POST[$key] ?? '0') === '1' ? '1' : '0'; $value = empty($_POST[$key]) ? '0' : '1';
$db->setSetting($key, $value); $db->setSetting($key, $value);
$newValues[$key] = $value; $newValues[$key] = $value;
} }
@@ -46,8 +49,8 @@ if ($section === 'formulaire_restrictions') {
} }
} elseif ($section === 'objet_types') { } elseif ($section === 'objet_types') {
$newValues = [ $newValues = [
'objet_these_enabled' => ($_POST['objet_these_enabled'] ?? '0') === '1' ? '1' : '0', 'objet_these_enabled' => empty($_POST['objet_these_enabled']) ? '0' : '1',
'objet_frart_enabled' => ($_POST['objet_frart_enabled'] ?? '0') === '1' ? '1' : '0', 'objet_frart_enabled' => empty($_POST['objet_frart_enabled']) ? '0' : '1',
]; ];
$db->setSetting('objet_these_enabled', $newValues['objet_these_enabled']); $db->setSetting('objet_these_enabled', $newValues['objet_these_enabled']);
$db->setSetting('objet_frart_enabled', $newValues['objet_frart_enabled']); $db->setSetting('objet_frart_enabled', $newValues['objet_frart_enabled']);

View File

@@ -1,11 +1,63 @@
-- XAMXAM Thesis Database Schema
-- SQLite Database for managing final thesis projects (TFE) and doctoral theses
-- ============================================================================ -- ============================================================================
-- CORE ENTITIES -- XAMXAM Database Schema — complete, fully migrated
-- Generated from local database on 2026-05-11
-- All 28 migrations merged into this single file.
-- ============================================================================ -- ============================================================================
-- Students/Authors table CREATE TABLE IF NOT EXISTS orientations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS ap_programs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
code TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS finality_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS languages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at TEXT DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS format_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
sort_order INTEGER NOT NULL DEFAULT 99
);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at TEXT DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS access_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS license_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS authors ( CREATE TABLE IF NOT EXISTS authors (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
@@ -15,7 +67,6 @@ CREATE TABLE IF NOT EXISTS authors (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
-- Supervisors/Promoters table
CREATE TABLE IF NOT EXISTS supervisors ( CREATE TABLE IF NOT EXISTS supervisors (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
@@ -23,294 +74,110 @@ CREATE TABLE IF NOT EXISTS supervisors (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
-- ============================================================================
-- PREDEFINED REFERENCE DATA (lookup tables)
-- ============================================================================
-- Orientations (predefined list from specifications)
CREATE TABLE IF NOT EXISTS orientations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Insert predefined orientations
INSERT OR IGNORE INTO orientations (name) VALUES
('Arts Numériques'),
('Dessin'),
('Cinéma d''animation'),
('Installation-Performance'),
('Peinture'),
('Photographie'),
('Sculpture'),
('Vidéographie'),
('Graphisme'),
('Typographie'),
('Design Numérique'),
('Illustration'),
('Bande-Dessinée'),
('Sérigraphie'),
('Gravure');
-- AP (Ateliers Pratiques) - predefined list
CREATE TABLE IF NOT EXISTS ap_programs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Insert predefined AP programs
INSERT OR IGNORE INTO ap_programs (name, code) VALUES
('Narration Spéculative', 'NS'),
('Design et Politique du Multiple', 'DPM'),
('Atelier Pratiques Situées', 'APS'),
('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS'),
('Pratique de l''art - outils critiques, arts et contexte simultanés', 'PACS');
-- Master finality types
CREATE TABLE IF NOT EXISTS finality_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO finality_types (name) VALUES
('Approfondi'),
('Enseignement'),
('Spécialisé');
-- Languages
CREATE TABLE IF NOT EXISTS languages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at TEXT DEFAULT NULL
);
INSERT OR IGNORE INTO languages (name) VALUES
('français'),
('anglais'),
('néerlandais');
-- Format types (can select multiple)
CREATE TABLE IF NOT EXISTS format_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO format_types (name) VALUES
('Site web'),
('Audio'),
('Vidéo'),
('Performance'),
('Objet éditorial'),
('Installation'),
('Autre');
-- Tags (keywords — canonical M2M table; formerly 'keywords'/'keyword' column)
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at TEXT DEFAULT NULL
);
CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
-- Access authorization types
CREATE TABLE IF NOT EXISTS access_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO access_types (name, description) VALUES
('Libre', 'TFE en libre accès à tout le monde sur la plateforme et en bibliothèque'),
('Interne', 'TFE accessible uniquement sur place en physique. Une note descriptive est disponible sur le site'),
('Interdit', 'TFE non disponible en physique ni sur le site. Une note descriptive est disponible sur le site');
-- License types
CREATE TABLE IF NOT EXISTS license_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO license_types (name) VALUES
('CC BY 4.0'),
('CC BY-SA 4.0'),
('CC BY-ND 4.0'),
('CC BY-NC 4.0'),
('CC BY-NC-SA 4.0'),
('CC BY-NC-ND 4.0'),
('Tous droits réservés'),
('Domaine public');
-- ============================================================================
-- MAIN THESIS TABLE
-- ============================================================================
CREATE TABLE IF NOT EXISTS theses ( CREATE TABLE IF NOT EXISTS theses (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
identifier TEXT UNIQUE, -- e.g., "2025-002" identifier TEXT,
-- Basic information
title TEXT NOT NULL, title TEXT NOT NULL,
subtitle TEXT, subtitle TEXT,
year INTEGER NOT NULL, year INTEGER NOT NULL,
is_doctoral BOOLEAN DEFAULT 0,
-- Type of work objet TEXT NOT NULL DEFAULT 'tfe',
is_doctoral BOOLEAN DEFAULT 0, -- 0 for TFE, 1 for doctoral thesis
objet TEXT NOT NULL DEFAULT 'tfe' CHECK (objet IN ('tfe', 'thèse', 'frart')),
-- Academic details
orientation_id INTEGER, orientation_id INTEGER,
ap_program_id INTEGER, ap_program_id INTEGER,
finality_id INTEGER, finality_id INTEGER,
synopsis TEXT,
-- Content context_note TEXT,
synopsis TEXT, -- ~200 words remarks TEXT,
context_note TEXT, -- Note added by jury president (max 150 words)
remarks TEXT, -- Internal remarks
-- Access and licensing
access_type_id INTEGER, access_type_id INTEGER,
license_id INTEGER, license_id INTEGER,
license_custom TEXT, -- free-text licence (if not in predefined list) jury_points DECIMAL(4,2),
jury_note_added BOOLEAN DEFAULT 0,
-- Jury information submitted_at DATETIME,
jury_points DECIMAL(4,2), -- Points out of 20 (backoffice only) defense_date DATETIME,
jury_note_added BOOLEAN DEFAULT 0, -- Whether jury president added a note published_at DATETIME,
-- Publication status
submitted_at DATETIME, -- When student submitted
defense_date DATETIME, -- Date of defense/soutenance
published_at DATETIME, -- When made public (after jury review)
is_published BOOLEAN DEFAULT 0, is_published BOOLEAN DEFAULT 0,
baiu_link TEXT,
-- External links
baiu_link TEXT, -- Link to institutional repository
-- Logistics checkboxes (backoffice only)
exemplaire_baiu BOOLEAN DEFAULT 0, -- Physical copy at BAIU
exemplaire_erg BOOLEAN DEFAULT 0, -- Physical copy at ERG
-- CC2r acceptance (collected in student form)
cc2r BOOLEAN DEFAULT 0,
-- Soft delete support
deleted_at TEXT DEFAULT NULL,
-- Timestamps
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
exemplaire_baiu INTEGER NOT NULL DEFAULT 0,
-- Foreign keys exemplaire_erg INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (orientation_id) REFERENCES orientations(id), cc2r INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (ap_program_id) REFERENCES ap_programs(id), license_custom TEXT,
FOREIGN KEY (finality_id) REFERENCES finality_types(id), deleted_at TEXT DEFAULT NULL,
FOREIGN KEY (license_id) REFERENCES license_types(id),
FOREIGN KEY (access_type_id) REFERENCES access_types(id), FOREIGN KEY (access_type_id) REFERENCES access_types(id),
FOREIGN KEY (license_id) REFERENCES license_types(id) FOREIGN KEY (finality_id) REFERENCES finality_types(id),
FOREIGN KEY (ap_program_id) REFERENCES ap_programs(id),
FOREIGN KEY (orientation_id) REFERENCES orientations(id)
); );
-- ============================================================================
-- JUNCTION TABLES (Many-to-Many relationships)
-- ============================================================================
-- Authors per thesis (can have multiple authors)
CREATE TABLE IF NOT EXISTS thesis_authors ( CREATE TABLE IF NOT EXISTS thesis_authors (
thesis_id INTEGER NOT NULL, thesis_id INTEGER NOT NULL,
author_id INTEGER NOT NULL, author_id INTEGER NOT NULL,
author_order INTEGER DEFAULT 1, -- Order of authors if multiple author_order INTEGER DEFAULT 1,
PRIMARY KEY (thesis_id, author_id), PRIMARY KEY (thesis_id, author_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE, FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE,
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
); );
-- Supervisors per thesis (jury: president, promoteur, lecteurs)
CREATE TABLE IF NOT EXISTS thesis_supervisors ( CREATE TABLE IF NOT EXISTS thesis_supervisors (
thesis_id INTEGER NOT NULL, thesis_id INTEGER NOT NULL,
supervisor_id INTEGER NOT NULL, supervisor_id INTEGER NOT NULL,
supervisor_order INTEGER DEFAULT 1, supervisor_order INTEGER DEFAULT 1,
role TEXT NOT NULL DEFAULT 'promoteur', -- 'president'|'promoteur'|'lecteur' role TEXT NOT NULL DEFAULT 'promoteur',
is_external INTEGER NOT NULL DEFAULT 0, -- 0 = internal, 1 = external is_external INTEGER NOT NULL DEFAULT 0,
is_ulb INTEGER NOT NULL DEFAULT 0, -- 1 = ULB promoteur is_ulb INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (thesis_id, supervisor_id), PRIMARY KEY (thesis_id, supervisor_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE, FOREIGN KEY (supervisor_id) REFERENCES supervisors(id) ON DELETE CASCADE,
FOREIGN KEY (supervisor_id) REFERENCES supervisors(id) ON DELETE CASCADE FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
); );
-- Languages per thesis (can be multilingual)
CREATE TABLE IF NOT EXISTS thesis_languages ( CREATE TABLE IF NOT EXISTS thesis_languages (
thesis_id INTEGER NOT NULL, thesis_id INTEGER NOT NULL,
language_id INTEGER NOT NULL, language_id INTEGER NOT NULL,
PRIMARY KEY (thesis_id, language_id), PRIMARY KEY (thesis_id, language_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE, FOREIGN KEY (language_id) REFERENCES languages(id) ON DELETE CASCADE,
FOREIGN KEY (language_id) REFERENCES languages(id) ON DELETE CASCADE FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
); );
-- Formats per thesis (can have multiple formats)
CREATE TABLE IF NOT EXISTS thesis_formats ( CREATE TABLE IF NOT EXISTS thesis_formats (
thesis_id INTEGER NOT NULL, thesis_id INTEGER NOT NULL,
format_id INTEGER NOT NULL, format_id INTEGER NOT NULL,
PRIMARY KEY (thesis_id, format_id), PRIMARY KEY (thesis_id, format_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE, FOREIGN KEY (format_id) REFERENCES format_types(id) ON DELETE CASCADE,
FOREIGN KEY (format_id) REFERENCES format_types(id) ON DELETE CASCADE FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
); );
-- Tags per thesis (max 10 as per specs; formerly thesis_keywords)
CREATE TABLE IF NOT EXISTS thesis_tags ( CREATE TABLE IF NOT EXISTS thesis_tags (
tag_id INTEGER NOT NULL, tag_id INTEGER NOT NULL,
thesis_id INTEGER NOT NULL, thesis_id INTEGER NOT NULL,
PRIMARY KEY (tag_id, thesis_id), PRIMARY KEY (tag_id, thesis_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
); );
-- ============================================================================
-- FILE ATTACHMENTS
-- ============================================================================
CREATE TABLE IF NOT EXISTS thesis_files ( CREATE TABLE IF NOT EXISTS thesis_files (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
thesis_id INTEGER NOT NULL, thesis_id INTEGER NOT NULL,
file_type TEXT NOT NULL, -- 'main', 'annex', 'written_part', 'other' file_type TEXT NOT NULL,
file_path TEXT NOT NULL, file_path TEXT NOT NULL,
file_name TEXT NOT NULL, file_name TEXT NOT NULL,
file_size INTEGER, -- in bytes file_size INTEGER,
mime_type TEXT, mime_type TEXT,
description TEXT, description TEXT,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
sort_order INTEGER NOT NULL DEFAULT 0,
display_label TEXT,
file_hash TEXT,
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
); );
-- ============================================================================
-- SITE SETTINGS
-- ============================================================================
CREATE TABLE IF NOT EXISTS site_settings ( CREATE TABLE IF NOT EXISTS site_settings (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '', value TEXT NOT NULL DEFAULT '',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
INSERT OR IGNORE INTO site_settings (key, value) VALUES
('access_type_interdit_enabled', '1'),
('access_type_interne_enabled', '1'),
('access_type_libre_enabled', '0'),
('admin_password_hash', ''),
('objet_these_enabled', '1'),
('objet_frart_enabled', '1');
-- ============================================================================
-- STATIC PAGES / CONTENT MANAGEMENT
-- ============================================================================
-- For managing editable static pages (charte, about, licenses, contact)
CREATE TABLE IF NOT EXISTS system_cache ( CREATE TABLE IF NOT EXISTS system_cache (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL, value TEXT NOT NULL,
@@ -319,177 +186,211 @@ CREATE TABLE IF NOT EXISTS system_cache (
CREATE TABLE IF NOT EXISTS pages ( CREATE TABLE IF NOT EXISTS pages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE, -- 'charte', 'about', 'licenses', 'contact' slug TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
content TEXT, -- Markdown or HTML content content TEXT,
is_published BOOLEAN DEFAULT 1, is_published BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
-- Initialize default pages
INSERT OR IGNORE INTO pages (slug, title, content) VALUES
('charte', 'Charte', 'Contenu à venir'),
('about', 'À propos', 'Contenu à venir'),
('licenses', 'Licences', 'Contenu à venir');
-- ============================================================================
-- SHARE LINKS
-- ============================================================================
CREATE TABLE IF NOT EXISTS share_links ( CREATE TABLE IF NOT EXISTS share_links (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE, -- Format: YYYYMMDD-<random>, e.g. 20260416-a3f9k2 slug TEXT NOT NULL,
name TEXT, -- user-defined label (optional) objet_restriction TEXT,
objet_restriction TEXT CHECK (objet_restriction IN ('tfe', 'thèse', 'frart')), -- NULL = no restriction password_hash TEXT,
password_hash TEXT, -- bcrypt hash; NULL = no password required is_active INTEGER NOT NULL DEFAULT 1,
is_active INTEGER NOT NULL DEFAULT 1, -- 1 = active, 0 = disabled usage_count INTEGER NOT NULL DEFAULT 0,
is_archived INTEGER NOT NULL DEFAULT 0, -- 1 = archived (link inaccessible, stats preserved) created_by INTEGER NOT NULL,
usage_count INTEGER NOT NULL DEFAULT 0, -- Number of successful submissions via this link
created_by INTEGER NOT NULL, -- admin user ID
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME -- NULL = never expires expires_at DATETIME,
is_archived INTEGER NOT NULL DEFAULT 0,
name TEXT
); );
CREATE INDEX IF NOT EXISTS idx_share_links_slug ON share_links(slug); CREATE TABLE IF NOT EXISTS smtp_settings (
CREATE INDEX IF NOT EXISTS idx_share_links_active ON share_links(is_active); id INTEGER PRIMARY KEY AUTOINCREMENT,
CREATE INDEX IF NOT EXISTS idx_share_links_archived ON share_links(is_archived); host TEXT NOT NULL DEFAULT '',
port INTEGER NOT NULL DEFAULT 587,
encryption TEXT NOT NULL DEFAULT 'tls',
username TEXT NOT NULL DEFAULT '',
password TEXT NOT NULL DEFAULT '',
from_email TEXT NOT NULL DEFAULT '',
from_name TEXT NOT NULL DEFAULT 'Post-ERG',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
notify_email TEXT NOT NULL DEFAULT ''
);
-- ============================================================================ CREATE TABLE IF NOT EXISTS apropos_contents (
-- ADMIN AUDIT LOG id INTEGER PRIMARY KEY AUTOINCREMENT,
-- ============================================================================ key TEXT NOT NULL,
-- Mirrors every admin action logged to /var/log/xamxam.log. value TEXT,
-- Best-effort: application never fails if this table is absent. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
-- ============================================================================ );
CREATE TABLE IF NOT EXISTS file_access_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
thesis_id INTEGER NOT NULL,
email TEXT NOT NULL,
justification TEXT,
status TEXT NOT NULL DEFAULT 'pending',
admin_notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
approved_at DATETIME,
approved_by_admin_id INTEGER,
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS file_access_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER NOT NULL,
token TEXT NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_valid INTEGER NOT NULL DEFAULT 1,
used_at DATETIME DEFAULT NULL,
FOREIGN KEY (request_id) REFERENCES file_access_requests(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS file_access_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER NOT NULL,
session_token TEXT NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_valid INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (request_id) REFERENCES file_access_requests(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS file_access_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER NOT NULL,
event TEXT NOT NULL,
ip TEXT,
user_agent TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (request_id) REFERENCES file_access_requests(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS form_help_blocks (
key TEXT PRIMARY KEY,
content TEXT NOT NULL DEFAULT '',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
sort_order INTEGER NOT NULL DEFAULT 0,
name TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS admin_audit_log ( CREATE TABLE IF NOT EXISTS admin_audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
ip TEXT NOT NULL, ip TEXT NOT NULL,
user_agent TEXT, user_agent TEXT,
resource TEXT NOT NULL, -- e.g. thesis, tag, share_link, settings, system resource TEXT NOT NULL,
action TEXT NOT NULL, -- e.g. publish, delete, smtp_update action TEXT NOT NULL,
status TEXT NOT NULL, -- success | error status TEXT NOT NULL,
context TEXT -- JSON blob with extra fields context TEXT
); );
CREATE INDEX IF NOT EXISTS idx_admin_audit_log_created_at ON admin_audit_log(created_at); CREATE TABLE IF NOT EXISTS peertube_settings (
CREATE INDEX IF NOT EXISTS idx_admin_audit_log_resource ON admin_audit_log(resource); id INTEGER PRIMARY KEY AUTOINCREMENT,
CREATE INDEX IF NOT EXISTS idx_admin_audit_log_action ON admin_audit_log(action); instance_url TEXT NOT NULL DEFAULT '',
-- ============================================================================
-- SMTP SETTINGS
-- ============================================================================
-- Singleton row — id is always 1. Credentials stored in clear for now.
CREATE TABLE IF NOT EXISTS smtp_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
host TEXT NOT NULL DEFAULT '',
port INTEGER NOT NULL DEFAULT 587,
encryption TEXT NOT NULL DEFAULT 'tls', -- 'tls' | 'ssl' | 'none'
username TEXT NOT NULL DEFAULT '', username TEXT NOT NULL DEFAULT '',
password TEXT NOT NULL DEFAULT '', -- stored in clear for now; encrypt later password TEXT NOT NULL DEFAULT '',
from_email TEXT NOT NULL DEFAULT '', channel_id INTEGER NOT NULL DEFAULT 1,
from_name TEXT NOT NULL DEFAULT 'XAMXAM', privacy INTEGER NOT NULL DEFAULT 1,
notify_email TEXT NOT NULL DEFAULT '', -- recipient for admin notifications updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
INSERT OR IGNORE INTO smtp_settings (id) VALUES (1); CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
actor TEXT NOT NULL,
action TEXT NOT NULL,
table_name TEXT NOT NULL,
record_id INTEGER,
old_data TEXT,
new_data TEXT
);
CREATE VIEW IF NOT EXISTS v_smtp_active AS -- ============================================================================
-- INDEXES
-- ============================================================================
CREATE INDEX idx_admin_audit_log_action ON admin_audit_log(action);
CREATE INDEX idx_admin_audit_log_created_at ON admin_audit_log(created_at);
CREATE INDEX idx_admin_audit_log_resource ON admin_audit_log(resource);
CREATE INDEX idx_audit_log_table_record ON audit_log(table_name, record_id);
CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp);
CREATE INDEX idx_authors_email ON authors(email);
CREATE INDEX idx_file_access_audit_request
ON file_access_audit(request_id);
CREATE INDEX idx_file_access_requests_email
ON file_access_requests(email);
CREATE INDEX idx_file_access_requests_status
ON file_access_requests(status);
CREATE INDEX idx_file_access_requests_thesis_id
ON file_access_requests(thesis_id);
CREATE INDEX idx_file_access_sessions_expires
ON file_access_sessions(expires_at);
CREATE INDEX idx_file_access_sessions_token
ON file_access_sessions(session_token);
CREATE INDEX idx_file_access_tokens_expires_at
ON file_access_tokens(expires_at);
CREATE INDEX idx_file_access_tokens_token
ON file_access_tokens(token);
CREATE INDEX idx_share_links_active ON share_links(is_active);
CREATE INDEX idx_share_links_archived ON share_links(is_archived);
CREATE INDEX idx_share_links_slug ON share_links(slug);
CREATE INDEX idx_tags_name ON tags(name);
CREATE INDEX idx_theses_access_type ON theses(access_type_id);
CREATE INDEX idx_theses_ap_program ON theses(ap_program_id);
CREATE INDEX idx_theses_identifier ON theses(identifier);
CREATE INDEX idx_theses_orientation ON theses(orientation_id);
CREATE INDEX idx_theses_pub_year ON theses(is_published, year DESC);
CREATE INDEX idx_theses_published ON theses(is_published);
CREATE INDEX idx_theses_year ON theses(year);
CREATE INDEX idx_thesis_authors_author ON thesis_authors(author_id);
CREATE INDEX idx_thesis_authors_thesis ON thesis_authors(thesis_id);
CREATE INDEX idx_thesis_tags_tag ON thesis_tags(tag_id);
CREATE INDEX idx_thesis_tags_thesis ON thesis_tags(thesis_id);
-- ============================================================================
-- VIEWS
-- ============================================================================
CREATE VIEW v_smtp_active AS
SELECT * FROM smtp_settings WHERE id = 1; SELECT * FROM smtp_settings WHERE id = 1;
-- ============================================================================ CREATE VIEW v_theses_full AS
-- APROPOS CONTENTS (structured data for the "À propos" page)
-- ============================================================================
CREATE TABLE IF NOT EXISTS apropos_contents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE, -- 'contacts'
value TEXT, -- JSON array
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO apropos_contents (key, value) VALUES
('contacts', '[
{
"role": "Bibliothèque d''architecture, d''ingénierie architecturale, d''urbanisme (BAIU) :",
"entries": [
{"text": "Laurent Leprince", "url": "", "email": "laurent.leprince@uclouvain.be"}
]
},
{
"role": "Responsable des mémoires de l''ERG :",
"entries": [
{"text": "Xavier Gorgol", "url": "", "email": "xavier.gorgol@erg.be"}
]
},
{
"role": "Cours de suivi de mémoire :",
"entries": [
{"text": "Brigitte Ledune", "url": "", "email": "brigitte.ledune@erg.be"}
]
}
]');
-- ============================================================================
-- INDEXES for performance
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_theses_year ON theses(year);
CREATE INDEX IF NOT EXISTS idx_theses_published ON theses(is_published);
CREATE INDEX IF NOT EXISTS idx_theses_pub_year ON theses(is_published, year DESC);
CREATE INDEX IF NOT EXISTS idx_theses_identifier ON theses(identifier);
CREATE INDEX IF NOT EXISTS idx_theses_orientation ON theses(orientation_id);
CREATE INDEX IF NOT EXISTS idx_theses_ap_program ON theses(ap_program_id);
CREATE INDEX IF NOT EXISTS idx_theses_access_type ON theses(access_type_id);
CREATE INDEX IF NOT EXISTS idx_authors_email ON authors(email);
CREATE INDEX IF NOT EXISTS idx_thesis_authors_thesis ON thesis_authors(thesis_id);
CREATE INDEX IF NOT EXISTS idx_thesis_authors_author ON thesis_authors(author_id);
CREATE INDEX IF NOT EXISTS idx_thesis_tags_thesis ON thesis_tags(thesis_id);
CREATE INDEX IF NOT EXISTS idx_thesis_tags_tag ON thesis_tags(tag_id);
-- ============================================================================
-- TRIGGERS for automatic timestamp updates
-- ============================================================================
CREATE TRIGGER IF NOT EXISTS update_theses_timestamp
AFTER UPDATE ON theses
BEGIN
UPDATE theses SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_authors_timestamp
AFTER UPDATE ON authors
BEGIN
UPDATE authors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_supervisors_timestamp
AFTER UPDATE ON supervisors
BEGIN
UPDATE supervisors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_pages_timestamp
AFTER UPDATE ON pages
BEGIN
UPDATE pages SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_apropos_contents_timestamp
AFTER UPDATE ON apropos_contents
BEGIN
UPDATE apropos_contents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
-- ============================================================================
-- VIEWS for common queries
-- ============================================================================
-- Full thesis information view
CREATE VIEW IF NOT EXISTS v_theses_full AS
SELECT SELECT
t.id, t.id,
t.identifier, t.identifier,
@@ -529,7 +430,6 @@ SELECT
GROUP_CONCAT(DISTINCT UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2)) as languages, GROUP_CONCAT(DISTINCT UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2)) as languages,
GROUP_CONCAT(DISTINCT fmt.name) as formats, GROUP_CONCAT(DISTINCT fmt.name) as formats,
GROUP_CONCAT(DISTINCT tg.name) as keywords, GROUP_CONCAT(DISTINCT tg.name) as keywords,
-- First author's email and contact-visibility flag
(SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_interne, (SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_interne,
(SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_public (SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_public
FROM theses t FROM theses t
@@ -543,82 +443,165 @@ LEFT JOIN authors a ON ta.author_id = a.id
LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id
LEFT JOIN supervisors s ON ts.supervisor_id = s.id LEFT JOIN supervisors s ON ts.supervisor_id = s.id
LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id
LEFT JOIN languages l ON tl.language_id = l.id AND l.deleted_at IS NULL LEFT JOIN languages l ON tl.language_id = l.id
LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id
LEFT JOIN format_types fmt ON tf.format_id = fmt.id LEFT JOIN format_types fmt ON tf.format_id = fmt.id
LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id
LEFT JOIN tags tg ON tt.tag_id = tg.id AND tg.deleted_at IS NULL LEFT JOIN tags tg ON tt.tag_id = tg.id
WHERE t.deleted_at IS NULL
GROUP BY t.id; GROUP BY t.id;
-- Published theses only (for public view) CREATE VIEW v_theses_public AS
CREATE VIEW IF NOT EXISTS v_theses_public AS
SELECT * FROM v_theses_full SELECT * FROM v_theses_full
WHERE is_published = 1; WHERE is_published = 1;
-- ============================================================================ -- ============================================================================
-- FILE ACCESS RESTRICTION SYSTEM -- TRIGGERS
-- ============================================================================
-- Add support for restricting attached files on TFEs with email-based access
-- requests and cookie-based validation.
-- ============================================================================ -- ============================================================================
-- Add new site setting for enabling/disabling file access restriction CREATE TRIGGER update_apropos_contents_timestamp
INSERT OR IGNORE INTO site_settings (key, value) VALUES AFTER UPDATE ON apropos_contents
('restricted_files_enabled', '0'); BEGIN
UPDATE apropos_contents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER update_authors_timestamp
AFTER UPDATE ON authors
BEGIN
UPDATE authors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER update_form_help_blocks_timestamp
AFTER UPDATE ON form_help_blocks
BEGIN
UPDATE form_help_blocks SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;
END;
CREATE TRIGGER update_pages_timestamp
AFTER UPDATE ON pages
BEGIN
UPDATE pages SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER update_supervisors_timestamp
AFTER UPDATE ON supervisors
BEGIN
UPDATE supervisors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER update_theses_timestamp
AFTER UPDATE ON theses
BEGIN
UPDATE theses SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
-- ============================================================================ -- ============================================================================
-- FILE ACCESS REQUESTS TABLE -- SEED DATA (reference data + initial settings)
-- ============================================================================
-- Stores requests from users wanting access to restricted TFE files.
-- Supports approval workflow: pending → approved/rejected
-- ============================================================================ -- ============================================================================
CREATE TABLE IF NOT EXISTS file_access_requests ( INSERT OR IGNORE INTO orientations (name) VALUES ('Arts Numériques');
id INTEGER PRIMARY KEY AUTOINCREMENT, INSERT OR IGNORE INTO orientations (name) VALUES ('Bande-Dessinée');
thesis_id INTEGER NOT NULL, INSERT OR IGNORE INTO orientations (name) VALUES ('Cinéma d''animation');
email TEXT NOT NULL, INSERT OR IGNORE INTO orientations (name) VALUES ('Design Numérique');
justification TEXT, INSERT OR IGNORE INTO orientations (name) VALUES ('Dessin');
status TEXT NOT NULL DEFAULT 'pending' INSERT OR IGNORE INTO orientations (name) VALUES ('Graphisme');
CHECK(status IN ('pending', 'approved', 'rejected')), INSERT OR IGNORE INTO orientations (name) VALUES ('Gravure');
admin_notes TEXT, INSERT OR IGNORE INTO orientations (name) VALUES ('Illustration');
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INSERT OR IGNORE INTO orientations (name) VALUES ('Installation-Performance');
approved_at DATETIME, INSERT OR IGNORE INTO orientations (name) VALUES ('Peinture');
approved_by_admin_id INTEGER, INSERT OR IGNORE INTO orientations (name) VALUES ('Photographie');
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE INSERT OR IGNORE INTO orientations (name) VALUES ('Sculpture');
); INSERT OR IGNORE INTO orientations (name) VALUES ('Sérigraphie');
INSERT OR IGNORE INTO orientations (name) VALUES ('Typographie');
INSERT OR IGNORE INTO orientations (name) VALUES ('Vidéographie');
-- Index for efficient lookup by thesis and email INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('Narration Spéculative', 'NS');
CREATE INDEX IF NOT EXISTS idx_file_access_requests_thesis_id INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('Design et Politique du Multiple', 'DPM');
ON file_access_requests(thesis_id); INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('Atelier Pratiques Situées', 'APS');
INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS');
INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('Pratique de l''art - outils critiques, arts et contexte simultanés', 'PACS');
CREATE INDEX IF NOT EXISTS idx_file_access_requests_email INSERT OR IGNORE INTO finality_types (name) VALUES ('Approfondi');
ON file_access_requests(email); INSERT OR IGNORE INTO finality_types (name) VALUES ('Enseignement');
INSERT OR IGNORE INTO finality_types (name) VALUES ('Spécialisé');
CREATE INDEX IF NOT EXISTS idx_file_access_requests_status INSERT OR IGNORE INTO languages (name) VALUES ('français');
ON file_access_requests(status); INSERT OR IGNORE INTO languages (name) VALUES ('anglais');
INSERT OR IGNORE INTO languages (name) VALUES ('néerlandais');
INSERT OR IGNORE INTO languages (name) VALUES ('italian');
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Site web', 5);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Audio', 3);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Vidéo', 4);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Performance', 6);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Objet éditorial', 7);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Installation', 8);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Autre', 9);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Écriture', 1);
INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Image', 2);
INSERT OR IGNORE INTO access_types (name, description) VALUES ('Libre', 'TFE en libre accès à tout le monde sur la plateforme et en bibliothèque');
INSERT OR IGNORE INTO access_types (name, description) VALUES ('Interne', 'TFE accessible uniquement sur place en physique. Une note descriptive est disponible sur le site');
INSERT OR IGNORE INTO access_types (name, description) VALUES ('Interdit', 'TFE non disponible en physique ni sur le site. Une note descriptive est disponible sur le site');
INSERT OR IGNORE INTO license_types (name) VALUES ('CC BY 4.0');
INSERT OR IGNORE INTO license_types (name) VALUES ('CC BY-NC 4.0');
INSERT OR IGNORE INTO license_types (name) VALUES ('CC BY-NC-ND 4.0');
INSERT OR IGNORE INTO license_types (name) VALUES ('CC BY-NC-SA 4.0');
INSERT OR IGNORE INTO license_types (name) VALUES ('CC BY-ND 4.0');
INSERT OR IGNORE INTO license_types (name) VALUES ('CC BY-SA 4.0');
INSERT OR IGNORE INTO license_types (name) VALUES ('Domaine public');
INSERT OR IGNORE INTO license_types (name) VALUES ('Tous droits réservés');
INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_interdit_enabled', '1');
INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_interne_enabled', '0');
INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_libre_enabled', '0');
INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_frart_enabled', '1');
INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_these_enabled', '1');
INSERT OR IGNORE INTO site_settings (key, value) VALUES ('peertube_upload_enabled', '0');
INSERT OR IGNORE INTO site_settings (key, value) VALUES ('restricted_files_enabled', '0');
INSERT OR IGNORE INTO pages (slug, title, content, is_published) VALUES ('about', 'À propos', 'Contenu à venir', 1);
INSERT OR IGNORE INTO pages (slug, title, content, is_published) VALUES ('charte', 'Charte', 'Contenu à venir', 1);
INSERT OR IGNORE INTO pages (slug, title, content, is_published) VALUES ('licenses', 'Licences', 'Contenu à venir', 1);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('partage_intro', 'Introduction', 'Hahahaha
## Beware the dog', 0, 0);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_tfe_info', 'Informations du TFE', 'Hello world', 0, 1);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_synopsis', 'Note Synopsis', '', 0, 2);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_jury', 'Composition du jury', '', 0, 3);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_academic', 'Cadre académique', '', 0, 4);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_files', 'Fichiers', '', 0, 5);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_access', 'Visibilité / Accès', 'qsldkjlfkjdsqmflkjq', 1, 6);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_email', 'E-mail de confirmation', '', 0, 7);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_languages', 'Langue(s)', 'Hahah', 0, 0);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_keywords', 'Mots-clés', 'lolz', 0, 0);
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_metadata', 'Métadonnées complémentaires', '', 0, 0);
INSERT OR IGNORE INTO apropos_contents (key, value) VALUES ('contacts', '[
{
"role": "Bibliothèque d''architecture, d''ingénierie architecturale, d''urbanisme (BAIU) :",
"entries": [
{"text": "Laurent Leprince", "url": "", "email": "laurent.leprince@uclouvain.be"}
]
},
{
"role": "Responsable des mémoires de l''ERG :",
"entries": [
{"text": "Xavier Gorgol", "url": "", "email": "xavier.gorgol@erg.be"}
]
},
{
"role": "Cours de suivi de mémoire :",
"entries": [
{"text": "Brigitte Ledune", "url": "", "email": "brigitte.ledune@erg.be"}
]
}
]');
-- Singleton table placeholders
INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);
INSERT OR IGNORE INTO peertube_settings (id) VALUES (1);
-- ============================================================================ -- ============================================================================
-- FILE ACCESS TOKENS TABLE -- END OF SCHEMA
-- ============================================================================ -- ============================================================================
-- Stores tokens for cookie-based access validation.
-- Each token is unique, time-limited, and linked to a specific request.
-- ============================================================================
CREATE TABLE IF NOT EXISTS file_access_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_valid INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (request_id) REFERENCES file_access_requests(id) ON DELETE CASCADE
);
-- Index for token lookup (most common query)
CREATE INDEX IF NOT EXISTS idx_file_access_tokens_token
ON file_access_tokens(token);
-- Index for cleanup of expired tokens
CREATE INDEX IF NOT EXISTS idx_file_access_tokens_expires_at
ON file_access_tokens(expires_at);

4464
app/storage/schema.sql.new Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -675,6 +675,58 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: kpvxplms 859e5316 "fix: settings handler was treating hidden value="0" as truthy" (rebased revision) +\\\\\\\ to: kpvxplms 859e5316 "fix: settings handler was treating hidden value="0" as truthy" (rebased revision)
++ $linkName = $link['name'] ?? ''; ++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: kpvxplms 859e5316 "fix: settings handler was treating hidden value="0" as truthy" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: rqrkkkuo 9141fd8e "feat(deploy): add deploy-verify-permissions recipe to check ownership/permissions after rsync" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: rqrkkkuo 5563460c "feat(deploy): add deploy-verify-permissions recipe to check ownership/permissions after rsync" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: rqrkkkuo 5563460c "feat(deploy): add deploy-verify-permissions recipe to check ownership/permissions after rsync" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: unnmorqw 5feff462 "feat(deploy): upload and run deploy-server.sh before permission verification" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: unnmorqw c30a41b6 "feat(deploy): upload and run deploy-server.sh before permission verification" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: unnmorqw c30a41b6 "feat(deploy): upload and run deploy-server.sh before permission verification" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: mysnnsru 3bfad924 "feat(deploy): upload and run deploy-server.sh before permission verification, run migrations" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: mysnnsru 4f314e92 "feat(deploy): upload and run deploy-server.sh before permission verification, run migrations" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: mysnnsru 4f314e92 "feat(deploy): upload and run deploy-server.sh before permission verification, run migrations" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: sstzwlpk 18bd561d "feat(deploy): upload deploy-server.sh, run migrations, fix migrate.sh server layout" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: sstzwlpk 0e83e8f7 "feat(deploy): upload deploy-server.sh, run migrations, fix migrate.sh server layout" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
?> ?>
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer"> <tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">

121
justfile
View File

@@ -60,10 +60,17 @@ deploy:
--exclude 'storage/docs' \ --exclude 'storage/docs' \
--exclude 'var/' \ --exclude 'var/' \
app/ xamxam:/var/www/xamxam/ app/ xamxam:/var/www/xamxam/
# Upload deploy-server.sh for post-deploy permission fix
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh xamxam "sudo bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh"
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 -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }' && php migrations/run.php /var/www/xamxam/storage/xamxam.db" ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'"
# Run pending migrations
ssh xamxam "cd /var/www/xamxam && bash scripts/migrate.sh"
# Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys) # Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys)
@just deploy-env @just deploy-env
@just deploy-verify-permissions
[group('deploy')] [group('deploy')]
deploy-env: deploy-env:
@@ -107,6 +114,118 @@ deploy-db:
rsync -v --progress app/storage/xamxam.db xamxam:/var/www/xamxam/storage/xamxam.db rsync -v --progress app/storage/xamxam.db xamxam:/var/www/xamxam/storage/xamxam.db
ssh xamxam "chown www-data:xamxam /var/www/xamxam/storage/xamxam.db && chmod 660 /var/www/xamxam/storage/xamxam.db" ssh xamxam "chown www-data:xamxam /var/www/xamxam/storage/xamxam.db && chmod 660 /var/www/xamxam/storage/xamxam.db"
[group('deploy')]
deploy-verify-permissions:
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/xamxam"
WEB_USER="www-data"
APP_GROUP="xamxam"
ERRORS=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ok() { printf "${GREEN}${NC} %s\n" "$*"; }
err() { printf "${RED}${NC} %s\n" "$*" >&2; ERRORS=$((ERRORS + 1)); }
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
printf "🔍 Verifying permissions on %s…\n\n" "$APP_DIR"
# ── Ownership ──────────────────────────────────────────────────────────────────
echo "── Ownership ───────────────────────────────────"
while IFS= read -r line; do
owner=$(echo "$line" | awk '{print $3}')
group=$(echo "$line" | awk '{print $4}')
path=$(echo "$line" | awk '{print $NF}')
if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then
err "$path$owner:$group (expected $WEB_USER:$APP_GROUP)"
else
ok "$path$owner:$group"
fi
done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null")
# ── Key directories: 2775 ─────────────────────────────────────────────────────
echo "── Directory permissions (expected 2775) ───────"
while IFS= read -r line; do
perms=$(echo "$line" | awk '{print $1}')
path=$(echo "$line" | awk '{print $NF}')
if [ "$perms" != "drwxrwsr-x" ]; then
err "$path$perms (expected drwxrwsr-x / 2775)"
else
ok "$path$perms"
fi
done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort")
# ── Key files: 664 ────────────────────────────────────────────────────────────
echo "── File permissions (expected 664 / 660) ───────"
# Spot-check a few critical files
while IFS= read -r path; do
perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING")
if [ "$perms" = "MISSING" ]; then
err "$path → FILE MISSING"
else
perm_num=$(echo "$perms" | awk '{print $1}')
owner=$(echo "$perms" | awk '{print $2}')
group=$(echo "$perms" | awk '{print $3}')
case "$path" in
*/storage/xamxam.db|*/storage/*.db)
expected_perm="660" ;;
*)
expected_perm="664" ;;
esac
if [ "$perm_num" != "$expected_perm" ]; then
err "$path$perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP"
elif [ "$owner" != "$WEB_USER" ]; then
err "$path → owner $owner, expected $WEB_USER (perm $perm_num OK)"
else
ok "$path$perm_num $owner:$group"
fi
fi
done < <(printf '%s\n' \
"$APP_DIR/.env" \
"$APP_DIR/app/router.php" \
"$APP_DIR/storage/xamxam.db")
# ── var/ subdirectories must be writable ──────────────────────────────────────
echo "── var/ writability ────────────────────────────"
for subdir in cache logs tmp; do
if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then
ok "var/$subdir → writable"
else
err "var/$subdir → NOT WRITABLE"
fi
done
# ── storage/cache/rate_limit writable ─────────────────────────────────────────
if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then
ok "storage/cache/rate_limit → writable"
else
err "storage/cache/rate_limit → NOT WRITABLE"
fi
# ── .env must be 640 ──────────────────────────────────────────────────────────
env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "")
if [ "$env_perm" = "640" ]; then
ok ".env → 640"
elif [ -z "$env_perm" ]; then
warn ".env → MISSING"
else
err ".env → $env_perm (expected 640)"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
if [ "$ERRORS" -eq 0 ]; then
printf "${GREEN}✅ All permissions OK${NC}\n"
else
printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS"
printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n"
exit 1
fi
[group('deploy')] [group('deploy')]
deploy-script script_name: deploy-script script_name:
# Generic script deployer (e.g., just deploy-script setup-server) # Generic script deployer (e.g., just deploy-script setup-server)

210
scripts/generate-schema.py Normal file
View File

@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Generate a clean schema.sql from the fully-migrated local database."""
import sqlite3
import re
import sys
DB_PATH = sys.argv[1] if len(sys.argv) > 1 else 'app/storage/xamxam.db'
db = sqlite3.connect(DB_PATH)
# Get all objects
tables = db.execute(
"SELECT name, sql FROM sqlite_master WHERE type='table' "
"AND name NOT LIKE 'sqlite_%' AND name != '_migrations' ORDER BY name"
).fetchall()
indexes = db.execute(
"SELECT sql FROM sqlite_master WHERE type='index' "
"AND name NOT LIKE 'sqlite_autoindex%' AND sql IS NOT NULL ORDER BY name"
).fetchall()
views = db.execute(
"SELECT sql FROM sqlite_master WHERE type='view' ORDER BY name"
).fetchall()
triggers = db.execute(
"SELECT sql FROM sqlite_master WHERE type='trigger' ORDER BY name"
).fetchall()
print("-- ============================================================================")
print("-- XAMXAM Database Schema — complete, fully migrated")
print(f"-- Generated from local database on {db.execute('SELECT date()').fetchone()[0]}")
print("-- All 28 migrations merged into this single file.")
print("-- ============================================================================")
print()
# We define the ideal CREATE TABLE statements by querying PRAGMA table_info
# and reconstructing clean DDL, because SQLite stores ALTER TABLE artifacts.
def build_clean_table_ddl(name):
"""Build a clean CREATE TABLE statement from PRAGMA table_info."""
cols = db.execute(f"PRAGMA table_info('{name}')").fetchall()
pk_cols = [c[1] for c in cols if c[5] > 0]
lines = [f"CREATE TABLE IF NOT EXISTS {name} ("]
col_defs = []
for cid, col_name, col_type, not_null, default, is_pk in cols:
part = f" {col_name} {col_type}"
if is_pk:
if len(pk_cols) == 1:
part += " PRIMARY KEY"
if col_type.upper() == 'INTEGER':
part += " AUTOINCREMENT"
if not_null:
part += " NOT NULL"
if default is not None:
if default.upper() in ('CURRENT_TIMESTAMP', 'CURRENT_TIME', 'CURRENT_DATE'):
part += f" DEFAULT {default}"
elif any(default.lower().startswith(fn + '(') for fn in ('datetime', 'date', 'time', 'strftime')):
part += f" DEFAULT ({default})"
elif default.startswith("'") or default.startswith('"'):
part += f" DEFAULT {default}"
else:
part += f" DEFAULT {default}"
col_defs.append(part)
# Multi-column PK
if len(pk_cols) > 1:
col_defs.append(f" PRIMARY KEY ({', '.join(pk_cols)})")
# Foreign keys
fks = db.execute(f"PRAGMA foreign_key_list('{name}')").fetchall()
for fk in fks:
fk_id, seq, ref_table, from_col, to_col, on_update, on_delete, match = fk
if seq == 0:
col_defs.append(
f" FOREIGN KEY ({from_col}) REFERENCES {ref_table}({to_col})"
+ (f" ON DELETE {on_delete}" if on_delete and on_delete != 'NO ACTION' else "")
)
lines.append(",\n".join(col_defs))
lines.append(");")
# Add CHECK constraints if any from original DDL
orig = db.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (name,)
).fetchone()
if orig and orig[0]:
for m in re.finditer(r'CHECK\s*\([^)]+\)', orig[0]):
constraint = m.group(0)
# INSERT OR IGNORE into site_settings uses ON CONFLICT — keep that pattern
pass # CHECK is already in the column definition from PRAGMA
return "\n".join(lines)
# Sort tables in dependency order (rough)
table_order = [
'orientations', 'ap_programs', 'finality_types', 'languages',
'format_types', 'tags', 'access_types', 'license_types',
'authors', 'supervisors',
'theses',
'thesis_authors', 'thesis_supervisors', 'thesis_languages',
'thesis_formats', 'thesis_tags', 'thesis_files',
'site_settings', 'system_cache', 'pages', 'share_links',
'smtp_settings', 'apropos_contents',
'file_access_requests', 'file_access_tokens', 'file_access_sessions',
'file_access_audit', 'form_help_blocks', 'admin_audit_log',
'peertube_settings', 'audit_log',
]
seen = set()
for name in table_order:
if name in seen:
continue
seen.add(name)
try:
ddl = build_clean_table_ddl(name)
print(ddl)
print()
except Exception as e:
print(f"-- ERROR building DDL for {name}: {e}", file=sys.stderr)
print("-- ============================================================================")
print("-- INDEXES")
print("-- ============================================================================")
print()
for (sql,) in indexes:
clean = sql.strip()
if not clean.endswith(';'):
clean += ';'
print(clean)
print()
print("-- ============================================================================")
print("-- VIEWS")
print("-- ============================================================================")
print()
for (sql,) in views:
clean = sql.strip()
if not clean.endswith(';'):
clean += ';'
print(clean)
print()
print("-- ============================================================================")
print("-- TRIGGERS")
print("-- ============================================================================")
print()
for (sql,) in triggers:
clean = sql.strip()
if not clean.endswith(';'):
clean += ';'
print(clean)
print()
print("-- ============================================================================")
print("-- SEED DATA (reference data + initial settings)")
print("-- ============================================================================")
print()
# Extract seed data from the local database
# (table, query, explicit_column_names)
seed_tables = [
('orientations', "SELECT name FROM orientations", ['name']),
('ap_programs', "SELECT name, code FROM ap_programs", ['name', 'code']),
('finality_types', "SELECT name FROM finality_types", ['name']),
('languages', "SELECT name FROM languages WHERE deleted_at IS NULL", ['name']),
('format_types', "SELECT name, sort_order FROM format_types", ['name', 'sort_order']),
('access_types', "SELECT name, description FROM access_types", ['name', 'description']),
('license_types', "SELECT name FROM license_types", ['name']),
('site_settings', "SELECT key, value FROM site_settings WHERE key IN ('access_type_interdit_enabled', 'access_type_interne_enabled', 'access_type_libre_enabled', 'objet_these_enabled', 'objet_frart_enabled', 'restricted_files_enabled', 'peertube_upload_enabled')", ['key', 'value']),
('pages', "SELECT slug, title, content, is_published FROM pages WHERE slug IN ('charte', 'about', 'licenses')", ['slug', 'title', 'content', 'is_published']),
('form_help_blocks', "SELECT key, name, content, enabled, sort_order FROM form_help_blocks", ['key', 'name', 'content', 'enabled', 'sort_order']),
('apropos_contents', "SELECT key, value FROM apropos_contents WHERE key = 'contacts'", ['key', 'value']),
]
for table, query, col_names in seed_tables:
rows = db.execute(query).fetchall()
if not rows:
continue
for row in rows:
vals = []
for i, val in enumerate(row):
if val is None:
vals.append('NULL')
elif isinstance(val, bool):
vals.append('1' if val else '0')
elif isinstance(val, (int, float)):
vals.append(str(val))
else:
escaped = str(val).replace("'", "''")
vals.append(f"'{escaped}'")
print(f"INSERT OR IGNORE INTO {table} ({', '.join(col_names)}) VALUES ({', '.join(vals)});")
print()
# Singleton table inserts
print("-- Singleton table placeholders")
print("INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);")
print("INSERT OR IGNORE INTO peertube_settings (id) VALUES (1);")
print()
print("-- ============================================================================")
print("-- END OF SCHEMA")
print("-- ============================================================================")

View File

@@ -7,9 +7,18 @@
set -euo pipefail set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
APP_DIR="$REPO_ROOT/app"
SCHEMA="$APP_DIR/storage/schema.sql" # Detect layout: local dev has app/ subdir, server has files at repo root
PROD_DB="$APP_DIR/storage/xamxam.db" if [ -d "$REPO_ROOT/app/storage" ]; then
SCHEMA="$REPO_ROOT/app/storage/schema.sql"
PROD_DB="$REPO_ROOT/app/storage/xamxam.db"
elif [ -f "$REPO_ROOT/storage/schema.sql" ]; then
SCHEMA="$REPO_ROOT/storage/schema.sql"
PROD_DB="$REPO_ROOT/storage/xamxam.db"
else
echo "ERROR: cannot find storage/schema.sql" >&2
exit 1
fi
init_db() { init_db() {
local db="$1" local db="$1"