fix(umind): auditoría de las cinco features — cinco defectos y un guard permanente
Revisión de coherencia de todo lo que entró. Cinco cosas mal, ninguna
detectable compilando:
1. LA PUERTA DE TOOLS DECIDÍA AL REVÉS. executeUmindTool encolaba antes de
clasificar, así que en un canal público CUALQUIER nombre de herramienta
terminaba como pendiente de aprobación — incluidos los que el modelo
inventa y los internos, cuyos guards ("solo el dueño") quedaban
inalcanzables porque vivían más abajo en el flujo. Un visitante del widget
podía llenarle al dueño la bandeja de solicitudes para ejecutar cosas que
no existen.
Ahora se clasifica primero: lectura ejecuta, interna se rechaza si el
pedido es público, acción se encola, desconocida se rechaza. Verificado
inyectando la falla: el test falla y vuelve a pasar al arreglarlo.
2. El enlace "Ver el documento" de la bandeja estaba muerto: archivo_id no lo
escribía nadie. Ahora la acción aprobada engancha el PDF que produjo.
3. Los PDF generados esquivaban la cuota de almacenamiento. El tope del plan
se evitaba pidiéndole documentos al agente en vez de subiendo archivos.
4. La vista de Consumo no conocía el tipo "documento" — el rubro nuevo salía
sin nombre ni color. Y el icono que le puse no existía en el set, así que
habría quedado invisible: el mismo bug de los iconos de hace unas semanas,
que compila y se ve vacío.
5. Las seis lecturas nuevas se registraron con w() en vez de r(), exigiendo
SoloAdmin para leer. No era una fuga —era más estricto— pero un usuario
staff sin admin veía la lista de agentes y 403 en sus avisos.
Y el guard que evita el próximo: TestTodoHandlerUmindValidaAlcance recorre los
65 handlers de uMind y exige que cada uno valide el alcance del tenant, o esté
en una lista de exenciones con el motivo escrito. Los mismos handlers se montan
para staff y para clientes; el middleware de scope deja los tenants permitidos
en el contexto pero no mira el :id de la URL — eso lo hace cada handler, y el
que se olvide compila igual. Al escribirlo el propio test encontró que mi lista
de exenciones sobraba en dos, y verifiqué con un handler con fuga deliberada
que lo detecta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
706263db92
commit
b7c52f174a
@@ -21,6 +21,7 @@ const ETIQUETAS = {
|
|||||||
ia: { nombre: 'Inteligencia artificial', icono: 'cerebro', color: '#8eb02f' },
|
ia: { nombre: 'Inteligencia artificial', icono: 'cerebro', color: '#8eb02f' },
|
||||||
ocr: { nombre: 'Lectura de imágenes', icono: 'imagen', color: '#2f7fb0' },
|
ocr: { nombre: 'Lectura de imágenes', icono: 'imagen', color: '#2f7fb0' },
|
||||||
whisper: { nombre: 'Transcripción de audio', icono: 'microfono', color: '#b0752f' },
|
whisper: { nombre: 'Transcripción de audio', icono: 'microfono', color: '#b0752f' },
|
||||||
|
documento: { nombre: 'Documentos generados', icono: 'archivo', color: '#7b5fb0' },
|
||||||
}
|
}
|
||||||
const etiqueta = (t) => ETIQUETAS[t] || { nombre: t, icono: 'grafico', color: '#94a3b8' }
|
const etiqueta = (t) => ETIQUETAS[t] || { nombre: t, icono: 'grafico', color: '#94a3b8' }
|
||||||
|
|
||||||
|
|||||||
@@ -114,3 +114,9 @@ func VencerAccionesViejas() int64 {
|
|||||||
Update("estado", "vencida")
|
Update("estado", "vencida")
|
||||||
return res.RowsAffected
|
return res.RowsAffected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VincularArchivoAAccion engancha el documento producido a la acción.
|
||||||
|
func VincularArchivoAAccion(accionID, archivoID uint) {
|
||||||
|
app.Http.Database.DB.Model(&UmindAccionPendiente{}).Where("id = ?", accionID).
|
||||||
|
Update("archivo_id", archivoID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
@@ -67,3 +68,19 @@ func VincularArchivoADocumento(archivoID, documentoID uint) error {
|
|||||||
|
|
||||||
// AhoraUnix da el prefijo de tiempo para los nombres de archivo en disco.
|
// AhoraUnix da el prefijo de tiempo para los nombres de archivo en disco.
|
||||||
func AhoraUnix() int64 { return time.Now().Unix() }
|
func AhoraUnix() int64 { return time.Now().Unix() }
|
||||||
|
|
||||||
|
// HayEspacioUmind dice si entra un archivo más en el espacio del plan.
|
||||||
|
func HayEspacioUmind(tenantID uint, bytes int64) error {
|
||||||
|
tenant, err := GetUmindTenantByID(tenantID)
|
||||||
|
if err != nil || tenant.PlanID == nil {
|
||||||
|
return nil // sin plan asignado no se limita
|
||||||
|
}
|
||||||
|
plan, err := GetUmindPlanByID(*tenant.PlanID)
|
||||||
|
if err != nil || plan.MaxAlmacenamientoMB <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if EspacioUsadoUmind(tenantID)+bytes > int64(plan.MaxAlmacenamientoMB)<<20 {
|
||||||
|
return fmt.Errorf("el espacio de archivos del plan está lleno (%d MB)", plan.MaxAlmacenamientoMB)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -155,8 +155,23 @@ func umindEmailTools() []agentTool {
|
|||||||
// esperando a una persona. El trabajo real vive en ejecutarHerramienta, que es
|
// esperando a una persona. El trabajo real vive en ejecutarHerramienta, que es
|
||||||
// lo que corre después una aprobación.
|
// lo que corre después una aprobación.
|
||||||
func executeUmindTool(agenteID uint, sessionID string, sesionInterna bool, name string, args map[string]interface{}) string {
|
func executeUmindTool(agenteID uint, sessionID string, sesionInterna bool, name string, args map[string]interface{}) string {
|
||||||
if RequiereAprobacion(sesionInterna, name) {
|
switch CategoriaTool(agenteID, name) {
|
||||||
return EncolarAccion(agenteID, sessionID, name, args)
|
case "desconocida":
|
||||||
|
// Un nombre que no existe se rechaza acá y no se encola. Si no, el
|
||||||
|
// modelo alucinando un nombre le llena la bandeja al dueño de
|
||||||
|
// solicitudes para ejecutar herramientas que nunca existieron.
|
||||||
|
return fmt.Sprintf(`{"error": "herramienta desconocida: %s"}`, name)
|
||||||
|
case "interna":
|
||||||
|
// Programar avisos y vigilancias gasta el plan del dueño. No se
|
||||||
|
// encolan para que las apruebe: desde un canal público directamente
|
||||||
|
// no se piden.
|
||||||
|
if !sesionInterna {
|
||||||
|
return `{"error": "eso solo puede pedirlo el dueño desde su canal privado"}`
|
||||||
|
}
|
||||||
|
case "accion":
|
||||||
|
if !sesionInterna {
|
||||||
|
return EncolarAccion(agenteID, sessionID, name, args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ejecutarHerramienta(agenteID, sessionID, sesionInterna, name, args)
|
return ejecutarHerramienta(agenteID, sessionID, sesionInterna, name, args)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ func EjecutarAccionAprobada(accion *models.UmindAccionPendiente, quien string) e
|
|||||||
if err := models.CerrarAccion(accion.ID, estado, quien, "", resultado); err != nil {
|
if err := models.CerrarAccion(accion.ID, estado, quien, "", resultado); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Si la acción produjo un documento, se engancha a la acción para que el
|
||||||
|
// que aprobó pueda abrirlo desde la misma fila donde apretó el botón.
|
||||||
|
if id := archivoDelResultado(resultado); id != nil {
|
||||||
|
models.VincularArchivoAAccion(accion.ID, *id)
|
||||||
|
}
|
||||||
if estado == "fallida" {
|
if estado == "fallida" {
|
||||||
models.RegistrarEventoUmind(accion.AgenteID, "error", "aprobacion",
|
models.RegistrarEventoUmind(accion.AgenteID, "error", "aprobacion",
|
||||||
"Una acción aprobada falló al ejecutarse: "+accion.Resumen, resultado)
|
"Una acción aprobada falló al ejecutarse: "+accion.Resumen, resultado)
|
||||||
@@ -148,3 +153,39 @@ func avisarAccionPendiente(agente *models.UmindAgente, accion *models.UmindAccio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CategoriaTool clasifica la llamada ANTES de decidir qué hacer con ella:
|
||||||
|
//
|
||||||
|
// lectura — no sale nada del negocio, se ejecuta siempre
|
||||||
|
// interna — gasta el plan del dueño, solo desde su canal privado
|
||||||
|
// accion — sale hacia afuera, espera aprobación si el pedido es público
|
||||||
|
// desconocida — el nombre no existe: se rechaza, nunca se encola
|
||||||
|
//
|
||||||
|
// La clasificación existe porque sin ella todo lo que no fuera lectura se
|
||||||
|
// encolaba, incluidos los nombres que el modelo inventa.
|
||||||
|
func CategoriaTool(agenteID uint, name string) string {
|
||||||
|
switch name {
|
||||||
|
case "buscar_conocimiento", "leer_bandeja":
|
||||||
|
return "lectura"
|
||||||
|
case "programar_aviso", "listar_avisos", "cancelar_aviso",
|
||||||
|
"crear_vigilancia", "listar_vigilancias", "cancelar_vigilancia":
|
||||||
|
return "interna"
|
||||||
|
case "enviar_correo", "generar_documento":
|
||||||
|
return "accion"
|
||||||
|
}
|
||||||
|
if _, err := models.GetUmindHerramientaByNombre(agenteID, name); err == nil {
|
||||||
|
return "accion"
|
||||||
|
}
|
||||||
|
return "desconocida"
|
||||||
|
}
|
||||||
|
|
||||||
|
// archivoDelResultado saca el archivo_id que devuelve generar_documento.
|
||||||
|
func archivoDelResultado(resultado string) *uint {
|
||||||
|
var r struct {
|
||||||
|
ArchivoID uint `json:"archivo_id"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(resultado), &r); err != nil || r.ArchivoID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &r.ArchivoID
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,3 +82,59 @@ func TestAccionAprobadaUsaElPayloadGuardado(t *testing.T) {
|
|||||||
t.Error("falta el reclamo: sin él, dos aprobaciones simultáneas ejecutan dos veces")
|
t.Error("falta el reclamo: sin él, dos aprobaciones simultáneas ejecutan dos veces")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// La clasificación es lo que decide entre ejecutar, encolar y rechazar. Sin
|
||||||
|
// ella, todo lo que no fuera lectura se encolaba —incluidos los nombres que el
|
||||||
|
// modelo inventa—, y la bandeja del dueño se llenaba de solicitudes para
|
||||||
|
// ejecutar herramientas que nunca existieron.
|
||||||
|
func TestCategoriaTool(t *testing.T) {
|
||||||
|
casos := map[string]string{
|
||||||
|
"buscar_conocimiento": "lectura",
|
||||||
|
"leer_bandeja": "lectura",
|
||||||
|
"programar_aviso": "interna",
|
||||||
|
"cancelar_aviso": "interna",
|
||||||
|
"listar_avisos": "interna",
|
||||||
|
"crear_vigilancia": "interna",
|
||||||
|
"listar_vigilancias": "interna",
|
||||||
|
"enviar_correo": "accion",
|
||||||
|
"generar_documento": "accion",
|
||||||
|
}
|
||||||
|
for tool, quiero := range casos {
|
||||||
|
// agenteID 0: las tools con nombre fijo no consultan la base.
|
||||||
|
if got := CategoriaTool(0, tool); got != quiero {
|
||||||
|
t.Errorf("CategoriaTool(%q) = %q, quiero %q", tool, got, quiero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// El camino completo desde un canal público, que es donde escribe cualquiera.
|
||||||
|
func TestPuertaDeToolsDesdeCanalPublico(t *testing.T) {
|
||||||
|
b, err := os.ReadFile("umind_agent_service.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
i := strings.Index(s, "func executeUmindTool")
|
||||||
|
cuerpo := s[i:]
|
||||||
|
cuerpo = cuerpo[:strings.Index(cuerpo, "\nfunc ejecutarHerramienta")]
|
||||||
|
|
||||||
|
// La clasificación tiene que decidir ANTES de encolar.
|
||||||
|
cat := strings.Index(cuerpo, "CategoriaTool")
|
||||||
|
enc := strings.Index(cuerpo, "EncolarAccion")
|
||||||
|
if cat < 0 || enc < 0 || cat > enc {
|
||||||
|
t.Fatal("la clasificación tiene que ir antes de encolar")
|
||||||
|
}
|
||||||
|
// Un nombre inexistente se rechaza, no se encola.
|
||||||
|
desc := cuerpo[strings.Index(cuerpo, `case "desconocida":`):strings.Index(cuerpo, `case "interna":`)]
|
||||||
|
if strings.Contains(desc, "EncolarAccion") {
|
||||||
|
t.Error("una herramienta desconocida no puede encolarse")
|
||||||
|
}
|
||||||
|
// Las internas se rechazan desde un canal público, no se encolan.
|
||||||
|
inter := cuerpo[strings.Index(cuerpo, `case "interna":`):strings.Index(cuerpo, `case "accion":`)]
|
||||||
|
if strings.Contains(inter, "EncolarAccion") {
|
||||||
|
t.Error("las tools internas no pueden encolarse desde un canal público")
|
||||||
|
}
|
||||||
|
if !strings.Contains(inter, "!sesionInterna") {
|
||||||
|
t.Error("falta el rechazo de tools internas en canal público")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ func renderPDFDesdePlantilla(contenidoHTML string, datos map[string]interface{})
|
|||||||
// guardarPDFEnRepositorio deja el documento en los archivos del espacio, para
|
// guardarPDFEnRepositorio deja el documento en los archivos del espacio, para
|
||||||
// que quede a la vista y descargable como cualquier otro.
|
// que quede a la vista y descargable como cualquier otro.
|
||||||
func guardarPDFEnRepositorio(tenantID uint, nombreBase string, pdf []byte) (*models.UmindArchivo, error) {
|
func guardarPDFEnRepositorio(tenantID uint, nombreBase string, pdf []byte) (*models.UmindArchivo, error) {
|
||||||
|
// Un PDF generado ocupa lo mismo que uno subido: si no contara para la
|
||||||
|
// cuota, el tope del plan se esquivaría pidiéndole documentos al agente.
|
||||||
|
if err := models.HayEspacioUmind(tenantID, int64(len(pdf))); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
dir := filepath.Join("uploads", "umind", strconv.FormatUint(uint64(tenantID), 10))
|
dir := filepath.Join("uploads", "umind", strconv.FormatUint(uint64(tenantID), 10))
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' rx='22' fill='%238eb02f'/%3E%3Cpath d='M32,42 V58 A14,14 0 0 0 60,58 V42' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M60,58 V64' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round'/%3E%3Ccircle cx='60' cy='28' r='7' fill='%23fff'/%3E%3C/svg%3E" />
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' rx='22' fill='%238eb02f'/%3E%3Cpath d='M32,42 V58 A14,14 0 0 0 60,58 V42' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M60,58 V64' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round'/%3E%3Ccircle cx='60' cy='28' r='7' fill='%23fff'/%3E%3C/svg%3E" />
|
||||||
<title>uMind Studio</title>
|
<title>uMind Studio</title>
|
||||||
<script type="module" crossorigin src="/orchestrator/assets/index-BTIDeKXh.js"></script>
|
<script type="module" crossorigin src="/orchestrator/assets/index-NgTBLrrq.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-CfqdBqVI.css">
|
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-CfqdBqVI.css">
|
||||||
</head>
|
</head>
|
||||||
<!-- Sin clase de fondo: el color lo pone body en style.css desde los tokens,
|
<!-- Sin clase de fondo: el color lo pone body en style.css desde los tokens,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Los mismos handlers de uMind se montan bajo /app (staff) y bajo /portal
|
||||||
|
// (clientes). El middleware de scope deja la lista de tenants permitidos en el
|
||||||
|
// contexto, pero NO mira el :id que llega en la URL — eso lo tiene que hacer
|
||||||
|
// cada handler. Un handler nuevo que se olvide del chequeo es una fuga entre
|
||||||
|
// clientes, y compila igual.
|
||||||
|
//
|
||||||
|
// Este test no prueba comportamiento: prueba que nadie agregue el próximo
|
||||||
|
// handler sin el chequeo. Es lo único que escala a 40 handlers.
|
||||||
|
|
||||||
|
var helpersDeAlcance = []string{
|
||||||
|
"accesoTenant(", "accesoAgente(", "accesoDocumento(", "accesoConexion(",
|
||||||
|
"accesoAiConfigPropia(", "accesoCanal(", "accesoHerramienta(",
|
||||||
|
"ConAcceso(c)", "UmindScopeDe(c)",
|
||||||
|
}
|
||||||
|
|
||||||
|
// sinAlcanceJustificado son los handlers que legítimamente no filtran por
|
||||||
|
// tenant, cada uno con el motivo por el que no lo necesita.
|
||||||
|
var sinAlcanceJustificado = map[string]string{
|
||||||
|
"UmindIndex": "renderiza la página del SPA, sin datos",
|
||||||
|
"UmindPlanesPage": "renderiza la página de planes, sin datos",
|
||||||
|
"GetPlantillasRubroHandler": "catálogo de plantillas de ejemplo, igual para todos",
|
||||||
|
"UmindOAuthCallbackHandler": "el alcance viene en el state firmado, no en la URL",
|
||||||
|
"CreateUmindTenantHandler": "crea el tenant: no hay uno previo contra el cual validar; solo staff",
|
||||||
|
"GetUmindPlanesHandler": "planes del sistema, solo staff",
|
||||||
|
"CreateUmindPlanHandler": "planes del sistema, solo staff",
|
||||||
|
"UpdateUmindPlanHandler": "planes del sistema, solo staff",
|
||||||
|
"DeleteUmindPlanHandler": "planes del sistema, solo staff",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTodoHandlerUmindValidaAlcance(t *testing.T) {
|
||||||
|
archivos, err := filepath.Glob("umind_*.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`func ([A-Z]\w*)\(c \*fiber\.Ctx\) error \{`)
|
||||||
|
revisados := 0
|
||||||
|
|
||||||
|
for _, f := range archivos {
|
||||||
|
if strings.HasSuffix(f, "_test.go") || strings.Contains(f, "scope_helpers") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
for _, m := range re.FindAllStringSubmatchIndex(s, -1) {
|
||||||
|
nombre := s[m[2]:m[3]]
|
||||||
|
ini := m[1]
|
||||||
|
fin := strings.Index(s[ini:], "\nfunc ")
|
||||||
|
cuerpo := s[ini:]
|
||||||
|
if fin > 0 {
|
||||||
|
cuerpo = s[ini : ini+fin]
|
||||||
|
}
|
||||||
|
revisados++
|
||||||
|
|
||||||
|
tiene := false
|
||||||
|
for _, h := range helpersDeAlcance {
|
||||||
|
if strings.Contains(cuerpo, h) {
|
||||||
|
tiene = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tiene {
|
||||||
|
if _, exento := sinAlcanceJustificado[nombre]; exento {
|
||||||
|
t.Errorf("%s (%s) ya valida alcance: sacalo de sinAlcanceJustificado", nombre, f)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exento := sinAlcanceJustificado[nombre]; exento {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Errorf("%s (%s) no valida el alcance del tenant.\n"+
|
||||||
|
" Usá accesoTenant/accesoAgente al entrar, o agregalo a sinAlcanceJustificado con el motivo.\n"+
|
||||||
|
" Sin esto, un cliente puede leer los datos de otro pasando el id en la URL.", nombre, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if revisados < 30 {
|
||||||
|
t.Fatalf("solo se revisaron %d handlers: el detector dejó de encontrarlos y el test no está protegiendo nada", revisados)
|
||||||
|
}
|
||||||
|
t.Logf("%d handlers uMind revisados", revisados)
|
||||||
|
}
|
||||||
@@ -65,21 +65,21 @@ func RegistrarRutasUmind(g fiber.Router, scope fiber.Handler, escritura fiber.Ha
|
|||||||
|
|
||||||
g.Get("/umind/conexiones", r(controllers.GetUmindConexionesHandler)...)
|
g.Get("/umind/conexiones", r(controllers.GetUmindConexionesHandler)...)
|
||||||
g.Get("/umind/conexiones/conectar", w(controllers.UmindConectarHandler)...)
|
g.Get("/umind/conexiones/conectar", w(controllers.UmindConectarHandler)...)
|
||||||
g.Get("/umind/vigilancias", w(controllers.GetUmindVigilanciasHandler)...)
|
g.Get("/umind/vigilancias", r(controllers.GetUmindVigilanciasHandler)...)
|
||||||
g.Delete("/umind/vigilancias/:id", w(controllers.DeleteUmindVigilanciaHandler)...)
|
g.Delete("/umind/vigilancias/:id", w(controllers.DeleteUmindVigilanciaHandler)...)
|
||||||
g.Get("/umind/plantillas", w(controllers.GetUmindPlantillasHandler)...)
|
g.Get("/umind/plantillas", r(controllers.GetUmindPlantillasHandler)...)
|
||||||
g.Post("/umind/plantillas", w(controllers.CreateUmindPlantillaHandler)...)
|
g.Post("/umind/plantillas", w(controllers.CreateUmindPlantillaHandler)...)
|
||||||
g.Post("/umind/plantillas/importar", w(controllers.ImportarUmindPlantillaHandler)...)
|
g.Post("/umind/plantillas/importar", w(controllers.ImportarUmindPlantillaHandler)...)
|
||||||
g.Delete("/umind/plantillas/:id", w(controllers.DeleteUmindPlantillaHandler)...)
|
g.Delete("/umind/plantillas/:id", w(controllers.DeleteUmindPlantillaHandler)...)
|
||||||
g.Get("/umind/acciones", w(controllers.GetUmindAccionesHandler)...)
|
g.Get("/umind/acciones", r(controllers.GetUmindAccionesHandler)...)
|
||||||
g.Post("/umind/acciones/:id/aprobar", w(controllers.AprobarUmindAccionHandler)...)
|
g.Post("/umind/acciones/:id/aprobar", w(controllers.AprobarUmindAccionHandler)...)
|
||||||
g.Post("/umind/acciones/:id/rechazar", w(controllers.RechazarUmindAccionHandler)...)
|
g.Post("/umind/acciones/:id/rechazar", w(controllers.RechazarUmindAccionHandler)...)
|
||||||
g.Get("/umind/archivos", w(controllers.GetUmindArchivosHandler)...)
|
g.Get("/umind/archivos", r(controllers.GetUmindArchivosHandler)...)
|
||||||
g.Post("/umind/archivos", w(controllers.CreateUmindArchivoRepoHandler)...)
|
g.Post("/umind/archivos", w(controllers.CreateUmindArchivoRepoHandler)...)
|
||||||
g.Get("/umind/archivos/:id/descargar", w(controllers.DownloadUmindArchivoHandler)...)
|
g.Get("/umind/archivos/:id/descargar", r(controllers.DownloadUmindArchivoHandler)...)
|
||||||
g.Post("/umind/archivos/:id/conocimiento", w(controllers.UsarArchivoComoConocimientoHandler)...)
|
g.Post("/umind/archivos/:id/conocimiento", w(controllers.UsarArchivoComoConocimientoHandler)...)
|
||||||
g.Delete("/umind/archivos/:id", w(controllers.DeleteUmindArchivoHandler)...)
|
g.Delete("/umind/archivos/:id", w(controllers.DeleteUmindArchivoHandler)...)
|
||||||
g.Get("/umind/avisos", w(controllers.GetUmindAvisosHandler)...)
|
g.Get("/umind/avisos", r(controllers.GetUmindAvisosHandler)...)
|
||||||
g.Delete("/umind/avisos/:id", w(controllers.DeleteUmindAvisoHandler)...)
|
g.Delete("/umind/avisos/:id", w(controllers.DeleteUmindAvisoHandler)...)
|
||||||
g.Post("/umind/conexiones/imap", w(controllers.CrearConexionImapHandler)...)
|
g.Post("/umind/conexiones/imap", w(controllers.CrearConexionImapHandler)...)
|
||||||
g.Post("/umind/conexiones/:id/probar", w(controllers.ProbarConexionImapAgenteHandler)...)
|
g.Post("/umind/conexiones/:id/probar", w(controllers.ProbarConexionImapAgenteHandler)...)
|
||||||
|
|||||||
Reference in New Issue
Block a user