|
|
||
|---|---|---|
| .gitignore | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
| marked.min.js | ||
| viewer.html | ||
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-pamlibpam-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.jslocal — 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 |