Add ACLs
This commit is contained in:
parent
263b5a3252
commit
c600b91109
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/TicketsBot/GoPanel/app/http/validation"
|
"github.com/TicketsBot/GoPanel/app/http/validation"
|
||||||
"github.com/TicketsBot/GoPanel/botcontext"
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
@ -8,11 +9,11 @@ import (
|
|||||||
"github.com/TicketsBot/GoPanel/rpc"
|
"github.com/TicketsBot/GoPanel/rpc"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/TicketsBot/GoPanel/utils/types"
|
"github.com/TicketsBot/GoPanel/utils/types"
|
||||||
"github.com/TicketsBot/common/collections"
|
|
||||||
"github.com/TicketsBot/common/premium"
|
"github.com/TicketsBot/common/premium"
|
||||||
"github.com/TicketsBot/database"
|
"github.com/TicketsBot/database"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
"github.com/rxdn/gdl/objects/guild/emoji"
|
"github.com/rxdn/gdl/objects/guild/emoji"
|
||||||
"github.com/rxdn/gdl/objects/interaction/component"
|
"github.com/rxdn/gdl/objects/interaction/component"
|
||||||
"github.com/rxdn/gdl/rest/request"
|
"github.com/rxdn/gdl/rest/request"
|
||||||
@ -22,25 +23,26 @@ import (
|
|||||||
const freePanelLimit = 3
|
const freePanelLimit = 3
|
||||||
|
|
||||||
type panelBody struct {
|
type panelBody struct {
|
||||||
ChannelId uint64 `json:"channel_id,string"`
|
ChannelId uint64 `json:"channel_id,string"`
|
||||||
MessageId uint64 `json:"message_id,string"`
|
MessageId uint64 `json:"message_id,string"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Colour uint32 `json:"colour"`
|
Colour uint32 `json:"colour"`
|
||||||
CategoryId uint64 `json:"category_id,string"`
|
CategoryId uint64 `json:"category_id,string"`
|
||||||
Emoji types.Emoji `json:"emote"`
|
Emoji types.Emoji `json:"emote"`
|
||||||
WelcomeMessage *types.CustomEmbed `json:"welcome_message" validate:"omitempty,dive"`
|
WelcomeMessage *types.CustomEmbed `json:"welcome_message" validate:"omitempty,dive"`
|
||||||
Mentions []string `json:"mentions"`
|
Mentions []string `json:"mentions"`
|
||||||
WithDefaultTeam bool `json:"default_team"`
|
WithDefaultTeam bool `json:"default_team"`
|
||||||
Teams []int `json:"teams"`
|
Teams []int `json:"teams"`
|
||||||
ImageUrl *string `json:"image_url,omitempty"`
|
ImageUrl *string `json:"image_url,omitempty"`
|
||||||
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
|
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
|
||||||
ButtonStyle component.ButtonStyle `json:"button_style,string"`
|
ButtonStyle component.ButtonStyle `json:"button_style,string"`
|
||||||
ButtonLabel string `json:"button_label"`
|
ButtonLabel string `json:"button_label"`
|
||||||
FormId *int `json:"form_id"`
|
FormId *int `json:"form_id"`
|
||||||
NamingScheme *string `json:"naming_scheme"`
|
NamingScheme *string `json:"naming_scheme"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
ExitSurveyFormId *int `json:"exit_survey_form_id"`
|
ExitSurveyFormId *int `json:"exit_survey_form_id"`
|
||||||
|
AccessControlList []database.PanelAccessControlRule `json:"access_control_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *panelBody) IntoPanelMessageData(customId string, isPremium bool) panelMessageData {
|
func (p *panelBody) IntoPanelMessageData(customId string, isPremium bool) panelMessageData {
|
||||||
@ -109,6 +111,12 @@ func CreatePanel(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roles, err := botContext.GetGuildRoles(guildId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Do custom validation
|
// Do custom validation
|
||||||
validationContext := PanelValidationContext{
|
validationContext := PanelValidationContext{
|
||||||
Data: data,
|
Data: data,
|
||||||
@ -116,6 +124,7 @@ func CreatePanel(ctx *gin.Context) {
|
|||||||
IsPremium: premiumTier > premium.None,
|
IsPremium: premiumTier > premium.None,
|
||||||
BotContext: botContext,
|
BotContext: botContext,
|
||||||
Channels: channels,
|
Channels: channels,
|
||||||
|
Roles: roles,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ValidatePanelBody(validationContext); err != nil {
|
if err := ValidatePanelBody(validationContext); err != nil {
|
||||||
@ -215,30 +224,19 @@ func CreatePanel(ctx *gin.Context) {
|
|||||||
ExitSurveyFormId: data.ExitSurveyFormId,
|
ExitSurveyFormId: data.ExitSurveyFormId,
|
||||||
}
|
}
|
||||||
|
|
||||||
panelId, err := dbclient.Client.Panel.Create(panel)
|
createOptions := panelCreateOptions{
|
||||||
if err != nil {
|
TeamIds: data.Teams, // Already validated
|
||||||
ctx.AbortWithStatusJSON(500, gin.H{
|
AccessControlRules: data.AccessControlList, // Already validated
|
||||||
"success": false,
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert role mention data
|
// insert role mention data
|
||||||
// string is role ID or "user" to mention the ticket opener
|
// string is role ID or "user" to mention the ticket opener
|
||||||
validRoles, err := getRoleHashSet(guildId)
|
validRoles := utils.ToSet(utils.Map(roles, utils.RoleToId))
|
||||||
if err != nil {
|
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var roleMentions []uint64
|
var roleMentions []uint64
|
||||||
for _, mention := range data.Mentions {
|
for _, mention := range data.Mentions {
|
||||||
if mention == "user" {
|
if mention == "user" {
|
||||||
if err = dbclient.Client.PanelUserMention.Set(panelId, true); err != nil {
|
createOptions.ShouldMentionUser = true
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
roleId, err := strconv.ParseUint(mention, 10, 64)
|
roleId, err := strconv.ParseUint(mention, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -247,18 +245,13 @@ func CreatePanel(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if validRoles.Contains(roleId) {
|
if validRoles.Contains(roleId) {
|
||||||
roleMentions = append(roleMentions, roleId)
|
createOptions.RoleMentions = append(roleMentions, roleId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbclient.Client.PanelRoleMentions.Replace(panelId, roleMentions); err != nil {
|
panelId, err := storePanel(ctx, panel, createOptions)
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
if err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already validated, we are safe to insert
|
|
||||||
if err := dbclient.Client.PanelTeams.Replace(panelId, data.Teams); err != nil {
|
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -269,27 +262,52 @@ func CreatePanel(ctx *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DB functions
|
||||||
|
|
||||||
|
type panelCreateOptions struct {
|
||||||
|
ShouldMentionUser bool
|
||||||
|
RoleMentions []uint64
|
||||||
|
TeamIds []int
|
||||||
|
AccessControlRules []database.PanelAccessControlRule
|
||||||
|
}
|
||||||
|
|
||||||
|
func storePanel(ctx context.Context, panel database.Panel, options panelCreateOptions) (int, error) {
|
||||||
|
var panelId int
|
||||||
|
err := dbclient.Client.Panel.BeginFunc(ctx, func(tx pgx.Tx) error {
|
||||||
|
var err error
|
||||||
|
panelId, err = dbclient.Client.Panel.CreateWithTx(tx, panel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.PanelUserMention.SetWithTx(tx, panelId, options.ShouldMentionUser); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.PanelRoleMentions.ReplaceWithTx(tx, panelId, options.RoleMentions); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already validated, we are safe to insert
|
||||||
|
if err := dbclient.Client.PanelTeams.ReplaceWithTx(tx, panelId, options.TeamIds); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.PanelAccessControlRules.ReplaceWithTx(ctx, tx, panelId, options.AccessControlRules); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return panelId, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Data must be validated before calling this function
|
// Data must be validated before calling this function
|
||||||
func (p *panelBody) getEmoji() *emoji.Emoji {
|
func (p *panelBody) getEmoji() *emoji.Emoji {
|
||||||
return p.Emoji.IntoGdl()
|
return p.Emoji.IntoGdl()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRoleHashSet(guildId uint64) (*collections.Set[uint64], error) {
|
|
||||||
ctx, err := botcontext.ContextForGuild(guildId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
roles, err := ctx.GetGuildRoles(guildId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
set := collections.NewSet[uint64]()
|
|
||||||
|
|
||||||
for _, role := range roles {
|
|
||||||
set.Add(role.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return set, nil
|
|
||||||
}
|
|
||||||
|
@ -14,12 +14,13 @@ import (
|
|||||||
func ListPanels(ctx *gin.Context) {
|
func ListPanels(ctx *gin.Context) {
|
||||||
type panelResponse struct {
|
type panelResponse struct {
|
||||||
database.Panel
|
database.Panel
|
||||||
WelcomeMessage *types.CustomEmbed `json:"welcome_message"`
|
WelcomeMessage *types.CustomEmbed `json:"welcome_message"`
|
||||||
UseCustomEmoji bool `json:"use_custom_emoji"`
|
UseCustomEmoji bool `json:"use_custom_emoji"`
|
||||||
Emoji types.Emoji `json:"emote"`
|
Emoji types.Emoji `json:"emote"`
|
||||||
Mentions []string `json:"mentions"`
|
Mentions []string `json:"mentions"`
|
||||||
Teams []int `json:"teams"`
|
Teams []int `json:"teams"`
|
||||||
UseServerDefaultNamingScheme bool `json:"use_server_default_naming_scheme"`
|
UseServerDefaultNamingScheme bool `json:"use_server_default_naming_scheme"`
|
||||||
|
AccessControlList []database.PanelAccessControlRule `json:"access_control_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
guildId := ctx.Keys["guildid"].(uint64)
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
@ -30,6 +31,12 @@ func ListPanels(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accessControlLists, err := dbclient.Client.PanelAccessControlRules.GetAllForGuild(ctx, guildId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
allFields, err := dbclient.Client.EmbedFields.GetAllFieldsForPanels(guildId)
|
allFields, err := dbclient.Client.EmbedFields.GetAllFieldsForPanels(guildId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
@ -85,6 +92,11 @@ func ListPanels(ctx *gin.Context) {
|
|||||||
welcomeMessage = types.NewCustomEmbed(p.WelcomeMessage, fields)
|
welcomeMessage = types.NewCustomEmbed(p.WelcomeMessage, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accessControlList := accessControlLists[p.PanelId]
|
||||||
|
if accessControlList == nil {
|
||||||
|
accessControlList = make([]database.PanelAccessControlRule, 0)
|
||||||
|
}
|
||||||
|
|
||||||
wrapped[i] = panelResponse{
|
wrapped[i] = panelResponse{
|
||||||
Panel: p.Panel,
|
Panel: p.Panel,
|
||||||
WelcomeMessage: welcomeMessage,
|
WelcomeMessage: welcomeMessage,
|
||||||
@ -93,6 +105,7 @@ func ListPanels(ctx *gin.Context) {
|
|||||||
Mentions: mentions,
|
Mentions: mentions,
|
||||||
Teams: teamIds,
|
Teams: teamIds,
|
||||||
UseServerDefaultNamingScheme: p.NamingScheme == nil,
|
UseServerDefaultNamingScheme: p.NamingScheme == nil,
|
||||||
|
AccessControlList: accessControlList,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/TicketsBot/database"
|
"github.com/TicketsBot/database"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
"github.com/rxdn/gdl/objects/interaction/component"
|
"github.com/rxdn/gdl/objects/interaction/component"
|
||||||
"github.com/rxdn/gdl/rest"
|
"github.com/rxdn/gdl/rest"
|
||||||
"github.com/rxdn/gdl/rest/request"
|
"github.com/rxdn/gdl/rest/request"
|
||||||
@ -71,6 +72,12 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roles, err := botContext.GetGuildRoles(guildId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Do custom validation
|
// Do custom validation
|
||||||
validationContext := PanelValidationContext{
|
validationContext := PanelValidationContext{
|
||||||
Data: data,
|
Data: data,
|
||||||
@ -78,6 +85,7 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
IsPremium: premiumTier > premium.None,
|
IsPremium: premiumTier > premium.None,
|
||||||
BotContext: botContext,
|
BotContext: botContext,
|
||||||
Channels: channels,
|
Channels: channels,
|
||||||
|
Roles: roles,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ValidatePanelBody(validationContext); err != nil {
|
if err := ValidatePanelBody(validationContext); err != nil {
|
||||||
@ -213,17 +221,8 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
ExitSurveyFormId: data.ExitSurveyFormId,
|
ExitSurveyFormId: data.ExitSurveyFormId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = dbclient.Client.Panel.Update(panel); err != nil {
|
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert mention data
|
// insert mention data
|
||||||
validRoles, err := getRoleHashSet(guildId)
|
validRoles := utils.ToSet(utils.Map(roles, utils.RoleToId))
|
||||||
if err != nil {
|
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// string is role ID or "user" to mention the ticket opener
|
// string is role ID or "user" to mention the ticket opener
|
||||||
var shouldMentionUser bool
|
var shouldMentionUser bool
|
||||||
@ -244,22 +243,37 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbclient.Client.PanelUserMention.Set(panel.PanelId, shouldMentionUser); err != nil {
|
err = dbclient.Client.Panel.BeginFunc(ctx, func(tx pgx.Tx) error {
|
||||||
ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
|
if err := dbclient.Client.Panel.UpdateWithTx(tx, panel); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbclient.Client.PanelRoleMentions.Replace(panel.PanelId, roleMentions); err != nil {
|
if err := dbclient.Client.PanelUserMention.SetWithTx(tx, panel.PanelId, shouldMentionUser); err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
return err
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
if err := dbclient.Client.PanelRoleMentions.ReplaceWithTx(tx, panel.PanelId, roleMentions); err != nil {
|
||||||
// We are safe to insert, team IDs already validated
|
return err
|
||||||
if err := dbclient.Client.PanelTeams.Replace(panel.PanelId, data.Teams); err != nil {
|
}
|
||||||
|
|
||||||
|
// We are safe to insert, team IDs already validated
|
||||||
|
if err := dbclient.Client.PanelTeams.ReplaceWithTx(tx, panel.PanelId, data.Teams); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.PanelAccessControlRules.ReplaceWithTx(ctx, tx, panel.PanelId, data.AccessControlList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This doesn't need to be done in a transaction
|
||||||
// Update multi panels
|
// Update multi panels
|
||||||
|
|
||||||
// check if this will break a multi-panel;
|
// check if this will break a multi-panel;
|
||||||
|
@ -9,14 +9,24 @@ import (
|
|||||||
"github.com/TicketsBot/GoPanel/botcontext"
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
"github.com/rxdn/gdl/objects/channel"
|
"github.com/rxdn/gdl/objects/channel"
|
||||||
|
"github.com/rxdn/gdl/objects/guild"
|
||||||
"github.com/rxdn/gdl/objects/interaction/component"
|
"github.com/rxdn/gdl/objects/interaction/component"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ApplyPanelDefaults(data *panelBody) []defaults.DefaultApplicator {
|
func ApplyPanelDefaults(data *panelBody) {
|
||||||
|
for _, applicator := range DefaultApplicators(data) {
|
||||||
|
if applicator.ShouldApply() {
|
||||||
|
applicator.Apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultApplicators(data *panelBody) []defaults.DefaultApplicator {
|
||||||
return []defaults.DefaultApplicator{
|
return []defaults.DefaultApplicator{
|
||||||
defaults.NewDefaultApplicator(defaults.EmptyStringCheck, &data.Title, "Open a ticket!"),
|
defaults.NewDefaultApplicator(defaults.EmptyStringCheck, &data.Title, "Open a ticket!"),
|
||||||
defaults.NewDefaultApplicator(defaults.EmptyStringCheck, &data.Content, "By clicking the button, a ticket will be opened for you."),
|
defaults.NewDefaultApplicator(defaults.EmptyStringCheck, &data.Content, "By clicking the button, a ticket will be opened for you."),
|
||||||
@ -34,6 +44,7 @@ type PanelValidationContext struct {
|
|||||||
IsPremium bool
|
IsPremium bool
|
||||||
BotContext botcontext.BotContext
|
BotContext botcontext.BotContext
|
||||||
Channels []channel.Channel
|
Channels []channel.Channel
|
||||||
|
Roles []guild.Role
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidatePanelBody(validationContext PanelValidationContext) error {
|
func ValidatePanelBody(validationContext PanelValidationContext) error {
|
||||||
@ -59,6 +70,7 @@ func panelValidators() []validation.Validator[PanelValidationContext] {
|
|||||||
validateTeams,
|
validateTeams,
|
||||||
validateNamingScheme,
|
validateNamingScheme,
|
||||||
validateWelcomeMessage,
|
validateWelcomeMessage,
|
||||||
|
validateAccessControlList,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,3 +294,44 @@ func validateWelcomeMessage(ctx PanelValidationContext) validation.ValidationFun
|
|||||||
return validation.NewInvalidInputError("Welcome message has no content")
|
return validation.NewInvalidInputError("Welcome message has no content")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateAccessControlList(ctx PanelValidationContext) validation.ValidationFunc {
|
||||||
|
return func() error {
|
||||||
|
acl := ctx.Data.AccessControlList
|
||||||
|
|
||||||
|
if len(acl) == 0 {
|
||||||
|
return validation.NewInvalidInputError("Access control list is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(acl) > 10 {
|
||||||
|
return validation.NewInvalidInputError("Access control list cannot have more than 10 roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
roles := utils.ToSet(utils.Map(ctx.Roles, utils.RoleToId))
|
||||||
|
|
||||||
|
if roles.Size() != len(ctx.Roles) {
|
||||||
|
return validation.NewInvalidInputError("Duplicate roles in access control list")
|
||||||
|
}
|
||||||
|
|
||||||
|
everyoneRoleFound := false
|
||||||
|
for _, rule := range acl {
|
||||||
|
if rule.RoleId == ctx.GuildId {
|
||||||
|
everyoneRoleFound = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Action != database.AccessControlActionDeny && rule.Action != database.AccessControlActionAllow {
|
||||||
|
return validation.NewInvalidInputErrorf("Invalid access control action \"%s\"", rule.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !roles.Contains(rule.RoleId) {
|
||||||
|
return validation.NewInvalidInputErrorf("Invalid role %d in access control list not found in the guild", rule.RoleId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !everyoneRoleFound {
|
||||||
|
return validation.NewInvalidInputError("Access control list does not contain @everyone rule")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
type InvalidInputError struct {
|
type InvalidInputError struct {
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
@ -11,3 +13,7 @@ func (e *InvalidInputError) Error() string {
|
|||||||
func NewInvalidInputError(message string) *InvalidInputError {
|
func NewInvalidInputError(message string) *InvalidInputError {
|
||||||
return &InvalidInputError{Message: message}
|
return &InvalidInputError{Message: message}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewInvalidInputErrorf(message string, args ...any) *InvalidInputError {
|
||||||
|
return &InvalidInputError{Message: fmt.Sprintf(message, args...)}
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
{loadOptions}
|
{loadOptions}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{placeholderAlwaysShow}
|
{placeholderAlwaysShow}
|
||||||
|
{disabled}
|
||||||
multiple={isMulti}
|
multiple={isMulti}
|
||||||
--background="#2e3136"
|
--background="#2e3136"
|
||||||
--border="#2e3136"
|
--border="#2e3136"
|
||||||
@ -46,6 +47,7 @@
|
|||||||
export let placeholderAlwaysShow = false;
|
export let placeholderAlwaysShow = false;
|
||||||
export let loadOptions;
|
export let loadOptions;
|
||||||
export let loadOptionsInterval;
|
export let loadOptionsInterval;
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
export let optionIdentifier;
|
export let optionIdentifier;
|
||||||
export let nameMapper = (x) => x;
|
export let nameMapper = (x) => x;
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<label class="form-label">{label}</label>
|
<label class="form-label">{label}</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<WrappedSelect placeholder="Search..." optionIdentifier="id" items={roles}
|
<WrappedSelect {placeholder} optionIdentifier="id" items={roles} {disabled}
|
||||||
bind:selectedValue={value} nameMapper={labelMapper}/>
|
bind:selectedValue={value} nameMapper={labelMapper} on:change />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {onMount} from 'svelte'
|
import {onMount} from 'svelte'
|
||||||
@ -11,8 +11,10 @@
|
|||||||
import WrappedSelect from "../WrappedSelect.svelte";
|
import WrappedSelect from "../WrappedSelect.svelte";
|
||||||
|
|
||||||
export let label;
|
export let label;
|
||||||
|
export let placeholder = "Search...";
|
||||||
export let roles = [];
|
export let roles = [];
|
||||||
export let guildId;
|
export let guildId;
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
|
174
frontend/src/components/manage/AccessControlList.svelte
Normal file
174
frontend/src/components/manage/AccessControlList.svelte
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<div class="super">
|
||||||
|
<RoleSelect {guildId} placeholder="Add another role..."
|
||||||
|
roles={roles.filter((r) => !acl.find((s) => s.role_id === r.id))} disabled={acl.length >= maxAclSize}
|
||||||
|
on:change={(e) => addToACL(e.detail)} bind:value={roleSelectorValue}/>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{#each acl as subject, i}
|
||||||
|
{@const role = roles.find(r => r.id === subject.role_id)}
|
||||||
|
<div class="subject">
|
||||||
|
<div class="inner-left">
|
||||||
|
<div class="row" style="gap: 10px">
|
||||||
|
<div class="arrow-container">
|
||||||
|
<i class="fa-solid fa-arrow-up position-arrow" class:disabled={i<=0} on:click={() => moveUp(i)}></i>
|
||||||
|
<i class="fa-solid fa-arrow-down position-arrow" class:disabled={i>=acl.length-1}
|
||||||
|
on:click={() => moveDown(i)}></i>
|
||||||
|
</div>
|
||||||
|
<span>{role.name}</span>
|
||||||
|
</div>
|
||||||
|
{#key rerender}
|
||||||
|
<Toggle on="Allow" off="Deny"
|
||||||
|
hideLabel
|
||||||
|
toggledColor="#66bb6a"
|
||||||
|
untoggledColor="#e84141"
|
||||||
|
toggled={subject.action === "allow"}
|
||||||
|
on:toggle={(e) => handleToggle(subject.role_id, e.detail)}/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
<div class="inner-right">
|
||||||
|
{#if subject.role_id !== guildId}
|
||||||
|
<div class="delete-button">
|
||||||
|
<i class="fas fa-x" on:click={() => removeFromACL(subject)}></i>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Toggle from "svelte-toggle";
|
||||||
|
import RoleSelect from "../form/RoleSelect.svelte";
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
export let roles;
|
||||||
|
export let maxAclSize = 10;
|
||||||
|
export let acl = [
|
||||||
|
{
|
||||||
|
role_id: guildId,
|
||||||
|
action: "allow"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let rerender = 0;
|
||||||
|
let roleSelectorValue;
|
||||||
|
|
||||||
|
function handleToggle(roleId, enabled) {
|
||||||
|
const subject = acl.find(s => s.role_id === roleId);
|
||||||
|
subject.action = enabled ? "allow" : "deny";
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToACL(role) {
|
||||||
|
acl = [
|
||||||
|
{
|
||||||
|
role_id: role.id,
|
||||||
|
action: "allow"
|
||||||
|
},
|
||||||
|
...acl
|
||||||
|
];
|
||||||
|
|
||||||
|
roleSelectorValue = null;
|
||||||
|
|
||||||
|
rerender++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromACL(subject) {
|
||||||
|
if (subject.role_id === guildId) return;
|
||||||
|
acl = acl.filter(s => s.role_id !== subject.role_id);
|
||||||
|
|
||||||
|
rerender++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp(index) {
|
||||||
|
if (index <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmp = acl[index];
|
||||||
|
acl[index] = acl[index - 1];
|
||||||
|
acl[index - 1] = tmp;
|
||||||
|
|
||||||
|
rerender++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown(index) {
|
||||||
|
if (index >= acl.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmp = acl[index];
|
||||||
|
acl[index] = acl[index + 1];
|
||||||
|
acl[index + 1] = tmp
|
||||||
|
|
||||||
|
rerender++;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.super {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 5px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #2e3136;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject > .inner-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 1;
|
||||||
|
gap: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject > .inner-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
cursor: pointer;
|
||||||
|
transform: scale(1.33333333333, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-arrow {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-arrow.disabled {
|
||||||
|
cursor: unset !important;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
</style>
|
@ -154,28 +154,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
|
<Collapsible>
|
||||||
|
<span slot="header">Access Control</span>
|
||||||
|
<div slot="content" class="col-1">
|
||||||
|
<div class="row">
|
||||||
|
<p>Control who can open tickets with from this panel. Rules are evaluated from <em>top to bottom</em>,
|
||||||
|
stopping after the first match.</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<AccessControlList {guildId} {roles} bind:acl={data.access_control_list} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Input from "../form/Input.svelte";
|
import Input from "../form/Input.svelte";
|
||||||
import Textarea from "../form/Textarea.svelte";
|
import Textarea from "../form/Textarea.svelte";
|
||||||
import Colour from "../form/Colour.svelte";
|
import Colour from "../form/Colour.svelte";
|
||||||
import Button from "../Button.svelte";
|
|
||||||
import ChannelDropdown from "../ChannelDropdown.svelte";
|
import ChannelDropdown from "../ChannelDropdown.svelte";
|
||||||
import EmbedBuilder from "../EmbedBuilder.svelte";
|
|
||||||
|
|
||||||
import {createEventDispatcher, onMount} from 'svelte';
|
import {createEventDispatcher, onMount} from 'svelte';
|
||||||
import {colourToInt, intToColour} from "../../js/util";
|
import {colourToInt, intToColour} from "../../js/util";
|
||||||
import CategoryDropdown from "../CategoryDropdown.svelte";
|
import CategoryDropdown from "../CategoryDropdown.svelte";
|
||||||
import EmojiInput from "../form/EmojiInput.svelte";
|
import EmojiInput from "../form/EmojiInput.svelte";
|
||||||
import EmojiItem from "../EmojiItem.svelte";
|
|
||||||
import Select from 'svelte-select';
|
|
||||||
import Dropdown from "../form/Dropdown.svelte";
|
import Dropdown from "../form/Dropdown.svelte";
|
||||||
import Toggle from "svelte-toggle";
|
import Toggle from "svelte-toggle";
|
||||||
import Checkbox from "../form/Checkbox.svelte";
|
import Checkbox from "../form/Checkbox.svelte";
|
||||||
import Collapsible from "../Collapsible.svelte";
|
import Collapsible from "../Collapsible.svelte";
|
||||||
import EmbedForm from "../EmbedForm.svelte";
|
import EmbedForm from "../EmbedForm.svelte";
|
||||||
import WrappedSelect from "../WrappedSelect.svelte";
|
import WrappedSelect from "../WrappedSelect.svelte";
|
||||||
|
import AccessControlList from "./AccessControlList.svelte";
|
||||||
|
|
||||||
export let guildId;
|
export let guildId;
|
||||||
export let seedDefault = true;
|
export let seedDefault = true;
|
||||||
@ -328,6 +338,12 @@
|
|||||||
footer: {},
|
footer: {},
|
||||||
description: 'Thank you for contacting support.\nPlease describe your issue and wait for a response.'
|
description: 'Thank you for contacting support.\nPlease describe your issue and wait for a response.'
|
||||||
},
|
},
|
||||||
|
access_control_list: [
|
||||||
|
{
|
||||||
|
role_id: guildId,
|
||||||
|
action: "allow"
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
applyOverrides();
|
applyOverrides();
|
||||||
|
2
go.mod
2
go.mod
@ -6,7 +6,7 @@ require (
|
|||||||
github.com/BurntSushi/toml v1.2.1
|
github.com/BurntSushi/toml v1.2.1
|
||||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
||||||
github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa
|
github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa
|
||||||
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27
|
github.com/TicketsBot/database v0.0.0-20230821182620-0130c7c2c5ad
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
||||||
github.com/TicketsBot/worker v0.0.0-20230731124103-99c6834d9134
|
github.com/TicketsBot/worker v0.0.0-20230731124103-99c6834d9134
|
||||||
github.com/apex/log v1.1.2
|
github.com/apex/log v1.1.2
|
||||||
|
2
go.sum
2
go.sum
@ -49,6 +49,8 @@ github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa h1:6lMp2fzZvLpIq
|
|||||||
github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa/go.mod h1:zN6qXS5AYkt4JTHtq7mHT3eBHomUWZoZ29dZ/CPMjHQ=
|
github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa/go.mod h1:zN6qXS5AYkt4JTHtq7mHT3eBHomUWZoZ29dZ/CPMjHQ=
|
||||||
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27 h1:VbMTyqRPza4PRv/mRqJstLRD0tXTttPNeO2nLW/APfU=
|
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27 h1:VbMTyqRPza4PRv/mRqJstLRD0tXTttPNeO2nLW/APfU=
|
||||||
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
||||||
|
github.com/TicketsBot/database v0.0.0-20230821182620-0130c7c2c5ad h1:tg/KYNLExb8MJbrxxlVgSjIzSEOibduE3ZV1S0uz+7Y=
|
||||||
|
github.com/TicketsBot/database v0.0.0-20230821182620-0130c7c2c5ad/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
||||||
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
||||||
|
2
locale
2
locale
@ -1 +1 @@
|
|||||||
Subproject commit e21b73209a122b7feb5c8672c34b73d0a7f6ab03
|
Subproject commit b6540fa03b4f32ce126aa1d31830bbd4a0c2c6c7
|
@ -1,7 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/TicketsBot/common/collections"
|
||||||
"github.com/rxdn/gdl/objects/channel/message"
|
"github.com/rxdn/gdl/objects/channel/message"
|
||||||
|
"github.com/rxdn/gdl/objects/guild"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Contains[T comparable](slice []T, value T) bool {
|
func Contains[T comparable](slice []T, value T) bool {
|
||||||
@ -21,3 +23,26 @@ func Reverse(slice []message.Message) []message.Message {
|
|||||||
}
|
}
|
||||||
return slice
|
return slice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Map[T comparable, U any](slice []T, f func(T) U) []U {
|
||||||
|
result := make([]U, len(slice))
|
||||||
|
for i, elem := range slice {
|
||||||
|
result[i] = f(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSet[T comparable](slice []T) *collections.Set[T] {
|
||||||
|
set := collections.NewSet[T]()
|
||||||
|
|
||||||
|
for _, el := range slice {
|
||||||
|
set.Add(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoleToId(role guild.Role) uint64 {
|
||||||
|
return role.Id
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user