Ctrl K
ComunidadGuías…

server/internal/handlers/invite.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handlers

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"time"

	"blackbox/server/internal/auth"
	"blackbox/server/internal/events"
	"blackbox/server/internal/models"
	"github.com/go-chi/chi/v5"
	"github.com/oklog/ulid/v2"
	"gorm.io/gorm"
)

func inviteTTL() time.Duration {
	if v := os.Getenv("INVITE_TTL"); v != "" {
		if d, err := time.ParseDuration(v); err == nil {
			return d
		}
	}
	return 72 * time.Hour
}

func CreateInvite(database *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims, ok := auth.ClaimsFromContext(r.Context())
		if !ok || !claims.IsAdmin {
			writeError(w, http.StatusForbidden, "admin required")
			return
		}

		b := make([]byte, 32)
		if _, err := rand.Read(b); err != nil {
			writeError(w, http.StatusInternalServerError, "failed to generate code")
			return
		}

		code := hex.EncodeToString(b)
		expiresAt := time.Now().Add(inviteTTL())
		invite := models.InviteCode{
			ID:        ulid.Make().String(),
			Code:      code,
			CreatedBy: claims.UserID,
			ExpiresAt: expiresAt,
			CreatedAt: time.Now(),
		}
		if err := database.Create(&invite).Error; err != nil {
			writeError(w, http.StatusInternalServerError, "failed to create invite")
			return
		}

		var admin models.User
		adminName := claims.UserID
		if err := database.First(&admin, "id = ?", claims.UserID).Error; err == nil {
			adminName = admin.Username
		}
		events.LogSystem(database, "auth", "invite.created", "invite created by "+adminName)

		WriteAuditLog(database, r, claims, "invite.create", "invite", invite.ID, map[string]interface{}{
			"expires_at": expiresAt.UTC().Format(time.RFC3339),
		})

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		_ = json.NewEncoder(w).Encode(map[string]string{
			"code":       code,
			"expires_at": expiresAt.UTC().Format(time.RFC3339),
		})
	}
}

func ListInvites(database *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims, ok := auth.ClaimsFromContext(r.Context())
		if !ok || !claims.IsAdmin {
			writeError(w, http.StatusForbidden, "admin required")
			return
		}

		var invites []models.InviteCode
		if err := database.Where("used_by = '' AND expires_at > ?", time.Now()).Find(&invites).Error; err != nil {
			writeError(w, http.StatusInternalServerError, "failed to list invites")
			return
		}

		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(invites)
	}
}

func RevokeInvite(database *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims, ok := auth.ClaimsFromContext(r.Context())
		if !ok || !claims.IsAdmin {
			writeError(w, http.StatusForbidden, "admin required")
			return
		}

		inviteID := chi.URLParam(r, "id")
		result := database.Where("id = ? AND used_by = ''", inviteID).Delete(&models.InviteCode{})
		if result.Error != nil {
			writeError(w, http.StatusInternalServerError, "failed to revoke invite")
			return
		}
		if result.RowsAffected == 0 {
			var invite models.InviteCode
			if err := database.First(&invite, "id = ?", inviteID).Error; err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					writeError(w, http.StatusNotFound, "invite not found")
					return
				}
				writeError(w, http.StatusInternalServerError, "failed to fetch invite")
				return
			}
			if invite.UsedBy != "" {
				writeError(w, http.StatusConflict, "invite already used")
				return
			}
			writeError(w, http.StatusInternalServerError, "failed to revoke invite")
			return
		}

		events.LogSystem(database, "auth", "invite.revoked", "invite "+inviteID+" revoked")

		WriteAuditLog(database, r, claims, "invite.revoke", "invite", inviteID, map[string]interface{}{})

		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]bool{"ok": true})
	}
}