diff --git a/backend/app/http/endpoints/api/admin/serverblacklist/list.go b/backend/app/http/endpoints/api/admin/serverblacklist/list.go index c97fb768..7cef7e21 100644 --- a/backend/app/http/endpoints/api/admin/serverblacklist/list.go +++ b/backend/app/http/endpoints/api/admin/serverblacklist/list.go @@ -1,15 +1,21 @@ package serverblacklist import ( + "net/http" "strconv" "github.com/gin-gonic/gin" + "github.com/ticketsbot-cloud/dashboard/backend/app" "github.com/ticketsbot-cloud/dashboard/backend/database" - "github.com/ticketsbot-cloud/dashboard/backend/utils" + "github.com/ticketsbot-cloud/dashboard/backend/log" + "github.com/ticketsbot-cloud/dashboard/backend/rpc/cache" + "go.uber.org/zap" ) type entryData struct { GuildId string `json:"guild_id"` + Name string `json:"name,omitempty"` + Icon string `json:"icon,omitempty"` Reason *string `json:"reason"` OwnerId *string `json:"owner_id"` RealOwnerId *string `json:"real_owner_id"` @@ -18,10 +24,21 @@ type entryData struct { func ListHandler(ctx *gin.Context) { entries, err := database.Client.ServerBlacklist.ListAllEntries(ctx) if err != nil { - ctx.JSON(500, utils.ErrorStr("Failed to fetch records. Please try again.")) + _ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to fetch records. Please try again.")) return } + guildIds := make([]uint64, len(entries)) + for i, entry := range entries { + guildIds[i] = entry.GuildId + } + + // AdminLayout's access check calls this route, so a cache failure must not 500 it + guilds, err := cache.Instance.GetGuilds(ctx, guildIds) + if err != nil { + log.Logger.Warn("Failed to resolve blacklisted servers", zap.Error(err)) + } + result := make([]entryData, len(entries)) for i, entry := range entries { data := entryData{ @@ -29,6 +46,11 @@ func ListHandler(ctx *gin.Context) { Reason: entry.Reason, } + if guild, ok := guilds[entry.GuildId]; ok { + data.Name = guild.Name + data.Icon = guild.Icon + } + if entry.OwnerId != nil { s := strconv.FormatUint(*entry.OwnerId, 10) data.OwnerId = &s diff --git a/backend/app/http/endpoints/api/admin/submissionblacklist/add.go b/backend/app/http/endpoints/api/admin/submissionblacklist/add.go new file mode 100644 index 00000000..f69a34af --- /dev/null +++ b/backend/app/http/endpoints/api/admin/submissionblacklist/add.go @@ -0,0 +1,80 @@ +package submissionblacklist + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + dbmodel "github.com/TicketsBot-cloud/database" + "github.com/gin-gonic/gin" + "github.com/ticketsbot-cloud/dashboard/backend/app" + "github.com/ticketsbot-cloud/dashboard/backend/app/http/audit" + "github.com/ticketsbot-cloud/dashboard/backend/database" + "github.com/ticketsbot-cloud/dashboard/backend/utils" +) + +type addBody struct { + Reason *string `json:"reason"` +} + +func AddHandler(feature dbmodel.SubmissionFeature) gin.HandlerFunc { + return func(ctx *gin.Context) { + authUserId := ctx.Keys["userid"].(uint64) + + targetType, targetId, ok := parseTarget(ctx, feature) + if !ok { + return + } + + var body addBody + _ = ctx.ShouldBindJSON(&body) + + var reason *string + if body.Reason != nil { + if trimmed := strings.TrimSpace(*body.Reason); trimmed != "" { + reason = &trimmed + } + } + + if err := database.Client.SubmissionBlacklist.Add(ctx, feature, targetType, targetId, reason); err != nil { + _ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to process request. Please try again.")) + return + } + + audit.LogStaff(audit.LogEntry{ + UserId: authUserId, + ActionType: dbmodel.AuditActionSubmissionBlacklistAdd, + ResourceType: dbmodel.AuditResourceSubmissionBlacklist, + ResourceId: audit.StringPtr(auditResourceId(feature, targetType, targetId)), + NewData: map[string]any{ + "feature": feature, + "target_type": targetType, + "target_id": strconv.FormatUint(targetId, 10), + "reason": reason, + }, + }) + ctx.Status(http.StatusNoContent) + } +} + +func parseTarget(ctx *gin.Context, feature dbmodel.SubmissionFeature) (dbmodel.SubmissionTargetType, uint64, bool) { + targetType := dbmodel.SubmissionTargetType(ctx.Param("type")) + if !feature.AllowsTarget(targetType) { + ctx.JSON(http.StatusBadRequest, utils.ErrorStr("Invalid target type")) + return "", 0, false + } + + // int8 column: an ID above MaxInt64 must be a 400, not a pgx 500 + targetId, err := strconv.ParseInt(ctx.Param("targetid"), 10, 64) + if err != nil || targetId <= 0 { + ctx.JSON(http.StatusBadRequest, utils.ErrorStr("Invalid target ID")) + return "", 0, false + } + + return targetType, uint64(targetId), true +} + +func auditResourceId(feature dbmodel.SubmissionFeature, targetType dbmodel.SubmissionTargetType, targetId uint64) string { + return fmt.Sprintf("%s:%s:%d", feature, targetType, targetId) +} diff --git a/backend/app/http/endpoints/api/admin/submissionblacklist/list.go b/backend/app/http/endpoints/api/admin/submissionblacklist/list.go new file mode 100644 index 00000000..116e9daa --- /dev/null +++ b/backend/app/http/endpoints/api/admin/submissionblacklist/list.go @@ -0,0 +1,77 @@ +package submissionblacklist + +import ( + "net/http" + + dbmodel "github.com/TicketsBot-cloud/database" + "github.com/gin-gonic/gin" + "github.com/ticketsbot-cloud/dashboard/backend/app" + "github.com/ticketsbot-cloud/dashboard/backend/database" + "github.com/ticketsbot-cloud/dashboard/backend/rpc/cache" +) + +type entryData struct { + TargetType dbmodel.SubmissionTargetType `json:"target_type"` + TargetId uint64 `json:"target_id,string"` + Name string `json:"name,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` + Icon string `json:"icon,omitempty"` + Reason *string `json:"reason"` +} + +func ListHandler(feature dbmodel.SubmissionFeature) gin.HandlerFunc { + return func(ctx *gin.Context) { + entries, err := database.Client.SubmissionBlacklist.List(ctx, feature) + if err != nil { + _ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to fetch records. Please try again.")) + return + } + + userIds := make([]uint64, 0, len(entries)) + guildIds := make([]uint64, 0, len(entries)) + for _, entry := range entries { + switch entry.TargetType { + case dbmodel.SubmissionTargetUser: + userIds = append(userIds, entry.TargetId) + case dbmodel.SubmissionTargetGuild: + guildIds = append(guildIds, entry.TargetId) + } + } + + users, err := cache.Instance.GetUsers(ctx, userIds) + if err != nil { + _ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to resolve blacklisted users")) + return + } + + guilds, err := cache.Instance.GetGuilds(ctx, guildIds) + if err != nil { + _ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to resolve blacklisted servers")) + return + } + + result := make([]entryData, len(entries)) + for i, entry := range entries { + result[i] = entryData{ + TargetType: entry.TargetType, + TargetId: entry.TargetId, + Reason: entry.Reason, + } + + switch entry.TargetType { + case dbmodel.SubmissionTargetUser: + if user, ok := users[entry.TargetId]; ok { + result[i].Name = user.Username + result[i].AvatarUrl = user.AvatarUrl(256) + } + case dbmodel.SubmissionTargetGuild: + if guild, ok := guilds[entry.TargetId]; ok { + result[i].Name = guild.Name + result[i].Icon = guild.Icon + } + } + } + + ctx.JSON(http.StatusOK, result) + } +} diff --git a/backend/app/http/endpoints/api/admin/submissionblacklist/remove.go b/backend/app/http/endpoints/api/admin/submissionblacklist/remove.go new file mode 100644 index 00000000..a14996c9 --- /dev/null +++ b/backend/app/http/endpoints/api/admin/submissionblacklist/remove.go @@ -0,0 +1,41 @@ +package submissionblacklist + +import ( + "net/http" + "strconv" + + dbmodel "github.com/TicketsBot-cloud/database" + "github.com/gin-gonic/gin" + "github.com/ticketsbot-cloud/dashboard/backend/app" + "github.com/ticketsbot-cloud/dashboard/backend/app/http/audit" + "github.com/ticketsbot-cloud/dashboard/backend/database" +) + +func RemoveHandler(feature dbmodel.SubmissionFeature) gin.HandlerFunc { + return func(ctx *gin.Context) { + authUserId := ctx.Keys["userid"].(uint64) + + targetType, targetId, ok := parseTarget(ctx, feature) + if !ok { + return + } + + if err := database.Client.SubmissionBlacklist.Delete(ctx, feature, targetType, targetId); err != nil { + _ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to delete record. Please try again.")) + return + } + + audit.LogStaff(audit.LogEntry{ + UserId: authUserId, + ActionType: dbmodel.AuditActionSubmissionBlacklistRemove, + ResourceType: dbmodel.AuditResourceSubmissionBlacklist, + ResourceId: audit.StringPtr(auditResourceId(feature, targetType, targetId)), + OldData: map[string]any{ + "feature": feature, + "target_type": targetType, + "target_id": strconv.FormatUint(targetId, 10), + }, + }) + ctx.Status(http.StatusNoContent) + } +} diff --git a/backend/app/http/endpoints/api/gallery/resubmit.go b/backend/app/http/endpoints/api/gallery/resubmit.go index 659019ef..beec08dc 100644 --- a/backend/app/http/endpoints/api/gallery/resubmit.go +++ b/backend/app/http/endpoints/api/gallery/resubmit.go @@ -21,6 +21,10 @@ func ResubmitHandler(ctx *gin.Context) { guildId := ctx.Keys["guildid"].(uint64) userId := ctx.Keys["userid"].(uint64) + if utils.RejectIfSubmissionBlacklisted(ctx, database.SubmissionFeatureGallery, userId, guildId) { + return + } + listingId, err := strconv.Atoi(ctx.Param("listingId")) if err != nil { ctx.JSON(http.StatusBadRequest, utils.ErrorStr("Invalid listing ID")) diff --git a/backend/app/http/endpoints/api/gallery/submit.go b/backend/app/http/endpoints/api/gallery/submit.go index 4773aed9..2b06b0c2 100644 --- a/backend/app/http/endpoints/api/gallery/submit.go +++ b/backend/app/http/endpoints/api/gallery/submit.go @@ -32,6 +32,10 @@ func SubmitHandler(ctx *gin.Context) { guildId := ctx.Keys["guildid"].(uint64) userId := ctx.Keys["userid"].(uint64) + if utils.RejectIfSubmissionBlacklisted(ctx, database.SubmissionFeatureGallery, userId, guildId) { + return + } + panelIdStr := ctx.Param("panelid") panelId, err := strconv.Atoi(panelIdStr) if err != nil { diff --git a/backend/app/http/endpoints/api/gallery/submit_form.go b/backend/app/http/endpoints/api/gallery/submit_form.go index f682fb5d..a58d2a12 100644 --- a/backend/app/http/endpoints/api/gallery/submit_form.go +++ b/backend/app/http/endpoints/api/gallery/submit_form.go @@ -21,6 +21,10 @@ func SubmitFormHandler(ctx *gin.Context) { guildId := ctx.Keys["guildid"].(uint64) userId := ctx.Keys["userid"].(uint64) + if utils.RejectIfSubmissionBlacklisted(ctx, database.SubmissionFeatureGallery, userId, guildId) { + return + } + formIdStr := ctx.Param("formid") formId, err := strconv.Atoi(formIdStr) if err != nil { diff --git a/backend/app/http/endpoints/api/gallery/submit_tag.go b/backend/app/http/endpoints/api/gallery/submit_tag.go index f342cad5..5ba3d519 100644 --- a/backend/app/http/endpoints/api/gallery/submit_tag.go +++ b/backend/app/http/endpoints/api/gallery/submit_tag.go @@ -20,6 +20,10 @@ func SubmitTagHandler(ctx *gin.Context) { guildId := ctx.Keys["guildid"].(uint64) userId := ctx.Keys["userid"].(uint64) + if utils.RejectIfSubmissionBlacklisted(ctx, database.SubmissionFeatureGallery, userId, guildId) { + return + } + tagId := ctx.Param("tagid") var body submitBody diff --git a/backend/app/http/endpoints/api/integrations/setpublic.go b/backend/app/http/endpoints/api/integrations/setpublic.go index f61a531a..b3e6de01 100644 --- a/backend/app/http/endpoints/api/integrations/setpublic.go +++ b/backend/app/http/endpoints/api/integrations/setpublic.go @@ -54,6 +54,10 @@ func SetIntegrationPublicHandler(ctx *gin.Context) { return } + if utils.RejectIfSubmissionBlacklisted(ctx, dbmodel.SubmissionFeatureIntegrations, userId, 0) { + return + } + e := embed.NewEmbed(). SetTitle("Public Integration Request"). SetColor(0xfcb97d). diff --git a/backend/app/http/endpoints/api/integrations/updateintegration.go b/backend/app/http/endpoints/api/integrations/updateintegration.go index d67cc34d..7449ddc1 100644 --- a/backend/app/http/endpoints/api/integrations/updateintegration.go +++ b/backend/app/http/endpoints/api/integrations/updateintegration.go @@ -90,6 +90,10 @@ func UpdateIntegrationHandler(ctx *gin.Context) { return } + if integration.Public && utils.RejectIfSubmissionBlacklisted(ctx, database.SubmissionFeatureIntegrations, userId, 0) { + return + } + if data.ValidationUrl != nil { sameHost, err := isSameValidationUrlHost(data.WebhookUrl, *data.ValidationUrl) if err != nil { diff --git a/backend/app/http/server.go b/backend/app/http/server.go index 5006cd8d..483cc345 100644 --- a/backend/app/http/server.go +++ b/backend/app/http/server.go @@ -5,6 +5,7 @@ import ( "time" "github.com/TicketsBot-cloud/common/permission" + dbmodel "github.com/TicketsBot-cloud/database" sentrygin "github.com/getsentry/sentry-go/gin" "github.com/gin-gonic/gin" "github.com/penglongli/gin-metrics/ginmetrics" @@ -21,6 +22,7 @@ import ( admin_premiumkeys "github.com/ticketsbot-cloud/dashboard/backend/app/http/endpoints/api/admin/premiumkeys" admin_serverblacklist "github.com/ticketsbot-cloud/dashboard/backend/app/http/endpoints/api/admin/serverblacklist" admin_skus "github.com/ticketsbot-cloud/dashboard/backend/app/http/endpoints/api/admin/skus" + admin_submissionblacklist "github.com/ticketsbot-cloud/dashboard/backend/app/http/endpoints/api/admin/submissionblacklist" admin_utilities "github.com/ticketsbot-cloud/dashboard/backend/app/http/endpoints/api/admin/utilities" api_affiliate "github.com/ticketsbot-cloud/dashboard/backend/app/http/endpoints/api/affiliate" api_analytics "github.com/ticketsbot-cloud/dashboard/backend/app/http/endpoints/api/analytics" @@ -489,13 +491,9 @@ func StartServer(logger *zap.Logger, sm *livechat.SocketManager) *nethttp.Server adminTier.DELETE("/gallery/:id", admin_gallery.RemoveHandler) adminTier.POST("/server-blacklist/:guildid", admin_serverblacklist.AddHandler) - adminTier.DELETE("/server-blacklist/:guildid", admin_serverblacklist.RemoveHandler) - adminTier.GET("/integrations", admin_integrations.ListIntegrationsHandler) - adminTier.GET("/integrations/:integrationid", admin_integrations.GetIntegrationDetailHandler) - adminTier.POST("/integrations/:integrationid/approve", admin_integrations.ApproveIntegrationHandler) - adminTier.POST("/integrations/:integrationid/reject", admin_integrations.RejectIntegrationHandler) - adminTier.POST("/integrations/:integrationid/unapprove", admin_integrations.UnapproveIntegrationHandler) + adminTier.GET("/submission-blacklist/gallery", admin_submissionblacklist.ListHandler(dbmodel.SubmissionFeatureGallery)) + adminTier.POST("/submission-blacklist/gallery/:type/:targetid", admin_submissionblacklist.AddHandler(dbmodel.SubmissionFeatureGallery)) adminTier.GET("/skus", admin_skus.ListHandler) @@ -522,6 +520,11 @@ func StartServer(logger *zap.Logger, sm *livechat.SocketManager) *nethttp.Server ownerTier.GET("/global-blacklist", admin_globalblacklist.ListHandler) ownerTier.POST("/global-blacklist/:userid", admin_globalblacklist.AddHandler) ownerTier.DELETE("/global-blacklist/:userid", admin_globalblacklist.RemoveHandler) + ownerTier.DELETE("/server-blacklist/:guildid", admin_serverblacklist.RemoveHandler) + ownerTier.DELETE("/submission-blacklist/gallery/:type/:targetid", admin_submissionblacklist.RemoveHandler(dbmodel.SubmissionFeatureGallery)) + ownerTier.GET("/submission-blacklist/integrations", admin_submissionblacklist.ListHandler(dbmodel.SubmissionFeatureIntegrations)) + ownerTier.POST("/submission-blacklist/integrations/:type/:targetid", admin_submissionblacklist.AddHandler(dbmodel.SubmissionFeatureIntegrations)) + ownerTier.DELETE("/submission-blacklist/integrations/:type/:targetid", admin_submissionblacklist.RemoveHandler(dbmodel.SubmissionFeatureIntegrations)) ownerTier.POST("/skus", admin_skus.CreateHandler) ownerTier.PUT("/skus/:skuid", admin_skus.UpdateHandler) ownerTier.DELETE("/skus/:skuid", admin_skus.DeleteHandler) @@ -531,6 +534,12 @@ func StartServer(logger *zap.Logger, sm *livechat.SocketManager) *nethttp.Server ownerTier.PUT("/polar-products/:productid", admin_polarproducts.UpdateHandler) ownerTier.DELETE("/polar-products/:productid", admin_polarproducts.DeleteHandler) + ownerTier.GET("/integrations", admin_integrations.ListIntegrationsHandler) + ownerTier.GET("/integrations/:integrationid", admin_integrations.GetIntegrationDetailHandler) + ownerTier.POST("/integrations/:integrationid/approve", admin_integrations.ApproveIntegrationHandler) + ownerTier.POST("/integrations/:integrationid/reject", admin_integrations.RejectIntegrationHandler) + ownerTier.POST("/integrations/:integrationid/unapprove", admin_integrations.UnapproveIntegrationHandler) + ownerTier.POST("/affiliate", admin_affiliate.CreateHandler) ownerTier.POST("/affiliate/:id/approve", admin_affiliate.ApproveHandler) ownerTier.POST("/affiliate/:id/revoke", admin_affiliate.RevokeHandler) diff --git a/backend/go.mod b/backend/go.mod index a82fb5d4..d85483f3 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -13,8 +13,8 @@ replace github.com/go-playground/validator/v10 => github.com/go-playground/valid require ( github.com/BurntSushi/toml v1.2.1 github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 - github.com/TicketsBot-cloud/common v0.0.0-20260905165836-38e4090764a4 - github.com/TicketsBot-cloud/database v0.0.0-20260913165941-c2ecc7191b02 + github.com/TicketsBot-cloud/common v0.0.0-20260927072029-424264fc4be2 + github.com/TicketsBot-cloud/database v0.0.0-20260927071905-7a22c7be0e4a github.com/TicketsBot-cloud/gdl v0.0.0-20260612070331-a3947b410d3e github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc github.com/TicketsBot-cloud/worker v0.0.0-20260827073646-455b39e53841 diff --git a/backend/go.sum b/backend/go.sum index bf472c0e..1b37bf57 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -39,10 +39,10 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/ReneKroon/ttlcache v1.6.0/go.mod h1:DG6nbhXKUQhrExfwwLuZUdH7UnRDDRA1IW+nBuCssvs= github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 h1:liLfvCrzoJ89DXFHzsd1iK3cyP8s4i0CnZPRFEj53zg= github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704/go.mod h1:Mux1bEPpOHwRw1wo6Fa6qJLJH9Erk9qv1yAIfLi1Wmw= -github.com/TicketsBot-cloud/common v0.0.0-20260905165836-38e4090764a4 h1:zcPcJ+03xW9qalqzqllM6j0ZPdhuL+mrtGw8woLrAMo= -github.com/TicketsBot-cloud/common v0.0.0-20260905165836-38e4090764a4/go.mod h1:yL+VPSYNVK5gxUkA+fbb0WdTjGfWXnr7ScfL0rwk29g= -github.com/TicketsBot-cloud/database v0.0.0-20260913165941-c2ecc7191b02 h1:3Z8PpXBUbdYn2srMRg3Nj0MDxnTB53CzIEAOUBIubhM= -github.com/TicketsBot-cloud/database v0.0.0-20260913165941-c2ecc7191b02/go.mod h1:0fKn7nPX6KeljYNQtGQiuZ5aJ/TczgXvBhnbz+4GEc8= +github.com/TicketsBot-cloud/common v0.0.0-20260927072029-424264fc4be2 h1:qeebeGGdrsDXrngOVrKMDt9G3JUTJwT/IEPTB61d46s= +github.com/TicketsBot-cloud/common v0.0.0-20260927072029-424264fc4be2/go.mod h1:yL+VPSYNVK5gxUkA+fbb0WdTjGfWXnr7ScfL0rwk29g= +github.com/TicketsBot-cloud/database v0.0.0-20260927071905-7a22c7be0e4a h1:Y1MPuFHwMmkoGeqnUV8CuXx4yqChdvPpqy3vGWBYnZo= +github.com/TicketsBot-cloud/database v0.0.0-20260927071905-7a22c7be0e4a/go.mod h1:0fKn7nPX6KeljYNQtGQiuZ5aJ/TczgXvBhnbz+4GEc8= github.com/TicketsBot-cloud/gdl v0.0.0-20260612070331-a3947b410d3e h1:qCibZQmO2rrBFrvn6+oAchPEQ7k/0Izr3ruL5P9aVZU= github.com/TicketsBot-cloud/gdl v0.0.0-20260612070331-a3947b410d3e/go.mod h1:CdwBR2egPtxUXjD2CgC9ZwfuB8dz9HPePM8nuG6dt7Y= github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc h1:qTLNpCvIqM7UwZ6MdWQ9EztcDsIJfHh+VJdG+ULLEaA= diff --git a/backend/notify/categories.go b/backend/notify/categories.go index 30de150e..75985c4e 100644 --- a/backend/notify/categories.go +++ b/backend/notify/categories.go @@ -24,7 +24,7 @@ var AllCategories = []CategoryInfo{ {Key: CategoryIntegrations, Label: "Integrations", Description: "Updates when your public integration request is approved, rejected, or unapproved", MinTier: admin.AdminTierNone}, {Key: CategoryAdminGallery, Label: "Gallery Submissions", Description: "New gallery panel submissions for review", MinTier: admin.AdminTierAdmin}, {Key: CategoryAdminAffiliates, Label: "Affiliate Applications", Description: "New affiliate applications pending approval", MinTier: admin.AdminTierOwner}, - {Key: CategoryAdminIntegrations, Label: "Integration Requests", Description: "Integration public access requests", MinTier: admin.AdminTierAdmin}, + {Key: CategoryAdminIntegrations, Label: "Integration Requests", Description: "Integration public access requests", MinTier: admin.AdminTierOwner}, } func AdminCategoryKeys() []string { diff --git a/backend/rpc/cache/cache.go b/backend/rpc/cache/cache.go index 7b64e9c6..c8b8725f 100644 --- a/backend/rpc/cache/cache.go +++ b/backend/rpc/cache/cache.go @@ -2,8 +2,11 @@ package cache import ( "context" + "encoding/json" gdlcache "github.com/TicketsBot-cloud/gdl/cache" + "github.com/TicketsBot-cloud/gdl/objects/guild" + "github.com/jackc/pgtype" "github.com/jackc/pgx/v4/pgxpool" "github.com/ticketsbot-cloud/dashboard/backend/config" ) @@ -32,3 +35,38 @@ func NewCache() *Cache { PgCache: &cache, } } + +func (c *Cache) GetGuilds(ctx context.Context, ids []uint64) (map[uint64]guild.Guild, error) { + guilds := make(map[uint64]guild.Guild) + if len(ids) == 0 { + return guilds, nil + } + + idArray := &pgtype.Int8Array{} + if err := idArray.Set(ids); err != nil { + return nil, err + } + + rows, err := c.Query(ctx, `SELECT "guild_id", "data" FROM guilds WHERE "guild_id" = ANY($1);`, idArray) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var id uint64 + var raw string + if err := rows.Scan(&id, &raw); err != nil { + return nil, err + } + + var cached guild.CachedGuild + if err := json.Unmarshal([]byte(raw), &cached); err != nil { + return nil, err + } + + guilds[id] = cached.ToGuild(id) + } + + return guilds, rows.Err() +} diff --git a/backend/utils/submissionblacklist.go b/backend/utils/submissionblacklist.go new file mode 100644 index 00000000..b0a0c127 --- /dev/null +++ b/backend/utils/submissionblacklist.go @@ -0,0 +1,36 @@ +package utils + +import ( + "net/http" + + "github.com/TicketsBot-cloud/database" + "github.com/gin-gonic/gin" + "github.com/ticketsbot-cloud/dashboard/backend/app" + dbclient "github.com/ticketsbot-cloud/dashboard/backend/database" +) + +type submissionBlacklistKey struct { + feature database.SubmissionFeature + targetType database.SubmissionTargetType +} + +var submissionBlacklistMessages = map[submissionBlacklistKey]string{ + {database.SubmissionFeatureGallery, database.SubmissionTargetUser}: "You are blacklisted from submitting to the gallery", + {database.SubmissionFeatureGallery, database.SubmissionTargetGuild}: "This server is blacklisted from submitting to the gallery", + {database.SubmissionFeatureIntegrations, database.SubmissionTargetUser}: "You are blacklisted from submitting integrations for public review", +} + +func RejectIfSubmissionBlacklisted(ctx *gin.Context, feature database.SubmissionFeature, userId, guildId uint64) bool { + blacklisted, targetType, err := dbclient.Client.SubmissionBlacklist.IsBlacklisted(ctx, feature, userId, guildId) + if err != nil { + _ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to process request. Please try again.")) + return true + } + + if blacklisted { + ctx.JSON(http.StatusForbidden, ErrorStr("%s", submissionBlacklistMessages[submissionBlacklistKey{feature, targetType}])) + return true + } + + return false +} diff --git a/frontend/src/components/AdminTabBar.tsx b/frontend/src/components/AdminTabBar.tsx index 4a3f8130..b1e7c6d1 100644 --- a/frontend/src/components/AdminTabBar.tsx +++ b/frontend/src/components/AdminTabBar.tsx @@ -16,13 +16,19 @@ const tabs: Tab[] = [ { label: "Polar Products", path: "/admin/polar-products", minTier: "owner" }, { label: "SKUs", path: "/admin/skus", minTier: "owner" }, { label: "Gallery", path: "/admin/gallery", minTier: "helper" }, - { label: "Integrations", path: "/admin/integrations", minTier: "admin" }, + { label: "Integrations", path: "/admin/integrations", minTier: "owner" }, { label: "Affiliates", path: "/admin/affiliate", minTier: "admin" }, { label: "Analytics", path: "/admin/analytics", minTier: "admin" }, { label: "Feature Flags", path: "/admin/flags", minTier: "owner" }, { label: "Audit Log", path: "/admin/audit-log", minTier: "admin" }, { label: "Global Blacklist", path: "/admin/global-blacklist", minTier: "owner" }, { label: "Server Blacklist", path: "/admin/server-blacklist", minTier: "helper" }, + { label: "Gallery Blacklist", path: "/admin/submission-blacklist/gallery", minTier: "admin" }, + { + label: "Integration Blacklist", + path: "/admin/submission-blacklist/integrations", + minTier: "owner", + }, { label: "Utilities", path: "/admin/utilities", minTier: "owner" }, ]; @@ -44,7 +50,7 @@ const AdminTabBar: FC = () => { setImageFailed(true)} + /> + ); + } + + if (icon && !imageFailed) { + return ( + {`${label} setImageFailed(true)} + /> + ); + } + + return ( +
+ {name ? ( + getGuildInitials(name) + ) : ( +
+ ); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0cb65886..f9abb9c0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -40,6 +40,9 @@ import type { BotStaffMember, GlobalBlacklistEntry, ServerBlacklistEntry, + SubmissionBlacklistEntry, + SubmissionBlacklistFeature, + SubmissionBlacklistTarget, AdminEntitlement, PremiumKeyEntry, SkuWithDetails, @@ -769,6 +772,18 @@ export const apiClient = { api.post(`/api/admin/server-blacklist/${guildId}`, data), remove: (guildId: string) => api.delete(`/api/admin/server-blacklist/${guildId}`), }, + submissionBlacklist: { + list: (feature: SubmissionBlacklistFeature) => + api.get(`/api/admin/submission-blacklist/${feature}`), + add: ( + feature: SubmissionBlacklistFeature, + type: SubmissionBlacklistTarget, + id: string, + data?: { reason?: string }, + ) => api.post(`/api/admin/submission-blacklist/${feature}/${type}/${id}`, data), + remove: (feature: SubmissionBlacklistFeature, type: SubmissionBlacklistTarget, id: string) => + api.delete(`/api/admin/submission-blacklist/${feature}/${type}/${id}`), + }, featureFlags: { list: () => api.get("/api/admin/feature-flags"), experiments: () => api.get("/api/admin/feature-flags/experiments"), diff --git a/frontend/src/lib/auditlog.ts b/frontend/src/lib/auditlog.ts index e833dc70..2570668e 100644 --- a/frontend/src/lib/auditlog.ts +++ b/frontend/src/lib/auditlog.ts @@ -139,6 +139,9 @@ const ACTION_TYPE_LABELS: Record = { 440: "Feature Flag Toggle", 441: "Feature Flag Create", 442: "Feature Flag Rules Update", + + 450: "Submission Blacklist Add", + 451: "Submission Blacklist Remove", }; const RESOURCE_TYPE_LABELS: Record = { @@ -180,6 +183,7 @@ const RESOURCE_TYPE_LABELS: Record = { 36: "Notification", 37: "User Email Verification", 38: "Feature Flag", + 39: "Submission Blacklist", }; export function formatActionType(type: number): string { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a039ff82..86ea1ad6 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -29,6 +29,8 @@ import { faUserShield, faSquareCaretLeft, faRightToBracket, + faTrash, + faSync, } from "@fortawesome/free-solid-svg-icons"; library.add( @@ -54,6 +56,8 @@ library.add( faUserShield, faSquareCaretLeft, faRightToBracket, + faTrash, + faSync, ); const sentryDsn = import.meta.env.VITE_SENTRY_DSN; diff --git a/frontend/src/pages/admin/global-blacklist/_index.tsx b/frontend/src/pages/admin/global-blacklist/_index.tsx index 9e25f3bd..1940660d 100644 --- a/frontend/src/pages/admin/global-blacklist/_index.tsx +++ b/frontend/src/pages/admin/global-blacklist/_index.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useMemo, useCallback } from "react"; -import { userAvatarUrl } from "@/lib/discord-cdn"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import BlacklistAvatar from "@/components/BlacklistAvatar"; import ConfirmModal from "@/components/modals/ConfirmModal"; import Button from "@/components/Button"; import TextInput from "@/components/TextInput"; @@ -75,9 +75,6 @@ export default function GlobalBlacklistPage() { } }; - const getAvatarUrl = (entry: GlobalBlacklistEntry): string => - userAvatarUrl(entry.id, entry.avatar_url ?? null); - if (isLoading) { return ; } @@ -123,7 +120,7 @@ export default function GlobalBlacklistPage() { {/* User Grid */}
@@ -131,13 +128,14 @@ export default function GlobalBlacklistPage() {
- {`${entry.username}
@@ -148,7 +146,7 @@ export default function GlobalBlacklistPage() { variant="ghost" size="icon" onClick={() => setDeleteTarget(entry)} - className="opacity-0 group-hover:opacity-100 text-green-400 hover:text-green-300 hover:bg-green-900/30 transition-all" + className="text-red-400 hover:text-red-300 hover:bg-red-900/30 transition-all" title={`Remove ${entry.username} from blacklist`} >
{/* Server Grid */}
@@ -153,33 +155,49 @@ export default function ServerBlacklistPage() {
-

{entry.guild_id}

+
+
+ +
+
+

{entry.name ?? "Unknown Server"}

+

{entry.guild_id}

+
+
{entry.reason && ( -

{entry.reason}

+

{entry.reason}

+ )} + {(entry.owner_id || entry.real_owner_id) && ( +
+ {entry.owner_id && ( +

+ Owner: {entry.owner_id} +

+ )} + {entry.real_owner_id && ( +

+ Real Owner: {entry.real_owner_id} +

+ )} +
)} -
- {entry.owner_id && ( -

- Owner: {entry.owner_id} -

- )} - {entry.real_owner_id && ( -

- Real Owner: {entry.real_owner_id} -

- )} -
- {canModify && ( + {canRemove && (