feat: SQLite DB with migrations replaces all JSON file storage
- Add db.php with PDO singleton, migration runner, and all helper functions - Add migrations/001_initial_schema.sql (full schema) - Add migrations/002_import_json.php (one-time JSON → DB importer) - Add _incl/switch_tenant.php POST endpoint for tenant/centro switching - Update tools.auth.php: DB-backed login, cookie auth, session reload, init_active_centro() - Update all sysadmin pages (users, centros, aularios, invitations, reset_password) to use DB - Update aulatek/index.php, aulario.php, supercafe.php, supercafe_edit.php to use DB - Update aulatek/comedor.php and api/comedor.php to use DB - Update aulatek/paneldiario.php: aulario config + comedor data from DB - Update aulatek/proyectos.php: aulario config + sharing metadata from DB - Update club/cal.php, index.php, edit_data.php, upload/upload.php to use DB - Update account/index.php: rich profile, tenant list, aula list, session info, permissions - Update pre-body.php account dropdown: shows active org + inline tenant switcher - Update DATA_STRUCTURE.md to document DB approach and migration system Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
This commit is contained in:
144
public_html/_incl/migrations/001_initial_schema.sql
Normal file
144
public_html/_incl/migrations/001_initial_schema.sql
Normal file
@@ -0,0 +1,144 @@
|
||||
-- Axia4 Migration 001: Initial Schema
|
||||
-- Converts all JSON file-based storage to a proper relational schema.
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- ── Application configuration (replaces /DATA/AuthConfig.json) ─────────────
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
-- ── System installation flag (replaces /DATA/SISTEMA_INSTALADO.txt) ────────
|
||||
-- Stored as a config row: key='installed', value='1'
|
||||
|
||||
-- ── Users (replaces /DATA/Usuarios/*.json) ─────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
password_hash TEXT NOT NULL DEFAULT '',
|
||||
permissions TEXT NOT NULL DEFAULT '[]', -- JSON array
|
||||
google_auth INTEGER NOT NULL DEFAULT 0,
|
||||
meta TEXT NOT NULL DEFAULT '{}', -- JSON for extra fields
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ── Invitations (replaces /DATA/Invitaciones_de_usuarios.json) ─────────────
|
||||
CREATE TABLE IF NOT EXISTS invitations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
single_use INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ── Centros/Organizations ───────────────────────────────────────────────────
|
||||
-- Replaces directory existence at /DATA/entreaulas/Centros/{centro_id}/
|
||||
CREATE TABLE IF NOT EXISTS centros (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ── User ↔ Centro assignments (many-to-many) ───────────────────────────────
|
||||
-- Replaces entreaulas.centro + entreaulas.aulas fields in user JSON.
|
||||
-- A single user can belong to multiple centros (multi-tenant).
|
||||
CREATE TABLE IF NOT EXISTS user_centros (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL DEFAULT '',
|
||||
aulas TEXT NOT NULL DEFAULT '[]', -- JSON array of aulario_ids
|
||||
PRIMARY KEY (user_id, centro_id)
|
||||
);
|
||||
|
||||
-- ── Aularios (replaces /DATA/entreaulas/Centros/{}/Aularios/{id}.json) ──────
|
||||
CREATE TABLE IF NOT EXISTS aularios (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
aulario_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
icon TEXT NOT NULL DEFAULT '',
|
||||
extra TEXT NOT NULL DEFAULT '{}', -- JSON for extra config
|
||||
UNIQUE (centro_id, aulario_id)
|
||||
);
|
||||
|
||||
-- ── SuperCafe menu (replaces .../SuperCafe/Menu.json) ──────────────────────
|
||||
CREATE TABLE IF NOT EXISTS supercafe_menu (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
data TEXT NOT NULL DEFAULT '{}', -- JSON matching existing format
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (centro_id)
|
||||
);
|
||||
|
||||
-- ── SuperCafe orders (replaces .../SuperCafe/Comandas/*.json) ───────────────
|
||||
CREATE TABLE IF NOT EXISTS supercafe_orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
order_ref TEXT NOT NULL,
|
||||
fecha TEXT NOT NULL,
|
||||
persona TEXT NOT NULL,
|
||||
comanda TEXT NOT NULL DEFAULT '',
|
||||
notas TEXT NOT NULL DEFAULT '',
|
||||
estado TEXT NOT NULL DEFAULT 'Pedido',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (centro_id, order_ref)
|
||||
);
|
||||
|
||||
-- ── Comedor menu types (replaces .../Comedor-MenuTypes.json) ────────────────
|
||||
CREATE TABLE IF NOT EXISTS comedor_menu_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
aulario_id TEXT NOT NULL,
|
||||
data TEXT NOT NULL DEFAULT '[]', -- JSON array of menu type objs
|
||||
UNIQUE (centro_id, aulario_id)
|
||||
);
|
||||
|
||||
-- ── Comedor daily entries (replaces .../Comedor/{ym}/{day}/_datos.json) ─────
|
||||
CREATE TABLE IF NOT EXISTS comedor_entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
aulario_id TEXT NOT NULL,
|
||||
year_month TEXT NOT NULL, -- "2024-01"
|
||||
day TEXT NOT NULL, -- "15"
|
||||
data TEXT NOT NULL DEFAULT '{}',
|
||||
UNIQUE (centro_id, aulario_id, year_month, day)
|
||||
);
|
||||
|
||||
-- ── Diary entries (replaces .../Diario/*.json) ──────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS diario_entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
aulario_id TEXT NOT NULL,
|
||||
entry_date TEXT NOT NULL,
|
||||
data TEXT NOT NULL DEFAULT '{}',
|
||||
UNIQUE (centro_id, aulario_id, entry_date)
|
||||
);
|
||||
|
||||
-- ── Panel diario per-student data (replaces .../Alumnos/*/Panel.json) ───────
|
||||
CREATE TABLE IF NOT EXISTS panel_alumno (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
centro_id TEXT NOT NULL REFERENCES centros(centro_id) ON DELETE CASCADE,
|
||||
aulario_id TEXT NOT NULL,
|
||||
alumno TEXT NOT NULL,
|
||||
data TEXT NOT NULL DEFAULT '{}',
|
||||
UNIQUE (centro_id, aulario_id, alumno)
|
||||
);
|
||||
|
||||
-- ── Club event metadata (replaces /DATA/club/IMG/{date}/data.json) ──────────
|
||||
CREATE TABLE IF NOT EXISTS club_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date_ref TEXT UNIQUE NOT NULL,
|
||||
data TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- ── Club configuration (replaces /DATA/club/config.json) ────────────────────
|
||||
CREATE TABLE IF NOT EXISTS club_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
data TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
255
public_html/_incl/migrations/002_import_json.php
Normal file
255
public_html/_incl/migrations/002_import_json.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
/**
|
||||
* Migration 002: Import existing JSON data from the filesystem into the DB.
|
||||
* This runs once on first boot after the schema is created.
|
||||
* It is safe to run even if /DATA doesn't have all files – missing files are skipped.
|
||||
*
|
||||
* $db (PDO) is provided by the migration runner in db.php.
|
||||
*/
|
||||
|
||||
// ── AuthConfig → config table ────────────────────────────────────────────────
|
||||
$auth_config_file = '/DATA/AuthConfig.json';
|
||||
if (file_exists($auth_config_file)) {
|
||||
$auth_config = json_decode(file_get_contents($auth_config_file), true) ?? [];
|
||||
$ins = $db->prepare("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)");
|
||||
foreach ($auth_config as $k => $v) {
|
||||
$ins->execute([$k, is_string($v) ? $v : json_encode($v)]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── SISTEMA_INSTALADO marker ─────────────────────────────────────────────────
|
||||
if (file_exists('/DATA/SISTEMA_INSTALADO.txt')) {
|
||||
$db->prepare("INSERT OR IGNORE INTO config (key, value) VALUES ('installed', '1')")->execute();
|
||||
}
|
||||
|
||||
// ── Users (/DATA/Usuarios/*.json) ────────────────────────────────────────────
|
||||
$users_dir = '/DATA/Usuarios';
|
||||
if (is_dir($users_dir)) {
|
||||
$ins_user = $db->prepare(
|
||||
"INSERT OR IGNORE INTO users
|
||||
(username, display_name, email, password_hash, permissions, google_auth, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$ins_uc = $db->prepare(
|
||||
"INSERT OR IGNORE INTO user_centros (user_id, centro_id, role, aulas)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$ins_centro = $db->prepare("INSERT OR IGNORE INTO centros (centro_id) VALUES (?)");
|
||||
|
||||
foreach (glob("$users_dir/*.json") ?: [] as $user_file) {
|
||||
$username = basename($user_file, '.json');
|
||||
$data = json_decode(file_get_contents($user_file), true);
|
||||
if (!is_array($data)) {
|
||||
continue;
|
||||
}
|
||||
$permissions = isset($data['permissions']) ? json_encode($data['permissions']) : '[]';
|
||||
// Store remaining non-standard keys in meta
|
||||
$meta_keys = ['display_name', 'email', 'password_hash', 'permissions', 'entreaulas', 'google_auth'];
|
||||
$meta = [];
|
||||
foreach ($data as $k => $v) {
|
||||
if (!in_array($k, $meta_keys, true)) {
|
||||
$meta[$k] = $v;
|
||||
}
|
||||
}
|
||||
$ins_user->execute([
|
||||
$username,
|
||||
$data['display_name'] ?? '',
|
||||
$data['email'] ?? '',
|
||||
$data['password_hash'] ?? '',
|
||||
$permissions,
|
||||
(int) ($data['google_auth'] ?? 0),
|
||||
json_encode($meta),
|
||||
]);
|
||||
$user_id = (int) $db->lastInsertId();
|
||||
if ($user_id === 0) {
|
||||
// Already existed – look it up
|
||||
$row = $db->prepare("SELECT id FROM users WHERE username = ?")->execute([$username]);
|
||||
$user_id = (int) $db->query("SELECT id FROM users WHERE username = " . $db->quote($username))->fetchColumn();
|
||||
}
|
||||
|
||||
// Entreaulas centro assignment
|
||||
$ea = $data['entreaulas'] ?? [];
|
||||
// Support both old single "centro" and new "centros" array
|
||||
$centros = [];
|
||||
if (!empty($ea['centros']) && is_array($ea['centros'])) {
|
||||
$centros = $ea['centros'];
|
||||
} elseif (!empty($ea['centro'])) {
|
||||
$centros = [$ea['centro']];
|
||||
}
|
||||
$role = $ea['role'] ?? '';
|
||||
$aulas = json_encode($ea['aulas'] ?? []);
|
||||
foreach ($centros as $cid) {
|
||||
if ($cid === '') {
|
||||
continue;
|
||||
}
|
||||
$ins_centro->execute([$cid]);
|
||||
$ins_uc->execute([$user_id, $cid, $role, $aulas]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Invitations (/DATA/Invitaciones_de_usuarios.json) ────────────────────────
|
||||
$inv_file = '/DATA/Invitaciones_de_usuarios.json';
|
||||
if (file_exists($inv_file)) {
|
||||
$invs = json_decode(file_get_contents($inv_file), true) ?? [];
|
||||
$ins = $db->prepare(
|
||||
"INSERT OR IGNORE INTO invitations (code, active, single_use) VALUES (?, ?, ?)"
|
||||
);
|
||||
foreach ($invs as $code => $inv) {
|
||||
$ins->execute([
|
||||
strtoupper($code),
|
||||
(int) ($inv['active'] ?? 1),
|
||||
(int) ($inv['single_use'] ?? 1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Centros & Aularios (directory structure) ──────────────────────────────────
|
||||
$centros_base = '/DATA/entreaulas/Centros';
|
||||
if (is_dir($centros_base)) {
|
||||
$ins_centro = $db->prepare("INSERT OR IGNORE INTO centros (centro_id) VALUES (?)");
|
||||
$ins_aulario = $db->prepare(
|
||||
"INSERT OR IGNORE INTO aularios (centro_id, aulario_id, name, icon, extra) VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
foreach (glob("$centros_base/*", GLOB_ONLYDIR) ?: [] as $centro_dir) {
|
||||
$centro_id = basename($centro_dir);
|
||||
$ins_centro->execute([$centro_id]);
|
||||
|
||||
$aularios_dir = "$centro_dir/Aularios";
|
||||
foreach (glob("$aularios_dir/*.json") ?: [] as $aulario_file) {
|
||||
$aulario_id = basename($aulario_file, '.json');
|
||||
$adata = json_decode(file_get_contents($aulario_file), true);
|
||||
if (!is_array($adata)) {
|
||||
continue;
|
||||
}
|
||||
$name = $adata['name'] ?? $aulario_id;
|
||||
$icon = $adata['icon'] ?? '';
|
||||
$extra_keys = ['name', 'icon'];
|
||||
$extra = [];
|
||||
foreach ($adata as $k => $v) {
|
||||
if (!in_array($k, $extra_keys, true)) {
|
||||
$extra[$k] = $v;
|
||||
}
|
||||
}
|
||||
$ins_aulario->execute([$centro_id, $aulario_id, $name, $icon, json_encode($extra)]);
|
||||
}
|
||||
|
||||
// SuperCafe menu
|
||||
$menu_file = "$centro_dir/SuperCafe/Menu.json";
|
||||
if (file_exists($menu_file)) {
|
||||
$menu_data = file_get_contents($menu_file);
|
||||
$db->prepare("INSERT OR IGNORE INTO supercafe_menu (centro_id, data) VALUES (?, ?)")
|
||||
->execute([$centro_id, $menu_data]);
|
||||
}
|
||||
|
||||
// SuperCafe orders
|
||||
$comandas_dir = "$centro_dir/SuperCafe/Comandas";
|
||||
if (is_dir($comandas_dir)) {
|
||||
$ins_order = $db->prepare(
|
||||
"INSERT OR IGNORE INTO supercafe_orders
|
||||
(centro_id, order_ref, fecha, persona, comanda, notas, estado)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
foreach (glob("$comandas_dir/*.json") ?: [] as $order_file) {
|
||||
$order_ref = basename($order_file, '.json');
|
||||
$odata = json_decode(file_get_contents($order_file), true);
|
||||
if (!is_array($odata)) {
|
||||
continue;
|
||||
}
|
||||
$ins_order->execute([
|
||||
$centro_id,
|
||||
$order_ref,
|
||||
$odata['Fecha'] ?? '',
|
||||
$odata['Persona'] ?? '',
|
||||
$odata['Comanda'] ?? '',
|
||||
$odata['Notas'] ?? '',
|
||||
$odata['Estado'] ?? 'Pedido',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Comedor menu types & daily entries per aulario
|
||||
foreach (glob("$aularios_dir/*.json") ?: [] as $aulario_file) {
|
||||
$aulario_id = basename($aulario_file, '.json');
|
||||
|
||||
$menu_types_file = "$aularios_dir/$aulario_id/Comedor-MenuTypes.json";
|
||||
if (file_exists($menu_types_file)) {
|
||||
$db->prepare(
|
||||
"INSERT OR IGNORE INTO comedor_menu_types (centro_id, aulario_id, data) VALUES (?, ?, ?)"
|
||||
)->execute([$centro_id, $aulario_id, file_get_contents($menu_types_file)]);
|
||||
}
|
||||
|
||||
$comedor_base = "$aularios_dir/$aulario_id/Comedor";
|
||||
if (is_dir($comedor_base)) {
|
||||
$ins_centry = $db->prepare(
|
||||
"INSERT OR IGNORE INTO comedor_entries (centro_id, aulario_id, year_month, day, data) VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
foreach (glob("$comedor_base/*", GLOB_ONLYDIR) ?: [] as $ym_dir) {
|
||||
$ym = basename($ym_dir);
|
||||
foreach (glob("$ym_dir/*", GLOB_ONLYDIR) ?: [] as $day_dir) {
|
||||
$day = basename($day_dir);
|
||||
$data_file = "$day_dir/_datos.json";
|
||||
if (file_exists($data_file)) {
|
||||
$ins_centry->execute([
|
||||
$centro_id, $aulario_id, $ym, $day,
|
||||
file_get_contents($data_file),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diario entries
|
||||
$diario_base = "$aularios_dir/$aulario_id/Diario";
|
||||
if (is_dir($diario_base)) {
|
||||
$ins_d = $db->prepare(
|
||||
"INSERT OR IGNORE INTO diario_entries (centro_id, aulario_id, entry_date, data) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
foreach (glob("$diario_base/*.json") ?: [] as $diario_file) {
|
||||
$entry_date = basename($diario_file, '.json');
|
||||
$ins_d->execute([$centro_id, $aulario_id, $entry_date, file_get_contents($diario_file)]);
|
||||
}
|
||||
}
|
||||
|
||||
// Panel alumno data
|
||||
$alumnos_base = "$aularios_dir/$aulario_id/Alumnos";
|
||||
if (is_dir($alumnos_base)) {
|
||||
$ins_pa = $db->prepare(
|
||||
"INSERT OR IGNORE INTO panel_alumno (centro_id, aulario_id, alumno, data) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
foreach (glob("$alumnos_base/*/", GLOB_ONLYDIR) ?: [] as $alumno_dir) {
|
||||
$alumno = basename($alumno_dir);
|
||||
// Look for Panel.json (used by paneldiario)
|
||||
$panel_files = glob("$alumno_dir/Panel*.json") ?: [];
|
||||
foreach ($panel_files as $pf) {
|
||||
$ins_pa->execute([
|
||||
$centro_id, $aulario_id, $alumno,
|
||||
file_get_contents($pf),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Club config (/DATA/club/config.json) ──────────────────────────────────────
|
||||
$club_config_file = '/DATA/club/config.json';
|
||||
if (file_exists($club_config_file)) {
|
||||
$db->prepare("INSERT OR IGNORE INTO club_config (id, data) VALUES (1, ?)")
|
||||
->execute([file_get_contents($club_config_file)]);
|
||||
}
|
||||
|
||||
// ── Club events (/DATA/club/IMG/{date}/data.json) ─────────────────────────────
|
||||
$club_img_dir = '/DATA/club/IMG';
|
||||
if (is_dir($club_img_dir)) {
|
||||
$ins_ev = $db->prepare("INSERT OR IGNORE INTO club_events (date_ref, data) VALUES (?, ?)");
|
||||
foreach (glob("$club_img_dir/*/", GLOB_ONLYDIR) ?: [] as $event_dir) {
|
||||
$date_ref = basename($event_dir);
|
||||
$event_data_file = "$event_dir/data.json";
|
||||
$ins_ev->execute([
|
||||
$date_ref,
|
||||
file_exists($event_data_file) ? file_get_contents($event_data_file) : '{}',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user