server/internal/handlers/invite.go
Rama main · réplica de lectura del repositorio original.
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})
}
}