Aplicación que publica Notas via API. El uso de hacks Nginx y SSHFS hacen innecesario el uso de un backend de la aplicación como tal.
Go to file
fenix d270ba3ce8 3,2.1..0 2026-02-22 22:26:28 +01:00
.gitignore 3,2.1..0 2026-02-22 22:26:28 +01:00
CLAUDE.md 3,2.1..0 2026-02-22 22:26:28 +01:00
LICENSE Initial commit 2026-02-22 20:59:52 +00:00
README.md 3,2.1..0 2026-02-22 22:26:28 +01:00
marked.min.js 3,2.1..0 2026-02-22 22:26:28 +01:00
viewer.html 3,2.1..0 2026-02-22 22:26:28 +01:00

README.md

napi-data — Notas Personalizadas para Estudiantes

Sistema minimalista para servir retroalimentación personalizada a cada alumno, directamente desde ficheros Markdown editados con Emacs.

Estado: Producción — https://notas.qu3v3d0.tech (2026-02-22)


¿Qué hace esto?

Cada alumno entra en https://notas.TU_DOMINIO con sus credenciales SFTP y ve únicamente sus propias notas en formato HTML renderizado. El profesor edita un fichero .md desde Emacs y el alumno lo ve al refrescar el navegador.

Sin backend. Sin base de datos. Sin framework. Sin CDN.


Arquitectura

PROFESOR (aldebaran / máquina local)
────────────────────────────────────
~/napi-data/                         ← sshfs mount
  ├── _plantilla/notas.md
  ├── alumno01/notas.md    ──┐
  ├── alumno02/notas.md    ──┤  escribe con Emacs
  └── alumnoN/notas.md    ──┘  C-x C-s → visible al instante

           │ sshfs (SSH port 22)
           ▼

SERVIDOR (zzz / Debian VPS)
────────────────────────────
/var/www/api/
  ├── viewer.html          ← app completa (~40 líneas)
  ├── marked.min.js        ← renderer Markdown local (sin CDN)
  └── data/
        ├── alumno01/notas.md
        ├── alumno02/notas.md
        └── alumnoN/notas.md

Nginx + libnginx-mod-http-auth-pam
  ↓ auth con credenciales SFTP del alumno
  ↓ $remote_user = "alumno01"
  ↓ sirve data/alumno01/notas.md
  ↓ viewer.html lo renderiza en el navegador

ALUMNO (cualquier dispositivo)
───────────────────────────────
https://notas.TU_DOMINIO
  → login con sus credenciales SFTP
  → ve sus notas en HTML
  → refresca → cambios inmediatos

Gestión de registros DNS

  • Se requiere un https://TU_DOMINIO ad-hoc

  • Por simplicidad, necesitas apuntar TODOS los subodminios a la dirección IP del servidor (usa 'wildcard' - '*')

Requisitos del servidor (Debian)

  • Debian 11/12/...
  • Nginx
  • libnginx-mod-http-auth-pam
  • libpam-runtime (incluido por defecto)
  • Certificado SSL (Let's Encrypt o autofirmado)
  • Usuarios SFTP ya configurados en el sistema (grupo sftpusers) usando 'chroot' , partiendo de que cada estudiante tiene su 'usuario' del sistema.
  • SSH accesible desde la máquina del profesor (para sshfs)

Despliegue desde cero en un Debian nuevo

1. Instalar dependencias

sudo apt update
sudo apt install -y nginx libnginx-mod-http-auth-pam

2. Crear estructura de directorios en el servidor

sudo mkdir -p /var/www/api/data
sudo chown -R www-data:www-data /var/www/api
sudo chmod 755 /var/www/api
sudo chmod 755 /var/www/api/data

3. Desplegar viewer.html y marked.min.js

# Copiar viewer.html al servidor (ver sección "Ficheros de la app" más abajo)
sudo cp viewer.html /var/www/api/viewer.html
sudo chown www-data:www-data /var/www/api/viewer.html

# Descargar marked.min.js (o copiar desde backup)
# https://cdn.jsdelivr.net/npm/marked/marked.min.js
sudo wget -O /var/www/api/marked.min.js \
  https://cdn.jsdelivr.net/npm/marked/marked.min.js
sudo chown www-data:www-data /var/www/api/marked.min.js

⚠️ Una vez descargado marked.min.js, la app funciona sin conexión a CDNs — es el punto del diseño. No actualices el fichero sin probarlo primero.

4. Añadir www-data al grupo shadow (para auth_pam)

sudo usermod -aG shadow www-data
# Reiniciar nginx para que tome el cambio de grupo
sudo systemctl restart nginx

5. Configurar Nginx

Crear /etc/nginx/sites-available/napi:

server {
    listen 443 ssl;
    server_name notas.TU_DOMINIO;

    # --- SSL ---
    ssl_certificate     /etc/ssl/certs/TU_DOMINIO.crt;
    ssl_certificate_key /etc/ssl/private/TU_DOMINIO.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    root /var/www/api;
    index viewer.html;

    # --- Autenticación PAM ---
    auth_pam              "Notas DDAW2";
    auth_pam_service_name "common-auth";

    # --- Servir notas del alumno autenticado ---
    location = /notas.md {
        alias /var/www/api/data/$remote_user/notas.md;
        default_type text/plain;
        charset utf-8;
    }

    # --- Assets estáticos (viewer + marked) ---
    location ~* \.(html|js)$ {
        auth_pam off;    # viewer.html y marked.min.js son públicos
        expires 1h;
    }

    access_log /var/log/nginx/napi-access.log;
    error_log  /var/log/nginx/napi-error.log;
}

# Redirección HTTP → HTTPS
server {
    listen 80;
    server_name notas.TU_DOMINIO;
    return 301 https://$server_name$request_uri;
}
sudo ln -s /etc/nginx/sites-available/napi /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

6. Crear carpeta de datos para cada alumno

# Para un alumno:
ALUMNO="alumno01"
sudo mkdir -p /var/www/api/data/$ALUMNO
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$ALUMNO/notas.md
sudo chown -R www-data:www-data /var/www/api/data/$ALUMNO

# Para todos los alumnos de una vez (si ya existen como usuarios del sistema):
for user in $(getent group sftpusers | cut -d: -f4 | tr ',' ' '); do
    sudo mkdir -p /var/www/api/data/$user
    sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$user/notas.md 2>/dev/null || true
    sudo chown -R www-data:www-data /var/www/api/data/$user
    echo "✅ $user"
done

7. Verificar que funciona

# Probar autenticación y respuesta
curl -u alumno01:SU_CONTRASEÑA -sk https://notas.TU_DOMINIO/notas.md | head -5

# Debe devolver las primeras líneas del notas.md del alumno

Configurar sshfs en la máquina del profesor

Instalar sshfs

# Debian/Ubuntu
sudo apt install sshfs

# Fedora/RHEL
sudo dnf install fuse-sshfs

Crear punto de montaje

mkdir -p ~/napi-data

Montar manualmente (prueba)

sshfs USUARIO@TU_SERVIDOR:/var/www/api/data ~/napi-data \
  -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3

Montaje persistente con systemd (recomendado)

Crear ~/.config/systemd/user/home-TUUSUARIO-napi\x2ddata.mount (sustituye TUUSUARIO por tu usuario real, ej: fenix):

[Unit]
Description=napi-data → servidor:/var/www/api/data (sshfs)
After=network-online.target

[Mount]
What=USUARIO@TU_SERVIDOR:/var/www/api/data
Where=/home/TUUSUARIO/napi-data
Type=fuse.sshfs
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now 'home-TUUSUARIO-napi\x2ddata.mount'

# Verificar
systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount'

💡 El nombre de la unit debe coincidir exactamente con la ruta del Where (reemplazando / por - y los guiones por \x2d).


Workflow del profesor

1.  C-x C-f ~/napi-data/pablo/notas.md     ← abrir en Emacs
2.  Editar feedback, notas, próximos pasos
3.  C-x C-s                                 ← guardar
4.  Alumno refresca el navegador            ← cambios visibles

No hay paso 5.


Añadir un alumno nuevo

# En el servidor:
ALUMNO="nuevo_alumno"
sudo mkdir -p /var/www/api/data/$ALUMNO
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$ALUMNO/notas.md
sudo chown -R www-data:www-data /var/www/api/data/$ALUMNO

# En la máquina del profesor (aparece automáticamente via sshfs):
ls ~/napi-data/$ALUMNO/   # → notas.md
# Editar con Emacs y personalizar

Formato del fichero notas.md

# Notas — NombreAlumno

> 🏫 **Módulo:** NOMBRE_MODULO
> 📅 **Última actualización:** YYYY-MM-DD

---

## 📊 Resumen

| Práctica | Título | Nota | Estado |
|:---------|:-------|:----:|:------:|
| P2.3 | Nginx via SFTP | 7/10 | ✅ |
| P2.4 | HTTP y HTTPS   | 8/10 | ✅ |
| P2.7 | Multi-sitio    | 9/10 | ✅ |

---

## P2.7 — Multi-sitio Web con Nginx

**Nota: 9/10** · _Escaneado: 2026-02-19_

NombreAlumno, [feedback personalizado].

### Criterios

| Criterio | Puntos | Estado |
|:---------|:------:|:------:|
| Hextris desplegado + HTTPS | 1,5/1,5 | ✅ |
| Gzip | 1/1 | ✅ |
| CSP correcta | 1/1,5 | ⚠️ unsafe-inline |

### Próximos pasos

1. Corregir CSP: eliminar `unsafe-inline`
2. Subir capturas de verificación

Ver _plantilla/notas.md como punto de partida para nuevos alumnos.


Ficheros de la app (en el servidor)

viewer.html

App completa en ~40 líneas. Fetcha /notas.md (que Nginx resuelve al fichero del alumno autenticado) y lo renderiza con marked.min.js:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Notas</title>
  <style>
    body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
    table { border-collapse: collapse; width: 100%; }
    th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
    th { background: #f4f4f4; }
    code { background: #f0f0f0; padding: 0.1em 0.4em; border-radius: 3px; }
    pre code { display: block; padding: 1rem; overflow-x: auto; }
    blockquote { border-left: 4px solid #ccc; margin: 0; padding-left: 1rem; color: #555; }
  </style>
</head>
<body>
  <div id="content">Cargando...</div>
  <script src="marked.min.js"></script>
  <script>
    fetch('/notas.md')
      .then(r => r.ok ? r.text() : Promise.reject(r.status))
      .then(md => { document.getElementById('content').innerHTML = marked.parse(md); })
      .catch(e => { document.getElementById('content').textContent = 'Error: ' + e; });
  </script>
</body>
</html>

marked.min.js

Descargar de https://cdn.jsdelivr.net/npm/marked/marked.min.js y guardar como fichero local en /var/www/api/marked.min.js. No referenciar CDN externo (rompe la CSP y crea dependencia de terceros).


Gestión del mount sshfs

# Estado
systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount'

# Montar
systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount'

# Desmontar
systemctl --user stop 'home-TUUSUARIO-napi\x2ddata.mount'

# Si se cuelga (mount zombie):
fusermount -uz ~/napi-data
systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount'

Troubleshooting

Síntoma Causa probable Solución
403 Forbidden al acceder www-data no está en grupo shadow sudo usermod -aG shadow www-data && sudo systemctl restart nginx
401 Unauthorized con credenciales correctas PAM no configurado Verificar que auth_pam_service_name "common-auth" existe en /etc/pam.d/
/notas.md devuelve 404 Carpeta del alumno no existe en data/ sudo mkdir -p /var/www/api/data/$ALUMNO
sshfs mount desaparece Pérdida de conexión SSH La opción reconnect lo recupera solo; si no, systemctl --user restart
El alumno ve las notas de otro $remote_user vacío o PAM no activo Verificar que auth_pam y auth_pam_service_name están en el bloque correcto
marked is not defined marked.min.js no accesible Verificar que el fichero existe en /var/www/api/ y tiene permisos de lectura

Seguridad

  • Cada alumno solo ve sus propias notas — Nginx resuelve el path con $remote_user
  • Sin ejecución de código en el servidor — todo es estático
  • marked.min.js local — sin dependencia de CDNs externos
  • Credenciales: las mismas que usa el alumno para subir archivos por SFTP
  • SSL/TLS obligatorio (redirección 301 desde HTTP)

Historial

Fecha Hito
2026-02-22 MVP desplegado: sshfs + Nginx auth_pam + viewer.html + marked.js
2026-02-22 notas.md generadas para los 19 alumnos de DDAW2