ENRE_WEB/gestor/modelo.go
SITO 7d5dbeec60 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.
2026-09-23 12:15:33 +02:00

183 lines
5.1 KiB
Go

package main
import (
"fmt"
"time"
)
// Dia es una fecha civil en formato ISO "2006-01-02". Se guarda como texto
// porque asi se ordena y se compara igual que como se lee en el registro.
type Dia string
const formatoDia = "2006-01-02"
func DiaDe(t time.Time) Dia { return Dia(t.Format(formatoDia)) }
func (d Dia) Time() (time.Time, error) {
t, err := time.ParseInLocation(formatoDia, string(d), time.Local)
if err != nil {
return time.Time{}, fmt.Errorf("fecha %q no válida (formato AAAA-MM-DD)", string(d))
}
return t, nil
}
// MustTime solo se usa sobre fechas ya validadas al entrar en el sistema.
func (d Dia) MustTime() time.Time {
t, err := d.Time()
if err != nil {
panic(err)
}
return t
}
func (d Dia) Mas(dias int) Dia { return DiaDe(d.MustTime().AddDate(0, 0, dias)) }
// DiaISO devuelve 1 (lunes) a 7 (domingo).
func (d Dia) DiaISO() int { return int(d.MustTime().Weekday()+6)%7 + 1 }
func (d Dia) Legible() string {
t := d.MustTime()
return fmt.Sprintf("%s %d de %s", diasSemana[d.DiaISO()-1], t.Day(), mesesMin[int(t.Month())-1])
}
var (
diasSemana = []string{"lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"}
meses = []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
"Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"}
mesesMin = []string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio",
"agosto", "septiembre", "octubre", "noviembre", "diciembre"}
)
// Rango de dias que ocupa un evento, ambos extremos incluidos.
func Rango(desde, hasta Dia) []Dia {
if hasta < desde {
return nil
}
var out []Dia
for d := desde; d <= hasta; d = d.Mas(1) {
out = append(out, d)
if len(out) > 400 {
break
}
}
return out
}
type Rol string
const (
RolEnre Rol = "enre" // la asamblea: confirma y rechaza
RolInterno Rol = "interno" // colectivo de la Enre: solicita y amadrina
// RolExternas es la cuenta de la asamblea de externas: apunta los eventos
// de colectivos de fuera que se hablan allí, sin cupo, y la asamblea de la
// Enre los confirma.
RolExternas Rol = "externas"
)
func (r Rol) Valido() bool { return r == RolEnre || r == RolInterno || r == RolExternas }
type Cuenta struct {
Usuario string `json:"usuario"`
Nombre string `json:"nombre"` // nombre visible del colectivo
Rol Rol `json:"rol"`
Hash string `json:"hash"` // argon2id codificado
Temporal bool `json:"temporal,omitempty"` // obliga a cambiar la clave al entrar
Colectivo string `json:"colectivo,omitempty"` // clave en datos/horarios.json, para el color
Activa bool `json:"activa"`
Creada time.Time `json:"creada"`
}
type Estado string
const (
Solicitado Estado = "solicitado"
Confirmado Estado = "confirmado"
Rechazado Estado = "rechazado"
Caducado Estado = "caducado"
Cancelado Estado = "cancelado"
)
// Ocupa indica si el estado reserva la fecha frente a otras solicitudes.
func (e Estado) Ocupa() bool { return e == Solicitado || e == Confirmado }
func (e Estado) Etiqueta() string {
switch e {
case Solicitado:
return "SOLICITADO"
case Confirmado:
return "CONFIRMADO"
case Rechazado:
return "RECHAZADO"
case Caducado:
return "CADUCADO"
case Cancelado:
return "CANCELADO"
}
return string(e)
}
type Evento struct {
ID string `json:"id"`
Titulo string `json:"titulo"`
Solicitante string `json:"solicitante"` // usuario de la cuenta
Amadrinado string `json:"amadrinado,omitempty"` // colectivo externo, vacio si es propio
Contacto string `json:"contacto,omitempty"` // contacto del externo
Desde Dia `json:"desde"`
Hasta Dia `json:"hasta"`
Hora string `json:"hora,omitempty"`
Notas string `json:"notas,omitempty"`
Estado Estado `json:"estado"`
Creado time.Time `json:"creado"`
Resuelto time.Time `json:"resuelto,omitempty"`
ResueltoPor string `json:"resuelto_por,omitempty"`
Motivo string `json:"motivo,omitempty"`
}
func (e *Evento) Dias() []Dia { return Rango(e.Desde, e.Hasta) }
func (e *Evento) NumDias() int { return len(e.Dias()) }
// Externo indica si el evento lo organiza un colectivo de fuera amadrinado.
func (e *Evento) Externo() bool { return e.Amadrinado != "" }
type Turno string
const (
Tranqui Turno = "tranqui"
Ruido Turno = "ruido"
)
func (t Turno) Etiqueta() string {
switch t {
case Tranqui:
return "FINDE TRANQUI"
case Ruido:
return "FINDE CON RUIDO"
}
return ""
}
// Excepcion fija a mano el turno de un finde concreto, identificado por su viernes.
type Excepcion struct {
Viernes Dia `json:"viernes"`
Turno Turno `json:"turno"`
}
type Sesion struct {
ID string
Usuario string
CSRF string
Creada time.Time
Vista time.Time
// Clave temporal recien generada, para ensenarla una sola vez sin que pase
// por la URL y acabe en el historial o en el log de nginx.
ClaveDe string
ClaveNueva string
// Aviso o error de la siguiente página. Van por la sesión y no por la URL:
// con «?a=» cualquiera podía fabricar un enlace de laenre.net que
// enseñara el texto que quisiera.
Aviso string
Error string
}