149 lines
3.5 KiB
Go
149 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type mintInvitesRequest struct {
|
|
Note string `json:"note"`
|
|
ExpiresInDays int `json:"expires_in_days"` // 0 → default 14; <0 → no expiry
|
|
Count int `json:"count"` // default 1, max 20
|
|
}
|
|
|
|
func inviteStatus(inv InviteCode, now time.Time) string {
|
|
if inv.RevokedAt != nil {
|
|
return "revoked"
|
|
}
|
|
if inv.UsedAt != nil {
|
|
return "used"
|
|
}
|
|
if inv.ExpiresAt != nil && !inv.ExpiresAt.After(now) {
|
|
return "expired"
|
|
}
|
|
return "unused"
|
|
}
|
|
|
|
func inviteUsable(inv InviteCode, now time.Time) (ok bool, detail string) {
|
|
switch inviteStatus(inv, now) {
|
|
case "unused":
|
|
return true, ""
|
|
case "used":
|
|
return false, "invite code already used"
|
|
case "revoked":
|
|
return false, "invite code revoked"
|
|
case "expired":
|
|
return false, "invite code expired"
|
|
default:
|
|
return false, "invite code not usable"
|
|
}
|
|
}
|
|
|
|
func inviteJSON(inv InviteCode, now time.Time) gin.H {
|
|
out := gin.H{
|
|
"code": inv.Code,
|
|
"note": inv.Note,
|
|
"status": inviteStatus(inv, now),
|
|
"created_at": inv.CreatedAt,
|
|
}
|
|
if inv.ExpiresAt != nil {
|
|
out["expires_at"] = inv.ExpiresAt
|
|
}
|
|
if inv.RevokedAt != nil {
|
|
out["revoked_at"] = inv.RevokedAt
|
|
}
|
|
if inv.UsedAt != nil {
|
|
out["used_at"] = inv.UsedAt
|
|
out["used_by_user_id"] = inv.UsedByUserID
|
|
}
|
|
return out
|
|
}
|
|
|
|
func registerInviteOpsRoutes(r *gin.Engine, db *gorm.DB) {
|
|
r.POST("/invites", func(c *gin.Context) {
|
|
if !requireAdmin(c) {
|
|
return
|
|
}
|
|
var req mintInvitesRequest
|
|
_ = c.ShouldBindJSON(&req) // body optional for backward compat
|
|
count := req.Count
|
|
if count <= 0 {
|
|
count = 1
|
|
}
|
|
if count > 20 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "count must be <= 20"})
|
|
return
|
|
}
|
|
expiresIn := req.ExpiresInDays
|
|
if expiresIn == 0 {
|
|
expiresIn = 14
|
|
}
|
|
|
|
now := time.Now()
|
|
codes := make([]gin.H, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
code, err := generateInviteCode()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
|
return
|
|
}
|
|
invite := InviteCode{Code: code, Note: req.Note}
|
|
if expiresIn > 0 {
|
|
exp := now.Add(time.Duration(expiresIn) * 24 * time.Hour)
|
|
invite.ExpiresAt = &exp
|
|
}
|
|
if err := db.Create(&invite).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
|
return
|
|
}
|
|
codes = append(codes, inviteJSON(invite, now))
|
|
}
|
|
if count == 1 {
|
|
c.JSON(http.StatusOK, codes[0])
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"invites": codes})
|
|
})
|
|
|
|
r.GET("/invites", func(c *gin.Context) {
|
|
if !requireAdmin(c) {
|
|
return
|
|
}
|
|
var invites []InviteCode
|
|
db.Order("id desc").Limit(200).Find(&invites)
|
|
now := time.Now()
|
|
out := make([]gin.H, 0, len(invites))
|
|
for _, inv := range invites {
|
|
out = append(out, inviteJSON(inv, now))
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"invites": out})
|
|
})
|
|
|
|
r.POST("/invites/:code/revoke", func(c *gin.Context) {
|
|
if !requireAdmin(c) {
|
|
return
|
|
}
|
|
code := c.Param("code")
|
|
var invite InviteCode
|
|
if err := db.Where("code = ?", code).First(&invite).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"detail": "invite not found"})
|
|
return
|
|
}
|
|
if invite.UsedAt != nil {
|
|
c.JSON(http.StatusConflict, gin.H{"detail": "cannot revoke a used invite"})
|
|
return
|
|
}
|
|
if invite.RevokedAt != nil {
|
|
c.JSON(http.StatusOK, inviteJSON(invite, time.Now()))
|
|
return
|
|
}
|
|
now := time.Now()
|
|
invite.RevokedAt = &now
|
|
db.Save(&invite)
|
|
c.JSON(http.StatusOK, inviteJSON(invite, now))
|
|
})
|
|
}
|