iykyka/core-backend/invite_ops.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))
})
}