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>
283 lines
8.2 KiB
Go
Executable File
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
|
|
}
|