TP-0000 | Add tag condition for not resolved (#19)
* TP-0000 | Add tag condition for not resolved * TP-0000 | archive uncomment * TP-0000 | ping api * TP-0000 | mjolnir
This commit is contained in:
committed by
GitHub Enterprise
parent
9e8195d298
commit
810b314649
@@ -3,14 +3,17 @@ package app
|
||||
import (
|
||||
"fmt"
|
||||
"houston/cmd/app/handler"
|
||||
"houston/internal/metrics"
|
||||
"houston/internal/metrics"
|
||||
"houston/pkg/slackbot"
|
||||
"houston/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
||||
"houston/internal/clients"
|
||||
"houston/internal/metrics"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
@@ -18,16 +21,18 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
gin *gin.Engine
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
gin *gin.Engine
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
mjolnirClient *clients.MjolnirClient
|
||||
}
|
||||
|
||||
func NewServer(gin *gin.Engine, logger *zap.Logger, db *gorm.DB) *Server {
|
||||
func NewServer(gin *gin.Engine, logger *zap.Logger, db *gorm.DB, mjolnirClient *clients.MjolnirClient) *Server {
|
||||
return &Server{
|
||||
gin: gin,
|
||||
logger: logger,
|
||||
db: db,
|
||||
gin: gin,
|
||||
logger: logger,
|
||||
db: db,
|
||||
mjolnirClient: mjolnirClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +55,16 @@ func (s *Server) createMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
whitelistedDomains := getWhitelistedDomains()
|
||||
|
||||
//auth handling
|
||||
isAuthEnabled := viper.GetBool("auth.enabled")
|
||||
if isAuthEnabled {
|
||||
sessionResponse, err := s.mjolnirClient.GetSessionResponse(c.Request.Header.Get("X-Session-Token"))
|
||||
if err != nil || sessionResponse.StatusCode == 401 {
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//cors handling
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", viper.GetString("allowed.custom.headers"))
|
||||
@@ -98,7 +113,7 @@ func (s *Server) teamHandler() {
|
||||
houstonClient := NewHoustonClient(s.logger)
|
||||
slackClient := slackbot.NewSlackClient(s.logger, houstonClient.socketModeClient)
|
||||
teamHandler := service.NewTeamService(s.gin, s.logger, s.db, slackClient)
|
||||
|
||||
|
||||
s.gin.GET("/teams", teamHandler.GetTeams)
|
||||
s.gin.GET("/teams/:id", teamHandler.GetTeams)
|
||||
s.gin.POST("/teams", teamHandler.UpdateTeam)
|
||||
@@ -106,7 +121,7 @@ func (s *Server) teamHandler() {
|
||||
|
||||
func (s *Server) severityHandler() {
|
||||
severityHandler := service.NewSeverityService(s.gin, s.logger, s.db)
|
||||
|
||||
|
||||
s.gin.GET("/severities", severityHandler.GetSeverities)
|
||||
s.gin.POST("/severities", severityHandler.UpdateSeverities)
|
||||
}
|
||||
@@ -114,7 +129,7 @@ func (s *Server) severityHandler() {
|
||||
func (s *Server) incidentHandler() {
|
||||
houstonClient := NewHoustonClient(s.logger)
|
||||
incidentHandler := service.NewIncidentService(s.gin, s.logger, s.db, houstonClient.socketModeClient)
|
||||
|
||||
|
||||
s.gin.GET("/incidents", incidentHandler.GetIncidents)
|
||||
s.gin.GET("/incidents/:id", incidentHandler.GetIncidents)
|
||||
s.gin.POST("/incidents", incidentHandler.UpdateIncident)
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"houston/cmd/app"
|
||||
"houston/config"
|
||||
"houston/internal/clients"
|
||||
"houston/pkg/postgres"
|
||||
"os"
|
||||
"time"
|
||||
@@ -34,7 +35,11 @@ func main() {
|
||||
db := postgres.NewGormClient(viper.GetString("postgres.dsn"), viper.GetString("postgres.connection.max.idle.time"),
|
||||
viper.GetString("postgres.connection.max.lifetime"), viper.GetInt("postgres.connections.max.idle"),
|
||||
viper.GetInt("postgres.connections.max.open"), logger)
|
||||
sv := app.NewServer(r, logger, db)
|
||||
httpClient := clients.NewHttpClient()
|
||||
mjolnirClient := clients.NewMjolnirClient(httpClient.HttpClient, logger,
|
||||
viper.GetString("mjolnir.service.url"), viper.GetString("mjolnir.realm.id"),
|
||||
)
|
||||
sv := app.NewServer(r, logger, db, mjolnirClient)
|
||||
|
||||
sv.Handler()
|
||||
sv.Start()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
env=ENV
|
||||
port=PORT
|
||||
metrics.port=METRIC_POST
|
||||
metrics.port=METRICS_PORT
|
||||
|
||||
#houston token config
|
||||
houston.slack.app.token=HOUSTON_SLACK_APP_TOKEN
|
||||
@@ -23,10 +23,22 @@ cron.job.name=CRON_JOB_NAME
|
||||
incidents.show.limit=INCIDENTS_SHOW_LIMIT
|
||||
|
||||
# Prometheus Config
|
||||
prometheus.app.namePROMETHEUS_APP_NAME
|
||||
prometheus.host:PROMETHEUS_HOST
|
||||
prometheus.port:PROMETHEUS_PORT
|
||||
prometheus.enabled:PROMETHEUS_ENABLED
|
||||
prometheus.timeout:PROMETHEUS_TIMEOUT
|
||||
prometheus.flush.interval.in.ms:PROMETHEUS_FLUSH_INTERVAL_IN_MS
|
||||
prometheus.histogram.buckets:PROMETHEUS_HISTOGRAM_BUCKETS
|
||||
prometheus.app.name=PROMETHEUS_APP_NAME
|
||||
prometheus.host=PROMETHEUS_HOST
|
||||
prometheus.port=PROMETHEUS_PORT
|
||||
prometheus.enabled=PROMETHEUS_ENABLED
|
||||
prometheus.timeout=PROMETHEUS_TIMEOUT
|
||||
prometheus.flush.interval.in.ms=PROMETHEUS_FLUSH_INTERVAL_IN_MS
|
||||
prometheus.histogram.buckets=PROMETHEUS_HISTOGRAM_BUCKETS
|
||||
|
||||
#http
|
||||
http.max.idle.connection.pool=HTTP_MAX_IDLE_CONNECTION_POOL
|
||||
http.max.connection=HTTP_MAX_CONNECTION
|
||||
http.max.timeout.seconds=HTTP_MAX_TIMEOUT_SECONDS
|
||||
whitelisted.domains=WHITELISTED_DOMAINS
|
||||
allowed.custom.headers=ALLOWED_CUSTOM_HEADERS
|
||||
auth.enabled=AUTH_ENABLED
|
||||
|
||||
#client
|
||||
mjolnir.service.url=MJOLNIR_SERVICE_URL
|
||||
mjolnir.realm.id=MJOLNIR_REALM_ID
|
||||
|
||||
@@ -31,6 +31,7 @@ CREATE TABLE team
|
||||
active boolean DEFAULT false,
|
||||
confluence_link text,
|
||||
version bigint default 0,
|
||||
oncall_handle varchar(50),
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
deleted_at timestamp without time zone
|
||||
|
||||
23
internal/clients/http_config.go
Normal file
23
internal/clients/http_config.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HttpClient struct {
|
||||
HttpClient *http.Client
|
||||
}
|
||||
|
||||
func NewHttpClient() *HttpClient {
|
||||
return &HttpClient{
|
||||
HttpClient: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: viper.GetInt("http.max.idle.connection.pool"),
|
||||
MaxConnsPerHost: viper.GetInt("http.max.connection"),
|
||||
},
|
||||
Timeout: time.Duration(viper.GetInt("http.max.timeout.seconds")) * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
60
internal/clients/mjolnir.go
Normal file
60
internal/clients/mjolnir.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"houston/model/clients"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type MjolnirClient struct {
|
||||
HttpClient *http.Client
|
||||
Logger *zap.Logger
|
||||
baseUrl string
|
||||
realmId string
|
||||
}
|
||||
|
||||
func NewMjolnirClient(httpClient *http.Client, logger *zap.Logger, baseUrl, realmId string) *MjolnirClient {
|
||||
return &MjolnirClient{
|
||||
HttpClient: httpClient,
|
||||
Logger: logger,
|
||||
baseUrl: baseUrl,
|
||||
realmId: realmId,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
url = "%s/session/%s"
|
||||
)
|
||||
|
||||
func (m *MjolnirClient) GetSessionResponse(sessionToken string) (*clients.MjolnirSessionResponse, error) {
|
||||
if sessionToken == "null" {
|
||||
return nil, errors.New("unauthorized request")
|
||||
}
|
||||
client := m.HttpClient
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf(url, m.baseUrl, m.realmId), nil)
|
||||
req.Header.Add("X-Session-Token", sessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response clients.MjolnirSessionResponse
|
||||
err = json.Unmarshal(responseBody, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Logger.Info(fmt.Sprintf("%v", response))
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package action
|
||||
import (
|
||||
"fmt"
|
||||
"houston/model/incident"
|
||||
"houston/model/tag"
|
||||
"time"
|
||||
|
||||
"github.com/slack-go/slack"
|
||||
@@ -14,13 +15,15 @@ type ResolveIncidentAction struct {
|
||||
client *socketmode.Client
|
||||
logger *zap.Logger
|
||||
incidentService *incident.Repository
|
||||
tagService *tag.Repository
|
||||
}
|
||||
|
||||
func NewIncidentResolveProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository) *ResolveIncidentAction {
|
||||
func NewIncidentResolveProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository, tagService *tag.Repository) *ResolveIncidentAction {
|
||||
return &ResolveIncidentAction{
|
||||
client: client,
|
||||
logger: logger,
|
||||
incidentService: incidentService,
|
||||
tagService: tagService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,32 +38,73 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact
|
||||
|
||||
incidentStatusEntity, _ := irp.incidentService.FindIncidentStatusByName(incident.Resolved)
|
||||
|
||||
now := time.Now()
|
||||
incidentEntity.Status = incidentStatusEntity.ID
|
||||
incidentEntity.EndTime = &now
|
||||
tags, err := irp.tagService.FindTagsByTeamId(incidentEntity.TeamId)
|
||||
if err != nil || tags == nil {
|
||||
irp.logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID))
|
||||
return
|
||||
}
|
||||
var flag = true
|
||||
for _, t := range *tags {
|
||||
if t.Optional == false {
|
||||
incidentTag, err := irp.incidentService.GetIncidentTagByTagId(incidentEntity.ID, t.Id)
|
||||
if err != nil {
|
||||
irp.logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentEntity.ID))
|
||||
return
|
||||
}
|
||||
if nil == incidentTag {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
if t.Type == "free_text" {
|
||||
if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
|
||||
err = irp.incidentService.UpdateIncident(incidentEntity)
|
||||
if err != nil {
|
||||
irp.logger.Error("failed to update incident to resolve state",
|
||||
} else {
|
||||
if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if flag == true {
|
||||
now := time.Now()
|
||||
incidentEntity.Status = incidentStatusEntity.ID
|
||||
incidentEntity.EndTime = &now
|
||||
|
||||
err = irp.incidentService.UpdateIncident(incidentEntity)
|
||||
if err != nil {
|
||||
irp.logger.Error("failed to update incident to resolve state",
|
||||
zap.String("channel", channelId),
|
||||
zap.String("user_id", callback.User.ID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
irp.logger.Info("successfully resolved the incident",
|
||||
zap.String("channel", channelId),
|
||||
zap.String("user_id", callback.User.ID), zap.Error(err))
|
||||
return
|
||||
zap.String("user_id", callback.User.ID))
|
||||
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set status to %s`", callback.User.ID,
|
||||
incident.Resolved), false)
|
||||
_, _, errMessage := irp.client.PostMessage(callback.Channel.ID, msgOption)
|
||||
if errMessage != nil {
|
||||
irp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
|
||||
return
|
||||
}
|
||||
irp.client.ArchiveConversation(channelId)
|
||||
} else {
|
||||
msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false)
|
||||
_, errMessage := irp.client.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption)
|
||||
if errMessage != nil {
|
||||
irp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
irp.logger.Info("successfully resolved the incident",
|
||||
zap.String("channel", channelId),
|
||||
zap.String("user_id", callback.User.ID))
|
||||
|
||||
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set status to %s`", callback.User.ID,
|
||||
incident.Resolved), false)
|
||||
_, _, errMessage := irp.client.PostMessage(callback.Channel.ID, msgOption)
|
||||
if errMessage != nil {
|
||||
irp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
|
||||
return
|
||||
}
|
||||
|
||||
irp.client.ArchiveConversation(channelId)
|
||||
|
||||
var payload interface{}
|
||||
irp.client.Ack(*request, payload)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"houston/common/util"
|
||||
"houston/internal/processor/action/view"
|
||||
"houston/model/incident"
|
||||
"houston/model/tag"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -16,13 +17,15 @@ type UpdateIncidentAction struct {
|
||||
client *socketmode.Client
|
||||
logger *zap.Logger
|
||||
incidentService *incident.Repository
|
||||
tagService *tag.Repository
|
||||
}
|
||||
|
||||
func NewIncidentUpdateAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository) *UpdateIncidentAction {
|
||||
func NewIncidentUpdateAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository, tagService *tag.Repository) *UpdateIncidentAction {
|
||||
return &UpdateIncidentAction{
|
||||
client: client,
|
||||
logger: logger,
|
||||
incidentService: incidentService,
|
||||
tagService: tagService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +77,58 @@ func (isp *UpdateIncidentAction) IncidentUpdateStatus(callback slack.Interaction
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := isp.tagService.FindTagsByTeamId(incidentEntity.TeamId)
|
||||
if err != nil || tags == nil {
|
||||
isp.logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID))
|
||||
return
|
||||
}
|
||||
var flag = true
|
||||
for _, t := range *tags {
|
||||
if t.Optional == false {
|
||||
incidentTag, err := isp.incidentService.GetIncidentTagByTagId(incidentEntity.ID, t.Id)
|
||||
if err != nil {
|
||||
isp.logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentEntity.ID))
|
||||
return
|
||||
}
|
||||
if nil == incidentTag {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
if t.Type == "free_text" {
|
||||
if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
|
||||
} else {
|
||||
if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
incidentEntity.Status = result.ID
|
||||
incidentEntity.UpdatedBy = user.ID
|
||||
|
||||
if result.IsTerminalStatus {
|
||||
now := time.Now()
|
||||
incidentEntity.EndTime = &now
|
||||
if flag == false {
|
||||
msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false)
|
||||
_, errMessage := isp.client.PostEphemeral(callback.View.PrivateMetadata, callback.User.ID, msgOption)
|
||||
if errMessage != nil {
|
||||
isp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
|
||||
return
|
||||
}
|
||||
var payload interface{}
|
||||
isp.client.Ack(*request, payload)
|
||||
return
|
||||
} else {
|
||||
now := time.Now()
|
||||
incidentEntity.EndTime = &now
|
||||
}
|
||||
|
||||
}
|
||||
err = isp.incidentService.UpdateIncident(incidentEntity)
|
||||
if err != nil {
|
||||
|
||||
@@ -112,7 +112,7 @@ func (cip *CreateIncidentAction) InviteOnCallPersonToIncident(channelId, ts stri
|
||||
}
|
||||
|
||||
func (cip *CreateIncidentAction) createSlackChannel(incidentEntity *incident.IncidentEntity) (*string, error) {
|
||||
channelName := fmt.Sprintf("houston-%d", incidentEntity.ID)
|
||||
channelName := fmt.Sprintf("_houston-%d", incidentEntity.ID)
|
||||
channelID, err := cip.slackbotClient.CreateChannel(channelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -44,8 +44,8 @@ func NewBlockActionProcessor(logger *zap.Logger, socketModeClient *socketmode.Cl
|
||||
startIncidentBlockAction: action.NewStartIncidentBlockAction(socketModeClient, logger, teamService, severityService),
|
||||
showIncidentsAction: action.ShowIncidentsProcessor(socketModeClient, logger, incidentService),
|
||||
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, logger, incidentService),
|
||||
incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, logger, incidentService),
|
||||
incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService),
|
||||
incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, logger, incidentService, tagService),
|
||||
incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService, tagService),
|
||||
incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, logger, incidentService, teamService, severityService, slackbotClient),
|
||||
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, logger, incidentService, severityService, teamService, slackbotClient),
|
||||
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, logger, incidentService),
|
||||
@@ -176,7 +176,7 @@ func NewViewSubmissionProcessor(logger *zap.Logger, socketModeClient *socketmode
|
||||
incidentChannelMessageUpdateAction: action.NewIncidentChannelMessageUpdateAction(socketModeClient, logger, incidentService, teamService, severityService),
|
||||
createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, logger, incidentService, teamService, severityService, slackbotClient),
|
||||
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, logger, incidentService),
|
||||
updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService),
|
||||
updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService, tagService),
|
||||
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, logger, incidentService),
|
||||
incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, logger, incidentService),
|
||||
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, logger, incidentService, severityService, teamService, slackbotClient),
|
||||
|
||||
21
model/clients/response_model.go
Normal file
21
model/clients/response_model.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package clients
|
||||
|
||||
type MjolnirError struct {
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Params interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type MjolnirSessionResponse struct {
|
||||
SessionToken string `json:"sessionToken,omitempty"`
|
||||
ClientId string `json:"clientId,omitempty"`
|
||||
EmailId string `json:"emailId,omitempty"`
|
||||
AccountId string `json:"accountId,omitempty"`
|
||||
PhoneNumber string `json:"phoneNumber,omitempty"`
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
Errors []MjolnirError `json:"errors"`
|
||||
}
|
||||
Reference in New Issue
Block a user