diff --git a/cmd/contacts.go b/cmd/contacts.go
index 7c9f47b..d8ca8e6 100644
--- a/cmd/contacts.go
+++ b/cmd/contacts.go
@@ -168,7 +168,13 @@ func handleUpdateContact(r *fastglue.Request) error {
return sendErrorEnvelope(r, err)
}
}
- return r.SendEnvelope(true)
+
+ // Refetch contact and return it
+ contact, err = app.user.GetContact(id, "")
+ if err != nil {
+ return sendErrorEnvelope(r, err)
+ }
+ return r.SendEnvelope(contact)
}
// handleGetContactNotes returns all notes for a contact.
@@ -195,18 +201,17 @@ func handleCreateContactNote(r *fastglue.Request) error {
auser = r.RequestCtx.UserValue("user").(amodels.User)
req = createContactNoteReq{}
)
-
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)
}
- if err := app.user.CreateNote(contactID, auser.ID, req.Note); err != nil {
+ n, err := app.user.CreateNote(contactID, auser.ID, req.Note)
+ if err != nil {
return sendErrorEnvelope(r, err)
}
- return r.SendEnvelope(true)
+ return r.SendEnvelope(n)
}
// handleDeleteContactNote deletes a note for a contact.
@@ -240,6 +245,8 @@ func handleDeleteContactNote(r *fastglue.Request) error {
}
}
+ app.lo.Info("deleting contact note", "note_id", noteID, "contact_id", contactID, "actor_id", auser.ID)
+
if err := app.user.DeleteNote(noteID, contactID); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -251,6 +258,7 @@ func handleBlockContact(r *fastglue.Request) error {
var (
app = r.Context.(*App)
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
+ auser = r.RequestCtx.UserValue("user").(amodels.User)
req = blockContactReq{}
)
@@ -262,8 +270,15 @@ func handleBlockContact(r *fastglue.Request) error {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
+ app.lo.Info("setting contact block status", "contact_id", contactID, "enabled", req.Enabled, "actor_id", auser.ID)
+
if err := app.user.ToggleEnabled(contactID, models.UserTypeContact, req.Enabled); err != nil {
return sendErrorEnvelope(r, err)
}
- return r.SendEnvelope(true)
+
+ contact, err := app.user.GetContact(contactID, "")
+ if err != nil {
+ return sendErrorEnvelope(r, err)
+ }
+ return r.SendEnvelope(contact)
}
diff --git a/cmd/conversation.go b/cmd/conversation.go
index 2962efa..f6ac22d 100644
--- a/cmd/conversation.go
+++ b/cmd/conversation.go
@@ -49,6 +49,7 @@ type createConversationRequest struct {
Subject string `json:"subject"`
Content string `json:"content"`
Attachments []int `json:"attachments"`
+ Initiator string `json:"initiator"` // "contact" | "agent"
}
// handleGetAllConversations retrieves all conversations.
@@ -672,39 +673,17 @@ func handleCreateConversation(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
}
+ // Validate the request
+ if err := validateCreateConversationRequest(req, app); err != nil {
+ return sendErrorEnvelope(r, err)
+ }
+
to := []string{req.Email}
-
- // Validate required fields
- if req.InboxID <= 0 {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`inbox_id`"), nil, envelope.InputError)
- }
- if req.Content == "" {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`content`"), nil, envelope.InputError)
- }
- if req.Email == "" {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`contact_email`"), nil, envelope.InputError)
- }
- if req.FirstName == "" {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`first_name`"), nil, envelope.InputError)
- }
- if !stringutil.ValidEmail(req.Email) {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`contact_email`"), nil, envelope.InputError)
- }
-
user, err := app.user.GetAgent(auser.ID, "")
if err != nil {
return sendErrorEnvelope(r, err)
}
- // Check if inbox exists and is enabled.
- inbox, err := app.inbox.GetDBRecord(req.InboxID)
- if err != nil {
- return sendErrorEnvelope(r, err)
- }
- if !inbox.Enabled {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "inbox"), nil, envelope.InputError)
- }
-
// Find or create contact.
contact := umodels.User{
Email: null.StringFrom(req.Email),
@@ -717,7 +696,7 @@ func handleCreateConversation(r *fastglue.Request) error {
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.contact}"), nil))
}
- // Create conversation
+ // Create conversation first.
conversationID, conversationUUID, err := app.conversation.CreateConversation(
contact.ID,
contact.ContactChannelID,
@@ -725,14 +704,14 @@ func handleCreateConversation(r *fastglue.Request) error {
"", /** last_message **/
time.Now(), /** last_message_at **/
req.Subject,
- true, /** append reference number to subject **/
+ true, /** append reference number to subject? **/
)
if err != nil {
app.lo.Error("error creating conversation", "error", err)
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.conversation}"), nil))
}
- // Prepare attachments.
+ // Get media for the attachment ids.
var media = make([]medModels.Media, 0, len(req.Attachments))
for _, id := range req.Attachments {
m, err := app.media.Get(id, "")
@@ -743,13 +722,29 @@ func handleCreateConversation(r *fastglue.Request) error {
media = append(media, m)
}
- // Send reply to the created conversation.
- if _, err := app.conversation.SendReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
- // Delete the conversation if reply fails.
- if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
- app.lo.Error("error deleting conversation", "error", err)
+ // Handle sending initial message based on initiator using switch-case.
+ switch req.Initiator {
+ case umodels.UserTypeAgent:
+ // Queue reply.
+ if _, err := app.conversation.QueueReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
+ // Delete the conversation if msg queue fails.
+ if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
+ app.lo.Error("error deleting conversation", "error", err)
+ }
+ return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil))
}
- return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil))
+ case umodels.UserTypeContact:
+ // Create message on behalf of contact.
+ if _, err := app.conversation.CreateContactMessage(media, contact.ID, conversationUUID, req.Content, cmodels.ContentTypeHTML); err != nil {
+ // Delete the conversation if message creation fails.
+ if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
+ app.lo.Error("error deleting conversation", "error", err)
+ }
+ return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil))
+ }
+ default:
+ // Guard anyway.
+ return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`initiator`"), nil, envelope.InputError)
}
// Assign the conversation to the agent or team.
@@ -768,3 +763,36 @@ func handleCreateConversation(r *fastglue.Request) error {
return r.SendEnvelope(conversation)
}
+
+// validateCreateConversationRequest validates the create conversation request fields.
+func validateCreateConversationRequest(req createConversationRequest, app *App) error {
+ if req.InboxID <= 0 {
+ return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`inbox_id`"), nil)
+ }
+ if req.Content == "" {
+ return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`content`"), nil)
+ }
+ if req.Email == "" {
+ return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`contact_email`"), nil)
+ }
+ if req.FirstName == "" {
+ return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`first_name`"), nil)
+ }
+ if !stringutil.ValidEmail(req.Email) {
+ return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`contact_email`"), nil)
+ }
+ if req.Initiator != umodels.UserTypeContact && req.Initiator != umodels.UserTypeAgent {
+ return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`initiator`"), nil)
+ }
+
+ // Check if inbox exists and is enabled.
+ inbox, err := app.inbox.GetDBRecord(req.InboxID)
+ if err != nil {
+ return err
+ }
+ if !inbox.Enabled {
+ return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.disabled", "name", "inbox"), nil)
+ }
+
+ return nil
+}
diff --git a/cmd/messages.go b/cmd/messages.go
index 7ce1633..ecdddc4 100644
--- a/cmd/messages.go
+++ b/cmd/messages.go
@@ -99,7 +99,7 @@ func handleGetMessage(r *fastglue.Request) error {
return r.SendEnvelope(message)
}
-// handleRetryMessage changes message status so it can be retried for sending.
+// handleRetryMessage changes message status to `pending`, so it's enqueued for sending.
func handleRetryMessage(r *fastglue.Request) error {
var (
app = r.Context.(*App)
@@ -168,7 +168,7 @@ func handleSendMessage(r *fastglue.Request) error {
}
return r.SendEnvelope(message)
}
- message, err := app.conversation.SendReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/)
+ message, err := app.conversation.QueueReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/)
if err != nil {
return sendErrorEnvelope(r, err)
}
diff --git a/cmd/oidc.go b/cmd/oidc.go
index 8c6c048..13fa252 100644
--- a/cmd/oidc.go
+++ b/cmd/oidc.go
@@ -11,7 +11,7 @@ import (
"github.com/zerodha/fastglue"
)
-// handleGetAllEnabledOIDC returns all enabled OIDC records
+// handleGetAllEnabledOIDC returns all enabled OIDC records i.e. all OIDC configurable available for login with client secret stripped.
func handleGetAllEnabledOIDC(r *fastglue.Request) error {
app := r.Context.(*App)
out, err := app.oidc.GetAllEnabled()
@@ -74,10 +74,10 @@ func handleCreateOIDC(r *fastglue.Request) error {
if err := reloadAuth(app); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError)
}
-
+
// Clear client secret before returning
createdOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10)
-
+
return r.SendEnvelope(createdOIDC)
}
@@ -110,10 +110,10 @@ func handleUpdateOIDC(r *fastglue.Request) error {
if err := reloadAuth(app); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError)
}
-
+
// Clear client secret before returning
updatedOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10)
-
+
return r.SendEnvelope(updatedOIDC)
}
diff --git a/cmd/teams.go b/cmd/teams.go
index 5d4046b..127e185 100644
--- a/cmd/teams.go
+++ b/cmd/teams.go
@@ -83,7 +83,7 @@ func handleUpdateTeam(r *fastglue.Request) error {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
}
- updatedTeam, err := app.team.Update(id, req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations);
+ updatedTeam, err := app.team.Update(id, req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations)
if err != nil {
return sendErrorEnvelope(r, err)
}
diff --git a/cmd/users.go b/cmd/users.go
index d62d899..8afebfe 100644
--- a/cmd/users.go
+++ b/cmd/users.go
@@ -26,34 +26,38 @@ const (
maxAvatarSizeMB = 2
)
-// Request structs for user-related endpoints
-
-// UpdateAvailabilityRequest represents the request to update user availability
-type UpdateAvailabilityRequest struct {
+type updateAvailabilityRequest struct {
Status string `json:"status"`
}
-// ResetPasswordRequest represents the password reset request
-type ResetPasswordRequest struct {
+type resetPasswordRequest struct {
Email string `json:"email"`
}
-// SetPasswordRequest represents the set password request
-type SetPasswordRequest struct {
+type setPasswordRequest struct {
Token string `json:"token"`
Password string `json:"password"`
}
-// AvailabilityRequest represents the request to update agent availability
-type AvailabilityRequest struct {
+type availabilityRequest struct {
Status string `json:"status"`
}
+type agentReq struct {
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Email string `json:"email"`
+ SendWelcomeEmail bool `json:"send_welcome_email"`
+ Teams []string `json:"teams"`
+ Roles []string `json:"roles"`
+ Enabled bool `json:"enabled"`
+ AvailabilityStatus string `json:"availability_status"`
+ NewPassword string `json:"new_password,omitempty"`
+}
+
// handleGetAgents returns all agents.
func handleGetAgents(r *fastglue.Request) error {
- var (
- app = r.Context.(*App)
- )
+ var app = r.Context.(*App)
agents, err := app.user.GetAgents()
if err != nil {
return sendErrorEnvelope(r, err)
@@ -73,9 +77,7 @@ func handleGetAgentsCompact(r *fastglue.Request) error {
// handleGetAgent returns an agent.
func handleGetAgent(r *fastglue.Request) error {
- var (
- app = r.Context.(*App)
- )
+ var app = r.Context.(*App)
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
if err != nil || id <= 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
@@ -93,7 +95,7 @@ func handleUpdateAgentAvailability(r *fastglue.Request) error {
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
ip = realip.FromRequest(r.RequestCtx)
- availReq AvailabilityRequest
+ availReq availabilityRequest
)
// Decode JSON request
@@ -101,6 +103,7 @@ func handleUpdateAgentAvailability(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
}
+ // Fetch entire agent
agent, err := app.user.GetAgent(auser.ID, "")
if err != nil {
return sendErrorEnvelope(r, err)
@@ -108,10 +111,10 @@ func handleUpdateAgentAvailability(r *fastglue.Request) error {
// Same status?
if agent.AvailabilityStatus == availReq.Status {
- return r.SendEnvelope(true)
+ return r.SendEnvelope(agent)
}
- // Update availability status.
+ // Update availability status
if err := app.user.UpdateAvailability(auser.ID, availReq.Status); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -123,21 +126,22 @@ func handleUpdateAgentAvailability(r *fastglue.Request) error {
}
}
- return r.SendEnvelope(true)
+ // Fetch updated agent and return
+ agent, err = app.user.GetAgent(auser.ID, "")
+ if err != nil {
+ return sendErrorEnvelope(r, err)
+ }
+
+ return r.SendEnvelope(agent)
}
-// handleGetCurrentAgentTeams returns the teams of an agent.
+// handleGetCurrentAgentTeams returns the teams of current agent.
func handleGetCurrentAgentTeams(r *fastglue.Request) error {
var (
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
)
- agent, err := app.user.GetAgent(auser.ID, "")
- if err != nil {
- return sendErrorEnvelope(r, err)
- }
-
- teams, err := app.team.GetUserTeams(agent.ID)
+ teams, err := app.team.GetUserTeams(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
@@ -150,11 +154,6 @@ func handleUpdateCurrentAgent(r *fastglue.Request) error {
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
)
- agent, err := app.user.GetAgent(auser.ID, "")
- if err != nil {
- return sendErrorEnvelope(r, err)
- }
-
form, err := r.RequestCtx.MultipartForm()
if err != nil {
app.lo.Error("error parsing form data", "error", err)
@@ -165,54 +164,53 @@ func handleUpdateCurrentAgent(r *fastglue.Request) error {
// Upload avatar?
if ok && len(files) > 0 {
+ agent, err := app.user.GetAgent(auser.ID, "")
+ if err != nil {
+ return sendErrorEnvelope(r, err)
+ }
if err := uploadUserAvatar(r, &agent, files); err != nil {
return sendErrorEnvelope(r, err)
}
}
- return r.SendEnvelope(true)
+
+ // Fetch updated agent and return.
+ agent, err := app.user.GetAgent(auser.ID, "")
+ if err != nil {
+ return sendErrorEnvelope(r, err)
+ }
+
+ return r.SendEnvelope(agent)
}
// handleCreateAgent creates a new agent.
func handleCreateAgent(r *fastglue.Request) error {
var (
- app = r.Context.(*App)
- user = models.User{}
+ app = r.Context.(*App)
+ req = agentReq{}
)
- if err := r.Decode(&user, "json"); err != nil {
+
+ if err := r.Decode(&req, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
}
- if user.Email.String == "" {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil, envelope.InputError)
- }
- user.Email = null.StringFrom(strings.TrimSpace(strings.ToLower(user.Email.String)))
-
- if !stringutil.ValidEmail(user.Email.String) {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`email`"), nil, envelope.InputError)
+ // Validate agent request
+ if err := validateAgentRequest(r, &req); err != nil {
+ return err
}
- if user.Roles == nil {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`role`"), nil, envelope.InputError)
- }
-
- if user.FirstName == "" {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`first_name`"), nil, envelope.InputError)
- }
-
- if err := app.user.CreateAgent(&user); err != nil {
+ agent, err := app.user.CreateAgent(req.FirstName, req.LastName, req.Email, req.Roles)
+ if err != nil {
return sendErrorEnvelope(r, err)
}
// Upsert user teams.
- if len(user.Teams) > 0 {
- if err := app.team.UpsertUserTeams(user.ID, user.Teams.Names()); err != nil {
- return sendErrorEnvelope(r, err)
- }
+ if len(req.Teams) > 0 {
+ app.team.UpsertUserTeams(agent.ID, req.Teams)
}
- if user.SendWelcomeEmail {
+ if req.SendWelcomeEmail {
// Generate reset token.
- resetToken, err := app.user.SetResetPasswordToken(user.ID)
+ resetToken, err := app.user.SetResetPasswordToken(agent.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
@@ -220,31 +218,36 @@ func handleCreateAgent(r *fastglue.Request) error {
// Render template and send email.
content, err := app.tmpl.RenderInMemoryTemplate(tmpl.TmplWelcome, map[string]any{
"ResetToken": resetToken,
- "Email": user.Email.String,
+ "Email": req.Email,
})
if err != nil {
app.lo.Error("error rendering template", "error", err)
- return r.SendEnvelope(true)
}
if err := app.notifier.Send(notifier.Message{
- RecipientEmails: []string{user.Email.String},
- Subject: "Welcome to Libredesk",
+ RecipientEmails: []string{req.Email},
+ Subject: app.i18n.T("globals.messages.welcomeToLibredesk"),
Content: content,
Provider: notifier.ProviderEmail,
}); err != nil {
app.lo.Error("error sending notification message", "error", err)
- return r.SendEnvelope(true)
}
}
- return r.SendEnvelope(true)
+
+ // Refetch agent as other details might've changed.
+ agent, err = app.user.GetAgent(agent.ID, "")
+ if err != nil {
+ return sendErrorEnvelope(r, err)
+ }
+
+ return r.SendEnvelope(agent)
}
// handleUpdateAgent updates an agent.
func handleUpdateAgent(r *fastglue.Request) error {
var (
app = r.Context.(*App)
- user = models.User{}
+ req = agentReq{}
auser = r.RequestCtx.UserValue("user").(amodels.User)
ip = realip.FromRequest(r.RequestCtx)
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
@@ -253,25 +256,13 @@ func handleUpdateAgent(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`id`"), nil, envelope.InputError)
}
- if err := r.Decode(&user, "json"); err != nil {
+ if err := r.Decode(&req, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
}
- if user.Email.String == "" {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil, envelope.InputError)
- }
- user.Email = null.StringFrom(strings.TrimSpace(strings.ToLower(user.Email.String)))
-
- if !stringutil.ValidEmail(user.Email.String) {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`email`"), nil, envelope.InputError)
- }
-
- if user.Roles == nil {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`role`"), nil, envelope.InputError)
- }
-
- if user.FirstName == "" {
- return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`first_name`"), nil, envelope.InputError)
+ // Validate agent request
+ if err := validateAgentRequest(r, &req); err != nil {
+ return err
}
agent, err := app.user.GetAgent(id, "")
@@ -280,8 +271,8 @@ func handleUpdateAgent(r *fastglue.Request) error {
}
oldAvailabilityStatus := agent.AvailabilityStatus
- // Update agent.
- if err = app.user.UpdateAgent(id, user); err != nil {
+ // Update agent with individual fields
+ if err = app.user.UpdateAgent(id, req.FirstName, req.LastName, req.Email, req.Roles, req.Enabled, req.AvailabilityStatus, req.NewPassword); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -289,18 +280,24 @@ func handleUpdateAgent(r *fastglue.Request) error {
defer app.authz.InvalidateUserCache(id)
// Create activity log if user availability status changed.
- if oldAvailabilityStatus != user.AvailabilityStatus {
- if err := app.activityLog.UserAvailability(auser.ID, auser.Email, user.AvailabilityStatus, ip, user.Email.String, id); err != nil {
+ if oldAvailabilityStatus != req.AvailabilityStatus {
+ if err := app.activityLog.UserAvailability(auser.ID, auser.Email, req.AvailabilityStatus, ip, req.Email, id); err != nil {
app.lo.Error("error creating activity log", "error", err)
}
}
// Upsert agent teams.
- if err := app.team.UpsertUserTeams(id, user.Teams.Names()); err != nil {
+ if err := app.team.UpsertUserTeams(id, req.Teams); err != nil {
return sendErrorEnvelope(r, err)
}
- return r.SendEnvelope(true)
+ // Refetch agent and return.
+ agent, err = app.user.GetAgent(id, "")
+ if err != nil {
+ return sendErrorEnvelope(r, err)
+ }
+
+ return r.SendEnvelope(agent)
}
// handleDeleteAgent soft deletes an agent.
@@ -381,7 +378,7 @@ func handleResetPassword(r *fastglue.Request) error {
var (
app = r.Context.(*App)
auser, ok = r.RequestCtx.UserValue("user").(amodels.User)
- resetReq ResetPasswordRequest
+ resetReq resetPasswordRequest
)
if ok && auser.ID > 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userAlreadyLoggedIn"), nil, envelope.InputError)
@@ -399,7 +396,7 @@ func handleResetPassword(r *fastglue.Request) error {
agent, err := app.user.GetAgent(0, resetReq.Email)
if err != nil {
// Send 200 even if user not found, to prevent email enumeration.
- return r.SendEnvelope("Reset password email sent successfully.")
+ return r.SendEnvelope(true)
}
token, err := app.user.SetResetPasswordToken(agent.ID)
@@ -434,7 +431,7 @@ func handleSetPassword(r *fastglue.Request) error {
var (
app = r.Context.(*App)
agent, ok = r.RequestCtx.UserValue("user").(amodels.User)
- req = SetPasswordRequest{}
+ req setPasswordRequest
)
if ok && agent.ID > 0 {
@@ -513,7 +510,7 @@ func uploadUserAvatar(r *fastglue.Request, user *models.User, files []*multipart
app.lo.Debug("error getting path from URL", "url", media.URL, "error", err)
return envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorUploading", "name", "{globals.terms.file}"), nil)
}
- fmt.Println("path", path)
+
if err := app.user.UpdateAvatar(user.ID, path); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -577,3 +574,28 @@ func handleRevokeAPIKey(r *fastglue.Request) error {
return r.SendEnvelope(true)
}
+
+// validateAgentRequest validates common agent request fields and normalizes the email
+func validateAgentRequest(r *fastglue.Request, req *agentReq) error {
+ var app = r.Context.(*App)
+
+ // Normalize email
+ req.Email = strings.TrimSpace(strings.ToLower(req.Email))
+ if req.Email == "" {
+ return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil, envelope.InputError)
+ }
+
+ if !stringutil.ValidEmail(req.Email) {
+ return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`email`"), nil, envelope.InputError)
+ }
+
+ if req.Roles == nil {
+ return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`role`"), nil, envelope.InputError)
+ }
+
+ if req.FirstName == "" {
+ return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`first_name`"), nil, envelope.InputError)
+ }
+
+ return nil
+}
diff --git a/frontend/src/constants/user.js b/frontend/src/constants/user.js
index 002b087..e6c88e1 100644
--- a/frontend/src/constants/user.js
+++ b/frontend/src/constants/user.js
@@ -1 +1,3 @@
-export const Roles = ["Admin", "Agent"]
\ No newline at end of file
+export const Roles = ["Admin", "Agent"]
+export const UserTypeAgent = "agent"
+export const UserTypeContact = "contact"
\ No newline at end of file
diff --git a/frontend/src/features/admin/agents/AgentForm.vue b/frontend/src/features/admin/agents/AgentForm.vue
index 3a333ee..0565fc7 100644
--- a/frontend/src/features/admin/agents/AgentForm.vue
+++ b/frontend/src/features/admin/agents/AgentForm.vue
@@ -418,7 +418,6 @@ const onSubmit = form.handleSubmit((values) => {
if (values.availability_status === 'active_group') {
values.availability_status = 'online'
}
- values.teams = values.teams.map((team) => ({ name: team }))
props.submitForm(values)
})
diff --git a/frontend/src/features/conversation/CreateConversation.vue b/frontend/src/features/conversation/CreateConversation.vue
index c57115c..4ce2c66 100644
--- a/frontend/src/features/conversation/CreateConversation.vue
+++ b/frontend/src/features/conversation/CreateConversation.vue
@@ -10,7 +10,7 @@
})
}}
-