Files
soft_usite/rest/controllers/register_controller.go
T
Lizandro GuarnizoandClaude Sonnet 5 08265510ea feat(umind): mide el consumo de IA, OCR y transcripción por tenant
Es la base del cobro por uso: hasta ahora no había ninguna medición de
consumo en todo el repo.

- UmindUso registra cada evento facturable con el costo YA calculado al
  precio vigente del plan. Congelarlo evita que subir un precio revalúe
  consumo pasado, que haría indefendible una factura ante un reclamo.
- callAI devuelve los tokens que reportó el proveedor (campo usage, igual
  en todos los OpenAI-compatibles; input+output en Anthropic). Se mide
  cada ronda de tool-calling, no solo la última: todas gastan tokens.
- ExtraerTextoOCR y TranscribirAudioSelfHosted reciben agenteID; 0 = no
  medir, que es lo que pasan los botones "Probar" del panel de staff.
- Aviso al superar el tope del plan, una vez por mes y sin cortar el
  servicio. El flag de "ya avisé" es en memoria a propósito.
- GET /app/umind/uso con filtros de fecha: resumen por tipo + detalle.
- Test del cálculo de costo por tipo, incluida fracción de 1k tokens y
  tenant sin plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:41:23 -05:00

283 lines
8.2 KiB
Go
Executable File

package controllers
import (
"crypto/rand"
"errors"
"fmt"
"log"
"math/big"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/app"
"github.com/sujit-baniya/fiber-boilerplate/config"
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// Función para renderizar la página de registro
func RegisterGet(c *fiber.Ctx) error {
data := getFlashData(c)
data["title"] = "Register | "
if err := c.Render("auth/register", data, "layouts/landing"); err != nil { //nolint:wsl
panic(err.Error())
}
return nil
}
// Función para manejar la solicitud de registro
func RegisterPost(c *fiber.Ctx) error {
register := c.Locals("register").(models.RegisterForm)
user, err := register.Signup()
if err != nil {
return app.Http.Flash.WithError(c, fiber.Map{
"error": true,
"message": "Error on register request: " + err.Error(),
}).Redirect("/register")
}
store := app.Http.Session.Get(c) // obtener/crear nueva sesión
store.Set("user_id", user.ID) // guardar en el almacenamiento
_ = store.Save()
go services.SendConfirmationEmail(user.Email, services.GetPublicURL(), user.NombreUsuario)
return c.Redirect("/")
}
// Función para verificar el correo electrónico registrado
func VerifyRegisteredEmail(c *fiber.Ctx) error {
return c.Redirect("/app")
}
// Función para reenviar el correo de confirmación
func ResendConfirmEmail(c *fiber.Ctx) error {
user, _ := auth.User(c)
go services.SendConfirmationEmail(user.Email, services.GetPublicURL(), user.NombreUsuario)
return c.Redirect("/")
}
func ReenvioEmail(c *fiber.Ctx) error {
usuarioID := c.Params("id") // Usa Params para obtener el parámetro de la ruta
user, err := models.GetUserById(usuarioID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"message": "Usuario no encontrado: " + err.Error(),
})
}
// Reenvío del correo electrónico de forma asíncrona
go services.SendConfirmationEmail(user.Email, services.GetPublicURL(), user.NombreUsuario)
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"success": true,
"message": "Email reenviado correctamente",
})
}
// Función para manejar la solicitud de restablecimiento de contraseña
func RequestPasswordResetPost(c *fiber.Ctx) error {
email := strings.TrimSpace(c.FormValue("email"))
if email == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "El correo electrónico es requerido.",
})
}
// Intentar enviar el correo solo si el usuario existe — mismo mensaje en ambos casos por seguridad
user, err := models.GetUserByEmail(email)
if err == nil && user != nil && user.Email != "" {
log.Println("[Reset] Enviando enlace de recuperación a:", email)
go services.SendPasswordResetEmail(user.Email, services.GetPublicURL())
} else {
log.Println("[Reset] Correo no encontrado (no se revela al cliente):", email)
}
return c.JSON(fiber.Map{
"success": true,
"message": "Si el correo está registrado, recibirás instrucciones en los próximos minutos.",
})
}
// todo: Generar contrasenas aleatorias /do/generate-password
func generatePassword(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&"
var password strings.Builder
for i := 0; i < length; i++ {
randomIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
password.WriteByte(charset[randomIndex.Int64()])
}
return password.String(), nil
}
func hashPassword(password string) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashed), nil
}
func updatePassword(nombreUsuario, plainPassword string) error {
var user models.RegisterForm
result := app.Http.Database.DB.Where("nombre_usuario = ?", nombreUsuario).First(&user)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return errors.New("codigo no pertenece a ningun usuario")
}
if user.Password != "" {
return errors.New("este usuario ya cuenta con una contraseña")
}
hashedPassword, err := hashPassword(plainPassword)
if err != nil {
return err
}
user.Password = hashedPassword
if err := app.Http.Database.DB.Save(&user).Error; err != nil {
return err
}
return nil
}
func GeneratePasswordPost(c *fiber.Ctx) error {
// Parsear el cuerpo de la solicitud
var body struct {
NombreUsuario string `json:"nombre_usuario"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Error al analizar el cuerpo de la solicitud",
})
}
if body.NombreUsuario == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "El codigo de usuario es requerido",
})
}
// Generar la contraseña
password, err := generatePassword(12)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"message": "Error generando contraseña",
})
}
// Actualizar la contraseña en la base de datos
if err := updatePassword(body.NombreUsuario, password); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"message": err.Error(),
})
}
// Responder con la contraseña generada
return c.JSON(fiber.Map{
"success": true,
"message": "Contrasena generada exitosamente",
"password": password,
})
}
// Función para renderizar la página de restablecimiento de contraseña
func PasswordReset(c *fiber.Ctx) error {
token := c.Query("t")
if err := c.Render("auth/password-reset", fiber.Map{
"Title": "Password Reset",
"Token": token,
}, "layouts/landing"); err != nil {
panic(err.Error())
}
return nil
}
func PasswordResetPost(c *fiber.Ctx) error {
// El email fue validado y extraído del token por el middleware ValidatePasswordResetPost
emailLocal := c.Locals("email")
if emailLocal == nil {
return c.Status(fiber.StatusBadRequest).Render("auth/password-reset", fiber.Map{
"Token": c.Params("token"),
"error": "Token inválido. Solicita un nuevo enlace.",
}, "layouts/landing")
}
email := fmt.Sprintf("%v", emailLocal)
password := c.FormValue("password")
cPassword := c.FormValue("c_password")
if password == "" || len(password) < 6 {
return c.Render("auth/password-reset", fiber.Map{
"Token": c.Params("token"),
"error": "La contraseña debe tener al menos 6 caracteres.",
}, "layouts/landing")
}
if password != cPassword {
return c.Render("auth/password-reset", fiber.Map{
"Token": c.Params("token"),
"error": "Las contraseñas no coinciden.",
}, "layouts/landing")
}
user, err := models.GetUserByEmail(email)
if err != nil || user == nil {
return c.Render("auth/password-reset", fiber.Map{
"Token": c.Params("token"),
"error": "No se encontró una cuenta asociada a este enlace.",
}, "layouts/landing")
}
hash := config.Hash{}
hashedPassword, err := hash.Create(password)
if err != nil {
return c.Render("auth/password-reset", fiber.Map{
"Token": c.Params("token"),
"error": "Error al procesar la contraseña. Intenta de nuevo.",
}, "layouts/landing")
}
if err := models.UpdateUserPassword(user.ID, hashedPassword); err != nil {
return c.Render("auth/password-reset", fiber.Map{
"Token": c.Params("token"),
"error": "No se pudo actualizar la contraseña. Intenta de nuevo.",
}, "layouts/landing")
}
return c.Redirect("/login")
}
// Función para renderizar la página de solicitud de restablecimiento de contraseña
func RequestPasswordReset(c *fiber.Ctx) error {
data := getFlashData(c)
data["title"] = "Password Reset | "
if err := c.Render("auth/request-password-reset", data, "layouts/landing"); err != nil { //nolint:wsl
panic(err.Error())
}
return nil
}
func TokenVencido(c *fiber.Ctx) error {
// Renderiza la vista con el layout layouts/main
if err := c.Render("token_vencido", nil, "layouts/landing"); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": "Error rendering template",
"error": err.Error(),
})
}
return nil
}