Cuenta de externas, arreglos de seguridad, títulos arriba y despliegue
- Rol «externas» para la asamblea de externas: pide sin cupo, siempre con el colectivo de fuera y su contacto, y sus solicitudes salen en naranja hasta que la asamblea de la Enre las confirma. - /pedir/ explica en una línea que los colectivos externos piden fecha en la asamblea de externas, los lunes de 19:00 a 20:00. - Seguridad: argon2 limitado a dos cálculos a la vez y el intento se cuenta antes de calcular (60 accesos simultáneos llevaban el proceso a 3,8 GB); bloqueo por cuenta a 20 fallos para que no se pueda dejar fuera a la asamblea; avisos por sesión en vez de por URL; sig sin «..»; eventos de 31 días como mucho; parámetros de argon2 acotados. - Títulos pegados arriba en todas las páginas; la franja de la enredadera recupera ese alto para seguir llenando la pantalla. - despliegue/: instalar.sh, cuenta.sh, servicio de systemd con el proceso encerrado, fragmento de nginx que además deja de servir .git y las notas internas, y configuración de ejemplo. Ninguna contraseña en el repositorio.
This commit is contained in:
parent
ce3127baff
commit
7d5dbeec60
24 changed files with 606 additions and 96 deletions
122
gestor/pedir.go
122
gestor/pedir.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -134,19 +135,20 @@ func ipCliente(r *http.Request) string {
|
|||
// Base es lo que necesita el armazón de la página: el título, quién ha entrado
|
||||
// y los avisos.
|
||||
type Base struct {
|
||||
Titulo string
|
||||
Sub string
|
||||
Actual string // qué entrada del menú de la sección va marcada
|
||||
Dentro bool
|
||||
Usuario string
|
||||
Nombre string
|
||||
EsEnre bool
|
||||
CSRF string
|
||||
Aviso string
|
||||
Error string
|
||||
Reglas Reglas
|
||||
Hasta string
|
||||
Sig string // adónde volver después de entrar, ya comprobado
|
||||
Titulo string
|
||||
Sub string
|
||||
Actual string // qué entrada del menú de la sección va marcada
|
||||
Dentro bool
|
||||
Usuario string
|
||||
Nombre string
|
||||
EsEnre bool
|
||||
EsExternas bool
|
||||
CSRF string
|
||||
Aviso string
|
||||
Error string
|
||||
Reglas Reglas
|
||||
Hasta string
|
||||
Sig string // adónde volver después de entrar, ya comprobado
|
||||
}
|
||||
|
||||
// destinoSeguro deja pasar solo rutas de la propia sección: nada de otro
|
||||
|
|
@ -159,6 +161,14 @@ func destinoSeguro(sig string) string {
|
|||
strings.HasPrefix(sig, "//") {
|
||||
return ""
|
||||
}
|
||||
// Nada de «/pedir/../»: la ruta tiene que estar ya limpia.
|
||||
limpia := path.Clean(u.Path)
|
||||
if strings.HasSuffix(u.Path, "/") && limpia != "/" {
|
||||
limpia += "/"
|
||||
}
|
||||
if limpia != u.Path || !strings.HasPrefix(limpia, "/pedir/") {
|
||||
return ""
|
||||
}
|
||||
return u.RequestURI()
|
||||
}
|
||||
|
||||
|
|
@ -181,6 +191,7 @@ func (a *App) base(titulo, sub, actual string, c Cuenta, ses *Sesion) Base {
|
|||
b.Usuario = c.Usuario
|
||||
b.Nombre = c.Nombre
|
||||
b.EsEnre = c.Rol == RolEnre
|
||||
b.EsExternas = c.Rol == RolExternas
|
||||
b.CSRF = ses.CSRF
|
||||
}
|
||||
return b
|
||||
|
|
@ -275,6 +286,12 @@ func (a *App) csrfValido(ses *Sesion, r *http.Request) bool {
|
|||
return iguales(r.PostFormValue("csrf"), ses.CSRF)
|
||||
}
|
||||
|
||||
// avisar deja un aviso para la página siguiente y redirige a ella.
|
||||
func (a *App) avisar(w http.ResponseWriter, r *http.Request, ses *Sesion, ruta, aviso string) {
|
||||
a.ses.PonerAviso(ses.ID, aviso, "")
|
||||
http.Redirect(w, r, ruta, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ portada
|
||||
|
||||
func (a *App) verPortada(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -302,9 +319,9 @@ func (a *App) verPortada(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
datos.Sig = sig
|
||||
datos.Aviso = r.URL.Query().Get("a")
|
||||
if sig != "" {
|
||||
datos.Aviso = "Para pedir esa fecha, entra con las credenciales de tu colectivo."
|
||||
datos.Aviso = "Para pedir esa fecha, entra con las credenciales de tu colectivo. " +
|
||||
"Colectivos externos: se pide en la asamblea de externas, los lunes de 19:00 a 20:00."
|
||||
}
|
||||
a.pintar(w, "portada.tmpl.html", datos)
|
||||
}
|
||||
|
|
@ -325,9 +342,19 @@ func (a *App) entrar(w http.ResponseWriter, r *http.Request) {
|
|||
usuario := strings.ToLower(strings.TrimSpace(r.PostFormValue("usuario")))
|
||||
clave := r.PostFormValue("clave")
|
||||
ip := ipCliente(r)
|
||||
cerrojos := []string{"ip:" + ip, "usuario:" + usuario}
|
||||
// Un usuario con forma imposible cae siempre en el mismo cajón: si no,
|
||||
// cada nombre inventado (hasta 64 KB) sería una entrada más en memoria.
|
||||
llaveU := usuario
|
||||
if ValidarUsuario(usuario) != nil {
|
||||
llaveU = "?"
|
||||
}
|
||||
|
||||
if bloqueado, falta := a.cer.Bloqueado(cerrojos...); bloqueado {
|
||||
// El intento se apunta ANTES de calcular la clave: ver Cerrojo.Intentar.
|
||||
pasa, falta := a.cer.Intentar(ip)
|
||||
if pasa {
|
||||
pasa, falta = a.cerU.Intentar(llaveU)
|
||||
}
|
||||
if !pasa {
|
||||
a.auth.Escribir("AUTH_BLOCK", ip, usuario, "demasiados_intentos")
|
||||
a.errorAcceso(w, r, fmt.Sprintf("Demasiados intentos. Prueba otra vez en %d minutos.",
|
||||
int(falta.Minutes())+1))
|
||||
|
|
@ -339,19 +366,18 @@ func (a *App) entrar(w http.ResponseWriter, r *http.Request) {
|
|||
// Se gasta el mismo tiempo que con una cuenta real para que no se pueda
|
||||
// averiguar que usuarios existen.
|
||||
VerificarClave(hashInutil, clave)
|
||||
a.cer.Fallo(cerrojos...)
|
||||
a.auth.Escribir("AUTH_FAIL", ip, usuario, "cuenta_desconocida")
|
||||
a.errorAcceso(w, r, "Usuario o contraseña incorrectos.")
|
||||
return
|
||||
}
|
||||
if !VerificarClave(c.Hash, clave) {
|
||||
a.cer.Fallo(cerrojos...)
|
||||
a.auth.Escribir("AUTH_FAIL", ip, usuario, "clave_incorrecta")
|
||||
a.errorAcceso(w, r, "Usuario o contraseña incorrectos.")
|
||||
return
|
||||
}
|
||||
|
||||
a.cer.Limpiar(cerrojos...)
|
||||
a.cer.Limpiar(ip)
|
||||
a.cerU.Limpiar(llaveU)
|
||||
a.auth.Escribir("AUTH_OK", ip, usuario, "-")
|
||||
ses := a.ses.Crear(c.Usuario)
|
||||
a.ponerCookie(w, ses.ID, 0)
|
||||
|
|
@ -415,6 +441,7 @@ type FilaMia struct {
|
|||
Etiqueta string
|
||||
Caduca string
|
||||
Liberable bool
|
||||
Externas bool // la pidió la asamblea de externas: va en naranja
|
||||
}
|
||||
|
||||
func (a *App) filaMia(e Evento) FilaMia {
|
||||
|
|
@ -422,6 +449,10 @@ func (a *App) filaMia(e Evento) FilaMia {
|
|||
Evento: e, Dias: e.NumDias(), Legible: rangoLegible(e),
|
||||
Etiqueta: e.Estado.Etiqueta(), Liberable: e.Estado.Ocupa(),
|
||||
}
|
||||
if c, ok := a.alm.BuscarCuenta(e.Solicitante); ok && c.Rol == RolExternas {
|
||||
f.Externas = true
|
||||
f.Etiqueta = etiquetaEstado(e.Estado, true)
|
||||
}
|
||||
if e.Estado == Solicitado {
|
||||
limite := e.Desde.Mas(-a.cfg.Reglas.DiasMinimoConfirmar)
|
||||
porTiempo := DiaDe(e.Creado.AddDate(0, 0, a.cfg.Reglas.DiasMaximoPendiente))
|
||||
|
|
@ -459,7 +490,7 @@ func (a *App) verMisFechas(w http.ResponseWriter, r *http.Request, c Cuenta, ses
|
|||
Restantes: max0(a.cfg.Reglas.CupoDias - len(usados)),
|
||||
DiasUsados: listaDias(usados),
|
||||
}
|
||||
datos.Aviso = r.URL.Query().Get("a")
|
||||
datos.Aviso, datos.Error = a.ses.TomarAviso(ses.ID)
|
||||
a.pintar(w, "mis-fechas.tmpl.html", datos)
|
||||
}
|
||||
|
||||
|
|
@ -537,7 +568,7 @@ func (a *App) hacerNueva(w http.ResponseWriter, r *http.Request, c Cuenta, ses *
|
|||
if err == nil {
|
||||
estado := Solicitado
|
||||
if c.Rol == RolEnre {
|
||||
estado = Confirmado // la asamblea de la casa no se pide permiso a si misma
|
||||
estado = Confirmado // la asamblea de la Enre no se pide permiso a si misma
|
||||
}
|
||||
e = Evento{
|
||||
ID: nuevoID(), Titulo: s.Titulo, Solicitante: c.Usuario,
|
||||
|
|
@ -558,8 +589,7 @@ func (a *App) hacerNueva(w http.ResponseWriter, r *http.Request, c Cuenta, ses *
|
|||
return
|
||||
}
|
||||
log.Printf("%s pide %s (%s) como %s", c.Usuario, rangoLegible(e), e.Titulo, e.Estado)
|
||||
http.Redirect(w, r, "/pedir/mis-fechas?a="+url.QueryEscape("Fecha apuntada: "+rangoLegible(e)),
|
||||
http.StatusSeeOther)
|
||||
a.avisar(w, r, ses, "/pedir/mis-fechas", "Fecha apuntada: "+rangoLegible(e))
|
||||
}
|
||||
|
||||
func (a *App) hacerLiberar(w http.ResponseWriter, r *http.Request, c Cuenta, ses *Sesion) {
|
||||
|
|
@ -592,8 +622,7 @@ func (a *App) hacerLiberar(w http.ResponseWriter, r *http.Request, c Cuenta, ses
|
|||
if r.PostFormValue("volver") == "todas" && c.Rol == RolEnre {
|
||||
destino = "/pedir/todas"
|
||||
}
|
||||
http.Redirect(w, r, destino+"?a="+url.QueryEscape("Fecha liberada: "+rangoLegible(e)),
|
||||
http.StatusSeeOther)
|
||||
a.avisar(w, r, ses, destino, "Fecha liberada: "+rangoLegible(e))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- todas
|
||||
|
|
@ -619,10 +648,14 @@ func (a *App) verTodas(w http.ResponseWriter, r *http.Request, c Cuenta, ses *Se
|
|||
if quien == "" {
|
||||
quien = e.Solicitante
|
||||
}
|
||||
if e.Externo() {
|
||||
fila := a.filaMia(e)
|
||||
switch {
|
||||
case fila.Externas:
|
||||
quien = e.Amadrinado + " · asamblea de externas"
|
||||
case e.Externo():
|
||||
quien = fmt.Sprintf("%s · amadrina %s", e.Amadrinado, quien)
|
||||
}
|
||||
filas = append(filas, FilaTodas{FilaMia: a.filaMia(e), Quien: quien})
|
||||
filas = append(filas, FilaTodas{FilaMia: fila, Quien: quien})
|
||||
}
|
||||
sort.Slice(filas, func(i, j int) bool { return filas[i].Evento.Desde < filas[j].Evento.Desde })
|
||||
|
||||
|
|
@ -633,7 +666,7 @@ func (a *App) verTodas(w http.ResponseWriter, r *http.Request, c Cuenta, ses *Se
|
|||
Base: a.base("TODAS LAS FECHAS", "Lo que viene, de todos los colectivos", "todas", c, ses),
|
||||
Filas: filas,
|
||||
}
|
||||
datos.Aviso = r.URL.Query().Get("a")
|
||||
datos.Aviso, datos.Error = a.ses.TomarAviso(ses.ID)
|
||||
a.pintar(w, "todas.tmpl.html", datos)
|
||||
}
|
||||
|
||||
|
|
@ -710,8 +743,7 @@ func (a *App) hacerEditar(w http.ResponseWriter, r *http.Request, c Cuenta, ses
|
|||
a.formularioEditar(w, c, ses, nuevo, primeraMayuscula(err.Error()))
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/pedir/todas?a="+url.QueryEscape("Evento modificado: "+nuevo.Titulo),
|
||||
http.StatusSeeOther)
|
||||
a.avisar(w, r, ses, "/pedir/todas", "Evento modificado: "+nuevo.Titulo)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pendientes
|
||||
|
|
@ -759,7 +791,7 @@ func (a *App) verPendientes(w http.ResponseWriter, r *http.Request, c Cuenta, se
|
|||
Base
|
||||
Filas []FilaPendiente
|
||||
}{Base: a.base("PENDIENTES", "Lo que espera a la asamblea", "pendientes", c, ses), Filas: filas}
|
||||
datos.Aviso = r.URL.Query().Get("a")
|
||||
datos.Aviso, datos.Error = a.ses.TomarAviso(ses.ID)
|
||||
a.pintar(w, "pendientes.tmpl.html", datos)
|
||||
}
|
||||
|
||||
|
|
@ -796,9 +828,8 @@ func (a *App) hacerResolver(w http.ResponseWriter, r *http.Request, c Cuenta, se
|
|||
return
|
||||
}
|
||||
log.Printf("%s %s %q de %s", c.Usuario, e.Estado, e.Titulo, e.Solicitante)
|
||||
http.Redirect(w, r, "/pedir/pendientes?a="+url.QueryEscape(
|
||||
fmt.Sprintf("«%s» queda %s", e.Titulo, strings.ToLower(e.Estado.Etiqueta()))),
|
||||
http.StatusSeeOther)
|
||||
a.avisar(w, r, ses, "/pedir/pendientes",
|
||||
fmt.Sprintf("«%s» queda %s", e.Titulo, strings.ToLower(e.Estado.Etiqueta())))
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------- findes
|
||||
|
|
@ -832,7 +863,7 @@ func (a *App) verFindes(w http.ResponseWriter, r *http.Request, c Cuenta, ses *S
|
|||
Base
|
||||
Filas []FilaFinde
|
||||
}{Base: a.base("TURNO DE LOS FINDES", "Tranquilo o con ruido", "findes", c, ses), Filas: filas}
|
||||
datos.Aviso = r.URL.Query().Get("a")
|
||||
datos.Aviso, datos.Error = a.ses.TomarAviso(ses.ID)
|
||||
a.pintar(w, "findes.tmpl.html", datos)
|
||||
}
|
||||
|
||||
|
|
@ -857,7 +888,7 @@ func (a *App) hacerFinde(w http.ResponseWriter, r *http.Request, c Cuenta, ses *
|
|||
if turno == "" {
|
||||
aviso = fmt.Sprintf("El finde del %s vuelve a la alternancia automática", v.Legible())
|
||||
}
|
||||
http.Redirect(w, r, "/pedir/findes?a="+url.QueryEscape(aviso), http.StatusSeeOther)
|
||||
a.avisar(w, r, ses, "/pedir/findes", aviso)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- cuentas
|
||||
|
|
@ -873,8 +904,7 @@ func (a *App) verCuentas(w http.ResponseWriter, r *http.Request, c Cuenta, ses *
|
|||
Cuentas: a.alm.Cuentas(),
|
||||
}
|
||||
datos.DeQuien, datos.Clave = a.ses.TomarClave(ses.ID)
|
||||
datos.Aviso = r.URL.Query().Get("a")
|
||||
datos.Error = r.URL.Query().Get("e")
|
||||
datos.Aviso, datos.Error = a.ses.TomarAviso(ses.ID)
|
||||
a.pintar(w, "cuentas.tmpl.html", datos)
|
||||
}
|
||||
|
||||
|
|
@ -882,8 +912,11 @@ func (a *App) hacerCuenta(w http.ResponseWriter, r *http.Request, c Cuenta, ses
|
|||
accion := r.PostFormValue("accion")
|
||||
usuario := strings.ToLower(strings.TrimSpace(r.PostFormValue("usuario")))
|
||||
|
||||
volver := func(params string) { http.Redirect(w, r, "/pedir/cuentas"+params, http.StatusSeeOther) }
|
||||
problema := func(err error) { volver("?e=" + url.QueryEscape(primeraMayuscula(err.Error()))) }
|
||||
volver := func(aviso string) { a.avisar(w, r, ses, "/pedir/cuentas", aviso) }
|
||||
problema := func(err error) {
|
||||
a.ses.PonerAviso(ses.ID, "", primeraMayuscula(err.Error()))
|
||||
http.Redirect(w, r, "/pedir/cuentas", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
switch accion {
|
||||
case "crear":
|
||||
|
|
@ -897,7 +930,7 @@ func (a *App) hacerCuenta(w http.ResponseWriter, r *http.Request, c Cuenta, ses
|
|||
problema(fmt.Errorf("falta el nombre del colectivo"))
|
||||
return
|
||||
}
|
||||
if rol != RolEnre && rol != RolInterno {
|
||||
if !rol.Valido() {
|
||||
problema(fmt.Errorf("rol desconocido"))
|
||||
return
|
||||
}
|
||||
|
|
@ -952,7 +985,7 @@ func (a *App) hacerCuenta(w http.ResponseWriter, r *http.Request, c Cuenta, ses
|
|||
if otra.Activa {
|
||||
hecho = "activada"
|
||||
}
|
||||
volver("?a=" + url.QueryEscape(fmt.Sprintf("Cuenta «%s» %s", usuario, hecho)))
|
||||
volver(fmt.Sprintf("Cuenta «%s» %s", usuario, hecho))
|
||||
|
||||
default:
|
||||
a.fallo(w, r, http.StatusBadRequest, "Acción desconocida.")
|
||||
|
|
@ -1018,8 +1051,7 @@ func (a *App) hacerClave(w http.ResponseWriter, r *http.Request, c Cuenta, ses *
|
|||
http.Redirect(w, r, sig, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/pedir/mis-fechas?a="+url.QueryEscape("Contraseña cambiada"),
|
||||
http.StatusSeeOther)
|
||||
a.avisar(w, r, nuevaSesion, "/pedir/mis-fechas", "Contraseña cambiada")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------- varios
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue