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 (
+ setImageFailed(true)}
+ />
+ );
+ }
+
+ return (
+
{entry.guild_id}
+{entry.guild_id}
+{entry.reason}
+{entry.reason}
+ )} + {(entry.owner_id || entry.real_owner_id) && ( ++ Owner: {entry.owner_id} +
+ )} + {entry.real_owner_id && ( ++ Real Owner: {entry.real_owner_id} +
+ )} +- Owner: {entry.owner_id} -
- )} - {entry.real_owner_id && ( -- Real Owner: {entry.real_owner_id} -
- )} -