diff --git a/django_app/aulatek/admin.py b/django_app/aulatek/admin.py index 0ba6ec9..671e606 100644 --- a/django_app/aulatek/admin.py +++ b/django_app/aulatek/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Aulario, ComedorMenu, ComedorMenuType, StudentAttachment +from .models import Aulario, ComedorMenu, ComedorMenuType, Cuaderno, StudentAttachment @admin.register(Aulario) @@ -30,4 +30,11 @@ class ComedorMenuTypeAdmin(admin.ModelAdmin): list_display = ("org_id", "source_aulario_id", "type_id", "label", "color", "created_by") search_fields = ("org_id", "source_aulario_id", "type_id", "label", "created_by") list_filter = ("org_id", "source_aulario_id", "created_at") - filter_horizontal = ("shared_aularios",) \ No newline at end of file + filter_horizontal = ("shared_aularios",) + + +@admin.register(Cuaderno) +class CuadernoAdmin(admin.ModelAdmin): + list_display = ("org_id", "aulario_id", "alumno", "titulo", "created_by", "updated_at") + search_fields = ("org_id", "aulario_id", "alumno", "titulo", "created_by", "updated_by") + list_filter = ("org_id", "aulario_id", "created_at", "updated_at") \ No newline at end of file diff --git a/django_app/aulatek/migrations/0006_cuaderno.py b/django_app/aulatek/migrations/0006_cuaderno.py new file mode 100644 index 0000000..ca9da63 --- /dev/null +++ b/django_app/aulatek/migrations/0006_cuaderno.py @@ -0,0 +1,30 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("aulatek", "0005_comedormenutype"), + ] + + operations = [ + migrations.CreateModel( + name="Cuaderno", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("org_id", models.TextField()), + ("aulario_id", models.TextField()), + ("alumno", models.TextField(default="")), + ("titulo", models.TextField(default="")), + ("contenido_encriptado", models.TextField(default="")), + ("pin_hash", models.TextField(default="")), + ("pin_salt", models.TextField(default="")), + ("created_by", models.TextField(default="")), + ("updated_by", models.TextField(default="")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "aulatek_cuadernos", + }, + ), + ] diff --git a/django_app/aulatek/models.py b/django_app/aulatek/models.py index 595be91..e841b98 100644 --- a/django_app/aulatek/models.py +++ b/django_app/aulatek/models.py @@ -104,6 +104,28 @@ class ComedorMenuType(models.Model): return f"{self.org_id}/{self.source_aulario_id}/{self.type_id}" +class Cuaderno(models.Model): + org_id = models.TextField() + aulario_id = models.TextField() + alumno = models.TextField(default="") + titulo = models.TextField(default="") + contenido_encriptado = models.TextField(default="") + pin_hash = models.TextField(default="") + pin_salt = models.TextField(default="") + created_by = models.TextField(default="") + updated_by = models.TextField(default="") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "aulatek_cuadernos" + + def __str__(self): + alumno = self.alumno or "sin-alumno" + titulo = self.titulo or "sin-titulo" + return f"{self.org_id}/{self.aulario_id}/{alumno}/{titulo}" + + class Aulario(CoreAulario): class Meta: proxy = True diff --git a/django_app/aulatek/urls.py b/django_app/aulatek/urls.py index ebdf2e3..8f2bd35 100644 --- a/django_app/aulatek/urls.py +++ b/django_app/aulatek/urls.py @@ -10,6 +10,9 @@ urlpatterns = [ path("aulario/", views.aulatek_aulario, name="aulatek_aulario"), path("paneldiario/", views.paneldiario, name="paneldiario"), path("alumnos/", views.alumnos, name="alumnos"), + path("cuadernos/", views.cuadernos, name="cuadernos"), + path("cuadernos/new/", views.cuaderno_new, name="cuaderno_new"), + path("cuadernos//", views.cuaderno_edit, name="cuaderno_edit"), path("alumnos/new/", views.alumno_new, name="alumno_new"), path("alumnos/edit//", views.alumno_edit, name="alumno_edit"), path("alumnos/delete//", views.alumno_delete, name="alumno_delete"), diff --git a/django_app/aulatek/views.py b/django_app/aulatek/views.py index c52c809..a6f544a 100644 --- a/django_app/aulatek/views.py +++ b/django_app/aulatek/views.py @@ -12,7 +12,7 @@ from django.shortcuts import redirect, render from core.models import Aulario from core.shell import require_axia_login, shell_context -from .models import ComedorMenu, ComedorMenuType, StudentAttachment +from .models import ComedorMenu, ComedorMenuType, Cuaderno, StudentAttachment def _safe_organization_id(value: str) -> str: @@ -48,6 +48,13 @@ def _alumnos_permission(request: HttpRequest) -> bool: ) +def _cuadernos_permission(request: HttpRequest) -> bool: + return ( + request.axia_user.has_permission("aulatek:docente") + or request.axia_user.has_permission("entreaulas:docente") + ) + + def _alumnos_access(request: HttpRequest): gate = require_axia_login(request) if gate: @@ -60,6 +67,15 @@ def _alumnos_access(request: HttpRequest): return None, request.axia_user, org_id +def _cuadernos_access(request: HttpRequest): + gate, user, org_id = _alumnos_access(request) + if gate: + return gate, None, None + if not _cuadernos_permission(request): + return HttpResponseForbidden("No tienes permiso para ver cuadernos."), None, None + return None, user, org_id + + def _comedor_access(request: HttpRequest): gate, user, org_id = _alumnos_access(request) if gate: @@ -83,6 +99,16 @@ def _parse_menu_date(value: str): return None +def _safe_titulo(value: str) -> str: + value = (value or "").strip() + value = re.sub(r"[\x00-\x1F\x7F]", "", value) + return value[:140] + + +def _signature_verified(request: HttpRequest) -> bool: + return (request.POST.get("_firma_signed") or "") == "1" + + def aulatek_home(request: HttpRequest): gate = require_axia_login(request) if gate: @@ -139,6 +165,13 @@ def aulatek_aulario(request: HttpRequest): "variant": "info", "visible": _alumnos_permission(request), }, + { + "href": f"/aulatek/cuadernos/?aulario={aulario_id}", + "label": "Cuadernos", + "icon": "/static/iconexperience/contract.png", + "variant": "dark", + "visible": _cuadernos_permission(request), + }, { "href": f"/sysadmin/aularios/?aulario={aulario_id}", "label": "Cambiar Ajustes", @@ -198,6 +231,7 @@ def alumnos(request: HttpRequest): "module_title": "Gestion de Alumnos", "aulario_id": aulario_id, "students": students, + "can_view_cuadernos": _cuadernos_permission(request), } ) return render(request, "aulatek/alumnos.html", context) @@ -283,6 +317,8 @@ def alumno_delete(request: HttpRequest, student_id: int): return gate if request.method != "POST": return HttpResponseForbidden("Metodo no permitido.") + if not _signature_verified(request): + return HttpResponseForbidden("Firma requerida para eliminar.") student = StudentAttachment.objects.filter(id=student_id, org_id=org_id).first() if not student: @@ -313,6 +349,148 @@ def alumno_photo(request: HttpRequest, student_id: int): return FileResponse(student.photo.open("rb"), content_type=mime_type or "application/octet-stream") +def cuadernos(request: HttpRequest): + gate, _, org_id = _cuadernos_access(request) + if gate: + return gate + + aulario_id = _safe_aulario_id(request.GET.get("aulario", "")) + context = shell_context(request, "aulatek", "Cuadernos") + if not aulario_id or not org_id: + context.update( + { + "module_title": "Cuadernos", + "aulario_id": "", + "cuadernos": [], + "alumnos": [], + } + ) + return render(request, "aulatek/cuadernos.html", context) + + if not Aulario.objects.filter(org_id=org_id, aulario_id=aulario_id).exists(): + raise Http404("Aulario no encontrado") + + cuadernos_rows = Cuaderno.objects.filter(org_id=org_id, aulario_id=aulario_id).order_by("-updated_at", "-created_at") + context.update( + { + "module_title": "Cuadernos", + "aulario_id": aulario_id, + "cuadernos": cuadernos_rows, + } + ) + return render(request, "aulatek/cuadernos.html", context) + + +def cuaderno_new(request: HttpRequest): + gate, user, org_id = _cuadernos_access(request) + if gate: + return gate + + aulario_id = _safe_aulario_id(request.GET.get("aulario", "")) + if not aulario_id or not org_id: + messages.error(request, "No se ha indicado un aulario valido.") + return redirect("/aulatek/") + + if not Aulario.objects.filter(org_id=org_id, aulario_id=aulario_id).exists(): + raise Http404("Aulario no encontrado") + + if request.method == "POST": + titulo = _safe_titulo(request.POST.get("titulo", "")) + alumno = _safe_alumno_name(request.POST.get("alumno", "")) + contenido = (request.POST.get("contenido") or "").strip() + + if not titulo: + messages.error(request, "El titulo es obligatorio.") + return redirect(f"/aulatek/cuadernos/new/?aulario={aulario_id}") + if not contenido: + messages.error(request, "El contenido no puede estar vacio.") + return redirect(f"/aulatek/cuadernos/new/?aulario={aulario_id}") + Cuaderno.objects.create( + org_id=org_id, + aulario_id=aulario_id, + alumno=alumno, + titulo=titulo, + contenido_encriptado=contenido, + pin_hash="", + pin_salt="", + created_by=user.username if user else "", + updated_by=user.username if user else "", + ) + messages.success(request, "Cuaderno creado correctamente.") + return redirect(f"/aulatek/cuadernos/?aulario={aulario_id}") + + alumnos_rows = StudentAttachment.objects.filter(org_id=org_id, aulario_id=aulario_id).order_by("alumno") + context = shell_context(request, "aulatek", "Nuevo Cuaderno") + context.update( + { + "module_title": "Nuevo Cuaderno", + "aulario_id": aulario_id, + "alumnos": [row.alumno for row in alumnos_rows], + } + ) + return render(request, "aulatek/cuaderno_new.html", context) + + +def cuaderno_edit(request: HttpRequest, cuaderno_id: int): + gate, user, org_id = _cuadernos_access(request) + if gate: + return gate + + cuaderno = Cuaderno.objects.filter(id=cuaderno_id, org_id=org_id).first() + if not cuaderno: + raise Http404("Cuaderno no encontrado") + + aulario_id = cuaderno.aulario_id + decrypted_content = cuaderno.contenido_encriptado or "" + + if request.method == "POST": + action = (request.POST.get("action") or "unlock").strip().lower() + + if action == "unlock": + return redirect(f"/aulatek/cuadernos/{cuaderno.id}/") + + if action == "update": + titulo = _safe_titulo(request.POST.get("titulo", "")) + alumno = _safe_alumno_name(request.POST.get("alumno", "")) + contenido = (request.POST.get("contenido") or "").strip() + + if not titulo: + messages.error(request, "El titulo es obligatorio.") + return redirect(f"/aulatek/cuadernos/{cuaderno.id}/") + if not contenido: + messages.error(request, "El contenido no puede estar vacio.") + return redirect(f"/aulatek/cuadernos/{cuaderno.id}/") + + cuaderno.titulo = titulo + cuaderno.alumno = alumno + cuaderno.pin_salt = "" + cuaderno.pin_hash = "" + cuaderno.contenido_encriptado = contenido + cuaderno.updated_by = user.username if user else "" + cuaderno.save(update_fields=["titulo", "alumno", "pin_hash", "pin_salt", "contenido_encriptado", "updated_by", "updated_at"]) + messages.success(request, "Cuaderno actualizado correctamente.") + return redirect(f"/aulatek/cuadernos/{cuaderno.id}/") + + if action == "delete": + if not _signature_verified(request): + messages.error(request, "Firma requerida para eliminar el cuaderno.") + return redirect(f"/aulatek/cuadernos/{cuaderno.id}/") + cuaderno.delete() + messages.success(request, "Cuaderno eliminado.") + return redirect(f"/aulatek/cuadernos/?aulario={aulario_id}") + + context = shell_context(request, "aulatek", f"Cuaderno: {cuaderno.titulo}") + context.update( + { + "module_title": "Editar Cuaderno", + "cuaderno": cuaderno, + "aulario_id": aulario_id, + "decrypted_content": decrypted_content, + } + ) + return render(request, "aulatek/cuaderno_edit.html", context) + + def comedor(request: HttpRequest): gate, user, org_id, aulario_row = _comedor_access(request) if gate: @@ -400,6 +578,8 @@ def comedor(request: HttpRequest): return redirect(comedor_url(selected_date, row.type_id)) if action == "delete_type": + if not _signature_verified(request): + return HttpResponseForbidden("Firma requerida para eliminar tipo.") delete_id = (request.POST.get("delete_type_id") or "").strip() row = ComedorMenuType.objects.filter(org_id=org_id, source_aulario_id=aulario_row.aulario_id, type_id=delete_id).first() if row: @@ -475,6 +655,8 @@ def comedor(request: HttpRequest): if action == "delete": if not selected_type_row or not can_edit_selected_type: return HttpResponseForbidden("No puedes eliminar menu de un tipo compartido.") + if not _signature_verified(request): + return HttpResponseForbidden("Firma requerida para eliminar menu.") menu_id = int(request.POST.get("menu_id") or 0) menu = ComedorMenu.objects.filter( id=menu_id, diff --git a/django_app/axia4_django/settings.py b/django_app/axia4_django/settings.py index 0a0fc2a..331caeb 100644 --- a/django_app/axia4_django/settings.py +++ b/django_app/axia4_django/settings.py @@ -87,7 +87,7 @@ USE_I18N = True USE_TZ = True STATIC_URL = "/static/" -STATICFILES_DIRS = [REPO_ROOT / "public_html" / "static"] +STATICFILES_DIRS = [BASE_DIR / "static"] MEDIA_URL = "/media/" MEDIA_ROOT = DATA_ROOT / "attachments" diff --git a/public_html/static/arasaac/actividad.png b/django_app/static/arasaac/actividad.png similarity index 100% rename from public_html/static/arasaac/actividad.png rename to django_app/static/arasaac/actividad.png diff --git a/public_html/static/arasaac/alumnos.png b/django_app/static/arasaac/alumnos.png similarity index 100% rename from public_html/static/arasaac/alumnos.png rename to django_app/static/arasaac/alumnos.png diff --git a/public_html/static/arasaac/calendario.png b/django_app/static/arasaac/calendario.png similarity index 100% rename from public_html/static/arasaac/calendario.png rename to django_app/static/arasaac/calendario.png diff --git a/public_html/static/arasaac/carpeta.png b/django_app/static/arasaac/carpeta.png similarity index 100% rename from public_html/static/arasaac/carpeta.png rename to django_app/static/arasaac/carpeta.png diff --git a/public_html/static/arasaac/comedor.png b/django_app/static/arasaac/comedor.png similarity index 100% rename from public_html/static/arasaac/comedor.png rename to django_app/static/arasaac/comedor.png diff --git a/public_html/static/arasaac/diadelasemana/domingo.png b/django_app/static/arasaac/diadelasemana/domingo.png similarity index 100% rename from public_html/static/arasaac/diadelasemana/domingo.png rename to django_app/static/arasaac/diadelasemana/domingo.png diff --git a/public_html/static/arasaac/diadelasemana/jueves.png b/django_app/static/arasaac/diadelasemana/jueves.png similarity index 100% rename from public_html/static/arasaac/diadelasemana/jueves.png rename to django_app/static/arasaac/diadelasemana/jueves.png diff --git a/public_html/static/arasaac/diadelasemana/lunes.png b/django_app/static/arasaac/diadelasemana/lunes.png similarity index 100% rename from public_html/static/arasaac/diadelasemana/lunes.png rename to django_app/static/arasaac/diadelasemana/lunes.png diff --git a/public_html/static/arasaac/diadelasemana/martes.png b/django_app/static/arasaac/diadelasemana/martes.png similarity index 100% rename from public_html/static/arasaac/diadelasemana/martes.png rename to django_app/static/arasaac/diadelasemana/martes.png diff --git a/public_html/static/arasaac/diadelasemana/miércoles.png b/django_app/static/arasaac/diadelasemana/miércoles.png similarity index 100% rename from public_html/static/arasaac/diadelasemana/miércoles.png rename to django_app/static/arasaac/diadelasemana/miércoles.png diff --git a/public_html/static/arasaac/diadelasemana/viernes.png b/django_app/static/arasaac/diadelasemana/viernes.png similarity index 100% rename from public_html/static/arasaac/diadelasemana/viernes.png rename to django_app/static/arasaac/diadelasemana/viernes.png diff --git a/public_html/static/arasaac/documento.png b/django_app/static/arasaac/documento.png similarity index 100% rename from public_html/static/arasaac/documento.png rename to django_app/static/arasaac/documento.png diff --git a/public_html/static/arasaac/libro.png b/django_app/static/arasaac/libro.png similarity index 100% rename from public_html/static/arasaac/libro.png rename to django_app/static/arasaac/libro.png diff --git a/public_html/static/arasaac/mesesdelano/abril.png b/django_app/static/arasaac/mesesdelano/abril.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/abril.png rename to django_app/static/arasaac/mesesdelano/abril.png diff --git a/public_html/static/arasaac/mesesdelano/agosto.png b/django_app/static/arasaac/mesesdelano/agosto.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/agosto.png rename to django_app/static/arasaac/mesesdelano/agosto.png diff --git a/public_html/static/arasaac/mesesdelano/diciembre.png b/django_app/static/arasaac/mesesdelano/diciembre.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/diciembre.png rename to django_app/static/arasaac/mesesdelano/diciembre.png diff --git a/public_html/static/arasaac/mesesdelano/enero.png b/django_app/static/arasaac/mesesdelano/enero.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/enero.png rename to django_app/static/arasaac/mesesdelano/enero.png diff --git a/public_html/static/arasaac/mesesdelano/febrero.png b/django_app/static/arasaac/mesesdelano/febrero.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/febrero.png rename to django_app/static/arasaac/mesesdelano/febrero.png diff --git a/public_html/static/arasaac/mesesdelano/julio.png b/django_app/static/arasaac/mesesdelano/julio.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/julio.png rename to django_app/static/arasaac/mesesdelano/julio.png diff --git a/public_html/static/arasaac/mesesdelano/junio.png b/django_app/static/arasaac/mesesdelano/junio.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/junio.png rename to django_app/static/arasaac/mesesdelano/junio.png diff --git a/public_html/static/arasaac/mesesdelano/marzo.png b/django_app/static/arasaac/mesesdelano/marzo.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/marzo.png rename to django_app/static/arasaac/mesesdelano/marzo.png diff --git a/public_html/static/arasaac/mesesdelano/mayo.png b/django_app/static/arasaac/mesesdelano/mayo.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/mayo.png rename to django_app/static/arasaac/mesesdelano/mayo.png diff --git a/public_html/static/arasaac/mesesdelano/noviembre.png b/django_app/static/arasaac/mesesdelano/noviembre.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/noviembre.png rename to django_app/static/arasaac/mesesdelano/noviembre.png diff --git a/public_html/static/arasaac/mesesdelano/octubre.png b/django_app/static/arasaac/mesesdelano/octubre.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/octubre.png rename to django_app/static/arasaac/mesesdelano/octubre.png diff --git a/public_html/static/arasaac/mesesdelano/septiembre.png b/django_app/static/arasaac/mesesdelano/septiembre.png similarity index 100% rename from public_html/static/arasaac/mesesdelano/septiembre.png rename to django_app/static/arasaac/mesesdelano/septiembre.png diff --git a/public_html/static/arasaac/pdi.png b/django_app/static/arasaac/pdi.png similarity index 100% rename from public_html/static/arasaac/pdi.png rename to django_app/static/arasaac/pdi.png diff --git a/public_html/static/arasaac/yo.png b/django_app/static/arasaac/yo.png similarity index 100% rename from public_html/static/arasaac/yo.png rename to django_app/static/arasaac/yo.png diff --git a/public_html/static/bootstrap.bundle.min.js b/django_app/static/bootstrap.bundle.min.js similarity index 100% rename from public_html/static/bootstrap.bundle.min.js rename to django_app/static/bootstrap.bundle.min.js diff --git a/public_html/static/bootstrap.min.css b/django_app/static/bootstrap.min.css similarity index 100% rename from public_html/static/bootstrap.min.css rename to django_app/static/bootstrap.min.css diff --git a/public_html/static/filetype/file-pdf-box.svg b/django_app/static/filetype/file-pdf-box.svg similarity index 100% rename from public_html/static/filetype/file-pdf-box.svg rename to django_app/static/filetype/file-pdf-box.svg diff --git a/public_html/static/filetype/file-question.svg b/django_app/static/filetype/file-question.svg similarity index 100% rename from public_html/static/filetype/file-question.svg rename to django_app/static/filetype/file-question.svg diff --git a/public_html/static/filetype/folder.svg b/django_app/static/filetype/folder.svg similarity index 100% rename from public_html/static/filetype/folder.svg rename to django_app/static/filetype/folder.svg diff --git a/public_html/static/filetype/image.svg b/django_app/static/filetype/image.svg similarity index 100% rename from public_html/static/filetype/image.svg rename to django_app/static/filetype/image.svg diff --git a/public_html/static/html5-qrcode.min.js b/django_app/static/html5-qrcode.min.js similarity index 100% rename from public_html/static/html5-qrcode.min.js rename to django_app/static/html5-qrcode.min.js diff --git a/public_html/static/iconexperience/add.png b/django_app/static/iconexperience/add.png similarity index 100% rename from public_html/static/iconexperience/add.png rename to django_app/static/iconexperience/add.png diff --git a/public_html/static/iconexperience/barcode.png b/django_app/static/iconexperience/barcode.png similarity index 100% rename from public_html/static/iconexperience/barcode.png rename to django_app/static/iconexperience/barcode.png diff --git a/public_html/static/iconexperience/bell3.png b/django_app/static/iconexperience/bell3.png similarity index 100% rename from public_html/static/iconexperience/bell3.png rename to django_app/static/iconexperience/bell3.png diff --git a/public_html/static/iconexperience/calendar_preferences.png b/django_app/static/iconexperience/calendar_preferences.png similarity index 100% rename from public_html/static/iconexperience/calendar_preferences.png rename to django_app/static/iconexperience/calendar_preferences.png diff --git a/public_html/static/iconexperience/certificate_add.png b/django_app/static/iconexperience/certificate_add.png similarity index 100% rename from public_html/static/iconexperience/certificate_add.png rename to django_app/static/iconexperience/certificate_add.png diff --git a/public_html/static/iconexperience/certificate_ok.png b/django_app/static/iconexperience/certificate_ok.png similarity index 100% rename from public_html/static/iconexperience/certificate_ok.png rename to django_app/static/iconexperience/certificate_ok.png diff --git a/public_html/static/iconexperience/contract.png b/django_app/static/iconexperience/contract.png similarity index 100% rename from public_html/static/iconexperience/contract.png rename to django_app/static/iconexperience/contract.png diff --git a/public_html/static/iconexperience/delivery_man_parcel.png b/django_app/static/iconexperience/delivery_man_parcel.png similarity index 100% rename from public_html/static/iconexperience/delivery_man_parcel.png rename to django_app/static/iconexperience/delivery_man_parcel.png diff --git a/public_html/static/iconexperience/find.png b/django_app/static/iconexperience/find.png similarity index 100% rename from public_html/static/iconexperience/find.png rename to django_app/static/iconexperience/find.png diff --git a/public_html/static/iconexperience/floppy_disk.png b/django_app/static/iconexperience/floppy_disk.png similarity index 100% rename from public_html/static/iconexperience/floppy_disk.png rename to django_app/static/iconexperience/floppy_disk.png diff --git a/public_html/static/iconexperience/floppy_disk_green.png b/django_app/static/iconexperience/floppy_disk_green.png similarity index 100% rename from public_html/static/iconexperience/floppy_disk_green.png rename to django_app/static/iconexperience/floppy_disk_green.png diff --git a/public_html/static/iconexperience/garbage.png b/django_app/static/iconexperience/garbage.png similarity index 100% rename from public_html/static/iconexperience/garbage.png rename to django_app/static/iconexperience/garbage.png diff --git a/public_html/static/iconexperience/gear_edit.png b/django_app/static/iconexperience/gear_edit.png similarity index 100% rename from public_html/static/iconexperience/gear_edit.png rename to django_app/static/iconexperience/gear_edit.png diff --git a/django_app/static/iconexperience/key.png b/django_app/static/iconexperience/key.png new file mode 100644 index 0000000..f6f4519 Binary files /dev/null and b/django_app/static/iconexperience/key.png differ diff --git a/public_html/static/iconexperience/led_blue.png b/django_app/static/iconexperience/led_blue.png similarity index 100% rename from public_html/static/iconexperience/led_blue.png rename to django_app/static/iconexperience/led_blue.png diff --git a/public_html/static/iconexperience/led_green.png b/django_app/static/iconexperience/led_green.png similarity index 100% rename from public_html/static/iconexperience/led_green.png rename to django_app/static/iconexperience/led_green.png diff --git a/public_html/static/iconexperience/led_red.png b/django_app/static/iconexperience/led_red.png similarity index 100% rename from public_html/static/iconexperience/led_red.png rename to django_app/static/iconexperience/led_red.png diff --git a/public_html/static/iconexperience/led_white.png b/django_app/static/iconexperience/led_white.png similarity index 100% rename from public_html/static/iconexperience/led_white.png rename to django_app/static/iconexperience/led_white.png diff --git a/public_html/static/iconexperience/led_yellow.png b/django_app/static/iconexperience/led_yellow.png similarity index 100% rename from public_html/static/iconexperience/led_yellow.png rename to django_app/static/iconexperience/led_yellow.png diff --git a/public_html/static/iconexperience/lightbulb.png b/django_app/static/iconexperience/lightbulb.png similarity index 100% rename from public_html/static/iconexperience/lightbulb.png rename to django_app/static/iconexperience/lightbulb.png diff --git a/public_html/static/iconexperience/lightbulb_on.png b/django_app/static/iconexperience/lightbulb_on.png similarity index 100% rename from public_html/static/iconexperience/lightbulb_on.png rename to django_app/static/iconexperience/lightbulb_on.png diff --git a/django_app/static/iconexperience/lock_open.png b/django_app/static/iconexperience/lock_open.png new file mode 100644 index 0000000..7ff3c9c Binary files /dev/null and b/django_app/static/iconexperience/lock_open.png differ diff --git a/django_app/static/iconexperience/lock_warning.png b/django_app/static/iconexperience/lock_warning.png new file mode 100644 index 0000000..2b9105c Binary files /dev/null and b/django_app/static/iconexperience/lock_warning.png differ diff --git a/public_html/static/iconexperience/loudspeaker.png b/django_app/static/iconexperience/loudspeaker.png similarity index 100% rename from public_html/static/iconexperience/loudspeaker.png rename to django_app/static/iconexperience/loudspeaker.png diff --git a/public_html/static/iconexperience/loudspeaker2.png b/django_app/static/iconexperience/loudspeaker2.png similarity index 100% rename from public_html/static/iconexperience/loudspeaker2.png rename to django_app/static/iconexperience/loudspeaker2.png diff --git a/public_html/static/iconexperience/moon.png b/django_app/static/iconexperience/moon.png similarity index 100% rename from public_html/static/iconexperience/moon.png rename to django_app/static/iconexperience/moon.png diff --git a/public_html/static/iconexperience/moon_half.png b/django_app/static/iconexperience/moon_half.png similarity index 100% rename from public_html/static/iconexperience/moon_half.png rename to django_app/static/iconexperience/moon_half.png diff --git a/public_html/static/iconexperience/purchase_order_cart.png b/django_app/static/iconexperience/purchase_order_cart.png similarity index 100% rename from public_html/static/iconexperience/purchase_order_cart.png rename to django_app/static/iconexperience/purchase_order_cart.png diff --git a/public_html/static/iconexperience/shelf.png b/django_app/static/iconexperience/shelf.png similarity index 100% rename from public_html/static/iconexperience/shelf.png rename to django_app/static/iconexperience/shelf.png diff --git a/public_html/static/iconexperience/shopping_cart.png b/django_app/static/iconexperience/shopping_cart.png similarity index 100% rename from public_html/static/iconexperience/shopping_cart.png rename to django_app/static/iconexperience/shopping_cart.png diff --git a/public_html/static/iconexperience/signal_flag_checkered.png b/django_app/static/iconexperience/signal_flag_checkered.png similarity index 100% rename from public_html/static/iconexperience/signal_flag_checkered.png rename to django_app/static/iconexperience/signal_flag_checkered.png diff --git a/public_html/static/iconexperience/signal_flag_green.png b/django_app/static/iconexperience/signal_flag_green.png similarity index 100% rename from public_html/static/iconexperience/signal_flag_green.png rename to django_app/static/iconexperience/signal_flag_green.png diff --git a/public_html/static/iconexperience/signal_flag_red.png b/django_app/static/iconexperience/signal_flag_red.png similarity index 100% rename from public_html/static/iconexperience/signal_flag_red.png rename to django_app/static/iconexperience/signal_flag_red.png diff --git a/public_html/static/iconexperience/signal_flag_white.png b/django_app/static/iconexperience/signal_flag_white.png similarity index 100% rename from public_html/static/iconexperience/signal_flag_white.png rename to django_app/static/iconexperience/signal_flag_white.png diff --git a/public_html/static/iconexperience/signal_flag_yellow.png b/django_app/static/iconexperience/signal_flag_yellow.png similarity index 100% rename from public_html/static/iconexperience/signal_flag_yellow.png rename to django_app/static/iconexperience/signal_flag_yellow.png diff --git a/public_html/static/iconexperience/sort_down_minus.png b/django_app/static/iconexperience/sort_down_minus.png similarity index 100% rename from public_html/static/iconexperience/sort_down_minus.png rename to django_app/static/iconexperience/sort_down_minus.png diff --git a/public_html/static/iconexperience/step.png b/django_app/static/iconexperience/step.png similarity index 100% rename from public_html/static/iconexperience/step.png rename to django_app/static/iconexperience/step.png diff --git a/public_html/static/iconexperience/symbol_questionmark.png b/django_app/static/iconexperience/symbol_questionmark.png similarity index 100% rename from public_html/static/iconexperience/symbol_questionmark.png rename to django_app/static/iconexperience/symbol_questionmark.png diff --git a/public_html/static/iconexperience/truck_green.png b/django_app/static/iconexperience/truck_green.png similarity index 100% rename from public_html/static/iconexperience/truck_green.png rename to django_app/static/iconexperience/truck_green.png diff --git a/public_html/static/lazo_negro.png b/django_app/static/lazo_negro.png similarity index 100% rename from public_html/static/lazo_negro.png rename to django_app/static/lazo_negro.png diff --git a/django_app/static/lock_open.png b/django_app/static/lock_open.png new file mode 100644 index 0000000..7ff3c9c Binary files /dev/null and b/django_app/static/lock_open.png differ diff --git a/public_html/static/logo-account.png b/django_app/static/logo-account.png similarity index 100% rename from public_html/static/logo-account.png rename to django_app/static/logo-account.png diff --git a/public_html/static/logo-arroz.png b/django_app/static/logo-arroz.png similarity index 100% rename from public_html/static/logo-arroz.png rename to django_app/static/logo-arroz.png diff --git a/public_html/static/logo-aulatek.png b/django_app/static/logo-aulatek.png similarity index 100% rename from public_html/static/logo-aulatek.png rename to django_app/static/logo-aulatek.png diff --git a/public_html/static/logo-club.png b/django_app/static/logo-club.png similarity index 100% rename from public_html/static/logo-club.png rename to django_app/static/logo-club.png diff --git a/public_html/static/logo-entreaulas.old.png b/django_app/static/logo-entreaulas.old.png similarity index 100% rename from public_html/static/logo-entreaulas.old.png rename to django_app/static/logo-entreaulas.old.png diff --git a/public_html/static/logo-entreaulas.png b/django_app/static/logo-entreaulas.png similarity index 100% rename from public_html/static/logo-entreaulas.png rename to django_app/static/logo-entreaulas.png diff --git a/public_html/static/logo-hyper.png b/django_app/static/logo-hyper.png similarity index 100% rename from public_html/static/logo-hyper.png rename to django_app/static/logo-hyper.png diff --git a/public_html/static/logo-mail.png b/django_app/static/logo-mail.png similarity index 100% rename from public_html/static/logo-mail.png rename to django_app/static/logo-mail.png diff --git a/public_html/static/logo-malla.png b/django_app/static/logo-malla.png similarity index 100% rename from public_html/static/logo-malla.png rename to django_app/static/logo-malla.png diff --git a/public_html/static/logo-media.png b/django_app/static/logo-media.png similarity index 100% rename from public_html/static/logo-media.png rename to django_app/static/logo-media.png diff --git a/public_html/static/logo-nk4.old.svg b/django_app/static/logo-nk4.old.svg similarity index 100% rename from public_html/static/logo-nk4.old.svg rename to django_app/static/logo-nk4.old.svg diff --git a/public_html/static/logo-nk4.png b/django_app/static/logo-nk4.png similarity index 100% rename from public_html/static/logo-nk4.png rename to django_app/static/logo-nk4.png diff --git a/public_html/static/logo-nube.png b/django_app/static/logo-nube.png similarity index 100% rename from public_html/static/logo-nube.png rename to django_app/static/logo-nube.png diff --git a/public_html/static/logo-old.png b/django_app/static/logo-old.png similarity index 100% rename from public_html/static/logo-old.png rename to django_app/static/logo-old.png diff --git a/public_html/static/logo-oscar.png b/django_app/static/logo-oscar.png similarity index 100% rename from public_html/static/logo-oscar.png rename to django_app/static/logo-oscar.png diff --git a/public_html/static/logo-sysadmin.png b/django_app/static/logo-sysadmin.png similarity index 100% rename from public_html/static/logo-sysadmin.png rename to django_app/static/logo-sysadmin.png diff --git a/public_html/static/logo-telesec.png b/django_app/static/logo-telesec.png similarity index 100% rename from public_html/static/logo-telesec.png rename to django_app/static/logo-telesec.png diff --git a/public_html/static/logo.png b/django_app/static/logo.png similarity index 100% rename from public_html/static/logo.png rename to django_app/static/logo.png diff --git a/public_html/static/manifest.json b/django_app/static/manifest.json similarity index 100% rename from public_html/static/manifest.json rename to django_app/static/manifest.json diff --git a/public_html/static/masonry.pkgd.min.js b/django_app/static/masonry.pkgd.min.js similarity index 100% rename from public_html/static/masonry.pkgd.min.js rename to django_app/static/masonry.pkgd.min.js diff --git a/public_html/static/picnic.min.css b/django_app/static/picnic.min.css similarity index 100% rename from public_html/static/picnic.min.css rename to django_app/static/picnic.min.css diff --git a/public_html/static/portugalete.jpg b/django_app/static/portugalete.jpg similarity index 100% rename from public_html/static/portugalete.jpg rename to django_app/static/portugalete.jpg diff --git a/public_html/static/sounds/click.mp3 b/django_app/static/sounds/click.mp3 similarity index 100% rename from public_html/static/sounds/click.mp3 rename to django_app/static/sounds/click.mp3 diff --git a/public_html/static/sounds/lose.mp3 b/django_app/static/sounds/lose.mp3 similarity index 100% rename from public_html/static/sounds/lose.mp3 rename to django_app/static/sounds/lose.mp3 diff --git a/public_html/static/sounds/win.mp3 b/django_app/static/sounds/win.mp3 similarity index 100% rename from public_html/static/sounds/win.mp3 rename to django_app/static/sounds/win.mp3 diff --git a/django_app/templates/aulatek/alumnos.html b/django_app/templates/aulatek/alumnos.html index 7faeeee..d46d6da 100644 --- a/django_app/templates/aulatek/alumnos.html +++ b/django_app/templates/aulatek/alumnos.html @@ -6,6 +6,9 @@ {% if aulario_id %}

Aulario: {{ aulario_id }}

Nuevo alumno + {% if can_view_cuadernos %} + Cuadernos + {% endif %} {% else %}

No se ha indicado un aulario valido.

{% endif %} @@ -31,7 +34,7 @@
Editar -
+ {% csrf_token %}
diff --git a/django_app/templates/aulatek/comedor.html b/django_app/templates/aulatek/comedor.html index ac11f3a..1542115 100644 --- a/django_app/templates/aulatek/comedor.html +++ b/django_app/templates/aulatek/comedor.html @@ -246,7 +246,7 @@ {% if selected_menu %} -
+ {% csrf_token %} @@ -317,7 +317,7 @@
-
+ {% csrf_token %} diff --git a/django_app/templates/aulatek/cuaderno_edit.html b/django_app/templates/aulatek/cuaderno_edit.html new file mode 100644 index 0000000..0cf8b20 --- /dev/null +++ b/django_app/templates/aulatek/cuaderno_edit.html @@ -0,0 +1,47 @@ +{% extends 'core/base.html' %} + +{% block content %} +
+

Cuaderno: {{ cuaderno.titulo }}

+

+ {% if cuaderno.alumno %}Alumno: {{ cuaderno.alumno }}{% else %}Alumno sin especificar{% endif %} +

+

Aulario: {{ aulario_id }}

+

Volver a Cuadernos

+
+ +
+

Editar contenido

+ + {% csrf_token %} + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + + +
+ +
+

Eliminar cuaderno

+

Esta accion es irreversible y requiere firma para autorizar el cambio.

+
+ {% csrf_token %} + + +
+
+{% endblock %} diff --git a/django_app/templates/aulatek/cuaderno_new.html b/django_app/templates/aulatek/cuaderno_new.html new file mode 100644 index 0000000..e37b254 --- /dev/null +++ b/django_app/templates/aulatek/cuaderno_new.html @@ -0,0 +1,37 @@ +{% extends 'core/base.html' %} + +{% block content %} +
+

{{ module_title }}

+

Aulario: {{ aulario_id }}

+

Volver a Cuadernos

+
+ +
+

Nuevo Cuaderno

+

El documento se guarda en texto plano y podras editarlo en cualquier momento.

+
+ {% csrf_token %} +
+
+ + + + {% for alumno in alumnos %} + + {% endfor %} + +
+
+ + +
+
+
+ + +
+ +
+
+{% endblock %} diff --git a/django_app/templates/aulatek/cuadernos.html b/django_app/templates/aulatek/cuadernos.html new file mode 100644 index 0000000..dc42253 --- /dev/null +++ b/django_app/templates/aulatek/cuadernos.html @@ -0,0 +1,37 @@ +{% extends 'core/base.html' %} + +{% block content %} +
+

{{ module_title }}

+ {% if aulario_id %} +

Aulario: {{ aulario_id }}

+ Nuevo cuaderno + {% else %} +

No se ha indicado un aulario valido.

+ {% endif %} +
+ + {% if aulario_id %} +
+ {% for cuaderno in cuadernos %} +
+
+
+
{{ cuaderno.titulo }}
+
+ {% if cuaderno.alumno %}Alumno: {{ cuaderno.alumno }}{% else %}Alumno sin especificar{% endif %} +
+
+ Documento +
+
+ Actualizado: {{ cuaderno.updated_at|date:'d/m/Y H:i' }} +
+ Abrir / Editar +
+ {% empty %} +
Todavia no hay cuadernos en este aulario.
+ {% endfor %} +
+ {% endif %} +{% endblock %} diff --git a/django_app/templates/core/base.html b/django_app/templates/core/base.html index f2e51d7..8e01cf2 100644 --- a/django_app/templates/core/base.html +++ b/django_app/templates/core/base.html @@ -359,6 +359,113 @@ font-size: 14px; } + .firma-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: none; + align-items: center; + justify-content: center; + z-index: 2000; + padding: 16px; + } + + .firma-backdrop.open { + display: flex; + } + + .firma-card { + width: min(760px, 96vw); + background: #fff; + border-radius: 14px; + border: 1px solid var(--gw-border); + box-shadow: 0 18px 44px rgba(0, 0, 0, 0.25); + padding: 24px; + } + + .firma-title { + font-size: 1.45rem; + font-weight: 700; + margin-bottom: 6px; + } + + .firma-subtitle { + color: #5f6368; + margin-bottom: 14px; + font-size: 1.04rem; + } + + .firma-slider-wrap { + background: #f1f3f4; + border: 1px solid #dadce0; + border-radius: 999px; + padding: 16px 18px; + margin-bottom: 12px; + min-height: 92px; + display: flex; + align-items: center; + } + + .firma-slider { + width: 100%; + /* height: 72px; */ + accent-color: #1a73e8; + touch-action: pan-y; + -webkit-appearance: none; + appearance: none; + background: transparent; + } + + .firma-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 72px; + height: 72px; + border-radius: 50%; + background: #1a73e8; + border: 4px solid #fff; + box-shadow: 0 5px 14px rgba(0, 0, 0, 0.32); + cursor: pointer; + top: -35.5px; + position: relative; + } + + .firma-slider::-moz-range-thumb { + width: 72px; + height: 72px; + border-radius: 50%; + background: #1a73e8; + border: 4px solid #fff; + box-shadow: 0 5px 14px rgba(0, 0, 0, 0.32); + cursor: pointer; + top: -35.5px; + position: relative; + } + + .firma-slider::-webkit-slider-runnable-track { + height: 0; + border-radius: 999px; + background: transparent; + } + + .firma-slider::-moz-range-track { + height: 0; + border-radius: 999px; + background: transparent; + border: none; + } + + .firma-slider::-moz-range-progress { + background: transparent; + } + + .firma-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 12px; + } + @media (max-width: 768px) { .search-bar { display: none; @@ -548,8 +655,106 @@
+ + {% block extra_scripts %}{% endblock %} \ No newline at end of file diff --git a/django_app/templates/core/sysadmin_invitations.html b/django_app/templates/core/sysadmin_invitations.html index 2d44868..f240bda 100644 --- a/django_app/templates/core/sysadmin_invitations.html +++ b/django_app/templates/core/sysadmin_invitations.html @@ -35,7 +35,7 @@ {% csrf_token %} -
+ {% csrf_token %}
diff --git a/django_app/templates/core/sysadmin_orgs.html b/django_app/templates/core/sysadmin_orgs.html index c4aba13..9fb7177 100644 --- a/django_app/templates/core/sysadmin_orgs.html +++ b/django_app/templates/core/sysadmin_orgs.html @@ -38,7 +38,7 @@
ID: {{ org.org_id }}
Editar -
+ {% csrf_token %}
diff --git a/django_app/templates/core/sysadmin_users.html b/django_app/templates/core/sysadmin_users.html index b3b57d1..be9e6eb 100644 --- a/django_app/templates/core/sysadmin_users.html +++ b/django_app/templates/core/sysadmin_users.html @@ -65,7 +65,7 @@
Editar -
+ {% csrf_token %}
diff --git a/public_html/_incl/auth_redir.php b/public_html/_incl/auth_redir.php deleted file mode 100755 index f3ed008..0000000 --- a/public_html/_incl/auth_redir.php +++ /dev/null @@ -1,15 +0,0 @@ -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); - $pdo->exec('PRAGMA journal_mode = WAL'); - $pdo->exec('PRAGMA foreign_keys = ON'); - $pdo->exec('PRAGMA synchronous = NORMAL'); - - db_migrate($pdo); - - return $pdo; -} - -// ── Migration runner ────────────────────────────────────────────────────────── - -function db_migrate(PDO $pdo): void -{ - $pdo->exec( - 'CREATE TABLE IF NOT EXISTS schema_migrations ( - version INTEGER PRIMARY KEY, - applied_at TEXT NOT NULL DEFAULT (datetime(\'now\')) - )' - ); - - $applied = $pdo->query('SELECT version FROM schema_migrations ORDER BY version') - ->fetchAll(PDO::FETCH_COLUMN); - - $files = glob(MIGRATIONS_DIR . '/*.{sql,php}', GLOB_BRACE) ?: []; - sort($files); - - foreach ($files as $file) { - if (!preg_match('/^(\d+)/', basename($file), $m)) { - continue; - } - $version = (int) $m[1]; - if (in_array($version, $applied, true)) { - continue; - } - - if (str_ends_with($file, '.sql')) { - $pdo->exec((string) file_get_contents($file)); - } elseif (str_ends_with($file, '.php')) { - // PHP migration receives the connection as $db - $db = $pdo; - require $file; - } - - $pdo->prepare('INSERT INTO schema_migrations (version) VALUES (?)')->execute([$version]); - } -} - -// ── Config helpers ──────────────────────────────────────────────────────────── - -function db_get_config(string $key, $default = null) -{ - $stmt = db()->prepare('SELECT value FROM config WHERE key = ?'); - $stmt->execute([$key]); - $row = $stmt->fetch(); - if ($row === false) { - return $default; - } - $decoded = json_decode($row['value'], true); - return $decoded !== null ? $decoded : $row['value']; -} - -function db_set_config(string $key, $value): void -{ - db()->prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)') - ->execute([$key, is_string($value) ? $value : json_encode($value)]); -} - -function db_get_all_config(): array -{ - $rows = db()->query('SELECT key, value FROM config')->fetchAll(); - $result = []; - foreach ($rows as $row) { - $decoded = json_decode($row['value'], true); - $result[$row['key']] = ($decoded !== null) ? $decoded : $row['value']; - } - return $result; -} - -// ── User helpers ────────────────────────────────────────────────────────────── - -/** Find a user by username or email (always lower-cased). Returns DB row or null. */ -function db_get_user(string $username): ?array -{ - if (str_contains($username, "@")) { - $stmt = db()->prepare('SELECT * FROM users WHERE email = ?'); - $stmt->execute([strtolower($username)]); - $row = $stmt->fetch(); - return $row !== false ? $row : null; - } - $stmt = db()->prepare('SELECT * FROM users WHERE username = ?'); - $stmt->execute([strtolower($username)]); - $row = $stmt->fetch(); - return $row !== false ? $row : null; -} - -/** Return all user rows ordered by username. */ -function db_get_all_users(): array -{ - return db()->query('SELECT * FROM users ORDER BY username')->fetchAll(); -} - -/** - * Build the auth_data session array from a DB user row. - * Preserves the same format existing code expects: - * auth_data.permissions, auth_data.active_organizations auth_data.organizations. - */ -function db_build_auth_data(array $row): array -{ - $permissions = json_decode($row['permissions'] ?? '[]', true) ?: []; - $meta = json_decode($row['meta'] ?? '{}', true) ?: []; - $ea = [ - 'organization' => '', - 'organizations' => [], - 'role' => '', - 'aulas' => [], - 'organizations_data' => [], - ]; - - // Fetch all organization assignments for this user - $stmt = db()->prepare( - 'SELECT org_id, role, ea_aulas - FROM user_orgs - WHERE user_id = ? - ORDER BY org_id' - ); - $stmt->execute([$row['id']]); - $org_rows = $stmt->fetchAll(); - $orgs = []; - if (!empty($org_rows)) { - $first = $org_rows[0]; - foreach ($org_rows as $r) { - $orgs[] = $r['org_id']; - } - $ea['organization'] = $first['org_id']; - $ea['role'] = $first['role']; - $ea['aulas'] = json_decode($first['ea_aulas'] ?? '[]', true) ?: []; - $ea['organizations'] = $orgs; - $ea['organizations_data'] = $org_rows; - } - - $active_org = $ea['organization'] ?? ''; - $aulatek = [ - 'organizacion' => $active_org, - 'organizaciones' => $orgs, - 'organization' => $active_org, - 'organizations' => $orgs, - 'centro' => $active_org, - 'centros' => $orgs, - 'role' => $ea['role'] ?? '', - 'aulas' => $ea['aulas'] ?? [], - ]; - - return array_merge($meta, [ - 'display_name' => $row['display_name'], - 'email' => $row['email'], - 'password_hash' => $row['password_hash'], - 'permissions' => $permissions, - 'orgs' => $orgs, - 'organizations' => $orgs, - 'active_organization' => $active_org, - 'active_organizations' => $ea, - 'aulatek' => $aulatek, - 'entreaulas' => $aulatek, - 'google_auth' => (bool) $row['google_auth'], - ]); -} - -/** - * Create or update a user. - * $data keys: username, display_name, email, password_hash, permissions[], - * google_auth, entreaulas{organizacion,organizaciones[],role,aulas[]}, + any extra meta. - * Returns the user ID. - */ -function db_upsert_user(array $data): int -{ - $pdo = db(); - $username = strtolower((string) ($data['username'] ?? '')); - - $existing = $pdo->prepare('SELECT id FROM users WHERE username = ?'); - $existing->execute([$username]); - $existing_row = $existing->fetch(); - - $permissions = json_encode($data['permissions'] ?? []); - $meta_skip = ['username', 'display_name', 'email', 'password_hash', - 'permissions', 'entreaulas', 'google_auth', - 'orgs', 'organizations', 'organization', 'organizacion', - 'role', 'aulas']; - $meta = []; - foreach ($data as $k => $v) { - if (!in_array($k, $meta_skip, true)) { - $meta[$k] = $v; - } - } - - if ($existing_row) { - $user_id = (int) $existing_row['id']; - $upd = $pdo->prepare( - "UPDATE users SET - display_name = ?, - email = ?, - permissions = ?, - google_auth = ?, - meta = ?, - updated_at = datetime('now') - WHERE id = ?" - ); - $upd->execute([ - $data['display_name'] ?? '', - $data['email'] ?? '', - $permissions, - (int) ($data['google_auth'] ?? 0), - json_encode($meta), - $user_id, - ]); - if (!empty($data['password_hash'])) { - $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?') - ->execute([$data['password_hash'], $user_id]); - } - } else { - $pdo->prepare( - 'INSERT INTO users (username, display_name, email, password_hash, permissions, google_auth, meta) - VALUES (?, ?, ?, ?, ?, ?, ?)' - )->execute([ - $username, - $data['display_name'] ?? '', - $data['email'] ?? '', - $data['password_hash'] ?? '', - $permissions, - (int) ($data['google_auth'] ?? 0), - json_encode($meta), - ]); - $user_id = (int) $pdo->lastInsertId(); - } - - // Update organization assignments if tenant data is provided. - $has_org_payload = array_key_exists('entreaulas', $data) - || array_key_exists('orgs', $data) - || array_key_exists('organizations', $data) - || array_key_exists('organization', $data) - || array_key_exists('organizacion', $data); - - if ($has_org_payload) { - $ea = $data['entreaulas'] ?? []; - - $organizations = []; - $candidate_lists = [ - $data['organizations'] ?? null, - $data['orgs'] ?? null, - $ea['organizaciones'] ?? null, - $ea['organizations'] ?? null, - $ea['centros'] ?? null, - ]; - foreach ($candidate_lists as $list) { - if (is_array($list) && !empty($list)) { - $organizations = $list; - break; - } - } - if (empty($organizations)) { - foreach ([ - $data['organization'] ?? null, - $data['organizacion'] ?? null, - $ea['organizacion'] ?? null, - $ea['organization'] ?? null, - $ea['centro'] ?? null, - ] as $single) { - if (!empty($single)) { - $organizations = [$single]; - break; - } - } - } - - $organizations = array_values(array_unique(array_filter(array_map( - static function ($value): string { - return preg_replace('/[^a-zA-Z0-9._-]/', '', (string) $value); - }, - $organizations - )))); - - $role = (string) ($data['role'] ?? $ea['role'] ?? ''); - $aulas_payload = $data['aulas'] ?? $ea['aulas'] ?? []; - if (!is_array($aulas_payload)) { - $aulas_payload = []; - } - $aulas = json_encode($aulas_payload, JSON_UNESCAPED_UNICODE); - - $pdo->prepare('DELETE FROM user_orgs WHERE user_id = ?')->execute([$user_id]); - - $ins_org = $pdo->prepare('INSERT OR IGNORE INTO organizaciones (org_id, org_name) VALUES (?, ?)'); - $ins_uo = $pdo->prepare( - 'INSERT OR REPLACE INTO user_orgs (user_id, org_id, role, ea_aulas) VALUES (?, ?, ?, ?)' - ); - foreach ($organizations as $org_id) { - if ($org_id === '') { - continue; - } - $ins_org->execute([$org_id, $org_id]); - $ins_uo->execute([$user_id, $org_id, $role, $aulas]); - } - } - - return $user_id; -} - -/** Delete a user and their organization assignments. */ -function db_delete_user(string $username): void -{ - db()->prepare('DELETE FROM users WHERE username = ?')->execute([strtolower($username)]); -} - -// ── Organization helpers ───────────────────────────────────────────────────── - -function db_get_organizations(): array -{ - return db()->query('SELECT org_id, org_name FROM organizaciones ORDER BY org_id')->fetchAll(); -} - -function db_get_organization_ids(): array -{ - return db()->query('SELECT org_id FROM organizaciones ORDER BY org_id')->fetchAll(PDO::FETCH_COLUMN); -} - -function db_get_organizaciones(): array -{ - return db_get_organizations(); -} - -function get_organizations(): array -{ - return db_get_organizations(); -} - -function db_get_centros(): array -{ - $rows = db_get_organizations(); - return array_map(static function (array $row): array { - return [ - 'centro_id' => $row['org_id'], - 'name' => $row['org_name'], - ]; - }, $rows); -} - -function db_get_centro_ids(): array -{ - return db_get_organization_ids(); -} - -// ── Aulario helpers ─────────────────────────────────────────────────────────── - -/** Get a single aulario config. Returns merged array (name, icon, + extra fields) or null. */ -function db_get_aulario(string $centro_id, string $aulario_id): ?array -{ - $stmt = db()->prepare( - 'SELECT name, icon, extra FROM aularios WHERE org_id = ? AND aulario_id = ?' - ); - $stmt->execute([$centro_id, $aulario_id]); - $row = $stmt->fetch(); - if ($row === false) { - return null; - } - $extra = json_decode($row['extra'] ?? '{}', true) ?: []; - return array_merge($extra, ['name' => $row['name'], 'icon' => $row['icon']]); -} - -/** Get all aularios for a centro as aulario_id → config array. */ -function db_get_aularios(string $centro_id): array -{ - $stmt = db()->prepare( - 'SELECT aulario_id, name, icon, extra FROM aularios WHERE org_id = ? ORDER BY aulario_id' - ); - $stmt->execute([$centro_id]); - $result = []; - foreach ($stmt->fetchAll() as $row) { - $extra = json_decode($row['extra'] ?? '{}', true) ?: []; - $result[$row['aulario_id']] = array_merge($extra, [ - 'name' => $row['name'], - 'icon' => $row['icon'], - ]); - } - return $result; -} - -// ── SuperCafe helpers ───────────────────────────────────────────────────────── - -function db_get_supercafe_menu(string $centro_id): array -{ - $stmt = db()->prepare('SELECT data FROM supercafe_menu WHERE org_id = ?'); - $stmt->execute([$centro_id]); - $row = $stmt->fetch(); - if ($row === false) { - return []; - } - return json_decode($row['data'], true) ?: []; -} - -function db_set_supercafe_menu(string $centro_id, array $menu): void -{ - db()->prepare('INSERT OR REPLACE INTO supercafe_menu (org_id, data, updated_at) VALUES (?, ?, datetime(\'now\'))') - ->execute([$centro_id, json_encode($menu, JSON_UNESCAPED_UNICODE)]); -} - -/** Return all SC orders for a centro as an array of rows. */ -function db_get_supercafe_orders(string $centro_id): array -{ - $stmt = db()->prepare( - 'SELECT * FROM supercafe_orders WHERE org_id = ? ORDER BY created_at DESC' - ); - $stmt->execute([$centro_id]); - return $stmt->fetchAll(); -} - -/** Return a single SC order by ref, or null. */ -function db_get_supercafe_order(string $centro_id, string $order_ref): ?array -{ - $stmt = db()->prepare( - 'SELECT * FROM supercafe_orders WHERE org_id = ? AND order_ref = ?' - ); - $stmt->execute([$centro_id, $order_ref]); - $row = $stmt->fetch(); - return $row !== false ? $row : null; -} - -/** Create or update an SC order. */ -function db_upsert_supercafe_order( - string $centro_id, - string $order_ref, - string $fecha, - string $persona, - string $comanda, - string $notas, - string $estado -): void { - db()->prepare( - 'INSERT INTO supercafe_orders (org_id, order_ref, fecha, persona, comanda, notas, estado) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(org_id, order_ref) DO UPDATE SET - fecha = excluded.fecha, - persona = excluded.persona, - comanda = excluded.comanda, - notas = excluded.notas, - estado = excluded.estado' - )->execute([$centro_id, $order_ref, $fecha, $persona, $comanda, $notas, $estado]); -} - -/** Generate the next order_ref for a centro (sc001, sc002, …). */ -function db_next_supercafe_ref(string $centro_id): string -{ - $stmt = db()->prepare( - "SELECT order_ref FROM supercafe_orders WHERE org_id = ? ORDER BY id DESC LIMIT 1" - ); - $stmt->execute([$centro_id]); - $last = $stmt->fetchColumn(); - $n = 0; - if ($last && preg_match('/^sc(\d+)$/', $last, $m)) { - $n = (int) $m[1]; - } - return 'sc' . str_pad($n + 1, 3, '0', STR_PAD_LEFT); -} - -/** Count 'Deuda' orders for a persona in a centro. */ -function db_supercafe_count_debts(string $centro_id, string $persona_key): int -{ - $stmt = db()->prepare( - "SELECT COUNT(*) FROM supercafe_orders WHERE org_id = ? AND persona = ? AND estado = 'Deuda'" - ); - $stmt->execute([$centro_id, $persona_key]); - return (int) $stmt->fetchColumn(); -} - -// ── Comedor helpers ─────────────────────────────────────────────────────────── - -function db_get_comedor_menu_types(string $centro_id, string $aulario_id): array -{ - $stmt = db()->prepare( - 'SELECT data FROM comedor_menu_types WHERE org_id = ? AND aulario_id = ?' - ); - $stmt->execute([$centro_id, $aulario_id]); - $row = $stmt->fetch(); - if ($row === false) { - return []; - } - return json_decode($row['data'], true) ?: []; -} - -function db_set_comedor_menu_types(string $centro_id, string $aulario_id, array $types): void -{ - db()->prepare( - 'INSERT OR REPLACE INTO comedor_menu_types (org_id, aulario_id, data) VALUES (?, ?, ?)' - )->execute([$centro_id, $aulario_id, json_encode($types, JSON_UNESCAPED_UNICODE)]); -} - -function db_get_comedor_entry(string $centro_id, string $aulario_id, string $ym, string $day): array -{ - $stmt = db()->prepare( - 'SELECT data FROM comedor_entries WHERE org_id = ? AND aulario_id = ? AND year_month = ? AND day = ?' - ); - $stmt->execute([$centro_id, $aulario_id, $ym, $day]); - $row = $stmt->fetch(); - if ($row === false) { - return []; - } - return json_decode($row['data'], true) ?: []; -} - -function db_set_comedor_entry(string $centro_id, string $aulario_id, string $ym, string $day, array $data): void -{ - db()->prepare( - 'INSERT OR REPLACE INTO comedor_entries (org_id, aulario_id, year_month, day, data) VALUES (?, ?, ?, ?, ?)' - )->execute([$centro_id, $aulario_id, $ym, $day, json_encode($data, JSON_UNESCAPED_UNICODE)]); -} - -// ── Diario helpers ──────────────────────────────────────────────────────────── - -function db_get_diario_entry(string $centro_id, string $aulario_id, string $entry_date): array -{ - $stmt = db()->prepare( - 'SELECT data FROM diario_entries WHERE org_id = ? AND aulario_id = ? AND entry_date = ?' - ); - $stmt->execute([$centro_id, $aulario_id, $entry_date]); - $row = $stmt->fetch(); - if ($row === false) { - return []; - } - return json_decode($row['data'], true) ?: []; -} - -function db_set_diario_entry(string $centro_id, string $aulario_id, string $entry_date, array $data): void -{ - db()->prepare( - 'INSERT OR REPLACE INTO diario_entries (org_id, aulario_id, entry_date, data) VALUES (?, ?, ?, ?)' - )->execute([$centro_id, $aulario_id, $entry_date, json_encode($data, JSON_UNESCAPED_UNICODE)]); -} - -// ── Panel alumno helpers ────────────────────────────────────────────────────── - -function db_get_panel_alumno(string $centro_id, string $aulario_id, string $alumno): array -{ - $stmt = db()->prepare( - 'SELECT data FROM panel_alumno WHERE org_id = ? AND aulario_id = ? AND alumno = ?' - ); - $stmt->execute([$centro_id, $aulario_id, $alumno]); - $row = $stmt->fetch(); - if ($row === false) { - return []; - } - return json_decode($row['data'], true) ?: []; -} - -function db_set_panel_alumno(string $centro_id, string $aulario_id, string $alumno, array $data): void -{ - db()->prepare( - 'INSERT OR REPLACE INTO panel_alumno (org_id, aulario_id, alumno, data) VALUES (?, ?, ?, ?)' - )->execute([$centro_id, $aulario_id, $alumno, json_encode($data, JSON_UNESCAPED_UNICODE)]); -} - -// ── Invitation helpers ──────────────────────────────────────────────────────── - -function db_get_all_invitations(): array -{ - return db()->query('SELECT * FROM invitations ORDER BY code')->fetchAll(); -} - -function db_get_invitation(string $code): ?array -{ - $stmt = db()->prepare('SELECT * FROM invitations WHERE code = ?'); - $stmt->execute([strtoupper($code)]); - $row = $stmt->fetch(); - return $row !== false ? $row : null; -} - -function db_upsert_invitation(string $code, bool $active, bool $single_use): void -{ - db()->prepare( - 'INSERT OR REPLACE INTO invitations (code, active, single_use) VALUES (?, ?, ?)' - )->execute([strtoupper($code), (int) $active, (int) $single_use]); -} - -function db_deactivate_invitation(string $code): void -{ - db()->prepare('UPDATE invitations SET active = 0 WHERE code = ?')->execute([strtoupper($code)]); -} - -function db_delete_invitation(string $code): void -{ - db()->prepare('DELETE FROM invitations WHERE code = ?')->execute([strtoupper($code)]); -} - -// ── Club helpers ────────────────────────────────────────────────────────────── - -function db_get_club_config(): array -{ - $stmt = db()->query('SELECT data FROM club_config WHERE id = 1'); - $row = $stmt->fetch(); - if ($row === false) { - return []; - } - return json_decode($row['data'], true) ?: []; -} - -function db_set_club_config(array $config): void -{ - db()->prepare('INSERT OR REPLACE INTO club_config (id, data) VALUES (1, ?)') - ->execute([json_encode($config, JSON_UNESCAPED_UNICODE)]); -} - -function db_get_all_club_events(): array -{ - return db()->query('SELECT date_ref, data FROM club_events ORDER BY date_ref DESC')->fetchAll(); -} - -function db_get_club_event(string $date_ref): array -{ - $stmt = db()->prepare('SELECT data FROM club_events WHERE date_ref = ?'); - $stmt->execute([$date_ref]); - $row = $stmt->fetch(); - if ($row === false) { - return []; - } - return json_decode($row['data'], true) ?: []; -} - -function db_set_club_event(string $date_ref, array $data): void -{ - db()->prepare('INSERT OR REPLACE INTO club_events (date_ref, data) VALUES (?, ?)') - ->execute([$date_ref, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]); -} - -// ── Multi-tenant helpers ────────────────────────────────────────────────────── - -/** Return all organization IDs the authenticated user belongs to. */ -function get_user_organizations(?array $auth_data = null): array -{ - $data = $auth_data ?? $_SESSION['auth_data'] ?? []; - $orgs = $data['organizations'] - ?? $data['orgs'] - ?? $data['aulatek']['organizaciones'] - ?? $data['aulatek']['organizations'] - ?? $data['aulatek']['centros'] - ?? $data['entreaulas']['organizaciones'] - ?? $data['entreaulas']['organizations'] - ?? $data['entreaulas']['centros'] - ?? []; - - if (!empty($orgs) && is_array($orgs)) { - return array_values(array_unique(array_filter($orgs, static function ($value): bool { - return is_string($value) && $value !== ''; - }))); - } - if (!empty($orgs)) { - return [(string) $orgs]; - } - - foreach ([ - $data['active_organization'] ?? null, - $data['aulatek']['organizacion'] ?? null, - $data['aulatek']['organization'] ?? null, - $data['aulatek']['centro'] ?? null, - $data['entreaulas']['organizacion'] ?? null, - $data['entreaulas']['organization'] ?? null, - $data['entreaulas']['centro'] ?? null, - ] as $single) { - if (is_string($single) && $single !== '') { - return [$single]; - } - } - - return []; -} - -/** Spanish alias used by pre-body.php menu rendering. */ -function get_user_organizaciones(?array $auth_data = null): array -{ - $org_ids = get_user_organizations($auth_data); - if (empty($org_ids)) { - return []; - } - $name_by_id = []; - foreach (db_get_organizations() as $org_row) { - $name_by_id[$org_row['org_id']] = $org_row['org_name']; - } - - $result = []; - foreach ($org_ids as $org_id) { - $result[$org_id] = $name_by_id[$org_id] ?? $org_id; - } - return $result; -} - -function get_user_centros(?array $auth_data = null): array -{ - return get_user_organizations($auth_data); -} - -/** Ensure active organization session keys are set and mirrored for legacy code. */ -function init_active_org(?array $auth_data = null): void -{ - $organizations = get_user_organizations($auth_data); - if (empty($organizations)) { - $_SESSION['active_organization'] = null; - $_SESSION['active_organizacion'] = null; - $_SESSION['active_centro'] = null; - return; - } - - $current = $_SESSION['active_organization'] - ?? $_SESSION['active_organizacion'] - ?? $_SESSION['active_centro'] - ?? null; - - if (!is_string($current) || !in_array($current, $organizations, true)) { - $current = $organizations[0]; - } - - $_SESSION['active_organization'] = $current; - $_SESSION['active_organizacion'] = $current; - $_SESSION['active_centro'] = $current; - - if (!isset($_SESSION['auth_data']) || !is_array($_SESSION['auth_data'])) { - $_SESSION['auth_data'] = []; - } - $_SESSION['auth_data']['active_organization'] = $current; - if (!isset($_SESSION['auth_data']['aulatek']) || !is_array($_SESSION['auth_data']['aulatek'])) { - $_SESSION['auth_data']['aulatek'] = []; - } - $_SESSION['auth_data']['aulatek']['organizacion'] = $current; - $_SESSION['auth_data']['aulatek']['organization'] = $current; - $_SESSION['auth_data']['aulatek']['centro'] = $current; - if (!isset($_SESSION['auth_data']['entreaulas']) || !is_array($_SESSION['auth_data']['entreaulas'])) { - $_SESSION['auth_data']['entreaulas'] = []; - } - $_SESSION['auth_data']['entreaulas']['organizacion'] = $current; - $_SESSION['auth_data']['entreaulas']['organization'] = $current; - $_SESSION['auth_data']['entreaulas']['centro'] = $current; -} - -function init_active_centro(?array $auth_data = null): void -{ - init_active_org($auth_data); -} - -// ── User session helpers (Dispositivos conectados) ──────────────────────────── - -/** - * Register a new session record in user_sessions. - * $remember_token_hash is the SHA-256 hash of the raw remember-me cookie value. - * Stores a SHA-256 hash of the PHP session_id so the raw token never reaches the DB. - */ -function db_register_session(string $username, string $remember_token_hash = ''): void -{ - $token = hash('sha256', session_id()); - $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] - ?? $_SERVER['REMOTE_ADDR'] - ?? ''; - $ip = trim(explode(',', $ip)[0]); - $ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512); - - db()->prepare( - "INSERT INTO user_sessions (session_token, username, ip_address, user_agent, remember_token_hash) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(session_token) DO UPDATE SET - last_active = datetime('now'), - remember_token_hash = COALESCE(excluded.remember_token_hash, remember_token_hash)" - )->execute([$token, strtolower($username), $ip, $ua, $remember_token_hash ?: null]); -} - -/** - * Restore a session from a remember-me token hash. - * Updates the session_token to the current PHP session_id so revocation - * continues to work after the PHP session was re-created. - * Returns the session row (including username) on success, or null if not found. - */ -function db_restore_session_by_remember_token(string $token_hash): ?array -{ - $pdo = db(); - $stmt = $pdo->prepare( - 'SELECT * FROM user_sessions WHERE remember_token_hash = ? LIMIT 1' - ); - $stmt->execute([$token_hash]); - $row = $stmt->fetch(); - if (!$row) { - return null; - } - // Migrate the row to the new PHP session_id - $new_session_token = hash('sha256', session_id()); - $pdo->prepare( - "UPDATE user_sessions - SET session_token = ?, last_active = datetime('now') - WHERE remember_token_hash = ?" - )->execute([$new_session_token, $token_hash]); - return $row; -} - -/** Update last_active for the current session. */ -function db_touch_session(): void -{ - $token = hash('sha256', session_id()); - db()->prepare( - "UPDATE user_sessions SET last_active = datetime('now') WHERE session_token = ?" - )->execute([$token]); -} - -/** Delete the current session record from the DB. */ -function db_delete_session(): void -{ - $token = hash('sha256', session_id()); - db()->prepare('DELETE FROM user_sessions WHERE session_token = ?')->execute([$token]); -} - -/** - * Delete a specific session by token for a given user. - * Enforces that the session belongs to the requesting user. - */ -function db_revoke_session(string $token, string $username): void -{ - db()->prepare('DELETE FROM user_sessions WHERE session_token = ? AND username = ?') - ->execute([$token, strtolower($username)]); -} - -/** Delete all session records for a user (e.g. on password change or account wipe). */ -function db_delete_user_sessions(string $username): void -{ - db()->prepare('DELETE FROM user_sessions WHERE username = ?') - ->execute([strtolower($username)]); -} - -/** Return all session rows for a user, newest first. */ -function db_get_user_sessions(string $username): array -{ - $stmt = db()->prepare( - 'SELECT session_token, ip_address, user_agent, created_at, last_active - FROM user_sessions - WHERE username = ? - ORDER BY last_active DESC' - ); - $stmt->execute([strtolower($username)]); - return $stmt->fetchAll(); -} - -/** - * Check whether the current PHP session has a valid record in user_sessions. - * Returns false if the session was revoked or was never registered. - */ -function db_session_is_valid(string $username): bool -{ - $token = hash('sha256', session_id()); - $stmt = db()->prepare( - 'SELECT 1 FROM user_sessions WHERE session_token = ? AND username = ? LIMIT 1' - ); - $stmt->execute([$token, strtolower($username)]); - return $stmt->fetchColumn() !== false; -} diff --git a/public_html/_incl/logout.php b/public_html/_incl/logout.php deleted file mode 100644 index 7b1b2a2..0000000 --- a/public_html/_incl/logout.php +++ /dev/null @@ -1,13 +0,0 @@ - time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"]; -setcookie("auth_token", "", $cookie_options_expired); -db_delete_session(); -session_unset(); -session_destroy(); -header("Location: $redir"); -die(); diff --git a/public_html/_incl/migrations/001_initial_schema.sql b/public_html/_incl/migrations/001_initial_schema.sql deleted file mode 100644 index a73a859..0000000 --- a/public_html/_incl/migrations/001_initial_schema.sql +++ /dev/null @@ -1,144 +0,0 @@ --- 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 '{}' -); diff --git a/public_html/_incl/migrations/002_import_json.php b/public_html/_incl/migrations/002_import_json.php deleted file mode 100644 index 869c674..0000000 --- a/public_html/_incl/migrations/002_import_json.php +++ /dev/null @@ -1,256 +0,0 @@ -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 - $stmt2 = $db->prepare("SELECT id FROM users WHERE username = ?"); - $stmt2->execute([$username]); - $user_id = (int) $stmt2->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) : '{}', - ]); - } -} diff --git a/public_html/_incl/migrations/003_organizaciones.sql b/public_html/_incl/migrations/003_organizaciones.sql deleted file mode 100644 index a526e72..0000000 --- a/public_html/_incl/migrations/003_organizaciones.sql +++ /dev/null @@ -1,163 +0,0 @@ --- filepath: /workspaces/Axia4/public_html/_incl/migrations/003_organizaciones.sql --- Axia4 Migration 003: Rename centros to organizaciones --- Migrates the centros table to organizaciones with org_id and org_name columns. - -PRAGMA journal_mode = WAL; -PRAGMA foreign_keys = ON; - --- ── Create new organizaciones table ────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS organizaciones ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT UNIQUE NOT NULL, - org_name TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - --- ── Migrate data from centros to organizaciones ────────────────────────────── -INSERT INTO organizaciones (org_id, org_name, created_at) -SELECT centro_id, COALESCE(name, centro_id), created_at -FROM centros -WHERE NOT EXISTS ( - SELECT 1 FROM organizaciones WHERE org_id = centros.centro_id -); - --- ── Update foreign key references in user_centros ────────────────────────────── --- user_centros.centro_id → user_centros.org_id (rename column if needed via recreation) --- For SQLite, we need to recreate the table due to FK constraint changes - -CREATE TABLE user_centros_new ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE, - role TEXT NOT NULL DEFAULT '', - ea_aulas TEXT NOT NULL DEFAULT '[]', - PRIMARY KEY (user_id, org_id) -); - -INSERT INTO user_centros_new (user_id, org_id, role, ea_aulas) -SELECT user_id, centro_id, role, aulas FROM user_centros; - -DROP TABLE user_centros; -ALTER TABLE user_centros_new RENAME TO user_orgs; - --- ── Update foreign key references in aularios ────────────────────────────────── -CREATE TABLE aularios_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE, - aulario_id TEXT NOT NULL, - name TEXT NOT NULL DEFAULT '', - icon TEXT NOT NULL DEFAULT '', - extra TEXT NOT NULL DEFAULT '{}', - UNIQUE (org_id, aulario_id) -); - -INSERT INTO aularios_new (id, org_id, aulario_id, name, icon, extra) -SELECT id, centro_id, aulario_id, name, icon, extra FROM aularios; - -DROP TABLE aularios; -ALTER TABLE aularios_new RENAME TO aularios; - --- ── Update foreign key references in remaining tables ────────────────────────── --- supercafe_menu -CREATE TABLE supercafe_menu_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE, - data TEXT NOT NULL DEFAULT '{}', - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (org_id) -); - -INSERT INTO supercafe_menu_new (id, org_id, data, updated_at) -SELECT id, centro_id, data, updated_at FROM supercafe_menu; - -DROP TABLE supercafe_menu; -ALTER TABLE supercafe_menu_new RENAME TO supercafe_menu; - --- supercafe_orders -CREATE TABLE supercafe_orders_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT NOT NULL REFERENCES organizaciones(org_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 (org_id, order_ref) -); - -INSERT INTO supercafe_orders_new (id, org_id, order_ref, fecha, persona, comanda, notas, estado, created_at) -SELECT id, centro_id, order_ref, fecha, persona, comanda, notas, estado, created_at FROM supercafe_orders; - -DROP TABLE supercafe_orders; -ALTER TABLE supercafe_orders_new RENAME TO supercafe_orders; - --- comedor_menu_types -CREATE TABLE comedor_menu_types_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE, - aulario_id TEXT NOT NULL, - data TEXT NOT NULL DEFAULT '[]', - UNIQUE (org_id, aulario_id) -); - -INSERT INTO comedor_menu_types_new (id, org_id, aulario_id, data) -SELECT id, centro_id, aulario_id, data FROM comedor_menu_types; - -DROP TABLE comedor_menu_types; -ALTER TABLE comedor_menu_types_new RENAME TO comedor_menu_types; - --- comedor_entries -CREATE TABLE comedor_entries_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE, - aulario_id TEXT NOT NULL, - year_month TEXT NOT NULL, - day TEXT NOT NULL, - data TEXT NOT NULL DEFAULT '{}', - UNIQUE (org_id, aulario_id, year_month, day) -); - -INSERT INTO comedor_entries_new (id, org_id, aulario_id, year_month, day, data) -SELECT id, centro_id, aulario_id, year_month, day, data FROM comedor_entries; - -DROP TABLE comedor_entries; -ALTER TABLE comedor_entries_new RENAME TO comedor_entries; - --- diario_entries -CREATE TABLE diario_entries_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE, - aulario_id TEXT NOT NULL, - entry_date TEXT NOT NULL, - data TEXT NOT NULL DEFAULT '{}', - UNIQUE (org_id, aulario_id, entry_date) -); - -INSERT INTO diario_entries_new (id, org_id, aulario_id, entry_date, data) -SELECT id, centro_id, aulario_id, entry_date, data FROM diario_entries; - -DROP TABLE diario_entries; -ALTER TABLE diario_entries_new RENAME TO diario_entries; - --- panel_alumno -CREATE TABLE panel_alumno_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - org_id TEXT NOT NULL REFERENCES organizaciones(org_id) ON DELETE CASCADE, - aulario_id TEXT NOT NULL, - alumno TEXT NOT NULL, - data TEXT NOT NULL DEFAULT '{}', - UNIQUE (org_id, aulario_id, alumno) -); - -INSERT INTO panel_alumno_new (id, org_id, aulario_id, alumno, data) -SELECT id, centro_id, aulario_id, alumno, data FROM panel_alumno; - -DROP TABLE panel_alumno; -ALTER TABLE panel_alumno_new RENAME TO panel_alumno; - --- ── Drop old centros table ───────────────────────────────────────────────────── -DROP TABLE IF EXISTS centros; - --- ── Verify migration ─────────────────────────────────────────────────────────── --- SELECT COUNT(*) as total_organizaciones FROM organizaciones; \ No newline at end of file diff --git a/public_html/_incl/migrations/004_user_sessions.sql b/public_html/_incl/migrations/004_user_sessions.sql deleted file mode 100644 index 202a280..0000000 --- a/public_html/_incl/migrations/004_user_sessions.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Axia4 Migration 004: User Sessions (Dispositivos conectados) --- Tracks active authenticated sessions so users can see and revoke connected devices. - -PRAGMA journal_mode = WAL; -PRAGMA foreign_keys = ON; - -CREATE TABLE IF NOT EXISTS user_sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_token TEXT UNIQUE NOT NULL, -- SHA-256 hash of the PHP session_id() - username TEXT NOT NULL, - ip_address TEXT NOT NULL DEFAULT '', - user_agent TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - last_active TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_user_sessions_username ON user_sessions (username); diff --git a/public_html/_incl/migrations/005_remember_token.sql b/public_html/_incl/migrations/005_remember_token.sql deleted file mode 100644 index 4502e24..0000000 --- a/public_html/_incl/migrations/005_remember_token.sql +++ /dev/null @@ -1,12 +0,0 @@ --- Axia4 Migration 005: Add remember_token_hash to user_sessions --- Replaces the auth_user + auth_pass_b64 cookie pair with a secure opaque token. --- The raw token lives only in the browser cookie; only its SHA-256 hash is stored. - -PRAGMA journal_mode = WAL; -PRAGMA foreign_keys = ON; - -ALTER TABLE user_sessions ADD COLUMN remember_token_hash TEXT DEFAULT NULL; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_sessions_remember - ON user_sessions (remember_token_hash) - WHERE remember_token_hash IS NOT NULL; diff --git a/public_html/_incl/post-body.php b/public_html/_incl/post-body.php deleted file mode 100755 index dcb5b89..0000000 --- a/public_html/_incl/post-body.php +++ /dev/null @@ -1,23 +0,0 @@ - - -
- - - - - - \ No newline at end of file diff --git a/public_html/_incl/pre-body.php b/public_html/_incl/pre-body.php deleted file mode 100644 index 80cdab7..0000000 --- a/public_html/_incl/pre-body.php +++ /dev/null @@ -1,594 +0,0 @@ -4"; - $APP_TITLE = "Axia4"; -} else { - $APP_ROOT = "/$APP_CODE"; - $APP_ICON = "logo-$APP_CODE.png"; -} - -$displayName = $_SESSION["auth_data"]["display_name"] ?? "Invitado"; -$email = $_SESSION["auth_data"]["email"] ?? "Sin sesión"; -$initials = "?"; -if (!empty($displayName)) { - $parts = preg_split('/\s+/', trim($displayName)); - $first = mb_substr($parts[0] ?? "", 0, 1); - $last = mb_substr($parts[1] ?? "", 0, 1); - $initials = mb_strtoupper($first . $last); -} - -// Tenant (organización) management -$userOrganizaciones = get_user_organizaciones($_SESSION["auth_data"] ?? []); -$activeOrganizacionId = $_SESSION['active_organizacion'] - ?? ($_SESSION["auth_data"]["aulatek"]["organizacion"] ?? ($_SESSION["auth_data"]["entreaulas"]["organizacion"] ?? '')); -$activeOrganizacionName = $userOrganizaciones[$activeOrganizacionId] ?? ''; - -?> - - - - - - - <?php echo $PAGE_TITLE ?? $APP_TITLE ?? "Axia4"; ?> - - " /> - - - - - - - - - - - - -
- -
- - - - - -
- - - - - - -
-
- - - - -
- -
-
- - -
- - -
-
- - -
; text-align: center;"> -

-
- - diff --git a/public_html/_incl/switch_tenant.php b/public_html/_incl/switch_tenant.php deleted file mode 100644 index d0cb8b4..0000000 --- a/public_html/_incl/switch_tenant.php +++ /dev/null @@ -1,42 +0,0 @@ - time() - 3600, "path" => "/", "httponly" => true, - "secure" => true, "samesite" => "Lax"]; - $raw_token = $_COOKIE["auth_token"]; - $token_hash = hash('sha256', $raw_token); - $sess_row = db_restore_session_by_remember_token($token_hash); - if ($sess_row) { - $username = $sess_row['username']; - $row = db_get_user($username); - if ($row) { - $_SESSION["auth_user"] = $username; - $_SESSION["auth_data"] = db_build_auth_data($row); - $_SESSION["auth_ok"] = true; - if (empty($_SESSION["session_created"])) { - $_SESSION["session_created"] = time(); - } - init_active_org($_SESSION["auth_data"]); - } else { - // User no longer exists — clear the stale cookie - setcookie("auth_token", "", $expired); - } - } else { - // Token not found (revoked or expired) — clear the stale cookie - setcookie("auth_token", "", $expired); - } -} - -// ── Validate session is still active in user_sessions (enables revocation) ─── -if (!empty($_SESSION["auth_ok"]) && !empty($_SESSION["auth_user"]) - && empty($_SESSION["auth_external_lock"]) -) { - if (!db_session_is_valid($_SESSION["auth_user"])) { - session_unset(); - session_destroy(); - header("Location: /_login.php?_result=" . urlencode("Tu sesión fue revocada. Inicia sesión de nuevo.")); - die(); - } -} - -// ── Periodic session reload from DB ────────────────────────────────────────── -if (!empty($_SESSION["auth_ok"]) && !empty($_SESSION["auth_user"])) { - $load_mode = $AuthConfig["session_load_mode"] ?? ''; - if ($load_mode === "force") { - $row = db_get_user($_SESSION["auth_user"]); - if ($row) { - $_SESSION["auth_data"] = db_build_auth_data($row); - init_active_org($_SESSION["auth_data"]); - } - $_SESSION["last_reload_time"] = time(); - db_touch_session(); - } elseif ($load_mode !== "never") { - $last = $_SESSION["last_reload_time"] ?? 0; - if (time() - $last > 300) { - $row = db_get_user($_SESSION["auth_user"]); - if ($row) { - $_SESSION["auth_data"] = db_build_auth_data($row); - init_active_org($_SESSION["auth_data"]); - } - $_SESSION["last_reload_time"] = time(); - db_touch_session(); - } - if (!isset($_SESSION["last_reload_time"])) { - $_SESSION["last_reload_time"] = time(); - } - } -} - -function user_is_authenticated(): bool -{ - return isset($_SESSION["auth_ok"]) && $_SESSION["auth_ok"] === true; -} - -function user_has_permission(string $perm): bool -{ - return in_array($perm, $_SESSION["auth_data"]["permissions"] ?? [], true); -} diff --git a/public_html/_incl/tools.photos.php b/public_html/_incl/tools.photos.php deleted file mode 100644 index 2b4309b..0000000 --- a/public_html/_incl/tools.photos.php +++ /dev/null @@ -1,62 +0,0 @@ - $thumbAspect) { - // Source is wider - $newHeight = $targetHeight; - $newWidth = (int)($targetHeight * $sourceAspect); - } else { - // Source is taller or equal - $newWidth = $targetWidth; - $newHeight = (int)($targetWidth / $sourceAspect); - } - - // Center the image - $xOffset = (int)(($newWidth - $targetWidth) / 2); - $yOffset = (int)(($newHeight - $targetHeight) / 2); - - imagecopyresampled($thumb, $sourceImage, -$xOffset, -$yOffset, 0, 0, $newWidth, $newHeight, $width, $height); - // Save as jpg - imagejpeg($thumb, $dest, 85); - imagedestroy($sourceImage); - imagedestroy($thumb); - return true; -} \ No newline at end of file diff --git a/public_html/_incl/tools.security.php b/public_html/_incl/tools.security.php deleted file mode 100644 index b7cfa5e..0000000 --- a/public_html/_incl/tools.security.php +++ /dev/null @@ -1,266 +0,0 @@ - 1) { - $parts = explode('.', $name); - $ext = array_pop($parts); - $base = implode('_', $parts); - // Ensure extension is not empty - if ($ext === '') { - $name = $base === '' ? 'file' : $base; - } else { - $name = ($base === '' ? 'file' : $base) . '.' . $ext; - } - } - - // Trim stray dots/underscores from the start and end - $name = trim($name, "._"); - - // Enforce a maximum length (common filesystem limit is 255 bytes) - $maxLen = 255; - if (strlen($name) > $maxLen) { - $dotPos = strrpos($name, '.'); - if ($dotPos !== false) { - $ext = substr($name, $dotPos); - $base = substr($name, 0, $dotPos); - $baseMaxLen = $maxLen - strlen($ext); - if ($baseMaxLen < 1) { - // Fallback if extension is unusually long - $name = substr($name, 0, $maxLen); - } else { - $name = substr($base, 0, $baseMaxLen) . $ext; - } - } else { - $name = substr($name, 0, $maxLen); - } - } - - // Ensure we never return an empty or invalid filename - if ($name === '' || $name === '.' || $name === '..') { - $name = 'file'; - } - - return $name; -} -function safe_id_segment($value) -{ - $value = basename((string)$value); - return preg_replace('/[^A-Za-z0-9._-]/', '', $value); -} -function safe_id($value) -{ - return preg_replace('/[^a-zA-Z0-9._-]/', '', basename((string)$value)); -} -function safe_alumno_name($value) -{ - $value = basename((string)$value); - $value = trim($value); - $value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value); - $value = str_replace(['/', '\\'], '', $value); - return $value; -} - -function path_is_within($real_base, $real_path) -{ - if ($real_base === false || $real_path === false) { - return false; - } - $base_prefix = rtrim($real_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - return strpos($real_path, $base_prefix) === 0 || $real_path === rtrim($real_base, DIRECTORY_SEPARATOR); -} - -function aulatek_orgs_base_path() -{ - $orgs_path = '/DATA/entreaulas/Organizaciones'; - $legacy_path = '/DATA/entreaulas/Centros'; - if (is_dir($orgs_path)) { - return $orgs_path; - } - if (is_dir($legacy_path)) { - return $legacy_path; - } - return $orgs_path; -} - -function entreaulas_orgs_base_path() -{ - return aulatek_orgs_base_path(); -} - -function safe_aulario_config_path($centro_id, $aulario_id) -{ - $centro = safe_organization_id($centro_id); - $aulario = safe_id_segment($aulario_id); - if ($centro === '' || $aulario === '') { - return null; - } - return aulatek_orgs_base_path() . "/$centro/Aularios/$aulario.json"; -} - -function safe_redir($url, $default = "/") -{ - if (empty($url) || !is_string($url)) { - return $default; - } - // Only allow relative URLs that start with / - if (str_starts_with($url, "/") && !str_contains($url, "\0")) { - return $url; - } - return $default; -} \ No newline at end of file diff --git a/public_html/_incl/tools.session.php b/public_html/_incl/tools.session.php deleted file mode 100644 index a6012ee..0000000 --- a/public_html/_incl/tools.session.php +++ /dev/null @@ -1,9 +0,0 @@ - 604800, - 'cookie_httponly' => true, - 'cookie_secure' => true, - 'cookie_samesite' => 'Lax', -]); diff --git a/public_html/_install.php b/public_html/_install.php deleted file mode 100644 index 29073bb..0000000 --- a/public_html/_install.php +++ /dev/null @@ -1,72 +0,0 @@ - $admin_user, - 'display_name' => 'Administrador', - 'email' => "$admin_user@nomail.arpa", - 'permissions' => ['*', 'sysadmin:access', 'aulatek:access'], - 'password_hash' => password_hash($admin_password, PASSWORD_DEFAULT), - ]); - db_set_config('installed', '1'); - header("Location: /_login.php"); - exit; - break; -} - -switch ($_GET["step"]) { - case "0": - default: - require_once "_incl/pre-body.php"; -?> -
-
-

Instalación de Axia4

- Bienvenidx al asistente de instalación de Axia4. Por favor, sigue los pasos para completar la configuración inicial del sistema. -
    -
  1. Crear el usuario administrador.
  2. - -
  3. Finalizar la instalación y acceder al sistema.
  4. -
- Comenzar instalación -
-
- -
-
-

Crear usuario administrador

-
-
- - -
-
- - -
- -
-
-
- \ No newline at end of file diff --git a/public_html/_login.php b/public_html/_login.php deleted file mode 100644 index 21d6cad..0000000 --- a/public_html/_login.php +++ /dev/null @@ -1,220 +0,0 @@ - [ - "method" => "POST", - "header" => "Content-Type: application/x-www-form-urlencoded", - "content" => http_build_query([ - "code" => $code, - "client_id" => $AuthConfig["google_client_id"], - "client_secret" => $AuthConfig["google_client_secret"], - "redirect_uri" => "https://$DOMAIN/_login.php?google_callback=1", - "grant_type" => "authorization_code" - ]) - ] - ])); - - $token_data = json_decode($token_response, true); - - if (!isset($token_data["access_token"])) { - die("Error: No se pudo obtener el token de acceso de Google."); - } - - $access_token = $token_data["access_token"]; - - // Obtener la información del usuario con el token de acceso - $user_info_response = file_get_contents("https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=$access_token"); - $user_info = json_decode($user_info_response, true); - - if (!isset($user_info["email"])) { - die("Error: No se pudo obtener la información del usuario de Google."); - } - - $email = $user_info["email"]; - $name = $user_info["name"] ?? explode("@", $email)[0]; - $username = strtolower($email); - if ($username === "") { - die("Error: Dirección de correo inválida."); - } - $password = bin2hex(random_bytes(16)); - $existing = db_get_user($username); - if ($existing) { - $user_row = $existing; - } else { - db_upsert_user([ - 'username' => explode("@", $email)[0] + uniqid(), - 'display_name' => $name, - 'email' => $email, - 'permissions' => ['public'], - 'password_hash' => password_hash($password, PASSWORD_DEFAULT), - 'google_auth' => true, - '#' => 'Este usuario fue creado automáticamente al iniciar sesión con Google por primera vez.', - ]); - $user_row = db_get_user($username); - } - - session_regenerate_id(true); - $_SESSION['auth_user'] = $username; - $_SESSION['auth_data'] = db_build_auth_data($user_row); - $_SESSION['auth_ok'] = true; - $_SESSION['session_created'] = time(); - init_active_org($_SESSION['auth_data']); - $remember_token = bin2hex(random_bytes(32)); - $remember_token_hash = hash('sha256', $remember_token); - db_register_session($username, $remember_token_hash); - $cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"]; - setcookie("auth_token", $remember_token, $cookie_options); - - $redir = safe_redir($state["redir"] ?? "/"); - - header("Location: $redir"); - die(); -} -if (($_GET["google"] ?? "") === "1") { - if (!isset($AuthConfig["google_client_id"]) || !isset($AuthConfig["google_client_secret"])) { - die("Error: La autenticación de Google no está configurada."); - } - $url = "https://accounts.google.com/o/oauth2/auth"; - - // Generate a CSRF nonce and store it in the session - $oauth_nonce = bin2hex(random_bytes(16)); - $_SESSION["oauth_nonce"] = $oauth_nonce; - - // build the HTTP GET query - $params = array( - "response_type" => "code", - "client_id" => $AuthConfig["google_client_id"], - "redirect_uri" => "https://$DOMAIN/_login.php?google_callback=1", - "scope" => "email openid profile", - "state" => base64_encode(json_encode([ - "redir" => safe_redir($_GET["redir"] ?? "/"), - "nonce" => $oauth_nonce - ])) - ); - - $request_to = $url . '?' . http_build_query($params); - - // forward the user to the login access page on the OAuth 2 server - header("Location: " . $request_to); - die(); -} -if (($_GET["logout"] ?? "") === "1") { - $redir = safe_redir($_GET["redir"] ?? "/"); - $cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"]; - setcookie("auth_token", "", $cookie_options_expired); - db_delete_session(); - session_unset(); - session_destroy(); - header("Location: $redir"); - die(); -} -if (($_GET["clear_session"] ?? "") === "1") { - session_unset(); - session_destroy(); - $redir = safe_redir($_GET["redir"] ?? "/"); - header("Location: $redir"); - die(); -} -if (isset($_POST["user"])) { - $user = trim(strtolower($_POST["user"])); - $password = $_POST["password"]; - // Validate CSRF token - $csrf_token = $_POST["_csrf"] ?? ""; - if (!$csrf_token || !isset($_SESSION["login_csrf"]) || !hash_equals($_SESSION["login_csrf"], $csrf_token)) { - $_GET["_result"] = "Token de seguridad inválido. Por favor, recarga la página e inténtalo de nuevo."; - } else { - $row = db_get_user($user); - if (!$row || !isset($row["password_hash"])) { - $_GET["_result"] = "El usuario no existe."; - } elseif (password_verify($password, $row["password_hash"])) { - $remember_token = bin2hex(random_bytes(32)); - $remember_token_hash = hash('sha256', $remember_token); - session_regenerate_id(true); - $_SESSION['auth_user'] = $user; - $_SESSION['auth_data'] = db_build_auth_data($row); - $_SESSION['auth_ok'] = true; - $_SESSION['session_created'] = time(); - init_active_org($_SESSION['auth_data']); - db_register_session($user, $remember_token_hash); - $cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"]; - setcookie("auth_token", $remember_token, $cookie_options); - $redir = safe_redir($_GET["redir"] ?? "/"); - header("Location: $redir"); - die(); - } else { - $_GET["_result"] = "La contraseña no es correcta."; - } - } -} -if (strval(db_get_config('installed')) !== '1') { - header("Location: /_install.php"); - die(); -} -if (empty($_SESSION["login_csrf"])) { - $_SESSION["login_csrf"] = bin2hex(random_bytes(32)); -} -require_once "_incl/pre-body.php"; -?> - -
"> - "> -
-

Iniciar sesión en Axia4

-
-
- - -
-
- - -
- - - " class="btn btn-outline-danger">Google - -
-
-
- diff --git a/public_html/account/__menu.php b/public_html/account/__menu.php deleted file mode 100755 index 76769f3..0000000 --- a/public_html/account/__menu.php +++ /dev/null @@ -1,3 +0,0 @@ -Recargar Cuenta -Cerrar sesión -Limpiar sesión \ No newline at end of file diff --git a/public_html/account/_incl/auth_redir.php b/public_html/account/_incl/auth_redir.php deleted file mode 100755 index d1f33ac..0000000 --- a/public_html/account/_incl/auth_redir.php +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/public_html/account/_incl/pre-body.php b/public_html/account/_incl/pre-body.php deleted file mode 100755 index b08d145..0000000 --- a/public_html/account/_incl/pre-body.php +++ /dev/null @@ -1,5 +0,0 @@ - string, 'os' => string, 'icon' => string]. - */ -function parse_ua(string $ua): array -{ - // OS - $os = 'Desconocido'; - if (stripos($ua, 'Android') !== false) $os = 'Android'; - elseif (stripos($ua, 'iPhone') !== false - || stripos($ua, 'iPad') !== false) $os = 'iOS'; - elseif (stripos($ua, 'Windows') !== false) $os = 'Windows'; - elseif (stripos($ua, 'Macintosh') !== false - || stripos($ua, 'Mac OS') !== false) $os = 'macOS'; - elseif (stripos($ua, 'Linux') !== false) $os = 'Linux'; - elseif (stripos($ua, 'CrOS') !== false) $os = 'ChromeOS'; - - // Browser - $browser = 'Desconocido'; - if (str_starts_with($ua, 'Axia4Auth/')) $browser = 'Axia4 App'; - elseif (stripos($ua, 'Edg/') !== false) $browser = 'Edge'; - elseif (stripos($ua, 'OPR/') !== false - || stripos($ua, 'Opera') !== false) $browser = 'Opera'; - elseif (stripos($ua, 'Chrome') !== false) $browser = 'Chrome'; - elseif (stripos($ua, 'Firefox') !== false) $browser = 'Firefox'; - elseif (stripos($ua, 'Safari') !== false) $browser = 'Safari'; - - // Emoji icon for a quick visual cue - $icon = match ($browser) { - 'Chrome' => '🌐', - 'Firefox' => '🦊', - 'Safari' => '🧭', - 'Edge' => '🔷', - 'Opera' => '🔴', - 'Axia4 App' => '📱', - default => '💻', - }; - - return ['browser' => $browser, 'os' => $os, 'icon' => $icon]; -} -?> - - - - - diff --git a/public_html/account/register.php b/public_html/account/register.php deleted file mode 100644 index 1752e34..0000000 --- a/public_html/account/register.php +++ /dev/null @@ -1,62 +0,0 @@ - $username, - 'display_name' => $_POST['display_name'] ?? '', - 'email' => $_POST['email'] ?? '', - 'password_hash' => password_hash($_POST['password'], PASSWORD_DEFAULT), - 'permissions' => [], - '_meta_signup' => ['invitation_code' => $invi_code], - ]); - if ($invitation['single_use']) { - db_deactivate_invitation($invi_code); - } - header("Location: /?_result=" . urlencode("Cuenta creada correctamente. Ya puedes iniciar sesión.")); - exit; -} -?> -
-
-

¡Crea una cuenta!

-
-
-
-
- - - Codigo de invitación proporcionado por un administrador.
Formato: 123456-ABCDEF
-
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
-
-
-
\ No newline at end of file diff --git a/public_html/account/revoke_session.php b/public_html/account/revoke_session.php deleted file mode 100644 index ae61f9f..0000000 --- a/public_html/account/revoke_session.php +++ /dev/null @@ -1,42 +0,0 @@ -Política de Privacidad — Aularios de Aldamiz -

Última actualización: 27/11/2025

-

Versión: 1.1

-

Esta Política de Privacidad explica cómo EuskadiTech (en adelante, "la empresa", "nosotros") trata los datos personales de las personas que utilizan el servicio Wiki basado en WikiJS (en adelante, "el Servicio", “Aularios de Aldamiz”). El acceso al Servicio se realiza mediante inicio de sesión con Cuenta de Google (OAuth) o mediante cuenta local creada en el propio sistema.

-

1. Responsable del tratamiento

-

Nombre: Aularios de Aldamiz

-

Correo de contacto / administración: aularios@tech.eus (este buzón se reenvía automáticamente al Delegado de Protección de Datos de EuskadiTech).

-

Hosting: servidor local EuskadiTech - tech.eus.

-

2. Finalidades y datos tratados

-

2.1. Inicio de sesión con Google (OAuth)

-

Cuando el usuario inicia sesión con Google, se recogen únicamente los datos que el usuario autoriza a compartir durante el proceso de OAuth, que normalmente incluyen:

-
    -
  • Nombre y apellidos.
  • -
  • Dirección de correo electrónico.
  • -
  • Foto de perfil (si existe).
  • -
  • Identificador único de Google (Google ID).
  • -
-

Finalidad: autenticación, creación/actualización de la cuenta en el Servicio, asignación de roles y personalización mínima del perfil.

-

2.2. Cuentas locales

-

Para cuentas locales se tratan los siguientes datos:

-
    -
  • Nombre de usuario.
  • -
  • Correo electrónico.
  • -
  • Contraseña (almacenada de forma segura y siempre cifrada/hasheada).
  • -
  • Rol (ej. alumno, docente, administrador).
  • -
-

Finalidad: autenticación, gestión de acceso y permisos, comunicación administrativa y notificaciones internas.

-

2.3. Cuenta compartida de solo lectura

-

EuskadiTech dispone de una cuenta compartida entre alumnos con permisos de solo lectura. Dicha cuenta no recoge ni asocia datos personales de quienes la utilicen.

-

2.4. Registros técnicos y logs

-

Se registran de forma automática datos técnicos necesarios para la seguridad y funcionamiento del Servicio:

-
    -
  • Dirección IP.
  • -
  • Fecha y hora de acceso.
  • -
  • Páginas consultadas y acciones relevantes (crear/editar/eliminar).
  • -
  • Información del navegador y sistema operativo.
  • -
-

Finalidad: seguridad, auditoría, detección y resolución de incidencias, mejora y mantenimiento del Servicio.

-

2.5. Cookies

-

Se utilizan cookies estrictamente necesarias para la sesión, autenticación y protección CSRF. No se emplean cookies publicitarias ni analíticas por defecto; si en el futuro se activan cookies analíticas se solicitará el consentimiento cuando la normativa lo exija.

-

3. Base jurídica del tratamiento (RGPD y LOPDGDD)

-

Las bases legales que legitiman los tratamientos son, según proceda:

-
    -
  • El cumplimiento de una misión de interés público o el ejercicio de poderes públicos en materia educativa.
  • -
  • El interés legítimo de EuskadiTech para garantizar la seguridad, integridad y disponibilidad del Servicio.
  • -
  • El cumplimiento de obligaciones contractuales o precontractuales con el interesado.
  • -
  • El consentimiento del interesado, cuando el tratamiento concreto así lo requiera (por ejemplo, en casos no estrictamente necesarios para la prestación del Servicio).
  • -
-

4. Destinatarios y encargados

-
    -
  • Google: cuando se utiliza Google OAuth, Google actúa como proveedor/tercera parte y trata los datos conforme a su política de privacidad. El usuario puede revisar la política y condiciones de Google para conocer cómo procesa sus datos y qué transferencias pueden producirse.
  • -
  • Personal autorizado de EuskadiTech: acceso necesario para la administración y mantenimiento del Servicio.
  • -
  • No se cederán datos a terceros salvo obligación legal o en los supuestos previstos en esta política y con las garantías legalmente exigibles.
  • -
-

5. Transferencias internacionales

-

El Servicio está alojado en servidor local, por lo que no se realizan transferencias internacionales del tratamiento de datos desde el hosting. En el caso de uso de Google OAuth, el proveedor puede aplicar tratamientos o transferencias fuera del Espacio Económico Europeo conforme a su política; EuskadiTech recomienda consultar la documentación y garantías que Google proporciona.

-

6. Plazos de conservación

-
    -
  • Cuentas de usuario: se conservan mientras la cuenta esté activa y en uso. Las cuentas se eliminan a petición del interesado mediante correo a aularios@tech.eus, o automáticamente tras 3 años de inactividad o finalización del uso educativo.
  • -
  • Cuentas compartidas de solo lectura: mantenidas mientras formen parte del funcionamiento del Servicio y sin asociación a datos personales de los usuarios.
  • -
  • Logs y registros de acceso: conservados durante un máximo de 6 meses para fines de seguridad y auditoría.
  • -
  • Backups: conservados según la política interna de EuskadiTech, garantizando las mismas medidas de seguridad y plazos coherentes con lo indicado anteriormente.
  • -
-

7. Medidas de seguridad

-

EuskadiTech aplica medidas técnicas y organizativas para proteger los datos personales, incluyendo, entre otras:

-
    -
  • Almacenamiento seguro de contraseñas mediante hashing criptográfico robusto (por ejemplo, bcrypt/argon2 u otro equivalente).
  • -
  • Comunicación cifrada mediante HTTPS/TLS para accesos al Servicio.
  • -
  • Control de accesos y segregación por roles (principio de mínimos privilegios).
  • -
  • Registro y monitorización de accesos y acciones relevantes.
  • -
  • Copia de seguridad en entorno controlado y planes de recuperación ante desastres.
  • -
  • Seguridad física y lógica del servidor local (acceso restringido al personal autorizado).
  • -
-

Pese a las medidas adoptadas, ningún sistema es infalible; en caso de violación de la seguridad que suponga riesgo para los derechos y libertades de las personas afectadas, se notificará la brecha a la autoridad de control y, si procede, a los interesados, conforme al RGPD y la normativa aplicable.

-

8. Derechos de las personas

-

De conformidad con el RGPD y la LOPDGDD, las personas disponen de los siguientes derechos:

-
    -
  • Derecho de acceso a sus datos personales.
  • -
  • Derecho de rectificación de datos inexactos o incompletos.
  • -
  • Derecho de supresión ("derecho al olvido") cuando proceda.
  • -
  • Derecho a la limitación del tratamiento.
  • -
  • Derecho a la portabilidad de los datos cuando proceda.
  • -
  • Derecho de oposición al tratamiento por motivos legítimos.
  • -
  • Derecho a retirar el consentimiento en cualquier momento, sin que ello afecte a la licitud del tratamiento basado en el consentimiento previo a su retirada.
  • -
-

Para ejercer sus derechos, el interesado puede enviar una solicitud por correo electrónico a aularios@tech.eus (este buzón se reenvía automáticamente al Delegado de Protección de Datos). La solicitud deberá identificar al solicitante y especificar el derecho que desea ejercer; en su caso, podrá solicitarse documentación que acredite la identidad. Responderemos en los plazos legalmente establecidos.

-

Si el interesado no queda satisfecho con la respuesta, tiene derecho a presentar una reclamación ante la Agencia Española de Protección de Datos (AEPD).

-

9. Menores

-

El Servicio se utiliza en un contexto escolar. Conforme a la normativa española (LOPDGDD) la edad mínima para prestar consentimiento digital es de 14 años. Para menores por debajo de dicho umbral, se requerirá el consentimiento de los padres o tutores legales cuando la normativa así lo exija. EuskadiTech adoptará medidas adicionales de protección para los menores, incluida la supervisión educativa y la limitación de la publicación de datos personales.

-

10. Procedimiento de eliminación de cuentas

-

Las cuentas podrán solicitar su eliminación enviando un correo a aularios@tech.eus. A la recepción de la solicitud y previa verificación de identidad, EuskadiTech procederá a la supresión de los datos personales del usuario en los plazos razonables y conforme a las obligaciones legales de conservación que pudieran aplicarse.

-

11. Cambios en la política

-

Esta Política de Privacidad puede ser actualizada. Publicaremos la versión vigente en el Servicio con la fecha de última actualización. Cuando los cambios sean significativos se informará a la comunidad educativa por los canales habituales.

-

12. Contacto

-

Para consultas relativas a esta política, ejercicio de derechos o incidencias:

-

Correo: aularios@tech.eus (reenvío automático al Delegado de Protección de Datos).

-

13. Autoridad de control

-

Si considera que sus derechos no han sido atendidos adecuadamente, puede presentar una reclamación ante la Agencia Española de Protección de Datos (AEPD): https://www.aepd.es.

-

14. Brechas de seguridad

-

En caso de haber una brecha de seguridad, se avisara por correo en un plazo máximo de 90 días (plazo para reforzar la seguridad) con los tipos de datos afectados.

-
-

Documento generado para su publicación en el Servicio WikiJS de Aularios de Aldamiz.

diff --git a/public_html/aulatek/__menu.php b/public_html/aulatek/__menu.php deleted file mode 100755 index a5dd4fd..0000000 --- a/public_html/aulatek/__menu.php +++ /dev/null @@ -1,10 +0,0 @@ - - \ No newline at end of file diff --git a/public_html/aulatek/_filefetch.php b/public_html/aulatek/_filefetch.php deleted file mode 100755 index d4ada45..0000000 --- a/public_html/aulatek/_filefetch.php +++ /dev/null @@ -1,143 +0,0 @@ -isDir()) { - continue; - } - $meta = $fileinfo->getPathname() . "/_data_.eadat"; - if (!file_exists($meta)) { - continue; - } - $data = json_decode(file_get_contents($meta), true); - if (($data["id"] ?? "") === $project) { - $project_dir = $fileinfo->getPathname(); - break; - } - } - } - if (!$project_dir) { - header("HTTP/1.1 404 Not Found"); - die("Project not found"); - } - $path = $project_dir . "/" . $file; - $uripath = str_replace("/DATA", "", $path); - break; - default: - header("HTTP/1.1 400 Bad Request"); - die("Invalid type"); -} -if (!isset($path)) { - $path = "/DATA/$relpath"; -} -if (!isset($uripath)) { - $uripath = "/$relpath"; -} - -// Validate that the resolved path is within /DATA directory -$real_path = realpath($path); -$real_base = realpath("/DATA"); -$real_base_prefix = $real_base !== false ? rtrim($real_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : null; -if ($real_path === false || $real_base === false || $real_base_prefix === null || strpos($real_path, $real_base_prefix) !== 0) { - header("HTTP/1.1 403 Forbidden"); - die("Access denied"); -} - -if (!file_exists($real_path) || !is_file($real_path)) { - header("HTTP/1.1 404 Not Found"); - die("File not found"); -} -$mime = mime_content_type($real_path); - -// Check if thumbnail is requested -if (file_exists($real_path . ".thumbnail") && (($_GET["thumbnail"] ?? "") === "1")) { - $real_path .= ".thumbnail"; - $uripath .= ".thumbnail"; - $mime = "image/jpeg"; -} -header("Content-Type: " . $mime); -header('Content-Length: ' . filesize($real_path)); -//header('Cache-Control: max-age=7200'); -header("X-Accel-Redirect: $uripath"); - -// // stream the file -// $fp = fopen($path, 'rb'); -// fpassthru($fp); -exit; \ No newline at end of file diff --git a/public_html/aulatek/_incl/auth_redir.php b/public_html/aulatek/_incl/auth_redir.php deleted file mode 100755 index 9f9ef06..0000000 --- a/public_html/aulatek/_incl/auth_redir.php +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/public_html/aulatek/_incl/pre-body.php b/public_html/aulatek/_incl/pre-body.php deleted file mode 100755 index c293805..0000000 --- a/public_html/aulatek/_incl/pre-body.php +++ /dev/null @@ -1,5 +0,0 @@ - -
-

Gestión de Alumnos

-

No se ha indicado un aulario válido.

-
- -
-

Gestión de Alumnos

-

Error: Directorio base no encontrado.

-
- -
-

Añadir Alumno

-
-
- - -
-
- - - La foto se convertirá a formato JPG -
- - Cancelar -
-
- -
-

Editar Alumno:

- -
- -
- -
- - -
-
- - -
- Foto de <?= htmlspecialchars($nombre) ?> -
- -

No hay foto

- -
-
- - - Dejar vacío para mantener la foto actual -
- - Cancelar -
-
- -
-

Eliminar Alumno

-

¿Estás seguro de que quieres eliminar al alumno ?

-

Esta acción no se puede deshacer.

-
- - - Cancelar -
-
- - -
-
-

Gestión de Alumnos

- + Añadir Alumno -
- - -
- - - -

No hay alumnos registrados en este aulario.

-

Haz clic en "Añadir Alumno" para empezar.

- - - - - - - - - - - - - - - - - - - - -
FotoNombreDiarioAcciones
- - Foto de <?= htmlspecialchars($nombre) ?> - -
- ? -
- -
- - ✓ Hoy - - Sin diario - - - 📖 Diario - ✏️ Editar - 🗑️ Eliminar -
- - - Volver al Aulario -
- "Access denied", "code" => "FORBIDDEN"])); -} - -$tenant_data = $_SESSION["auth_data"]["aulatek"] ?? ($_SESSION["auth_data"]["entreaulas"] ?? []); -$centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? "")); -if ($centro_id === "") { - http_response_code(400); - die(json_encode(["error" => "Organizacion not found in session", "code" => "INVALID_SESSION"])); -} - -$action = $_GET["action"] ?? ($_POST["action"] ?? ""); -$aulario_id = safe_id_segment($_GET["aulario"] ?? $_POST["aulario"] ?? ""); - -if ($aulario_id === "") { - http_response_code(400); - die(json_encode(["error" => "aulario parameter is required", "code" => "MISSING_PARAM"])); -} - -$userAulas = array_values(array_filter(array_map('safe_id_segment', $tenant_data["aulas"] ?? []))); -if (!in_array($aulario_id, $userAulas, true)) { - http_response_code(403); - die(json_encode(["error" => "Access denied to this aulario", "code" => "FORBIDDEN"])); -} - -$aulario = db_get_aulario($centro_id, $aulario_id); - -$source_aulario_id = $aulario_id; -$is_shared = false; -if ($aulario && !empty($aulario["shared_comedor_from"])) { - $shared_from = safe_id_segment($aulario["shared_comedor_from"]); - if (db_get_aulario($centro_id, $shared_from)) { - $source_aulario_id = $shared_from; - $is_shared = true; - } -} - -$canEdit = in_array("sysadmin:access", $_SESSION["auth_data"]["permissions"] ?? []) && !$is_shared; - -$defaultMenuTypes = [ - ["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"], - ["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"], - ["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"], -]; - -function get_menu_types($centro_id, $source_aulario_id) { - global $defaultMenuTypes; - $types = db_get_comedor_menu_types($centro_id, $source_aulario_id); - if (empty($types)) { - db_set_comedor_menu_types($centro_id, $source_aulario_id, $defaultMenuTypes); - return $defaultMenuTypes; - } - return $types; -} - -function blank_menu() { - return [ - "plates" => [ - "primero" => ["name" => "", "pictogram" => ""], - "segundo" => ["name" => "", "pictogram" => ""], - "postre" => ["name" => "", "pictogram" => ""], - ] - ]; -} - -// Routes -switch ($action) { - case "get_menu_types": - handle_get_menu_types(); - break; - case "get_menu": - handle_get_menu(); - break; - case "save_menu": - if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); } - handle_save_menu(); - break; - case "add_menu_type": - if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); } - handle_add_menu_type(); - break; - case "delete_menu_type": - if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); } - handle_delete_menu_type(); - break; - case "rename_menu_type": - if (!$canEdit) { http_response_code(403); die(json_encode(["error" => "Insufficient permissions to edit", "code" => "FORBIDDEN"])); } - handle_rename_menu_type(); - break; - default: - http_response_code(400); - die(json_encode(["error" => "Invalid action", "code" => "INVALID_ACTION"])); -} - -function handle_get_menu_types() { - global $centro_id, $source_aulario_id; - echo json_encode(["success" => true, "menu_types" => get_menu_types($centro_id, $source_aulario_id)]); -} - -function handle_get_menu() { - global $centro_id, $source_aulario_id; - $date = $_GET["date"] ?? date("Y-m-d"); - $menuTypeId = safe_id_segment($_GET["menu"] ?? ""); - $dateObj = DateTime::createFromFormat("Y-m-d", $date); - if (!$dateObj) { http_response_code(400); die(json_encode(["error" => "Invalid date format", "code" => "INVALID_FORMAT"])); } - $date = $dateObj->format("Y-m-d"); - $menuTypes = get_menu_types($centro_id, $source_aulario_id); - $menuTypeIds = array_column($menuTypes, "id"); - if ($menuTypeId === "" || !in_array($menuTypeId, $menuTypeIds)) { $menuTypeId = $menuTypeIds[0] ?? "basal"; } - $ym = $dateObj->format("Y-m"); - $day = $dateObj->format("d"); - $menuData = ["date" => $date, "menus" => []]; - $existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day); - if (!empty($existing)) { $menuData = array_merge($menuData, $existing); } - if (!isset($menuData["menus"][$menuTypeId])) { $menuData["menus"][$menuTypeId] = blank_menu(); } - echo json_encode(["success" => true, "date" => $date, "menu_type" => $menuTypeId, "menu_types" => $menuTypes, "menu" => $menuData["menus"][$menuTypeId]]); -} - -function handle_save_menu() { - global $centro_id, $source_aulario_id; - $input = json_decode(file_get_contents("php://input"), true) ?: $_POST; - $date = $input["date"] ?? date("Y-m-d"); - $menuTypeId = safe_id_segment($input["menu_type"] ?? ""); - $plates = $input["plates"] ?? []; - $dateObj = DateTime::createFromFormat("Y-m-d", $date); - if (!$dateObj) { http_response_code(400); die(json_encode(["error" => "Invalid date format", "code" => "INVALID_FORMAT"])); } - $date = $dateObj->format("Y-m-d"); - $menuTypes = get_menu_types($centro_id, $source_aulario_id); - $validMenuTypeIds = array_column($menuTypes, "id"); - if (!in_array($menuTypeId, $validMenuTypeIds)) { http_response_code(400); die(json_encode(["error" => "Invalid menu type", "code" => "INVALID_MENU_TYPE"])); } - $ym = $dateObj->format("Y-m"); - $day = $dateObj->format("d"); - $menuData = ["date" => $date, "menus" => []]; - $existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day); - if (!empty($existing)) { $menuData = array_merge($menuData, $existing); } - if (!isset($menuData["menus"][$menuTypeId])) { $menuData["menus"][$menuTypeId] = blank_menu(); } - foreach (["primero", "segundo", "postre"] as $plateKey) { - if (isset($plates[$plateKey]["name"])) { - $menuData["menus"][$menuTypeId]["plates"][$plateKey]["name"] = trim($plates[$plateKey]["name"]); - } - } - db_set_comedor_entry($centro_id, $source_aulario_id, $ym, $day, $menuData); - echo json_encode(["success" => true, "date" => $date, "menu_type" => $menuTypeId, "menu" => $menuData["menus"][$menuTypeId]]); -} - -function handle_add_menu_type() { - global $centro_id, $source_aulario_id; - $input = json_decode(file_get_contents("php://input"), true) ?: $_POST; - $newId = safe_id_segment(strtolower(trim($input["id"] ?? ""))); - $newLabel = trim($input["label"] ?? ""); - $newColor = trim($input["color"] ?? "#0d6efd"); - if ($newId === "" || $newLabel === "") { http_response_code(400); die(json_encode(["error" => "id and label are required", "code" => "MISSING_PARAM"])); } - $menuTypes = get_menu_types($centro_id, $source_aulario_id); - foreach ($menuTypes as $t) { - if (($t["id"] ?? "") === $newId) { http_response_code(400); die(json_encode(["error" => "Menu type already exists", "code" => "DUPLICATE"])); } - } - $menuTypes[] = ["id" => $newId, "label" => $newLabel, "color" => $newColor]; - db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes); - echo json_encode(["success" => true, "menu_type" => ["id" => $newId, "label" => $newLabel, "color" => $newColor], "message" => "Menu type added successfully"]); -} - -function handle_delete_menu_type() { - global $centro_id, $source_aulario_id; - $input = json_decode(file_get_contents("php://input"), true) ?: $_POST; - $deleteId = safe_id_segment(trim($input["id"] ?? "")); - if ($deleteId === "") { http_response_code(400); die(json_encode(["error" => "id is required", "code" => "MISSING_PARAM"])); } - $menuTypes = get_menu_types($centro_id, $source_aulario_id); - $newMenuTypes = array_values(array_filter($menuTypes, fn($t) => ($t["id"] ?? "") !== $deleteId)); - if (count($newMenuTypes) === count($menuTypes)) { http_response_code(404); die(json_encode(["error" => "Menu type not found", "code" => "NOT_FOUND"])); } - db_set_comedor_menu_types($centro_id, $source_aulario_id, $newMenuTypes); - echo json_encode(["success" => true, "message" => "Menu type deleted successfully"]); -} - -function handle_rename_menu_type() { - global $centro_id, $source_aulario_id; - $input = json_decode(file_get_contents("php://input"), true) ?: $_POST; - $renameId = safe_id_segment(trim($input["id"] ?? "")); - $newLabel = trim($input["label"] ?? ""); - $newColor = trim($input["color"] ?? ""); - if ($renameId === "" || $newLabel === "") { http_response_code(400); die(json_encode(["error" => "id and label are required", "code" => "MISSING_PARAM"])); } - $menuTypes = get_menu_types($centro_id, $source_aulario_id); - $found = false; - foreach ($menuTypes as &$t) { - if (($t["id"] ?? "") === $renameId) { - $t["label"] = $newLabel; - if ($newColor !== "") { $t["color"] = $newColor; } - $found = true; - break; - } - } - unset($t); - if (!$found) { http_response_code(404); die(json_encode(["error" => "Menu type not found", "code" => "NOT_FOUND"])); } - db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes); - echo json_encode(["success" => true, "message" => "Menu type renamed successfully"]); -} diff --git a/public_html/aulatek/aulario.php b/public_html/aulatek/aulario.php deleted file mode 100644 index 7ae8d6e..0000000 --- a/public_html/aulatek/aulario.php +++ /dev/null @@ -1,95 +0,0 @@ - -
-

Aulario no encontrado

-

No se ha podido cargar la configuración del aulario.

-
- -
-
-

Aulario:

- - Bienvenidx al aulario . Aquí podrás gestionar las funcionalidades específicas de este aulario. - -
-
- - - - - - \ No newline at end of file diff --git a/public_html/aulatek/comedor.php b/public_html/aulatek/comedor.php deleted file mode 100644 index fb0543e..0000000 --- a/public_html/aulatek/comedor.php +++ /dev/null @@ -1,569 +0,0 @@ - -
-

Menú del Comedor

-

No se ha indicado un aulario válido.

-
- "basal", "label" => "Menú basal", "color" => "#0d6efd"], - ["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"], - ["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"], -]; - -$menuTypes = db_get_comedor_menu_types($centro_id, $source_aulario_id); -if (!is_array($menuTypes) || count($menuTypes) === 0) { - $menuTypes = $defaultMenuTypes; - db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes); -} - -$menuTypeIds = []; -foreach ($menuTypes as $t) { - if (!empty($t["id"])) { - $menuTypeIds[] = $t["id"]; - } -} - -$dateParam = $_GET["date"] ?? date("Y-m-d"); -$dateObj = DateTime::createFromFormat("Y-m-d", $dateParam) ?: new DateTime(); -$date = $dateObj->format("Y-m-d"); -$menuTypeId = $_GET["menu"] ?? ($menuTypeIds[0] ?? "basal"); -if (!in_array($menuTypeId, $menuTypeIds, true)) { - $menuTypeId = $menuTypeIds[0] ?? "basal"; -} - -$ym = $dateObj->format("Y-m"); -$day = $dateObj->format("d"); - - -function blank_menu() -{ - return [ - "plates" => [ - "primero" => ["name" => "", "pictogram" => ""], - "segundo" => ["name" => "", "pictogram" => ""], - "postre" => ["name" => "", "pictogram" => ""], - ] - ]; -} - -$menuData = [ - "date" => $date, - "menus" => [] -]; -$existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day); -if (is_array($existing) && !empty($existing)) { - $menuData = array_merge($menuData, $existing); -} -if (!isset($menuData["menus"][$menuTypeId])) { - $menuData["menus"][$menuTypeId] = blank_menu(); -} - -$canEdit = in_array("sysadmin:access", $_SESSION["auth_data"]["permissions"] ?? []) && !$is_shared; -$saveNotice = ""; -$uploadErrors = []; - -function handle_image_upload($fieldName, $targetBaseName, $baseDir, &$uploadErrors) -{ - if (!isset($_FILES[$fieldName]) || $_FILES[$fieldName]["error"] !== UPLOAD_ERR_OK) { - return null; - } - $ext = strtolower(pathinfo($_FILES[$fieldName]["name"], PATHINFO_EXTENSION)); - $allowed = ["jpg", "jpeg", "png", "webp", "gif"]; - - // Validate by extension first - if (!in_array($ext, $allowed, true)) { - $uploadErrors[] = "El archivo " . htmlspecialchars($_FILES[$fieldName]["name"]) . " no es una imagen válida."; - return null; - } - - // Also validate by MIME type / file contents to avoid spoofed extensions - $tmpPath = $_FILES[$fieldName]["tmp_name"]; - $mimeType = null; - - if (function_exists('finfo_open')) { - $finfo = @finfo_open(FILEINFO_MIME_TYPE); - if ($finfo !== false) { - $mime = @finfo_file($finfo, $tmpPath); - if ($mime !== false) { - $mimeType = $mime; - } - @finfo_close($finfo); - } - } - - // Fallback: try exif_imagetype if available and finfo did not work - if ($mimeType === null && function_exists('exif_imagetype')) { - $type = @exif_imagetype($tmpPath); - if ($type !== false) { - switch ($type) { - case IMAGETYPE_JPEG: - $mimeType = 'image/jpeg'; - break; - case IMAGETYPE_PNG: - $mimeType = 'image/png'; - break; - case IMAGETYPE_GIF: - $mimeType = 'image/gif'; - break; - case IMAGETYPE_WEBP: - $mimeType = 'image/webp'; - break; - } - } - } - - $allowedMime = [ - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'png' => 'image/png', - 'gif' => 'image/gif', - 'webp' => 'image/webp', - ]; - - if ($mimeType === null || !in_array($mimeType, $allowedMime, true)) { - $uploadErrors[] = "El archivo " . htmlspecialchars($_FILES[$fieldName]["name"]) . " no es una imagen válida."; - return null; - } - - if (!is_dir($baseDir)) { - mkdir($baseDir, 0777, true); - } - $target = "$targetBaseName.$ext"; - $targetPath = $baseDir . "/" . safe_filename($target); - if (move_uploaded_file($_FILES[$fieldName]["tmp_name"], $targetPath)) { - return basename($targetPath); - } - $uploadErrors[] = "No se pudo guardar " . htmlspecialchars($_FILES[$fieldName]["name"]) . "."; - return null; -} - -if ($_SERVER["REQUEST_METHOD"] === "POST" && $canEdit) { - $action = $_POST["action"] ?? ""; - - if ($action === "add_type") { - $newId = safe_id_segment(strtolower(trim($_POST["new_type_id"] ?? ""))); - $newLabel = trim($_POST["new_type_label"] ?? ""); - $newColor = trim($_POST["new_type_color"] ?? "#0d6efd"); - if ($newId !== "" && $newLabel !== "") { - $exists = false; - foreach ($menuTypes as $t) { - if (($t["id"] ?? "") === $newId) { $exists = true; break; } - } - if (!$exists) { - $menuTypes[] = ["id" => $newId, "label" => $newLabel, "color" => $newColor]; - db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes); - header("Location: /aulatek/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($newId)); - exit; - } - } - } - - if ($action === "delete_type") { - $deleteId = safe_id_segment(trim($_POST["delete_type_id"] ?? "")); - if ($deleteId !== "") { - $newMenuTypes = array_values(array_filter($menuTypes, fn($t) => ($t["id"] ?? "") !== $deleteId)); - if (count($newMenuTypes) !== count($menuTypes)) { - $menuTypes = $newMenuTypes; - db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes); - $redirectMenuId = !empty($menuTypes) ? $menuTypes[0]["id"] : "basal"; - header("Location: /aulatek/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($redirectMenuId)); - exit; - } - } - } - - if ($action === "rename_type") { - $renameId = safe_id_segment(trim($_POST["rename_type_id"] ?? "")); - $newLabel = trim($_POST["rename_type_label"] ?? ""); - $newColor = trim($_POST["rename_type_color"] ?? ""); - if ($renameId !== "" && $newLabel !== "") { - foreach ($menuTypes as &$t) { - if (($t["id"] ?? "") === $renameId) { - $t["label"] = $newLabel; - if ($newColor !== "") { $t["color"] = $newColor; } - break; - } - } - unset($t); - db_set_comedor_menu_types($centro_id, $source_aulario_id, $menuTypes); - header("Location: /aulatek/comedor.php?aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&menu=" . urlencode($renameId)); - exit; - } - } - - if ($action === "save") { - $menuTypeId = safe_id_segment($_POST["menu_type"] ?? $menuTypeId); - if (!isset($menuData["menus"][$menuTypeId])) { - $menuData["menus"][$menuTypeId] = blank_menu(); - } - - // Pictogram images still stored on filesystem in Comedor dir - $baseDir = aulatek_orgs_base_path() . "/$centro_id/Aularios/$source_aulario_id/Comedor/$ym/$day"; - $plates = ["primero", "segundo", "postre"]; - foreach ($plates as $plate) { - $name = trim($_POST["name_" . $plate] ?? ""); - $menuData["menus"][$menuTypeId]["plates"][$plate]["name"] = $name; - $pictUpload = handle_image_upload("pictogram_file_" . $plate, $menuTypeId . "_" . $plate . "_pict", $baseDir, $uploadErrors); - if ($pictUpload !== null) { - $menuData["menus"][$menuTypeId]["plates"][$plate]["pictogram"] = $pictUpload; - } - } - - db_set_comedor_entry($centro_id, $source_aulario_id, $ym, $day, $menuData); - $saveNotice = "Menú guardado correctamente."; - } -} - -$menuForType = $menuData["menus"][$menuTypeId] ?? blank_menu(); -function image_src($value, $centro_id, $source_aulario_id, $date) -{ - if (!$value) { - return ""; - } - if (filter_var($value, FILTER_VALIDATE_URL)) { - return $value; - } - return "/aulatek/_filefetch.php?type=comedor_image&org=" . urlencode($centro_id) . "&aulario=" . urlencode($source_aulario_id) . "&date=" . urlencode($date) . "&file=" . urlencode($value); -} - -$prevDate = (clone $dateObj)->modify("-1 day")->format("Y-m-d"); -$nextDate = (clone $dateObj)->modify("+1 day")->format("Y-m-d"); - -$userAulas = $tenant_data["aulas"] ?? []; -$aulaOptions = []; -foreach ($userAulas as $aulaId) { - $aulaIdSafe = safe_id_segment($aulaId); - if ($aulaIdSafe === "") { - continue; - } - $aulaData = db_get_aulario($centro_id, $aulaIdSafe); - $aulaOptions[] = [ - "id" => $aulaIdSafe, - "name" => $aulaData["name"] ?? $aulaIdSafe, - ]; -} -require_once "_incl/pre-body.php"; -?> - - -
- ℹ️ Datos compartidos: Este aulario está mostrando los menús del aulario . Para editar, debes acceder al aulario origen o desactivar el acceso compartido en la configuración. -
- - - -
- -
- - - 0): ?> -
-
    - -
  • - -
-
- - - -
- -
- - -
-
- - - - - - -
- Editar menú -
- - -
- $plateLabel): - $plate = $menuForType["plates"][$plateKey] ?? ["name" => "", "pictogram" => ""]; - ?> -
-

- - " placeholder="Ej. Lentejas"> - - - -
- -
- -
-
- -
- Administrar tipos de menú - - -
-

Añadir nuevo tipo de menú

-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-

Tipos de menú existentes

- -
;"> -
-
- -
- ID: -
- Color: ; border-radius: 3px; vertical-align: middle;"> -
-
- - - - -
- - "> - -
-
-
- -
" style="display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #ddd;"> -
- - "> -
-
- - " required> -
-
- - "> -
-
-
- - -
-
-
-
- -
-
- - - - - - - - - diff --git a/public_html/aulatek/diario.php b/public_html/aulatek/diario.php deleted file mode 100644 index 8d495f1..0000000 --- a/public_html/aulatek/diario.php +++ /dev/null @@ -1,379 +0,0 @@ - -
-

Diario del Alumno

-

No se ha indicado un aulario válido.

-
- -
-

Diario del Alumno

-

Error: Directorio base no encontrado.

-
- -
-
-

Diarios de Alumnos

-
- - -

No hay alumnos registrados en este aulario.

- -
- -
-
- - Foto de <?= htmlspecialchars($alumno_name) ?> - -
- ? -
- -
-
-

-
- - Diario disponible - - Sin diario - -
- -
-
- -
- - - -
- -
-

Diario del Alumno

-

Ruta de alumnos inválida.

-
- -
-

Diario del Alumno

-

Alumno no encontrado.

-
- [ - 'label' => 'Panel Diario', - 'icon' => '📋', - 'description' => 'Registro de actividades diarias del panel' - ] -]; - -require_once "_incl/pre-body.php"; -?> -
-
-

Diario de

-
- -
-

- Aulario:
- Fecha actual: -

-
- -
- $type_info): - $type_path = "$alumno_path/Diario"; - $type_file = "$type_path/" . date("Y-m-d") . "/$type_key.json"; - $has_data = file_exists($type_file); - - // Get all diary dates for this type - $diary_dates = []; - if (is_dir($type_path)) { - $dates = glob($type_path . "/*/", GLOB_ONLYDIR); - foreach ($dates as $date_dir) { - $date = basename($date_dir); - if (file_exists("$date_dir/$type_key.json")) { - $diary_dates[] = $date; - } - } - rsort($diary_dates); - } - ?> -
-
-
-

-

-
-
- -
-

Registros disponibles:

-
- -
-
- - - - - -
-
- -
- - -
- -format('Y-m-d') === $date; - } - if (!$is_valid_date || !array_key_exists($type, $diario_types)) { - $type = ""; - } -} - -if (!empty($type) && !empty($date)) { - $type_file = "$alumno_path/Diario/$date/$type.json"; - - if (file_exists($type_file)): - $diary_data = json_decode(file_get_contents($type_file), true); - - // For Panel type, show the data - if ($type === "Panel" && is_array($diary_data)): - ?> - -
-

Detalles del Registro - ()

- -
-

- Registro creado:
- Alumno: -

-
- -
- '¿Quién soy?', - 'calendar' => '¿Qué día es?', - 'calendario_diasemana' => 'Día de la semana', - 'calendario_mes' => 'Mes', - 'actividades' => '¿Qué vamos a hacer?', - 'menu' => '¿Qué vamos a comer?' - ]; - - foreach ($diary_data['panels'] ?? [] as $panel_name => $panel_info): - $label = $panel_labels[$panel_name] ?? $panel_name; - $completed = !is_null($panel_info) && isset($panel_info['completed']); - $timestamp = $completed ? $panel_info['timestamp'] : 'No completado'; - $panel_data = $completed && isset($panel_info['data']) ? $panel_info['data'] : []; - ?> -
-
-
- -
-
-

-

- -

- - -
- -

Alumno:

- - Foto - - -

Día:

- -

Día:

- - Pictograma - - -

Mes:

- - Pictograma - - -

Actividad:

- - Pictograma - - -

Menú:

- -
- -
-
-
- -
- -
-

- 💡 Nota: Este diario se genera automáticamente cuando el alumno completa los paneles en la aplicación del Panel Diario. -

-
-
- - - - - -
-
-

¡Hola, !

- - Bienvenidx a la plataforma de gestión de aularios conectados. Desde aquí podrás administrar los aularios asociados a tu cuenta. - -
-
-
- - ' . htmlspecialchars($aulario_name) . ' Icono -
- ' . htmlspecialchars($aulario_name) . ' - '; - } ?> - - - Icono SuperCafe -
- SuperCafe -
- -
- - - - - diff --git a/public_html/aulatek/paneldiario.php b/public_html/aulatek/paneldiario.php deleted file mode 100755 index ed18c43..0000000 --- a/public_html/aulatek/paneldiario.php +++ /dev/null @@ -1,1321 +0,0 @@ - date("Y-m-d H:i:s"), - "alumno" => $alumno, - "panels" => [ - "quien_soy" => null, - "calendar" => null, - "calendario_diasemana" => null, - "calendario_mes" => null, - "actividades" => null, - "menu" => null - ] - ]; - file_put_contents($panel_file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - } - } -} - -function guardarPanelDiario($panel_name, $data, $alumno, $centro_id, $aulario_id) { - $diario_path = getDiarioPath($alumno, $centro_id, $aulario_id); - if ($diario_path) { - $panel_file = $diario_path . "/Panel.json"; - if (file_exists($panel_file)) { - $existing = json_decode(file_get_contents($panel_file), true); - if (is_array($existing)) { - $existing["panels"][$panel_name] = [ - "completed" => true, - "timestamp" => date("Y-m-d H:i:s"), - "data" => $data - ]; - file_put_contents($panel_file, json_encode($existing, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - } - } - } -} - -// Manejo de AJAX para guardar paneles -if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_GET['api'])) { - header('Content-Type: application/json'); - $api_action = $_GET['api']; - $alumno = $_SESSION["aulatek_selected_alumno"] ?? ($_SESSION["entreaulas_selected_alumno"] ?? ''); - $centro_id = $tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? ''); - - if ($api_action === 'guardar_panel' && $alumno && $centro_id) { - $input = json_decode(file_get_contents('php://input'), true); - $panel_name = $input['panel'] ?? ''; - $panel_data = $input['data'] ?? []; - $aulario_id = $_SESSION["aulatek_selected_aulario"] ?? ($_SESSION["entreaulas_selected_aulario"] ?? ''); - guardarPanelDiario($panel_name, $panel_data, $alumno, $centro_id, $aulario_id); - echo json_encode(['success' => true]); - die(); - } -} - -$form_action = $_GET["form"] ?? ""; -switch ($form_action) { - case "alumno_selected": - $alumno = safe_id_segment($_GET["alumno"] ?? ""); - $centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? "")); - $aulario_id = safe_id_segment($_GET["aulario"] ?? ''); - $photo_url = $_GET["photo"] ?? ''; - if ($alumno !== "" && $centro_id !== "" && $aulario_id !== "") { - $_SESSION["aulatek_selected_alumno"] = $alumno; - $_SESSION["aulatek_selected_aulario"] = $aulario_id; - $_SESSION["entreaulas_selected_alumno"] = $alumno; - $_SESSION["entreaulas_selected_aulario"] = $aulario_id; - initDiario($alumno, $centro_id, $aulario_id); - // Guardar el panel "quien_soy" como completado con foto URL si existe - $who_am_i_data = ["alumno" => $alumno]; - if ($photo_url !== '') { - $who_am_i_data["photoUrl"] = $photo_url; - } - guardarPanelDiario("quien_soy", $who_am_i_data, $alumno, $centro_id, $aulario_id); - header("Location: paneldiario.php?aulario=" . urlencode($_GET["aulario"] ?? '')); - die(); - } - break; -} -require_once "_incl/pre-body.php"; -ini_set("display_errors", "0"); -?> - - - - - $panel_value) { - $progress[$panel_name] = !is_null($panel_value) && isset($panel_value['completed']); - } - } - } - } -} - -// Contar paneles completados -$paneles_totales = 6; // quien_soy, calendar, calendario_diasemana, calendario_mes, actividades, menu -$paneles_completados = count(array_filter($progress)); -$porcentaje = ($paneles_completados / $paneles_totales) * 100; -$todos_completados = ($paneles_completados === $paneles_totales); - -$view_action = $_GET["action"] ?? "index"; -switch ($view_action) { - default: - case "index": - if ($alumno_actual): -?> -
-

Panel de Diario -

-
-
-
-

- de paneles completados -

-
- - - - - - - - - - -
-
-

Resumen del Diario

-

- Alumno:
- Fecha:
- Hora de registro: -

-
- -
-

Paneles Completados

- - '¿Quién soy?', - 'calendar' => '¿Qué día es?', - 'calendario_diasemana' => 'Día de la semana', - 'calendario_mes' => 'Mes', - 'actividades' => '¿Qué vamos a hacer?', - 'menu' => '¿Qué vamos a comer?' - ]; - - foreach ($diario_data['panels'] as $panel_name => $panel_info): - $label = $panel_labels[$panel_name] ?? $panel_name; - $completed = !is_null($panel_info) && isset($panel_info['completed']); - $timestamp = $completed ? $panel_info['timestamp'] : 'No completado'; - $panel_data = $completed && isset($panel_info['data']) ? $panel_info['data'] : []; - ?> -
-
- -
-
-

-

- - -
- -

Alumno seleccionado:

- - Foto del alumno - - -

Día seleccionado:

- -

Día de la semana:

- - <?php echo htmlspecialchars($panel_data['nombre']); ?> - - -

Mes seleccionado:

- - <?php echo htmlspecialchars($panel_data['nombre']); ?> - - -

Actividad:

- - <?php echo htmlspecialchars($panel_data['actividad']); ?> - - -

Tipo de menú:

- -
- -
-
- -
- - -
- - -

Error: No hay datos de diario disponibles.

'; - endif; - break; - - case "quien_soy": - // ¿Quién soy? - Identificación del alumno - $aulario_id = safe_id_segment($_GET["aulario"] ?? ''); - $centro_id = safe_organization_id($tenant_data["organizacion"] ?? ($tenant_data["centro"] ?? '')); - - // Validate parameters - if (empty($aulario_id) || empty($centro_id)) { - echo '

Error: Parámetros inválidos.

'; - break; - } - - $base_path = aulatek_orgs_base_path(); - $alumnos_path = "$base_path/$centro_id/Aularios/$aulario_id/Alumnos"; - - // Validate the path is within the expected directory - $real_path = realpath($alumnos_path); - $real_base = realpath($base_path); - - $alumnos = []; - if ($real_path !== false && $real_base !== false && strpos($real_path, $real_base) === 0 && is_dir($real_path)) { - $alumnos = glob($real_path . "/*", GLOB_ONLYDIR); - } - ?> - -
-
-

¿Quién soy?

-
-
-
- -
-

No hay alumnos registrados en este aulario.

-

Para añadir alumnos, accede al inicio del aulario.

-
- - , "", "");' aria-label="Seleccionar alumno "> - - Foto de <?php echo htmlspecialchars($alumno_name); ?> - -
- ? -
- -
- -
- -
- - - -
-
-

¿Que vamos a hacer?

-
-
-
- - - - - - -
- - format("Y-m-d"); - - $defaultMenuTypes = [ - ["id" => "basal", "label" => "Menú basal", "color" => "#0d6efd"], - ["id" => "vegetariano", "label" => "Menú vegetariano", "color" => "#198754"], - ["id" => "alergias", "label" => "Menú alergias", "color" => "#dc3545"], - ]; - $menuTypes = ($centro_id !== '' && $source_aulario_id !== '') ? db_get_comedor_menu_types($centro_id, $source_aulario_id) : []; - if (!is_array($menuTypes) || count($menuTypes) === 0) { - $menuTypes = $defaultMenuTypes; - } - - $menuTypeIds = []; - foreach ($menuTypes as $t) { - if (!empty($t["id"])) { - $menuTypeIds[] = $t["id"]; - } - } - $menuTypeId = $_GET["menu"] ?? ($menuTypeIds[0] ?? "basal"); - if (!in_array($menuTypeId, $menuTypeIds, true)) { - $menuTypeId = $menuTypeIds[0] ?? "basal"; - } - - $ym = $dateObj->format("Y-m"); - $day = $dateObj->format("d"); - - $menuData = ["date" => $date, "menus" => []]; - if ($centro_id !== '' && $source_aulario_id !== '') { - $existing = db_get_comedor_entry($centro_id, $source_aulario_id, $ym, $day); - if (!empty($existing)) { - $menuData = array_merge($menuData, $existing); - } - } - $menuForType = $menuData["menus"][$menuTypeId] ?? null; - - function image_src_simple($value, $centro_id, $aulario_id, $date) - { - if (!$value) { - return ""; - } - return "/aulatek/_filefetch.php?type=comedor_image&org=" . urlencode($centro_id) . "&aulario=" . urlencode($aulario_id) . "&date=" . urlencode($date) . "&file=" . urlencode($value); - } - ?> - -
-

¿Qué vamos a comer?

-
-
- - - - - -
-

¿Que dia es?

-
-
- -
'; - } - - foreach (range(1, $days_in_month) as $dia) { - $ds = date('N', strtotime(date('Y-m-') . sprintf("%02d", $dia))); - if ($ds > 5) { - ?> -
- -
- - - - -
'; - } - ?> - - - -
-

¿Que día de la semana es?

-
-
- - "Lunes", - 2 => "Martes", - 3 => "Miércoles", - 4 => "Jueves", - 5 => "Viernes" - ]; - $dow_euskara = [ - 1 => "Astelehena", - 2 => "Asteartea", - 3 => "Asteazkena", - 4 => "Osteguna", - 5 => "Ostirala" - ]; - foreach ($days_of_week as $ds => $day_name) { - $pictogram_url = '/static/arasaac/diadelasemana/' . strtolower($day_name) . '.png'; - ?> - - - - - - -
- - - "Enero", - 2 => "Febrero", - 3 => "Marzo", - 4 => "Abril", - 5 => "Mayo", - 6 => "Junio", - 7 => "Julio", - 8 => "Agosto", - 9 => "Septiembre", - 10 => "Octubre", - 11 => "Noviembre", - 12 => "Diciembre" - ]; - $meses_eus = [ - 1 => "Urtarrila", - 2 => "Otsaila", - 3 => "Martxoa", - 4 => "Apirila", - 5 => "Maiatza", - 6 => "Ekaina", - 7 => "Uztaila", - 8 => "Abuztua", - 9 => "Iraila", - 10 => "Urria", - 11 => "Azaroa", - 12 => "Abendua" - ]; - ?> -
-

¿Que mes es?

-
-
- - $mes_name) { - $pictogram_url = '/static/arasaac/mesesdelano/' . strtolower($mes_name) . '.png'; - ?> - - - - - - -
- - - \ No newline at end of file diff --git a/public_html/aulatek/pdf_secure_viewer.php b/public_html/aulatek/pdf_secure_viewer.php deleted file mode 100644 index 4fb73cd..0000000 --- a/public_html/aulatek/pdf_secure_viewer.php +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - - PDF Seguro - - - -
- Este PDF se muestra en modo seguro. No se permite la descarga, impresión o copia del contenido. -
-
- - - - 100% -
-
- Cargando PDF... -
- - - - diff --git a/public_html/aulatek/proyectos.php b/public_html/aulatek/proyectos.php deleted file mode 100644 index b0721f7..0000000 --- a/public_html/aulatek/proyectos.php +++ /dev/null @@ -1,2670 +0,0 @@ - -
-

Proyectos

-

No se ha indicado un aulario válido.

-
-