feat: standardize API requests to use JSON instead of form data

This commit is contained in:
Abhinav Raut
2025-06-18 01:30:38 +05:30
parent f613cc237b
commit 1b2a5e4f36
14 changed files with 240 additions and 110 deletions

View File

@@ -5,6 +5,11 @@ import (
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
) )
type aiCompletionReq struct {
PromptKey string `json:"prompt_key"`
Content string `json:"content"`
}
type providerUpdateReq struct { type providerUpdateReq struct {
Provider string `json:"provider"` Provider string `json:"provider"`
APIKey string `json:"api_key"` APIKey string `json:"api_key"`
@@ -13,11 +18,15 @@ type providerUpdateReq struct {
// handleAICompletion handles AI completion requests // handleAICompletion handles AI completion requests
func handleAICompletion(r *fastglue.Request) error { func handleAICompletion(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
promptKey = string(r.RequestCtx.PostArgs().Peek("prompt_key")) req = aiCompletionReq{}
content = string(r.RequestCtx.PostArgs().Peek("content"))
) )
resp, err := app.ai.Completion(promptKey, content)
if err := r.Decode(&req, "json"); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
resp, err := app.ai.Completion(req.PromptKey, req.Content)
if err != nil { if err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }

View File

@@ -9,6 +9,10 @@ import (
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
) )
type updateAutomationRuleExecutionModeReq struct {
Mode string `json:"mode"`
}
// handleGetAutomationRules gets all automation rules // handleGetAutomationRules gets all automation rules
func handleGetAutomationRules(r *fastglue.Request) error { func handleGetAutomationRules(r *fastglue.Request) error {
var ( var (
@@ -118,14 +122,20 @@ func handleUpdateAutomationRuleWeights(r *fastglue.Request) error {
// handleUpdateAutomationRuleExecutionMode updates the execution mode of the automation rules for a given type // handleUpdateAutomationRuleExecutionMode updates the execution mode of the automation rules for a given type
func handleUpdateAutomationRuleExecutionMode(r *fastglue.Request) error { func handleUpdateAutomationRuleExecutionMode(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
mode = string(r.RequestCtx.PostArgs().Peek("mode")) req = updateAutomationRuleExecutionModeReq{}
) )
if mode != amodels.ExecutionModeAll && mode != amodels.ExecutionModeFirstMatch {
if err := r.Decode(&req, "json"); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
if req.Mode != amodels.ExecutionModeAll && req.Mode != amodels.ExecutionModeFirstMatch {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("automation.invalidRuleExecutionMode"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("automation.invalidRuleExecutionMode"), nil, envelope.InputError)
} }
// Only new conversation rules can be updated as they are the only ones that have execution mode. // Only new conversation rules can be updated as they are the only ones that have execution mode.
if err := app.automation.UpdateRuleExecutionMode(amodels.RuleTypeNewConversation, mode); err != nil { if err := app.automation.UpdateRuleExecutionMode(amodels.RuleTypeNewConversation, req.Mode); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope(true) return r.SendEnvelope(true)

View File

@@ -14,6 +14,14 @@ import (
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
) )
type createContactNoteReq struct {
Note string `json:"note"`
}
type blockContactReq struct {
Enabled bool `json:"enabled"`
}
// handleGetContacts returns a list of contacts from the database. // handleGetContacts returns a list of contacts from the database.
func handleGetContacts(r *fastglue.Request) error { func handleGetContacts(r *fastglue.Request) error {
var ( var (
@@ -185,12 +193,17 @@ func handleCreateContactNote(r *fastglue.Request) error {
app = r.Context.(*App) app = r.Context.(*App)
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
auser = r.RequestCtx.UserValue("user").(amodels.User) auser = r.RequestCtx.UserValue("user").(amodels.User)
note = string(r.RequestCtx.PostArgs().Peek("note")) req = createContactNoteReq{}
) )
if len(note) == 0 {
if err := r.Decode(&req, "json"); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
if len(req.Note) == 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "note"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "note"), nil, envelope.InputError)
} }
if err := app.user.CreateNote(contactID, auser.ID, note); err != nil { if err := app.user.CreateNote(contactID, auser.ID, req.Note); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope(true) return r.SendEnvelope(true)
@@ -238,12 +251,18 @@ func handleBlockContact(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
enabled = r.RequestCtx.PostArgs().GetBool("enabled") req = blockContactReq{}
) )
if contactID <= 0 { if contactID <= 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
} }
if err := app.user.ToggleEnabled(contactID, models.UserTypeContact, enabled); err != nil {
if err := r.Decode(&req, "json"); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
if err := app.user.ToggleEnabled(contactID, models.UserTypeContact, req.Enabled); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope(true) return r.SendEnvelope(true)

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"encoding/json"
"strconv" "strconv"
"time" "time"
@@ -19,16 +18,37 @@ import (
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
) )
type assigneeChangeReq struct {
AssigneeID int `json:"assignee_id"`
}
type teamAssigneeChangeReq struct {
AssigneeID int `json:"assignee_id"`
}
type priorityUpdateReq struct {
Priority string `json:"priority"`
}
type statusUpdateReq struct {
Status string `json:"status"`
SnoozedUntil string `json:"snoozed_until,omitempty"`
}
type tagsUpdateReq struct {
Tags []string `json:"tags"`
}
type createConversationRequest struct { type createConversationRequest struct {
InboxID int `json:"inbox_id" form:"inbox_id"` InboxID int `json:"inbox_id"`
AssignedAgentID int `json:"agent_id" form:"agent_id"` AssignedAgentID int `json:"agent_id"`
AssignedTeamID int `json:"team_id" form:"team_id"` AssignedTeamID int `json:"team_id"`
Email string `json:"contact_email" form:"contact_email"` Email string `json:"contact_email"`
FirstName string `json:"first_name" form:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name" form:"last_name"` LastName string `json:"last_name"`
Subject string `json:"subject" form:"subject"` Subject string `json:"subject"`
Content string `json:"content" form:"content"` Content string `json:"content"`
Attachments []int `json:"attachments" form:"attachments"` Attachments []int `json:"attachments"`
} }
// handleGetAllConversations retrieves all conversations. // handleGetAllConversations retrieves all conversations.
@@ -304,13 +324,15 @@ func handleGetConversationParticipants(r *fastglue.Request) error {
// handleUpdateUserAssignee updates the user assigned to a conversation. // handleUpdateUserAssignee updates the user assigned to a conversation.
func handleUpdateUserAssignee(r *fastglue.Request) error { func handleUpdateUserAssignee(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
uuid = r.RequestCtx.UserValue("uuid").(string) uuid = r.RequestCtx.UserValue("uuid").(string)
auser = r.RequestCtx.UserValue("user").(amodels.User) auser = r.RequestCtx.UserValue("user").(amodels.User)
assigneeID = r.RequestCtx.PostArgs().GetUintOrZero("assignee_id") req = assigneeChangeReq{}
) )
if assigneeID == 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`assignee_id`"), nil, envelope.InputError) if err := r.Decode(&req, "json"); err != nil {
app.lo.Error("error decoding assignee change request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
} }
user, err := app.user.GetAgent(auser.ID, "") user, err := app.user.GetAgent(auser.ID, "")
@@ -324,11 +346,11 @@ func handleUpdateUserAssignee(r *fastglue.Request) error {
} }
// Already assigned? // Already assigned?
if conversation.AssignedUserID.Int == assigneeID { if conversation.AssignedUserID.Int == req.AssigneeID {
return r.SendEnvelope(true) return r.SendEnvelope(true)
} }
if err := app.conversation.UpdateConversationUserAssignee(uuid, assigneeID, user); err != nil { if err := app.conversation.UpdateConversationUserAssignee(uuid, req.AssigneeID, user); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
@@ -341,12 +363,16 @@ func handleUpdateTeamAssignee(r *fastglue.Request) error {
app = r.Context.(*App) app = r.Context.(*App)
uuid = r.RequestCtx.UserValue("uuid").(string) uuid = r.RequestCtx.UserValue("uuid").(string)
auser = r.RequestCtx.UserValue("user").(amodels.User) auser = r.RequestCtx.UserValue("user").(amodels.User)
req = teamAssigneeChangeReq{}
) )
assigneeID, err := r.RequestCtx.PostArgs().GetUint("assignee_id")
if err != nil { if err := r.Decode(&req, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`assignee_id`"), nil, envelope.InputError) app.lo.Error("error decoding team assignee change request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
} }
assigneeID := req.AssigneeID
user, err := app.user.GetAgent(auser.ID, "") user, err := app.user.GetAgent(auser.ID, "")
if err != nil { if err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
@@ -376,11 +402,18 @@ func handleUpdateTeamAssignee(r *fastglue.Request) error {
// handleUpdateConversationPriority updates the priority of a conversation. // handleUpdateConversationPriority updates the priority of a conversation.
func handleUpdateConversationPriority(r *fastglue.Request) error { func handleUpdateConversationPriority(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
uuid = r.RequestCtx.UserValue("uuid").(string) uuid = r.RequestCtx.UserValue("uuid").(string)
auser = r.RequestCtx.UserValue("user").(amodels.User) auser = r.RequestCtx.UserValue("user").(amodels.User)
priority = string(r.RequestCtx.PostArgs().Peek("priority")) req = priorityUpdateReq{}
) )
if err := r.Decode(&req, "json"); err != nil {
app.lo.Error("error decoding priority update request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
}
priority := req.Priority
if priority == "" { if priority == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`priority`"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`priority`"), nil, envelope.InputError)
} }
@@ -403,13 +436,20 @@ func handleUpdateConversationPriority(r *fastglue.Request) error {
// handleUpdateConversationStatus updates the status of a conversation. // handleUpdateConversationStatus updates the status of a conversation.
func handleUpdateConversationStatus(r *fastglue.Request) error { func handleUpdateConversationStatus(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
status = string(r.RequestCtx.PostArgs().Peek("status")) uuid = r.RequestCtx.UserValue("uuid").(string)
snoozedUntil = string(r.RequestCtx.PostArgs().Peek("snoozed_until")) auser = r.RequestCtx.UserValue("user").(amodels.User)
uuid = r.RequestCtx.UserValue("uuid").(string) req = statusUpdateReq{}
auser = r.RequestCtx.UserValue("user").(amodels.User)
) )
if err := r.Decode(&req, "json"); err != nil {
app.lo.Error("error decoding status update request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
}
status := req.Status
snoozedUntil := req.SnoozedUntil
// Validate inputs // Validate inputs
if status == "" { if status == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`status`"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`status`"), nil, envelope.InputError)
@@ -463,18 +503,19 @@ func handleUpdateConversationStatus(r *fastglue.Request) error {
// handleUpdateConversationtags updates conversation tags. // handleUpdateConversationtags updates conversation tags.
func handleUpdateConversationtags(r *fastglue.Request) error { func handleUpdateConversationtags(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
tagNames = []string{} auser = r.RequestCtx.UserValue("user").(amodels.User)
tagJSON = r.RequestCtx.PostArgs().Peek("tags") uuid = r.RequestCtx.UserValue("uuid").(string)
auser = r.RequestCtx.UserValue("user").(amodels.User) req = tagsUpdateReq{}
uuid = r.RequestCtx.UserValue("uuid").(string)
) )
if err := json.Unmarshal(tagJSON, &tagNames); err != nil { if err := r.Decode(&req, "json"); err != nil {
app.lo.Error("error unmarshalling tags JSON", "error", err) app.lo.Error("error decoding tags update request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
} }
tagNames := req.Tags
user, err := app.user.GetAgent(auser.ID, "") user, err := app.user.GetAgent(auser.ID, "")
if err != nil { if err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)

View File

@@ -15,7 +15,7 @@ import (
// initHandlers initializes the HTTP routes and handlers for the application. // initHandlers initializes the HTTP routes and handlers for the application.
func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
// Authentication. // Authentication.
g.POST("/api/v1/login", handleLogin) g.POST("/api/v1/auth/login", handleLogin)
g.GET("/logout", auth(handleLogout)) g.GET("/logout", auth(handleLogout))
g.GET("/api/v1/oidc/{id}/login", handleOIDCLogin) g.GET("/api/v1/oidc/{id}/login", handleOIDCLogin)
g.GET("/api/v1/oidc/{id}/finish", handleOIDCCallback) g.GET("/api/v1/oidc/{id}/finish", handleOIDCCallback)

View File

@@ -14,7 +14,7 @@ import (
// authenticateUser handles both API key and session-based authentication // authenticateUser handles both API key and session-based authentication
// Returns the authenticated user or an error // Returns the authenticated user or an error
func authenticateUser(r *fastglue.Request, app *App) (models.User, error) { func authenticateUser(r *fastglue.Request, app *App, checkCsrf bool) (models.User, error) {
var user models.User var user models.User
// Check for Authorization header first (API key authentication) // Check for Authorization header first (API key authentication)
@@ -29,13 +29,15 @@ func authenticateUser(r *fastglue.Request, app *App) (models.User, error) {
} }
// Session-based authentication // Session-based authentication
cookieToken := string(r.RequestCtx.Request.Header.Cookie("csrf_token")) if checkCsrf {
hdrToken := string(r.RequestCtx.Request.Header.Peek("X-CSRFTOKEN")) cookieToken := string(r.RequestCtx.Request.Header.Cookie("csrf_token"))
hdrToken := string(r.RequestCtx.Request.Header.Peek("X-CSRFTOKEN"))
// Match CSRF token from cookie and header. // Match CSRF token from cookie and header.
if cookieToken == "" || hdrToken == "" || cookieToken != hdrToken { if cookieToken == "" || hdrToken == "" || cookieToken != hdrToken {
app.lo.Error("csrf token mismatch", "cookie_token", cookieToken, "header_token", hdrToken) app.lo.Error("csrf token mismatch", "cookie_token", cookieToken, "header_token", hdrToken)
return user, envelope.NewError(envelope.PermissionError, app.i18n.T("auth.csrfTokenMismatch"), nil) return user, envelope.NewError(envelope.PermissionError, app.i18n.T("auth.csrfTokenMismatch"), nil)
}
} }
// Validate session and fetch user. // Validate session and fetch user.
@@ -70,7 +72,7 @@ func tryAuth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
app := r.Context.(*App) app := r.Context.(*App)
// Try to authenticate user using shared authentication logic, but don't return errors // Try to authenticate user using shared authentication logic, but don't return errors
user, err := authenticateUser(r, app) user, err := authenticateUser(r, app, true)
if err != nil { if err != nil {
// Authentication failed, but this is optional, so continue without user // Authentication failed, but this is optional, so continue without user
return handler(r) return handler(r)
@@ -95,7 +97,7 @@ func auth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
var app = r.Context.(*App) var app = r.Context.(*App)
// Authenticate user using shared authentication logic // Authenticate user using shared authentication logic
user, err := authenticateUser(r, app) user, err := authenticateUser(r, app, false)
if err != nil { if err != nil {
if envErr, ok := err.(envelope.Error); ok { if envErr, ok := err.(envelope.Error); ok {
if envErr.ErrorType == envelope.PermissionError { if envErr.ErrorType == envelope.PermissionError {
@@ -125,7 +127,7 @@ func perm(handler fastglue.FastRequestHandler, perm string) fastglue.FastRequest
var app = r.Context.(*App) var app = r.Context.(*App)
// Authenticate user using shared authentication logic // Authenticate user using shared authentication logic
user, err := authenticateUser(r, app) user, err := authenticateUser(r, app, true)
if err != nil { if err != nil {
if envErr, ok := err.(envelope.Error); ok { if envErr, ok := err.(envelope.Error); ok {
if envErr.ErrorType == envelope.PermissionError { if envErr.ErrorType == envelope.PermissionError {

View File

@@ -4,8 +4,8 @@ import (
"strconv" "strconv"
"github.com/abhinavxd/libredesk/internal/envelope" "github.com/abhinavxd/libredesk/internal/envelope"
"github.com/abhinavxd/libredesk/internal/team/models"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/volatiletech/null/v9"
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
) )
@@ -52,16 +52,15 @@ func handleGetTeam(r *fastglue.Request) error {
// handleCreateTeam creates a new team. // handleCreateTeam creates a new team.
func handleCreateTeam(r *fastglue.Request) error { func handleCreateTeam(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
name = string(r.RequestCtx.PostArgs().Peek("name")) req = models.Team{}
timezone = string(r.RequestCtx.PostArgs().Peek("timezone"))
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id")))
slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id")))
maxAutoAssignedConversations, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("max_auto_assigned_conversations")))
) )
if err := app.team.Create(name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji, maxAutoAssignedConversations); err != nil {
if err := r.Decode(&req, "json"); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
if err := app.team.Create(req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope(true) return r.SendEnvelope(true)
@@ -70,20 +69,20 @@ func handleCreateTeam(r *fastglue.Request) error {
// handleUpdateTeam updates an existing team. // handleUpdateTeam updates an existing team.
func handleUpdateTeam(r *fastglue.Request) error { func handleUpdateTeam(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
name = string(r.RequestCtx.PostArgs().Peek("name")) id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
timezone = string(r.RequestCtx.PostArgs().Peek("timezone")) req = models.Team{}
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id")))
slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id")))
maxAutoAssignedConversations, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("max_auto_assigned_conversations")))
) )
if id < 1 { if id < 1 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid team `id`", nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
} }
if err := app.team.Update(id, name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji, maxAutoAssignedConversations); err != nil {
if err := r.Decode(&req, "json"); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
if err := app.team.Update(id, req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope(true) return r.SendEnvelope(true)

View File

@@ -434,20 +434,22 @@ func handleSetPassword(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
agent, ok = r.RequestCtx.UserValue("user").(amodels.User) agent, ok = r.RequestCtx.UserValue("user").(amodels.User)
p = r.RequestCtx.PostArgs() req = SetPasswordRequest{}
password = string(p.Peek("password"))
token = string(p.Peek("token"))
) )
if ok && agent.ID > 0 { if ok && agent.ID > 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userAlreadyLoggedIn"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userAlreadyLoggedIn"), nil, envelope.InputError)
} }
if password == "" { if err := r.Decode(&req, "json"); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
if req.Password == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "{globals.terms.password}"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "{globals.terms.password}"), nil, envelope.InputError)
} }
if err := app.user.ResetPassword(token, password); err != nil { if err := app.user.ResetPassword(req.Token, req.Password); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }

View File

@@ -38,7 +38,7 @@ describe('Login Component', () => {
it('should show error for invalid login attempt', () => { it('should show error for invalid login attempt', () => {
// Mock failed login API call // Mock failed login API call
cy.intercept('POST', '**/api/v1/login', { cy.intercept('POST', '**/api/v1/auth/login', {
statusCode: 401, statusCode: 401,
body: { body: {
message: 'Invalid credentials' message: 'Invalid credentials'
@@ -61,7 +61,7 @@ describe('Login Component', () => {
it('should login successfully with correct credentials', () => { it('should login successfully with correct credentials', () => {
// Mock successful login API call // Mock successful login API call
cy.intercept('POST', '**/api/v1/login', { cy.intercept('POST', '**/api/v1/auth/login', {
statusCode: 200, statusCode: 200,
body: { body: {
data: { data: {
@@ -111,7 +111,7 @@ describe('Login Component', () => {
it('should show loading state during login', () => { it('should show loading state during login', () => {
// Mock slow API response // Mock slow API response
cy.intercept('POST', '**/api/v1/login', { cy.intercept('POST', '**/api/v1/auth/login', {
statusCode: 200, statusCode: 200,
body: { body: {
data: { data: {

View File

@@ -27,9 +27,13 @@ http.interceptors.request.use((request) => {
// Set content type for POST/PUT requests if the content type is not set. // Set content type for POST/PUT requests if the content type is not set.
if ((request.method === 'post' || request.method === 'put') && !request.headers['Content-Type']) { if ((request.method === 'post' || request.method === 'put') && !request.headers['Content-Type']) {
request.headers['Content-Type'] = 'application/x-www-form-urlencoded' request.headers['Content-Type'] = 'application/json'
}
if (request.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
request.data = qs.stringify(request.data) request.data = qs.stringify(request.data)
} }
return request return request
}) })
@@ -135,7 +139,7 @@ const updateSettings = (key, data) =>
} }
}) })
const getSettings = (key) => http.get(`/api/v1/settings/${key}`) const getSettings = (key) => http.get(`/api/v1/settings/${key}`)
const login = (data) => http.post(`/api/v1/login`, data, { const login = (data) => http.post(`/api/v1/auth/login`, data, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
@@ -166,7 +170,11 @@ const updateAutomationRuleWeights = (data) =>
} }
}) })
const updateAutomationRulesExecutionMode = (data) => const updateAutomationRulesExecutionMode = (data) =>
http.put(`/api/v1/automations/rules/execution-mode`, data) http.put(`/api/v1/automations/rules/execution-mode`, data, {
headers: {
'Content-Type': 'application/json'
}
})
const getRoles = () => http.get('/api/v1/roles') const getRoles = () => http.get('/api/v1/roles')
const getRole = (id) => http.get(`/api/v1/roles/${id}`) const getRole = (id) => http.get(`/api/v1/roles/${id}`)
const createRole = (data) => const createRole = (data) =>
@@ -190,11 +198,23 @@ const updateContact = (id, data) =>
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} }
}) })
const blockContact = (id, data) => http.put(`/api/v1/contacts/${id}/block`, data) const blockContact = (id, data) => http.put(`/api/v1/contacts/${id}/block`, data, {
headers: {
'Content-Type': 'application/json'
}
})
const getTeam = (id) => http.get(`/api/v1/teams/${id}`) const getTeam = (id) => http.get(`/api/v1/teams/${id}`)
const getTeams = () => http.get('/api/v1/teams') const getTeams = () => http.get('/api/v1/teams')
const updateTeam = (id, data) => http.put(`/api/v1/teams/${id}`, data) const updateTeam = (id, data) => http.put(`/api/v1/teams/${id}`, data, {
const createTeam = (data) => http.post('/api/v1/teams', data) headers: {
'Content-Type': 'application/json'
}
})
const createTeam = (data) => http.post('/api/v1/teams', data, {
headers: {
'Content-Type': 'application/json'
}
})
const getTeamsCompact = () => http.get('/api/v1/teams/compact') const getTeamsCompact = () => http.get('/api/v1/teams/compact')
const deleteTeam = (id) => http.delete(`/api/v1/teams/${id}`) const deleteTeam = (id) => http.delete(`/api/v1/teams/${id}`)
const updateUser = (id, data) => const updateUser = (id, data) =>
@@ -238,9 +258,17 @@ const createUser = (data) =>
} }
}) })
const getTags = () => http.get('/api/v1/tags') const getTags = () => http.get('/api/v1/tags')
const upsertTags = (uuid, data) => http.post(`/api/v1/conversations/${uuid}/tags`, data) const upsertTags = (uuid, data) => http.post(`/api/v1/conversations/${uuid}/tags`, data, {
headers: {
'Content-Type': 'application/json'
}
})
const updateAssignee = (uuid, assignee_type, data) => const updateAssignee = (uuid, assignee_type, data) =>
http.put(`/api/v1/conversations/${uuid}/assignee/${assignee_type}`, data) http.put(`/api/v1/conversations/${uuid}/assignee/${assignee_type}`, data, {
headers: {
'Content-Type': 'application/json'
}
})
const removeAssignee = (uuid, assignee_type) => const removeAssignee = (uuid, assignee_type) =>
http.put(`/api/v1/conversations/${uuid}/assignee/${assignee_type}/remove`) http.put(`/api/v1/conversations/${uuid}/assignee/${assignee_type}/remove`)
const updateContactCustomAttribute = (uuid, data) => const updateContactCustomAttribute = (uuid, data) =>
@@ -262,9 +290,17 @@ const createConversation = (data) =>
} }
}) })
const updateConversationStatus = (uuid, data) => const updateConversationStatus = (uuid, data) =>
http.put(`/api/v1/conversations/${uuid}/status`, data) http.put(`/api/v1/conversations/${uuid}/status`, data, {
headers: {
'Content-Type': 'application/json'
}
})
const updateConversationPriority = (uuid, data) => const updateConversationPriority = (uuid, data) =>
http.put(`/api/v1/conversations/${uuid}/priority`, data) http.put(`/api/v1/conversations/${uuid}/priority`, data, {
headers: {
'Content-Type': 'application/json'
}
})
const updateAssigneeLastSeen = (uuid) => http.put(`/api/v1/conversations/${uuid}/last-seen`) const updateAssigneeLastSeen = (uuid) => http.put(`/api/v1/conversations/${uuid}/last-seen`)
const getConversationMessage = (cuuid, uuid) => const getConversationMessage = (cuuid, uuid) =>
http.get(`/api/v1/conversations/${cuuid}/messages/${uuid}`) http.get(`/api/v1/conversations/${cuuid}/messages/${uuid}`)
@@ -350,10 +386,22 @@ const updateView = (id, data) =>
}) })
const deleteView = (id) => http.delete(`/api/v1/views/me/${id}`) const deleteView = (id) => http.delete(`/api/v1/views/me/${id}`)
const getAiPrompts = () => http.get('/api/v1/ai/prompts') const getAiPrompts = () => http.get('/api/v1/ai/prompts')
const aiCompletion = (data) => http.post('/api/v1/ai/completion', data) const aiCompletion = (data) => http.post('/api/v1/ai/completion', data, {
const updateAIProvider = (data) => http.put('/api/v1/ai/provider', data) headers: {
'Content-Type': 'application/json'
}
})
const updateAIProvider = (data) => http.put('/api/v1/ai/provider', data, {
headers: {
'Content-Type': 'application/json'
}
})
const getContactNotes = (id) => http.get(`/api/v1/contacts/${id}/notes`) const getContactNotes = (id) => http.get(`/api/v1/contacts/${id}/notes`)
const createContactNote = (id, data) => http.post(`/api/v1/contacts/${id}/notes`, data) const createContactNote = (id, data) => http.post(`/api/v1/contacts/${id}/notes`, data, {
headers: {
'Content-Type': 'application/json'
}
})
const deleteContactNote = (id, noteId) => http.delete(`/api/v1/contacts/${id}/notes/${noteId}`) const deleteContactNote = (id, noteId) => http.delete(`/api/v1/contacts/${id}/notes/${noteId}`)
const getActivityLogs = (params) => http.get('/api/v1/activity-logs', { params }) const getActivityLogs = (params) => http.get('/api/v1/activity-logs', { params })
const getWebhooks = () => http.get('/api/v1/webhooks') const getWebhooks = () => http.get('/api/v1/webhooks')

View File

@@ -162,7 +162,7 @@ watch(
} }
conversationStore.upsertTags({ conversationStore.upsertTags({
tags: JSON.stringify(newTags) tags: newTags
}) })
}, },
{ immediate: false } { immediate: false }
@@ -184,13 +184,13 @@ const fetchTags = async () => {
const handleAssignedUserChange = (id) => { const handleAssignedUserChange = (id) => {
conversationStore.updateAssignee('user', { conversationStore.updateAssignee('user', {
assignee_id: id assignee_id: parseInt(id)
}) })
} }
const handleAssignedTeamChange = (id) => { const handleAssignedTeamChange = (id) => {
conversationStore.updateAssignee('team', { conversationStore.updateAssignee('team', {
assignee_id: id assignee_id: parseInt(id)
}) })
} }

View File

@@ -23,5 +23,5 @@ type ActivityLog struct {
TargetModelID int `db:"target_model_id" json:"target_model_id"` TargetModelID int `db:"target_model_id" json:"target_model_id"`
IP string `db:"ip" json:"ip"` IP string `db:"ip" json:"ip"`
Total int `db:"total" json:"total"` Total int `db:"total" json:"-"`
} }

View File

@@ -1,5 +1,5 @@
-- name: get-all -- name: get-all
SELECT id, name, description FROM roles; SELECT id, created_at, updated_at, name, description, permissions FROM roles;
-- name: get-role -- name: get-role
SELECT * FROM roles where id = $1; SELECT * FROM roles where id = $1;

View File

@@ -23,7 +23,7 @@ WHERE id != $1 AND $4 = TRUE;
SELECT id, type, name, body, subject FROM templates WHERE is_default is TRUE; SELECT id, type, name, body, subject FROM templates WHERE is_default is TRUE;
-- name: get-all -- name: get-all
SELECT id, type, name, is_default, updated_at FROM templates WHERE type = $1 ORDER BY updated_at DESC; SELECT id, created_at, updated_at, type, name, is_default, is_builtin FROM templates WHERE type = $1 ORDER BY updated_at DESC;
-- name: get-template -- name: get-template
SELECT id, type, name, body, subject, is_default, type FROM templates WHERE id = $1; SELECT id, type, name, body, subject, is_default, type FROM templates WHERE id = $1;