mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 16:14:12 +00:00
Translate welcome to libredesk email subject
- Update all SQL queries to add missing columns - Update the create conversation API to allow setting the initiator of a conversation. For example, we might want to use this API to create a conversation on behalf of a customer, with the first message coming from the customer instead of the agent. This param allows this. - Minor refactors and clean up - Tidy go.mod - Rename structs to reflect purpose - Create focus structs for scanning JSON payloads for clarity.
This commit is contained in:
@@ -168,7 +168,13 @@ func handleUpdateContact(r *fastglue.Request) error {
|
|||||||
return sendErrorEnvelope(r, err)
|
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.
|
// handleGetContactNotes returns all notes for a contact.
|
||||||
@@ -195,18 +201,17 @@ func handleCreateContactNote(r *fastglue.Request) error {
|
|||||||
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
||||||
req = createContactNoteReq{}
|
req = createContactNoteReq{}
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := r.Decode(&req, "json"); 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))
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Note) == 0 {
|
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, req.Note); err != nil {
|
n, err := app.user.CreateNote(contactID, auser.ID, req.Note)
|
||||||
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
return r.SendEnvelope(true)
|
return r.SendEnvelope(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDeleteContactNote deletes a note for a contact.
|
// 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 {
|
if err := app.user.DeleteNote(noteID, contactID); err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
@@ -251,6 +258,7 @@ 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))
|
||||||
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
||||||
req = blockContactReq{}
|
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))
|
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 {
|
if err := app.user.ToggleEnabled(contactID, models.UserTypeContact, req.Enabled); err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
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)
|
||||||
}
|
}
|
||||||
|
@@ -49,6 +49,7 @@ type createConversationRequest struct {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Attachments []int `json:"attachments"`
|
Attachments []int `json:"attachments"`
|
||||||
|
Initiator string `json:"initiator"` // "contact" | "agent"
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetAllConversations retrieves all conversations.
|
// 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)
|
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}
|
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, "")
|
user, err := app.user.GetAgent(auser.ID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
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.
|
// Find or create contact.
|
||||||
contact := umodels.User{
|
contact := umodels.User{
|
||||||
Email: null.StringFrom(req.Email),
|
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))
|
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(
|
conversationID, conversationUUID, err := app.conversation.CreateConversation(
|
||||||
contact.ID,
|
contact.ID,
|
||||||
contact.ContactChannelID,
|
contact.ContactChannelID,
|
||||||
@@ -725,14 +704,14 @@ func handleCreateConversation(r *fastglue.Request) error {
|
|||||||
"", /** last_message **/
|
"", /** last_message **/
|
||||||
time.Now(), /** last_message_at **/
|
time.Now(), /** last_message_at **/
|
||||||
req.Subject,
|
req.Subject,
|
||||||
true, /** append reference number to subject **/
|
true, /** append reference number to subject? **/
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.lo.Error("error creating conversation", "error", err)
|
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))
|
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))
|
var media = make([]medModels.Media, 0, len(req.Attachments))
|
||||||
for _, id := range req.Attachments {
|
for _, id := range req.Attachments {
|
||||||
m, err := app.media.Get(id, "")
|
m, err := app.media.Get(id, "")
|
||||||
@@ -743,13 +722,29 @@ func handleCreateConversation(r *fastglue.Request) error {
|
|||||||
media = append(media, m)
|
media = append(media, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send reply to the created conversation.
|
// Handle sending initial message based on initiator using switch-case.
|
||||||
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 {
|
switch req.Initiator {
|
||||||
// Delete the conversation if reply fails.
|
case umodels.UserTypeAgent:
|
||||||
if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
|
// Queue reply.
|
||||||
app.lo.Error("error deleting conversation", "error", err)
|
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.
|
// Assign the conversation to the agent or team.
|
||||||
@@ -768,3 +763,36 @@ func handleCreateConversation(r *fastglue.Request) error {
|
|||||||
|
|
||||||
return r.SendEnvelope(conversation)
|
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
|
||||||
|
}
|
||||||
|
@@ -99,7 +99,7 @@ func handleGetMessage(r *fastglue.Request) error {
|
|||||||
return r.SendEnvelope(message)
|
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 {
|
func handleRetryMessage(r *fastglue.Request) error {
|
||||||
var (
|
var (
|
||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
@@ -168,7 +168,7 @@ func handleSendMessage(r *fastglue.Request) error {
|
|||||||
}
|
}
|
||||||
return r.SendEnvelope(message)
|
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 {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
|
10
cmd/oidc.go
10
cmd/oidc.go
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/zerodha/fastglue"
|
"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 {
|
func handleGetAllEnabledOIDC(r *fastglue.Request) error {
|
||||||
app := r.Context.(*App)
|
app := r.Context.(*App)
|
||||||
out, err := app.oidc.GetAllEnabled()
|
out, err := app.oidc.GetAllEnabled()
|
||||||
@@ -74,10 +74,10 @@ func handleCreateOIDC(r *fastglue.Request) error {
|
|||||||
if err := reloadAuth(app); err != nil {
|
if err := reloadAuth(app); err != nil {
|
||||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError)
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear client secret before returning
|
// Clear client secret before returning
|
||||||
createdOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10)
|
createdOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10)
|
||||||
|
|
||||||
return r.SendEnvelope(createdOIDC)
|
return r.SendEnvelope(createdOIDC)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +110,10 @@ func handleUpdateOIDC(r *fastglue.Request) error {
|
|||||||
if err := reloadAuth(app); err != nil {
|
if err := reloadAuth(app); err != nil {
|
||||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError)
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear client secret before returning
|
// Clear client secret before returning
|
||||||
updatedOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10)
|
updatedOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10)
|
||||||
|
|
||||||
return r.SendEnvelope(updatedOIDC)
|
return r.SendEnvelope(updatedOIDC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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))
|
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 {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
|
204
cmd/users.go
204
cmd/users.go
@@ -26,34 +26,38 @@ const (
|
|||||||
maxAvatarSizeMB = 2
|
maxAvatarSizeMB = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request structs for user-related endpoints
|
type updateAvailabilityRequest struct {
|
||||||
|
|
||||||
// UpdateAvailabilityRequest represents the request to update user availability
|
|
||||||
type UpdateAvailabilityRequest struct {
|
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetPasswordRequest represents the password reset request
|
type resetPasswordRequest struct {
|
||||||
type ResetPasswordRequest struct {
|
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPasswordRequest represents the set password request
|
type setPasswordRequest struct {
|
||||||
type SetPasswordRequest struct {
|
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvailabilityRequest represents the request to update agent availability
|
type availabilityRequest struct {
|
||||||
type AvailabilityRequest struct {
|
|
||||||
Status string `json:"status"`
|
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.
|
// handleGetAgents returns all agents.
|
||||||
func handleGetAgents(r *fastglue.Request) error {
|
func handleGetAgents(r *fastglue.Request) error {
|
||||||
var (
|
var app = r.Context.(*App)
|
||||||
app = r.Context.(*App)
|
|
||||||
)
|
|
||||||
agents, err := app.user.GetAgents()
|
agents, err := app.user.GetAgents()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
@@ -73,9 +77,7 @@ func handleGetAgentsCompact(r *fastglue.Request) error {
|
|||||||
|
|
||||||
// handleGetAgent returns an agent.
|
// handleGetAgent returns an agent.
|
||||||
func handleGetAgent(r *fastglue.Request) error {
|
func handleGetAgent(r *fastglue.Request) error {
|
||||||
var (
|
var app = r.Context.(*App)
|
||||||
app = r.Context.(*App)
|
|
||||||
)
|
|
||||||
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 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)
|
||||||
@@ -93,7 +95,7 @@ func handleUpdateAgentAvailability(r *fastglue.Request) error {
|
|||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
||||||
ip = realip.FromRequest(r.RequestCtx)
|
ip = realip.FromRequest(r.RequestCtx)
|
||||||
availReq AvailabilityRequest
|
availReq availabilityRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decode JSON request
|
// 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)
|
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, "")
|
agent, err := app.user.GetAgent(auser.ID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
@@ -108,10 +111,10 @@ func handleUpdateAgentAvailability(r *fastglue.Request) error {
|
|||||||
|
|
||||||
// Same status?
|
// Same status?
|
||||||
if agent.AvailabilityStatus == availReq.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 {
|
if err := app.user.UpdateAvailability(auser.ID, availReq.Status); err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
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 {
|
func handleGetCurrentAgentTeams(r *fastglue.Request) error {
|
||||||
var (
|
var (
|
||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
||||||
)
|
)
|
||||||
agent, err := app.user.GetAgent(auser.ID, "")
|
teams, err := app.team.GetUserTeams(auser.ID)
|
||||||
if err != nil {
|
|
||||||
return sendErrorEnvelope(r, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
teams, err := app.team.GetUserTeams(agent.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
@@ -150,11 +154,6 @@ func handleUpdateCurrentAgent(r *fastglue.Request) error {
|
|||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
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()
|
form, err := r.RequestCtx.MultipartForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.lo.Error("error parsing form data", "error", err)
|
app.lo.Error("error parsing form data", "error", err)
|
||||||
@@ -165,54 +164,53 @@ func handleUpdateCurrentAgent(r *fastglue.Request) error {
|
|||||||
|
|
||||||
// Upload avatar?
|
// Upload avatar?
|
||||||
if ok && len(files) > 0 {
|
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 {
|
if err := uploadUserAvatar(r, &agent, files); err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
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.
|
// handleCreateAgent creates a new agent.
|
||||||
func handleCreateAgent(r *fastglue.Request) error {
|
func handleCreateAgent(r *fastglue.Request) error {
|
||||||
var (
|
var (
|
||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
user = models.User{}
|
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)
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Email.String == "" {
|
// Validate agent request
|
||||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil, envelope.InputError)
|
if err := validateAgentRequest(r, &req); err != nil {
|
||||||
}
|
return err
|
||||||
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 {
|
agent, err := app.user.CreateAgent(req.FirstName, req.LastName, req.Email, req.Roles)
|
||||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`role`"), nil, envelope.InputError)
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert user teams.
|
// Upsert user teams.
|
||||||
if len(user.Teams) > 0 {
|
if len(req.Teams) > 0 {
|
||||||
if err := app.team.UpsertUserTeams(user.ID, user.Teams.Names()); err != nil {
|
app.team.UpsertUserTeams(agent.ID, req.Teams)
|
||||||
return sendErrorEnvelope(r, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.SendWelcomeEmail {
|
if req.SendWelcomeEmail {
|
||||||
// Generate reset token.
|
// Generate reset token.
|
||||||
resetToken, err := app.user.SetResetPasswordToken(user.ID)
|
resetToken, err := app.user.SetResetPasswordToken(agent.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
@@ -220,31 +218,36 @@ func handleCreateAgent(r *fastglue.Request) error {
|
|||||||
// Render template and send email.
|
// Render template and send email.
|
||||||
content, err := app.tmpl.RenderInMemoryTemplate(tmpl.TmplWelcome, map[string]any{
|
content, err := app.tmpl.RenderInMemoryTemplate(tmpl.TmplWelcome, map[string]any{
|
||||||
"ResetToken": resetToken,
|
"ResetToken": resetToken,
|
||||||
"Email": user.Email.String,
|
"Email": req.Email,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.lo.Error("error rendering template", "error", err)
|
app.lo.Error("error rendering template", "error", err)
|
||||||
return r.SendEnvelope(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.notifier.Send(notifier.Message{
|
if err := app.notifier.Send(notifier.Message{
|
||||||
RecipientEmails: []string{user.Email.String},
|
RecipientEmails: []string{req.Email},
|
||||||
Subject: "Welcome to Libredesk",
|
Subject: app.i18n.T("globals.messages.welcomeToLibredesk"),
|
||||||
Content: content,
|
Content: content,
|
||||||
Provider: notifier.ProviderEmail,
|
Provider: notifier.ProviderEmail,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.lo.Error("error sending notification message", "error", err)
|
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.
|
// handleUpdateAgent updates an agent.
|
||||||
func handleUpdateAgent(r *fastglue.Request) error {
|
func handleUpdateAgent(r *fastglue.Request) error {
|
||||||
var (
|
var (
|
||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
user = models.User{}
|
req = agentReq{}
|
||||||
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
||||||
ip = realip.FromRequest(r.RequestCtx)
|
ip = realip.FromRequest(r.RequestCtx)
|
||||||
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
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)
|
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)
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Email.String == "" {
|
// Validate agent request
|
||||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil, envelope.InputError)
|
if err := validateAgentRequest(r, &req); err != nil {
|
||||||
}
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
agent, err := app.user.GetAgent(id, "")
|
agent, err := app.user.GetAgent(id, "")
|
||||||
@@ -280,8 +271,8 @@ func handleUpdateAgent(r *fastglue.Request) error {
|
|||||||
}
|
}
|
||||||
oldAvailabilityStatus := agent.AvailabilityStatus
|
oldAvailabilityStatus := agent.AvailabilityStatus
|
||||||
|
|
||||||
// Update agent.
|
// Update agent with individual fields
|
||||||
if err = app.user.UpdateAgent(id, user); err != nil {
|
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)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,18 +280,24 @@ func handleUpdateAgent(r *fastglue.Request) error {
|
|||||||
defer app.authz.InvalidateUserCache(id)
|
defer app.authz.InvalidateUserCache(id)
|
||||||
|
|
||||||
// Create activity log if user availability status changed.
|
// Create activity log if user availability status changed.
|
||||||
if oldAvailabilityStatus != user.AvailabilityStatus {
|
if oldAvailabilityStatus != req.AvailabilityStatus {
|
||||||
if err := app.activityLog.UserAvailability(auser.ID, auser.Email, user.AvailabilityStatus, ip, user.Email.String, id); err != nil {
|
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)
|
app.lo.Error("error creating activity log", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert agent teams.
|
// 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 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.
|
// handleDeleteAgent soft deletes an agent.
|
||||||
@@ -381,7 +378,7 @@ func handleResetPassword(r *fastglue.Request) error {
|
|||||||
var (
|
var (
|
||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
auser, ok = r.RequestCtx.UserValue("user").(amodels.User)
|
auser, ok = r.RequestCtx.UserValue("user").(amodels.User)
|
||||||
resetReq ResetPasswordRequest
|
resetReq resetPasswordRequest
|
||||||
)
|
)
|
||||||
if ok && auser.ID > 0 {
|
if ok && auser.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)
|
||||||
@@ -399,7 +396,7 @@ func handleResetPassword(r *fastglue.Request) error {
|
|||||||
agent, err := app.user.GetAgent(0, resetReq.Email)
|
agent, err := app.user.GetAgent(0, resetReq.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Send 200 even if user not found, to prevent email enumeration.
|
// 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)
|
token, err := app.user.SetResetPasswordToken(agent.ID)
|
||||||
@@ -434,7 +431,7 @@ 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)
|
||||||
req = SetPasswordRequest{}
|
req setPasswordRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
if ok && agent.ID > 0 {
|
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)
|
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)
|
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 {
|
if err := app.user.UpdateAvatar(user.ID, path); err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
@@ -577,3 +574,28 @@ func handleRevokeAPIKey(r *fastglue.Request) error {
|
|||||||
|
|
||||||
return r.SendEnvelope(true)
|
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
|
||||||
|
}
|
||||||
|
@@ -1 +1,3 @@
|
|||||||
export const Roles = ["Admin", "Agent"]
|
export const Roles = ["Admin", "Agent"]
|
||||||
|
export const UserTypeAgent = "agent"
|
||||||
|
export const UserTypeContact = "contact"
|
@@ -418,7 +418,6 @@ const onSubmit = form.handleSubmit((values) => {
|
|||||||
if (values.availability_status === 'active_group') {
|
if (values.availability_status === 'active_group') {
|
||||||
values.availability_status = 'online'
|
values.availability_status = 'online'
|
||||||
}
|
}
|
||||||
values.teams = values.teams.map((team) => ({ name: team }))
|
|
||||||
props.submitForm(values)
|
props.submitForm(values)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription/>
|
<DialogDescription />
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form @submit="createConversation" class="flex flex-col flex-1 overflow-hidden">
|
<form @submit="createConversation" class="flex flex-col flex-1 overflow-hidden">
|
||||||
<!-- Form Fields Section -->
|
<!-- Form Fields Section -->
|
||||||
@@ -263,6 +263,7 @@ import { useFileUpload } from '@/composables/useFileUpload'
|
|||||||
import Editor from '@/components/editor/TextEditor.vue'
|
import Editor from '@/components/editor/TextEditor.vue'
|
||||||
import { useMacroStore } from '@/stores/macro'
|
import { useMacroStore } from '@/stores/macro'
|
||||||
import SelectComboBox from '@/components/combobox/SelectCombobox.vue'
|
import SelectComboBox from '@/components/combobox/SelectCombobox.vue'
|
||||||
|
import { UserTypeAgent } from '@/constants/user'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
|
||||||
const dialogOpen = defineModel({
|
const dialogOpen = defineModel({
|
||||||
@@ -393,12 +394,14 @@ const selectContact = (contact) => {
|
|||||||
const createConversation = form.handleSubmit(async (values) => {
|
const createConversation = form.handleSubmit(async (values) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// convert ids to numbers if they are not already
|
// Convert ids to numbers if they are not already
|
||||||
values.inbox_id = Number(values.inbox_id)
|
values.inbox_id = Number(values.inbox_id)
|
||||||
values.team_id = values.team_id ? Number(values.team_id) : null
|
values.team_id = values.team_id ? Number(values.team_id) : null
|
||||||
values.agent_id = values.agent_id ? Number(values.agent_id) : null
|
values.agent_id = values.agent_id ? Number(values.agent_id) : null
|
||||||
// array of attachment ids.
|
// Array of attachment ids.
|
||||||
values.attachments = mediaFiles.value.map((file) => file.id)
|
values.attachments = mediaFiles.value.map((file) => file.id)
|
||||||
|
// Initiator of this conversation is always agent
|
||||||
|
values.initiator = UserTypeAgent
|
||||||
const conversation = await api.createConversation(values)
|
const conversation = await api.createConversation(values)
|
||||||
const conversationUUID = conversation.data.data.uuid
|
const conversationUUID = conversation.data.data.uuid
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@@ -7,6 +7,7 @@ require (
|
|||||||
github.com/coreos/go-oidc/v3 v3.11.0
|
github.com/coreos/go-oidc/v3 v3.11.0
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/emersion/go-imap/v2 v2.0.0-beta.3
|
github.com/emersion/go-imap/v2 v2.0.0-beta.3
|
||||||
|
github.com/emersion/go-message v0.18.1
|
||||||
github.com/fasthttp/websocket v1.5.9
|
github.com/fasthttp/websocket v1.5.9
|
||||||
github.com/ferluci/fast-realip v1.0.1
|
github.com/ferluci/fast-realip v1.0.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@@ -49,7 +50,6 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/emersion/go-message v0.18.1 // indirect
|
|
||||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||||
github.com/fasthttp/router v1.5.0 // indirect
|
github.com/fasthttp/router v1.5.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
|
@@ -188,6 +188,7 @@
|
|||||||
"globals.terms.recipient": "Recipient | Recipients",
|
"globals.terms.recipient": "Recipient | Recipients",
|
||||||
"globals.terms.tls": "TLS | TLSs",
|
"globals.terms.tls": "TLS | TLSs",
|
||||||
"globals.terms.credential": "Credential | Credentials",
|
"globals.terms.credential": "Credential | Credentials",
|
||||||
|
"globals.messages.welcomeToLibredesk": "Welcome to Libredesk",
|
||||||
"globals.messages.invalid": "Invalid {name}",
|
"globals.messages.invalid": "Invalid {name}",
|
||||||
"globals.messages.custom": "Custom {name}",
|
"globals.messages.custom": "Custom {name}",
|
||||||
"globals.messages.replying": "Replying",
|
"globals.messages.replying": "Replying",
|
||||||
@@ -568,7 +569,6 @@
|
|||||||
"search.noResultsForQuery": "No results found for query `{query}`. Try a different search term.",
|
"search.noResultsForQuery": "No results found for query `{query}`. Try a different search term.",
|
||||||
"search.minQueryLength": " Please enter at least {length} characters to search.",
|
"search.minQueryLength": " Please enter at least {length} characters to search.",
|
||||||
"search.searchBy": "Search by reference number, contact email address or messages in conversations.",
|
"search.searchBy": "Search by reference number, contact email address or messages in conversations.",
|
||||||
"search.adjustSearchTerms": "Try adjusting your search terms or filters.",
|
|
||||||
"sla.overdueBy": "Overdue by",
|
"sla.overdueBy": "Overdue by",
|
||||||
"sla.met": "SLA met",
|
"sla.met": "SLA met",
|
||||||
"view.form.description": "Create and save custom filter views for quick access to your conversations.",
|
"view.form.description": "Create and save custom filter views for quick access to your conversations.",
|
||||||
|
@@ -82,18 +82,9 @@ func (m *Manager) GetAll(order, orderBy, filtersJSON string, page, pageSize int)
|
|||||||
return activityLogs, nil
|
return activityLogs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create adds a new activity log.
|
|
||||||
func (m *Manager) Create(activityType, activityDescription string, actorID int, targetModelType string, targetModelID int, ip string) error {
|
|
||||||
if _, err := m.q.InsertActivity.Exec(activityType, activityDescription, actorID, targetModelType, targetModelID, ip); err != nil {
|
|
||||||
m.lo.Error("error inserting activity", "error", err)
|
|
||||||
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.activityLog}"), nil)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login records a login event for the given user.
|
// Login records a login event for the given user.
|
||||||
func (al *Manager) Login(userID int, email, ip string) error {
|
func (al *Manager) Login(userID int, email, ip string) error {
|
||||||
return al.Create(
|
return al.create(
|
||||||
models.AgentLogin,
|
models.AgentLogin,
|
||||||
fmt.Sprintf("%s (#%d) logged in", email, userID),
|
fmt.Sprintf("%s (#%d) logged in", email, userID),
|
||||||
userID,
|
userID,
|
||||||
@@ -105,7 +96,7 @@ func (al *Manager) Login(userID int, email, ip string) error {
|
|||||||
|
|
||||||
// Logout records a logout event for the given user.
|
// Logout records a logout event for the given user.
|
||||||
func (al *Manager) Logout(userID int, email, ip string) error {
|
func (al *Manager) Logout(userID int, email, ip string) error {
|
||||||
return al.Create(
|
return al.create(
|
||||||
models.AgentLogout,
|
models.AgentLogout,
|
||||||
fmt.Sprintf("%s (#%d) logged out", email, userID),
|
fmt.Sprintf("%s (#%d) logged out", email, userID),
|
||||||
userID,
|
userID,
|
||||||
@@ -123,7 +114,7 @@ func (al *Manager) Away(actorID int, actorEmail, ip string, targetID int, target
|
|||||||
} else {
|
} else {
|
||||||
description = fmt.Sprintf("%s (#%d) is away", actorEmail, actorID)
|
description = fmt.Sprintf("%s (#%d) is away", actorEmail, actorID)
|
||||||
}
|
}
|
||||||
return al.Create(
|
return al.create(
|
||||||
models.AgentAway, /* activity type*/
|
models.AgentAway, /* activity type*/
|
||||||
description,
|
description,
|
||||||
actorID, /*actor_id*/
|
actorID, /*actor_id*/
|
||||||
@@ -141,7 +132,7 @@ func (al *Manager) AwayReassigned(actorID int, actorEmail, ip string, targetID i
|
|||||||
} else {
|
} else {
|
||||||
description = fmt.Sprintf("%s (#%d) is away and reassigning", actorEmail, actorID)
|
description = fmt.Sprintf("%s (#%d) is away and reassigning", actorEmail, actorID)
|
||||||
}
|
}
|
||||||
return al.Create(
|
return al.create(
|
||||||
models.AgentAwayReassigned, /* activity type*/
|
models.AgentAwayReassigned, /* activity type*/
|
||||||
description,
|
description,
|
||||||
actorID, /*actor_id*/
|
actorID, /*actor_id*/
|
||||||
@@ -159,7 +150,7 @@ func (al *Manager) Online(actorID int, actorEmail, ip string, targetID int, targ
|
|||||||
} else {
|
} else {
|
||||||
description = fmt.Sprintf("%s (#%d) is online", actorEmail, actorID)
|
description = fmt.Sprintf("%s (#%d) is online", actorEmail, actorID)
|
||||||
}
|
}
|
||||||
return al.Create(
|
return al.create(
|
||||||
models.AgentOnline, /* activity type*/
|
models.AgentOnline, /* activity type*/
|
||||||
description,
|
description,
|
||||||
actorID, /*actor_id*/
|
actorID, /*actor_id*/
|
||||||
@@ -190,6 +181,16 @@ func (al *Manager) UserAvailability(actorID int, actorEmail, status, ip, targetE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create creates a new activity log in DB.
|
||||||
|
func (m *Manager) create(activityType, activityDescription string, actorID int, targetModelType string, targetModelID int, ip string) error {
|
||||||
|
var activityLog models.ActivityLog
|
||||||
|
if err := m.q.InsertActivity.Get(&activityLog, activityType, activityDescription, actorID, targetModelType, targetModelID, ip); err != nil {
|
||||||
|
m.lo.Error("error inserting activity log", "error", err)
|
||||||
|
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.activityLog}"), nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// makeQuery constructs the SQL query for fetching activity logs with filters and pagination.
|
// makeQuery constructs the SQL query for fetching activity logs with filters and pagination.
|
||||||
func (m *Manager) makeQuery(page, pageSize int, order, orderBy, filtersJSON string) (string, []any, error) {
|
func (m *Manager) makeQuery(page, pageSize int, order, orderBy, filtersJSON string) (string, []any, error) {
|
||||||
var (
|
var (
|
||||||
|
@@ -183,6 +183,7 @@ func (e *Enforcer) EnforceConversationAccess(user umodels.User, conversation cmo
|
|||||||
// EnforceMediaAccess checks for read access on linked model to media.
|
// EnforceMediaAccess checks for read access on linked model to media.
|
||||||
func (e *Enforcer) EnforceMediaAccess(user umodels.User, model string) (bool, error) {
|
func (e *Enforcer) EnforceMediaAccess(user umodels.User, model string) (bool, error) {
|
||||||
switch model {
|
switch model {
|
||||||
|
// TODO: Pick this table / model name from the package/models/models.go
|
||||||
case "messages":
|
case "messages":
|
||||||
allowed, err := e.Enforce(user, model, "read")
|
allowed, err := e.Enforce(user, model, "read")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -33,7 +33,7 @@ type conversationStore interface {
|
|||||||
|
|
||||||
type teamStore interface {
|
type teamStore interface {
|
||||||
GetAll() ([]tmodels.Team, error)
|
GetAll() ([]tmodels.Team, error)
|
||||||
GetMembers(teamID int) ([]umodels.User, error)
|
GetMembers(teamID int) ([]tmodels.TeamMember, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engine represents a manager for assigning unassigned conversations
|
// Engine represents a manager for assigning unassigned conversations
|
||||||
|
@@ -7,10 +7,10 @@ select
|
|||||||
from automation_rules where enabled is TRUE ORDER BY weight ASC;
|
from automation_rules where enabled is TRUE ORDER BY weight ASC;
|
||||||
|
|
||||||
-- name: get-all
|
-- name: get-all
|
||||||
SELECT id, created_at, updated_at, enabled, name, description, type, events, rules, execution_mode from automation_rules where type = $1 ORDER BY weight ASC;
|
SELECT id, created_at, updated_at, "name", description, "type", rules, events, enabled, weight, execution_mode from automation_rules where type = $1 ORDER BY weight ASC;
|
||||||
|
|
||||||
-- name: get-rule
|
-- name: get-rule
|
||||||
SELECT id, created_at, updated_at, enabled, name, description, type, events, rules, execution_mode from automation_rules where id = $1;
|
SELECT id, created_at, updated_at, "name", description, "type", rules, events, enabled, weight, execution_mode from automation_rules where id = $1;
|
||||||
|
|
||||||
-- name: update-rule
|
-- name: update-rule
|
||||||
INSERT INTO automation_rules(id, name, description, type, events, rules, enabled)
|
INSERT INTO automation_rules(id, name, description, type, events, rules, enabled)
|
||||||
|
@@ -15,7 +15,10 @@ SELECT id,
|
|||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
"name",
|
"name",
|
||||||
description
|
description,
|
||||||
|
is_always_open,
|
||||||
|
hours,
|
||||||
|
holidays
|
||||||
FROM business_hours
|
FROM business_hours
|
||||||
ORDER BY updated_at DESC;
|
ORDER BY updated_at DESC;
|
||||||
|
|
||||||
|
@@ -930,7 +930,7 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("making recipients for reply action: %w", err)
|
return fmt.Errorf("making recipients for reply action: %w", err)
|
||||||
}
|
}
|
||||||
_, err = m.SendReply(
|
_, err = m.QueueReply(
|
||||||
[]mmodels.Media{},
|
[]mmodels.Media{},
|
||||||
conv.InboxID,
|
conv.InboxID,
|
||||||
user.ID,
|
user.ID,
|
||||||
@@ -1001,8 +1001,8 @@ func (m *Manager) SendCSATReply(actorUserID int, conversation models.Conversatio
|
|||||||
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil)
|
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send CSAT reply.
|
// Queue CSAT reply.
|
||||||
_, err = m.SendReply(nil /**media**/, conversation.InboxID, actorUserID, conversation.UUID, message, to, cc, bcc, meta)
|
_, err = m.QueueReply(nil /**media**/, conversation.InboxID, actorUserID, conversation.UUID, message, to, cc, bcc, meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.lo.Error("error sending CSAT reply", "conversation_uuid", conversation.UUID, "error", err)
|
m.lo.Error("error sending CSAT reply", "conversation_uuid", conversation.UUID, "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil)
|
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil)
|
||||||
|
@@ -167,7 +167,7 @@ func (m *Manager) sendOutgoingMessage(message models.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// References is sorted in DESC i.e newest message first, so reverse it to keep the references in order.
|
// References is sorted in DESC i.e newest message first, so reverse it to keep the references in order.
|
||||||
stringutil.ReverseSlice(message.References)
|
slices.Reverse(message.References)
|
||||||
|
|
||||||
// Remove the current message ID from the references.
|
// Remove the current message ID from the references.
|
||||||
message.References = stringutil.RemoveItemByValue(message.References, message.SourceID.String)
|
message.References = stringutil.RemoveItemByValue(message.References, message.SourceID.String)
|
||||||
@@ -347,9 +347,10 @@ func (m *Manager) UpdateMessageStatus(messageUUID string, status string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkMessageAsPending updates message status to `Pending`, so if it's a outgoing message it can be picked up again by a worker.
|
// MarkMessageAsPending updates message status to `Pending`, enqueuing it for sending.
|
||||||
func (m *Manager) MarkMessageAsPending(uuid string) error {
|
func (m *Manager) MarkMessageAsPending(uuid string) error {
|
||||||
if err := m.UpdateMessageStatus(uuid, models.MessageStatusPending); err != nil {
|
if err := m.UpdateMessageStatus(uuid, models.MessageStatusPending); err != nil {
|
||||||
|
m.lo.Error("error marking message as pending", "uuid", uuid, "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil)
|
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -374,8 +375,27 @@ func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversat
|
|||||||
return message, nil
|
return message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendReply inserts a reply message in a conversation.
|
// CreateContactMessage creates a contact message in a conversation.
|
||||||
func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conversationUUID, content string, to, cc, bcc []string, meta map[string]interface{}) (models.Message, error) {
|
func (m *Manager) CreateContactMessage(media []mmodels.Media, contactID int, conversationUUID, content, contentType string) (models.Message, error) {
|
||||||
|
message := models.Message{
|
||||||
|
ConversationUUID: conversationUUID,
|
||||||
|
SenderID: contactID,
|
||||||
|
Type: models.MessageIncoming,
|
||||||
|
SenderType: models.SenderTypeContact,
|
||||||
|
Status: models.MessageStatusReceived,
|
||||||
|
Content: content,
|
||||||
|
ContentType: contentType,
|
||||||
|
Private: false,
|
||||||
|
Media: media,
|
||||||
|
}
|
||||||
|
if err := m.InsertMessage(&message); err != nil {
|
||||||
|
return models.Message{}, err
|
||||||
|
}
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueReply queues a reply message in a conversation.
|
||||||
|
func (m *Manager) QueueReply(media []mmodels.Media, inboxID, senderID int, conversationUUID, content string, to, cc, bcc []string, meta map[string]interface{}) (models.Message, error) {
|
||||||
var (
|
var (
|
||||||
message = models.Message{}
|
message = models.Message{}
|
||||||
)
|
)
|
||||||
@@ -402,7 +422,7 @@ func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conver
|
|||||||
return message, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorMarshalling", "name", "{globals.terms.meta}"), nil)
|
return message, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorMarshalling", "name", "{globals.terms.meta}"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generage unique source ID i.e. message-id for email.
|
// Generate unique source ID i.e. message-id for email.
|
||||||
inbox, err := m.inboxStore.GetDBRecord(inboxID)
|
inbox, err := m.inboxStore.GetDBRecord(inboxID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return message, err
|
return message, err
|
||||||
|
@@ -93,7 +93,7 @@ func (m *Manager) UpdateResponse(uuid string, score int, feedback string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if csat.Score > 0 || !csat.ResponseTimestamp.IsZero() {
|
if csat.Rating > 0 || !csat.ResponseTimestamp.IsZero() {
|
||||||
return envelope.NewError(envelope.InputError, m.i18n.T("csat.alreadySubmitted"), nil)
|
return envelope.NewError(envelope.InputError, m.i18n.T("csat.alreadySubmitted"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,11 +10,11 @@ import (
|
|||||||
// CSATResponse represents a customer satisfaction survey response.
|
// CSATResponse represents a customer satisfaction survey response.
|
||||||
type CSATResponse struct {
|
type CSATResponse struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
UUID string `db:"uuid"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
UUID string `db:"uuid"`
|
||||||
ConversationID int `db:"conversation_id"`
|
ConversationID int `db:"conversation_id"`
|
||||||
Score int `db:"rating"`
|
Rating int `db:"rating"`
|
||||||
Feedback null.String `db:"feedback"`
|
Feedback null.String `db:"feedback"`
|
||||||
ResponseTimestamp null.Time `db:"response_timestamp"`
|
ResponseTimestamp null.Time `db:"response_timestamp"`
|
||||||
}
|
}
|
||||||
|
@@ -10,9 +10,9 @@ type CustomAttribute struct {
|
|||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
AppliesTo string `db:"applies_to" json:"applies_to"`
|
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Description string `db:"description" json:"description"`
|
Description string `db:"description" json:"description"`
|
||||||
|
AppliesTo string `db:"applies_to" json:"applies_to"`
|
||||||
Key string `db:"key" json:"key"`
|
Key string `db:"key" json:"key"`
|
||||||
Values pq.StringArray `db:"values" json:"values"`
|
Values pq.StringArray `db:"values" json:"values"`
|
||||||
DataType string `db:"data_type" json:"data_type"`
|
DataType string `db:"data_type" json:"data_type"`
|
||||||
|
@@ -3,9 +3,9 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
applies_to,
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
applies_to,
|
||||||
key,
|
key,
|
||||||
values,
|
values,
|
||||||
data_type,
|
data_type,
|
||||||
@@ -25,9 +25,9 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
applies_to,
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
applies_to,
|
||||||
key,
|
key,
|
||||||
values,
|
values,
|
||||||
data_type,
|
data_type,
|
||||||
|
@@ -2,8 +2,6 @@
|
|||||||
package envelope
|
package envelope
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,13 +51,13 @@ func NewError(etype string, message string, data interface{}) error {
|
|||||||
case GeneralError:
|
case GeneralError:
|
||||||
err.Code = fasthttp.StatusInternalServerError
|
err.Code = fasthttp.StatusInternalServerError
|
||||||
case PermissionError:
|
case PermissionError:
|
||||||
err.Code = http.StatusForbidden
|
err.Code = fasthttp.StatusForbidden
|
||||||
case InputError:
|
case InputError:
|
||||||
err.Code = fasthttp.StatusBadRequest
|
err.Code = fasthttp.StatusBadRequest
|
||||||
case DataError:
|
case DataError:
|
||||||
err.Code = http.StatusBadGateway
|
err.Code = fasthttp.StatusUnprocessableEntity
|
||||||
case NetworkError:
|
case NetworkError:
|
||||||
err.Code = http.StatusGatewayTimeout
|
err.Code = fasthttp.StatusGatewayTimeout
|
||||||
case NotFoundError:
|
case NotFoundError:
|
||||||
err.Code = fasthttp.StatusNotFound
|
err.Code = fasthttp.StatusNotFound
|
||||||
case ConflictError:
|
case ConflictError:
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
-- name: get-active-inboxes
|
-- name: get-active-inboxes
|
||||||
SELECT * from inboxes where enabled is TRUE and deleted_at is NULL;
|
SELECT id, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from" FROM inboxes where enabled is TRUE and deleted_at is NULL;
|
||||||
|
|
||||||
-- name: get-all-inboxes
|
-- name: get-all-inboxes
|
||||||
SELECT id, created_at, updated_at, name, channel, enabled from inboxes where deleted_at is NULL;
|
SELECT id, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from" FROM inboxes where deleted_at is NULL;
|
||||||
|
|
||||||
-- name: insert-inbox
|
-- name: insert-inbox
|
||||||
INSERT INTO inboxes
|
INSERT INTO inboxes
|
||||||
@@ -11,7 +11,7 @@ VALUES($1, $2, $3, $4, $5)
|
|||||||
RETURNING *
|
RETURNING *
|
||||||
|
|
||||||
-- name: get-inbox
|
-- name: get-inbox
|
||||||
SELECT * from inboxes where id = $1 and deleted_at is NULL;
|
SELECT id, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from" FROM inboxes where id = $1 and deleted_at is NULL;
|
||||||
|
|
||||||
-- name: update
|
-- name: update
|
||||||
UPDATE inboxes
|
UPDATE inboxes
|
||||||
@@ -20,7 +20,7 @@ where id = $1 and deleted_at is NULL
|
|||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: soft-delete
|
-- name: soft-delete
|
||||||
UPDATE inboxes set deleted_at = now(), config = '{}' where id = $1 and deleted_at is NULL;
|
UPDATE inboxes set deleted_at = now(), updated_at = now(), config = '{}' where id = $1 and deleted_at is NULL;
|
||||||
|
|
||||||
-- name: toggle
|
-- name: toggle
|
||||||
UPDATE inboxes
|
UPDATE inboxes
|
||||||
|
@@ -12,11 +12,11 @@ type Macro struct {
|
|||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
MessageContent string `db:"message_content" json:"message_content"`
|
Actions json.RawMessage `db:"actions" json:"actions"`
|
||||||
VisibleWhen pq.StringArray `db:"visible_when" json:"visible_when"`
|
|
||||||
Visibility string `db:"visibility" json:"visibility"`
|
Visibility string `db:"visibility" json:"visibility"`
|
||||||
|
VisibleWhen pq.StringArray `db:"visible_when" json:"visible_when"`
|
||||||
|
MessageContent string `db:"message_content" json:"message_content"`
|
||||||
UserID *int `db:"user_id" json:"user_id,string"`
|
UserID *int `db:"user_id" json:"user_id,string"`
|
||||||
TeamID *int `db:"team_id" json:"team_id,string"`
|
TeamID *int `db:"team_id" json:"team_id,string"`
|
||||||
UsageCount int `db:"usage_count" json:"usage_count"`
|
UsageCount int `db:"usage_count" json:"usage_count"`
|
||||||
Actions json.RawMessage `db:"actions" json:"actions"`
|
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
-- name: get
|
-- name: get
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name,
|
|
||||||
message_content,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
name,
|
||||||
|
actions,
|
||||||
visibility,
|
visibility,
|
||||||
|
visible_when,
|
||||||
|
message_content,
|
||||||
user_id,
|
user_id,
|
||||||
team_id,
|
team_id,
|
||||||
actions,
|
|
||||||
visible_when,
|
|
||||||
usage_count
|
usage_count
|
||||||
FROM
|
FROM
|
||||||
macros
|
macros
|
||||||
@@ -19,15 +19,15 @@ WHERE
|
|||||||
-- name: get-all
|
-- name: get-all
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name,
|
|
||||||
message_content,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
name,
|
||||||
|
actions,
|
||||||
visibility,
|
visibility,
|
||||||
|
visible_when,
|
||||||
|
message_content,
|
||||||
user_id,
|
user_id,
|
||||||
team_id,
|
team_id,
|
||||||
actions,
|
|
||||||
visible_when,
|
|
||||||
usage_count
|
usage_count
|
||||||
FROM
|
FROM
|
||||||
macros
|
macros
|
||||||
@@ -67,7 +67,6 @@ WHERE
|
|||||||
UPDATE
|
UPDATE
|
||||||
macros
|
macros
|
||||||
SET
|
SET
|
||||||
usage_count = usage_count + 1,
|
usage_count = usage_count + 1
|
||||||
updated_at = NOW()
|
|
||||||
WHERE
|
WHERE
|
||||||
id = $1;
|
id = $1;
|
@@ -214,6 +214,7 @@ func (m *Manager) deleteUnlinkedMessageMedia() error {
|
|||||||
m.lo.Error("error deleting unlinked media", "error", err)
|
m.lo.Error("error deleting unlinked media", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// TODO: If it's an image also delete the `thumb_uuid` image.
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,31 +1,37 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/volatiletech/null/v9"
|
"github.com/volatiletech/null/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// TODO: pick these table names from their respective package/models/models.go
|
||||||
ModelMessages = "messages"
|
ModelMessages = "messages"
|
||||||
ModelUser = "users"
|
ModelUser = "users"
|
||||||
|
|
||||||
DispositionInline = "inline"
|
DispositionInline = "inline"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Media represents an uploaded object.
|
// Media represents an uploaded object in DB and storage backend.
|
||||||
type Media struct {
|
type Media struct {
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
UUID string `db:"uuid" json:"uuid"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
Filename string `db:"filename" json:"filename"`
|
UUID string `db:"uuid" json:"uuid"`
|
||||||
ContentType string `db:"content_type" json:"content_type"`
|
Store string `db:"store" json:"store"`
|
||||||
Model null.String `db:"model_type" json:"-"`
|
Filename string `db:"filename" json:"filename"`
|
||||||
ModelID null.Int `db:"model_id" json:"-"`
|
ContentType string `db:"content_type" json:"content_type"`
|
||||||
Size int `db:"size" json:"size"`
|
ContentID string `db:"content_id" json:"content_id"`
|
||||||
Store string `db:"store" json:"store"`
|
ModelID null.Int `db:"model_id" json:"model_id"`
|
||||||
Disposition null.String `db:"disposition" json:"disposition"`
|
Model null.String `db:"model_type" json:"model_type"`
|
||||||
URL string `json:"url"`
|
Disposition null.String `db:"disposition" json:"disposition"`
|
||||||
ContentID string `json:"-"`
|
Size int `db:"size" json:"size"`
|
||||||
Content []byte `json:"-"`
|
Meta json.RawMessage `db:"meta" json:"meta"`
|
||||||
|
|
||||||
|
// Pseudo fields
|
||||||
|
URL string `json:"url"`
|
||||||
|
Content []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ VALUES(
|
|||||||
RETURNING id;
|
RETURNING id;
|
||||||
|
|
||||||
-- name: get-media
|
-- name: get-media
|
||||||
SELECT id, created_at, "uuid", store, filename, content_type, model_id, model_type, "size", disposition
|
SELECT id, created_at, updated_at, "uuid", store, filename, content_type, content_id, model_id, model_type, disposition, "size", meta
|
||||||
FROM media
|
FROM media
|
||||||
WHERE
|
WHERE
|
||||||
($1 > 0 AND id = $1)
|
($1 > 0 AND id = $1)
|
||||||
@@ -23,7 +23,7 @@ WHERE
|
|||||||
($2 != '' AND uuid = $2::uuid)
|
($2 != '' AND uuid = $2::uuid)
|
||||||
|
|
||||||
-- name: get-media-by-uuid
|
-- name: get-media-by-uuid
|
||||||
SELECT id, created_at, "uuid", store, filename, content_type, model_id, model_type, "size", disposition
|
SELECT id, created_at, updated_at, "uuid", store, filename, content_type, content_id, model_id, model_type, disposition, "size", meta
|
||||||
FROM media
|
FROM media
|
||||||
WHERE uuid = $1;
|
WHERE uuid = $1;
|
||||||
|
|
||||||
@@ -38,13 +38,13 @@ SET model_type = $2,
|
|||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: get-model-media
|
-- name: get-model-media
|
||||||
SELECT id, created_at, "uuid", store, filename, content_type, model_id, model_type, "size", disposition
|
SELECT id, created_at, updated_at, "uuid", store, filename, content_type, content_id, model_id, model_type, disposition, "size", meta
|
||||||
FROM media
|
FROM media
|
||||||
WHERE model_type = $1
|
WHERE model_type = $1
|
||||||
AND model_id = $2;
|
AND model_id = $2;
|
||||||
|
|
||||||
-- name: get-unlinked-message-media
|
-- name: get-unlinked-message-media
|
||||||
SELECT id, created_at, "uuid", store, filename, content_type, model_id, model_type, "size", disposition
|
SELECT id, created_at, updated_at, "uuid", store, filename, content_type, content_id, model_id, model_type, disposition, "size", meta
|
||||||
FROM media
|
FROM media
|
||||||
WHERE model_type = 'messages'
|
WHERE model_type = 'messages'
|
||||||
AND (model_id IS NULL OR model_id = 0)
|
AND (model_id IS NULL OR model_id = 0)
|
||||||
|
@@ -4,6 +4,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// providerLogos holds known provider logos.
|
||||||
|
var providerLogos = map[string]string{
|
||||||
|
"Google": "/images/google-logo.png",
|
||||||
|
"Custom": "",
|
||||||
|
}
|
||||||
|
|
||||||
// OIDC represents an OpenID Connect configuration.
|
// OIDC represents an OpenID Connect configuration.
|
||||||
type OIDC struct {
|
type OIDC struct {
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
@@ -19,12 +25,6 @@ type OIDC struct {
|
|||||||
ProviderLogoURL string `db:"-" json:"logo_url"`
|
ProviderLogoURL string `db:"-" json:"logo_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// providerLogos holds known provider logos.
|
|
||||||
var providerLogos = map[string]string{
|
|
||||||
"Google": "/images/google-logo.png",
|
|
||||||
"Custom": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetProviderLogo provider logo to the OIDC model.
|
// SetProviderLogo provider logo to the OIDC model.
|
||||||
func (oidc *OIDC) SetProviderLogo() {
|
func (oidc *OIDC) SetProviderLogo() {
|
||||||
for provider, logo := range providerLogos {
|
for provider, logo := range providerLogos {
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
-- name: get-all-oidc
|
-- name: get-all-oidc
|
||||||
SELECT id, created_at, updated_at, name, provider, client_id, client_secret, provider_url, enabled FROM oidc order by updated_at desc;
|
SELECT id, created_at, updated_at, name, provider_url, client_id, client_secret, enabled, provider FROM oidc order by updated_at desc;
|
||||||
|
|
||||||
-- name: get-all-enabled
|
-- name: get-all-enabled
|
||||||
|
-- Skips the `client_secret` and returns all enabled OIDC configurations for login
|
||||||
SELECT id, name, enabled, provider, client_id, updated_at FROM oidc WHERE enabled = true order by updated_at desc;
|
SELECT id, name, enabled, provider, client_id, updated_at FROM oidc WHERE enabled = true order by updated_at desc;
|
||||||
|
|
||||||
-- name: get-oidc
|
-- name: get-oidc
|
||||||
SELECT * FROM oidc WHERE id = $1;
|
SELECT id, created_at, updated_at, name, provider_url, client_id, client_secret, enabled, provider FROM oidc WHERE id = $1;
|
||||||
|
|
||||||
-- name: insert-oidc
|
-- name: insert-oidc
|
||||||
INSERT INTO oidc (name, provider, provider_url, client_id, client_secret)
|
INSERT INTO oidc (name, provider, provider_url, client_id, client_secret)
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
SELECT id, created_at, updated_at, name, description, permissions 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 id, created_at, updated_at, name, description, permissions FROM roles where id = $1;
|
||||||
|
|
||||||
-- name: delete-role
|
-- name: delete-role
|
||||||
DELETE FROM roles where id = $1;
|
DELETE FROM roles where id = $1;
|
||||||
|
@@ -153,7 +153,7 @@ func (u *Manager) filterValidPermissions(permissions []string) ([]string, error)
|
|||||||
if amodels.PermissionExists(perm) {
|
if amodels.PermissionExists(perm) {
|
||||||
validPermissions = append(validPermissions, perm)
|
validPermissions = append(validPermissions, perm)
|
||||||
} else {
|
} else {
|
||||||
u.lo.Warn("ignoring unknown permission", "permission", perm)
|
u.lo.Warn("skipping unknown permission for role", "permission", perm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return validPermissions, nil
|
return validPermissions, nil
|
||||||
|
@@ -2,14 +2,14 @@ package models
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type Conversation struct {
|
type ConversationResult struct {
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
UUID string `db:"uuid" json:"uuid"`
|
UUID string `db:"uuid" json:"uuid"`
|
||||||
ReferenceNumber string `db:"reference_number" json:"reference_number"`
|
ReferenceNumber string `db:"reference_number" json:"reference_number"`
|
||||||
Subject string `db:"subject" json:"subject"`
|
Subject string `db:"subject" json:"subject"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type MessageResult struct {
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
TextContent string `db:"text_content" json:"text_content"`
|
TextContent string `db:"text_content" json:"text_content"`
|
||||||
ConversationCreatedAt time.Time `db:"conversation_created_at" json:"conversation_created_at"`
|
ConversationCreatedAt time.Time `db:"conversation_created_at" json:"conversation_created_at"`
|
||||||
@@ -17,7 +17,7 @@ type Message struct {
|
|||||||
ConversationReferenceNumber string `db:"conversation_reference_number" json:"conversation_reference_number"`
|
ConversationReferenceNumber string `db:"conversation_reference_number" json:"conversation_reference_number"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Contact struct {
|
type ContactResult struct {
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
FirstName string `db:"first_name" json:"first_name"`
|
FirstName string `db:"first_name" json:"first_name"`
|
||||||
LastName string `db:"last_name" json:"last_name"`
|
LastName string `db:"last_name" json:"last_name"`
|
||||||
|
@@ -13,73 +13,73 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed queries.sql
|
//go:embed queries.sql
|
||||||
efs embed.FS
|
efs embed.FS
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager is the search manager
|
// Manager is the search manager
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
q queries
|
q queries
|
||||||
lo *logf.Logger
|
lo *logf.Logger
|
||||||
i18n *i18n.I18n
|
i18n *i18n.I18n
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opts contains the options for creating a new search manager
|
// Opts contains the options for creating a new search manager
|
||||||
type Opts struct {
|
type Opts struct {
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
Lo *logf.Logger
|
Lo *logf.Logger
|
||||||
I18n *i18n.I18n
|
I18n *i18n.I18n
|
||||||
}
|
}
|
||||||
|
|
||||||
// queries contains all the prepared queries
|
// queries contains all the prepared queries
|
||||||
type queries struct {
|
type queries struct {
|
||||||
SearchConversationsByRefNum *sqlx.Stmt `query:"search-conversations-by-reference-number"`
|
SearchConversationsByRefNum *sqlx.Stmt `query:"search-conversations-by-reference-number"`
|
||||||
SearchConversationsByContactEmail *sqlx.Stmt `query:"search-conversations-by-contact-email"`
|
SearchConversationsByContactEmail *sqlx.Stmt `query:"search-conversations-by-contact-email"`
|
||||||
SearchMessages *sqlx.Stmt `query:"search-messages"`
|
SearchMessages *sqlx.Stmt `query:"search-messages"`
|
||||||
SearchContacts *sqlx.Stmt `query:"search-contacts"`
|
SearchContacts *sqlx.Stmt `query:"search-contacts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new search manager
|
// New creates a new search manager
|
||||||
func New(opts Opts) (*Manager, error) {
|
func New(opts Opts) (*Manager, error) {
|
||||||
var q queries
|
var q queries
|
||||||
if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
|
if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Manager{q: q, lo: opts.Lo, i18n: opts.I18n}, nil
|
return &Manager{q: q, lo: opts.Lo, i18n: opts.I18n}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversations searches conversations based on the query
|
// Conversations searches conversations based on the query
|
||||||
func (s *Manager) Conversations(query string) ([]models.Conversation, error) {
|
func (s *Manager) Conversations(query string) ([]models.ConversationResult, error) {
|
||||||
var refNumResults = make([]models.Conversation, 0)
|
var refNumResults = make([]models.ConversationResult, 0)
|
||||||
if err := s.q.SearchConversationsByRefNum.Select(&refNumResults, query); err != nil {
|
if err := s.q.SearchConversationsByRefNum.Select(&refNumResults, query); err != nil {
|
||||||
s.lo.Error("error searching conversations", "error", err)
|
s.lo.Error("error searching conversations", "error", err)
|
||||||
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.conversation")), nil)
|
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.conversation")), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var emailResults = make([]models.Conversation, 0)
|
var emailResults = make([]models.ConversationResult, 0)
|
||||||
if err := s.q.SearchConversationsByContactEmail.Select(&emailResults, query); err != nil {
|
if err := s.q.SearchConversationsByContactEmail.Select(&emailResults, query); err != nil {
|
||||||
s.lo.Error("error searching conversations", "error", err)
|
s.lo.Error("error searching conversations", "error", err)
|
||||||
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.conversation")), nil)
|
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.conversation")), nil)
|
||||||
}
|
}
|
||||||
return append(refNumResults, emailResults...), nil
|
return append(refNumResults, emailResults...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages searches messages based on the query
|
// Messages searches messages based on the query
|
||||||
func (s *Manager) Messages(query string) ([]models.Message, error) {
|
func (s *Manager) Messages(query string) ([]models.MessageResult, error) {
|
||||||
var results = make([]models.Message, 0)
|
var results = make([]models.MessageResult, 0)
|
||||||
if err := s.q.SearchMessages.Select(&results, query); err != nil {
|
if err := s.q.SearchMessages.Select(&results, query); err != nil {
|
||||||
s.lo.Error("error searching messages", "error", err)
|
s.lo.Error("error searching messages", "error", err)
|
||||||
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.message")), nil)
|
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.message")), nil)
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contacts searches contacts based on the query
|
// Contacts searches contacts based on the query
|
||||||
func (s *Manager) Contacts(query string) ([]models.Contact, error) {
|
func (s *Manager) Contacts(query string) ([]models.ContactResult, error) {
|
||||||
var results = make([]models.Contact, 0)
|
var results = make([]models.ContactResult, 0)
|
||||||
if err := s.q.SearchContacts.Select(&results, query); err != nil {
|
if err := s.q.SearchContacts.Select(&results, query); err != nil {
|
||||||
s.lo.Error("error searching contacts", "error", err)
|
s.lo.Error("error searching contacts", "error", err)
|
||||||
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.contact")), nil)
|
return nil, envelope.NewError(envelope.GeneralError, s.i18n.Ts("globals.messages.errorSearching", "name", s.i18n.Ts("globals.terms.contact")), nil)
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
@@ -85,8 +85,8 @@ func (m *Manager) GetAllJSON() (types.JSONText, error) {
|
|||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates settings.
|
// Update updates settings with the passed values.
|
||||||
func (m *Manager) Update(s interface{}) error {
|
func (m *Manager) Update(s any) error {
|
||||||
// Marshal settings.
|
// Marshal settings.
|
||||||
b, err := json.Marshal(s)
|
b, err := json.Marshal(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -109,7 +109,7 @@ func (m *Manager) Update(s interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByPrefix retrieves settings by prefix as JSON.
|
// GetByPrefix retrieves all settings start with the given prefix.
|
||||||
func (m *Manager) GetByPrefix(prefix string) (types.JSONText, error) {
|
func (m *Manager) GetByPrefix(prefix string) (types.JSONText, error) {
|
||||||
var b types.JSONText
|
var b types.JSONText
|
||||||
if err := m.q.GetByPrefix.Get(&b, prefix+"%"); err != nil {
|
if err := m.q.GetByPrefix.Get(&b, prefix+"%"); err != nil {
|
||||||
|
@@ -144,13 +144,6 @@ func GenerateEmailMessageID(messageID string, fromAddress string) (string, error
|
|||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReverseSlice reverses a slice of strings in place.
|
|
||||||
func ReverseSlice(source []string) {
|
|
||||||
for i, j := 0, len(source)-1; i < j; i, j = i+1, j-1 {
|
|
||||||
source[i], source[j] = source[j], source[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveItemByValue removes all instances of a value from a slice of strings.
|
// RemoveItemByValue removes all instances of a value from a slice of strings.
|
||||||
func RemoveItemByValue(slice []string, value string) []string {
|
func RemoveItemByValue(slice []string, value string) []string {
|
||||||
result := []string{}
|
result := []string{}
|
||||||
|
@@ -5,46 +5,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReverseSlice(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []string
|
|
||||||
expected []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty slice",
|
|
||||||
input: []string{},
|
|
||||||
expected: []string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single element",
|
|
||||||
input: []string{"a"},
|
|
||||||
expected: []string{"a"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple elements",
|
|
||||||
input: []string{"a", "b", "c"},
|
|
||||||
expected: []string{"c", "b", "a"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
input := make([]string, len(tt.input))
|
|
||||||
copy(input, tt.input)
|
|
||||||
ReverseSlice(input)
|
|
||||||
if len(input) != len(tt.expected) {
|
|
||||||
t.Errorf("got len %d, want %d", len(input), len(tt.expected))
|
|
||||||
}
|
|
||||||
for i := range input {
|
|
||||||
if input[i] != tt.expected[i] {
|
|
||||||
t.Errorf("at index %d got %s, want %s", i, input[i], tt.expected[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveItemByValue(t *testing.T) {
|
func TestRemoveItemByValue(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@@ -15,13 +15,25 @@ type Team struct {
|
|||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
Emoji null.String `db:"emoji" json:"emoji"`
|
Emoji null.String `db:"emoji" json:"emoji"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
ConversationAssignmentType string `db:"conversation_assignment_type" json:"conversation_assignment_type,omitempty"`
|
ConversationAssignmentType string `db:"conversation_assignment_type" json:"conversation_assignment_type"`
|
||||||
Timezone string `db:"timezone" json:"timezone,omitempty"`
|
Timezone string `db:"timezone" json:"timezone"`
|
||||||
BusinessHoursID null.Int `db:"business_hours_id" json:"business_hours_id,omitempty"`
|
BusinessHoursID null.Int `db:"business_hours_id" json:"business_hours_id"`
|
||||||
SLAPolicyID null.Int `db:"sla_policy_id" json:"sla_policy_id,omitempty"`
|
SLAPolicyID null.Int `db:"sla_policy_id" json:"sla_policy_id"`
|
||||||
MaxAutoAssignedConversations int `db:"max_auto_assigned_conversations" json:"max_auto_assigned_conversations"`
|
MaxAutoAssignedConversations int `db:"max_auto_assigned_conversations" json:"max_auto_assigned_conversations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamCompact struct {
|
||||||
|
ID int `db:"id" json:"id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Emoji null.String `db:"emoji" json:"emoji"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamMember struct {
|
||||||
|
ID int `db:"id" json:"id"`
|
||||||
|
AvailabilityStatus string `db:"availability_status" json:"availability_status"`
|
||||||
|
TeamID int `db:"team_id" json:"team_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type Teams []Team
|
type Teams []Team
|
||||||
|
|
||||||
// Scan implements the sql.Scanner interface for Teams
|
// Scan implements the sql.Scanner interface for Teams
|
||||||
@@ -44,15 +56,6 @@ func (t Teams) Value() (driver.Value, error) {
|
|||||||
return json.Marshal(t)
|
return json.Marshal(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Names returns the names of the teams in Teams slice.
|
|
||||||
func (t Teams) Names() []string {
|
|
||||||
names := make([]string, len(t))
|
|
||||||
for i, team := range t {
|
|
||||||
names[i] = team.Name
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// IDs returns a slice of all team IDs in the Teams slice.
|
// IDs returns a slice of all team IDs in the Teams slice.
|
||||||
func (t Teams) IDs() []int {
|
func (t Teams) IDs() []int {
|
||||||
ids := make([]int, len(t))
|
ids := make([]int, len(t))
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
-- name: get-teams
|
-- name: get-teams
|
||||||
SELECT id, emoji, created_at, updated_at, name, conversation_assignment_type, timezone, max_auto_assigned_conversations from teams order by updated_at desc;
|
SELECT id, created_at, updated_at, name, emoji, conversation_assignment_type, max_auto_assigned_conversations, business_hours_id, sla_policy_id, timezone from teams order by updated_at desc;
|
||||||
|
|
||||||
-- name: get-teams-compact
|
-- name: get-teams-compact
|
||||||
SELECT id, name, emoji from teams order by name;
|
SELECT id, name, emoji from teams order by name;
|
||||||
|
|
||||||
-- name: get-user-teams
|
-- name: get-user-teams
|
||||||
SELECT id, emoji, created_at, updated_at, name, conversation_assignment_type, timezone, max_auto_assigned_conversations from teams WHERE id IN (SELECT team_id FROM team_members WHERE user_id = $1) order by updated_at desc;
|
SELECT id, created_at, updated_at, name, emoji, conversation_assignment_type, max_auto_assigned_conversations, business_hours_id, sla_policy_id, timezone from teams WHERE id IN (SELECT team_id FROM team_members WHERE user_id = $1) order by updated_at desc;
|
||||||
|
|
||||||
-- name: get-team
|
-- name: get-team
|
||||||
SELECT id, emoji, name, conversation_assignment_type, timezone, business_hours_id, sla_policy_id, max_auto_assigned_conversations from teams where id = $1;
|
SELECT id, created_at, updated_at, name, emoji, conversation_assignment_type, max_auto_assigned_conversations, business_hours_id, sla_policy_id, timezone from teams where id = $1;
|
||||||
|
|
||||||
-- name: get-team-members
|
-- name: get-team-members
|
||||||
SELECT u.id, t.id as team_id, u.availability_status
|
SELECT u.id, t.id as team_id, u.availability_status
|
||||||
|
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/abhinavxd/libredesk/internal/dbutil"
|
"github.com/abhinavxd/libredesk/internal/dbutil"
|
||||||
"github.com/abhinavxd/libredesk/internal/envelope"
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
||||||
"github.com/abhinavxd/libredesk/internal/team/models"
|
"github.com/abhinavxd/libredesk/internal/team/models"
|
||||||
umodels "github.com/abhinavxd/libredesk/internal/user/models"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/knadh/go-i18n"
|
"github.com/knadh/go-i18n"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
@@ -78,8 +77,8 @@ func (u *Manager) GetAll() ([]models.Team, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAllCompact retrieves all teams with limited fields.
|
// GetAllCompact retrieves all teams with limited fields.
|
||||||
func (u *Manager) GetAllCompact() ([]models.Team, error) {
|
func (u *Manager) GetAllCompact() ([]models.TeamCompact, error) {
|
||||||
var teams = make([]models.Team, 0)
|
var teams = make([]models.TeamCompact, 0)
|
||||||
if err := u.q.GetTeamsCompact.Select(&teams); err != nil {
|
if err := u.q.GetTeamsCompact.Select(&teams); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return teams, nil
|
return teams, nil
|
||||||
@@ -169,14 +168,14 @@ func (u *Manager) UserBelongsToTeam(teamID, userID int) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMembers retrieves members of a team.
|
// GetMembers retrieves members of a team.
|
||||||
func (u *Manager) GetMembers(id int) ([]umodels.User, error) {
|
func (u *Manager) GetMembers(id int) ([]models.TeamMember, error) {
|
||||||
var users []umodels.User
|
var members = make([]models.TeamMember, 0)
|
||||||
if err := u.q.GetTeamMembers.Select(&users, id); err != nil {
|
if err := u.q.GetTeamMembers.Select(&members, id); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return users, nil
|
return members, nil
|
||||||
}
|
}
|
||||||
u.lo.Error("error fetching team members", "team_id", id, "error", err)
|
u.lo.Error("error fetching team members", "team_id", id, "error", err)
|
||||||
return users, fmt.Errorf("fetching team members: %w", err)
|
return members, fmt.Errorf("fetching team members: %w", err)
|
||||||
}
|
}
|
||||||
return users, nil
|
return members, nil
|
||||||
}
|
}
|
||||||
|
@@ -19,19 +19,19 @@ WITH u AS (
|
|||||||
SELECT * FROM u LIMIT 1;
|
SELECT * FROM u LIMIT 1;
|
||||||
|
|
||||||
-- name: get-default
|
-- name: get-default
|
||||||
SELECT id, type, name, body, subject FROM templates WHERE is_default is TRUE;
|
SELECT id, created_at, updated_at, type, body, is_default, name, subject, is_builtin FROM templates WHERE is_default is TRUE;
|
||||||
|
|
||||||
-- name: get-all
|
-- name: get-all
|
||||||
SELECT id, created_at, updated_at, type, name, is_default, is_builtin FROM templates WHERE type = $1 ORDER BY updated_at DESC;
|
SELECT id, created_at, updated_at, type, body, is_default, name, subject, 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, created_at, updated_at, type, body, is_default, name, subject, is_builtin FROM templates WHERE id = $1;
|
||||||
|
|
||||||
-- name: delete
|
-- name: delete
|
||||||
DELETE FROM templates WHERE id = $1;
|
DELETE FROM templates WHERE id = $1;
|
||||||
|
|
||||||
-- name: get-by-name
|
-- name: get-by-name
|
||||||
SELECT id, type, name, body, subject, is_default, type FROM templates WHERE name = $1;
|
SELECT id, created_at, updated_at, type, body, is_default, name, subject, is_builtin FROM templates WHERE name = $1;
|
||||||
|
|
||||||
-- name: is-builtin
|
-- name: is-builtin
|
||||||
SELECT EXISTS(SELECT 1 FROM templates WHERE id = $1 AND is_builtin is TRUE);
|
SELECT EXISTS(SELECT 1 FROM templates WHERE id = $1 AND is_builtin is TRUE);
|
@@ -2,8 +2,6 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -69,13 +67,10 @@ func (u *Manager) InvalidateAgentCache(id int) {
|
|||||||
delete(u.agentCache, id)
|
delete(u.agentCache, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAgentsCompact returns a compact list of users with limited fields.
|
// GetAgentsCompact returns a compact list of agents with limited fields.
|
||||||
func (u *Manager) GetAgentsCompact() ([]models.User, error) {
|
func (u *Manager) GetAgentsCompact() ([]models.UserCompact, error) {
|
||||||
var users = make([]models.User, 0)
|
var users = make([]models.UserCompact, 0)
|
||||||
if err := u.q.GetAgentsCompact.Select(&users); err != nil {
|
if err := u.db.Select(&users, u.q.GetUsersCompact, pq.Array([]string{models.UserTypeAgent})); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
u.lo.Error("error fetching users from db", "error", err)
|
u.lo.Error("error fetching users from db", "error", err)
|
||||||
return users, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorFetching", "name", u.i18n.P("globals.terms.user")), nil)
|
return users, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorFetching", "name", u.i18n.P("globals.terms.user")), nil)
|
||||||
}
|
}
|
||||||
@@ -83,36 +78,39 @@ func (u *Manager) GetAgentsCompact() ([]models.User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateAgent creates a new agent user.
|
// CreateAgent creates a new agent user.
|
||||||
func (u *Manager) CreateAgent(user *models.User) (error) {
|
func (u *Manager) CreateAgent(firstName, lastName, email string, roles []string) (models.User, error) {
|
||||||
password, err := u.generatePassword()
|
password, err := u.generatePassword()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.lo.Error("error generating password", "error", err)
|
u.lo.Error("error generating password", "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}"), nil)
|
return models.User{}, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}"), nil)
|
||||||
}
|
}
|
||||||
user.Email = null.NewString(strings.TrimSpace(strings.ToLower(user.Email.String)), user.Email.Valid)
|
|
||||||
if err := u.q.InsertAgent.QueryRow(user.Email, user.FirstName, user.LastName, password, user.AvatarURL, pq.Array(user.Roles)).Scan(&user.ID); err != nil {
|
var id = 0
|
||||||
|
avatarURL := null.String{}
|
||||||
|
email = strings.TrimSpace(strings.ToLower(email))
|
||||||
|
if err := u.q.InsertAgent.QueryRow(email, firstName, lastName, password, avatarURL, pq.Array(roles)).Scan(&id); err != nil {
|
||||||
if dbutil.IsUniqueViolationError(err) {
|
if dbutil.IsUniqueViolationError(err) {
|
||||||
return envelope.NewError(envelope.GeneralError, u.i18n.T("user.sameEmailAlreadyExists"), nil)
|
return models.User{}, envelope.NewError(envelope.GeneralError, u.i18n.T("user.sameEmailAlreadyExists"), nil)
|
||||||
}
|
}
|
||||||
u.lo.Error("error creating user", "error", err)
|
u.lo.Error("error creating user", "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}"), nil)
|
return models.User{}, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}"), nil)
|
||||||
}
|
}
|
||||||
return nil
|
return u.Get(id, "", models.UserTypeAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAgent updates an agent in the database, including their password if provided.
|
// UpdateAgent updates an agent with individual field parameters
|
||||||
func (u *Manager) UpdateAgent(id int, user models.User) error {
|
func (u *Manager) UpdateAgent(id int, firstName, lastName, email string, roles []string, enabled bool, availabilityStatus, newPassword string) error {
|
||||||
var (
|
var (
|
||||||
hashedPassword any
|
hashedPassword any
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set password?
|
// Set password?
|
||||||
if user.NewPassword != "" {
|
if newPassword != "" {
|
||||||
if !IsStrongPassword(user.NewPassword) {
|
if !IsStrongPassword(newPassword) {
|
||||||
return envelope.NewError(envelope.InputError, PasswordHint, nil)
|
return envelope.NewError(envelope.InputError, PasswordHint, nil)
|
||||||
}
|
}
|
||||||
hashedPassword, err = bcrypt.GenerateFromPassword([]byte(user.NewPassword), bcrypt.DefaultCost)
|
hashedPassword, err = bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.lo.Error("error generating bcrypt password", "error", err)
|
u.lo.Error("error generating bcrypt password", "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}"), nil)
|
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}"), nil)
|
||||||
@@ -121,7 +119,7 @@ func (u *Manager) UpdateAgent(id int, user models.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update user in the database and clear cache.
|
// Update user in the database and clear cache.
|
||||||
if _, err := u.q.UpdateAgent.Exec(id, user.FirstName, user.LastName, user.Email, pq.Array(user.Roles), user.AvatarURL, hashedPassword, user.Enabled, user.AvailabilityStatus); err != nil {
|
if _, err := u.q.UpdateAgent.Exec(id, firstName, lastName, email, pq.Array(roles), null.String{}, hashedPassword, enabled, availabilityStatus); err != nil {
|
||||||
u.lo.Error("error updating user", "error", err)
|
u.lo.Error("error updating user", "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}"), nil)
|
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}"), nil)
|
||||||
}
|
}
|
||||||
@@ -159,7 +157,7 @@ func (u *Manager) markInactiveAgentsOffline() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAgents returns a list of all agents.
|
// GetAllAgents returns a list of all agents.
|
||||||
func (u *Manager) GetAgents() ([]models.User, error) {
|
func (u *Manager) GetAgents() ([]models.UserCompact, error) {
|
||||||
// Some dirty hack.
|
// Some dirty hack.
|
||||||
return u.GetAllUsers(1, 999999999, models.UserTypeAgent, "desc", "users.updated_at", "")
|
return u.GetAllUsers(1, 999999999, models.UserTypeAgent, "desc", "users.updated_at", "")
|
||||||
}
|
}
|
||||||
|
@@ -42,7 +42,7 @@ func (u *Manager) GetContact(id int, email string) (models.User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAllContacts returns a list of all contacts.
|
// GetAllContacts returns a list of all contacts.
|
||||||
func (u *Manager) GetContacts(page, pageSize int, order, orderBy string, filtersJSON string) ([]models.User, error) {
|
func (u *Manager) GetContacts(page, pageSize int, order, orderBy string, filtersJSON string) ([]models.UserCompact, error) {
|
||||||
if pageSize > maxListPageSize {
|
if pageSize > maxListPageSize {
|
||||||
return nil, envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.pageTooLarge", "max", fmt.Sprintf("%d", maxListPageSize)), nil)
|
return nil, envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.pageTooLarge", "max", fmt.Sprintf("%d", maxListPageSize)), nil)
|
||||||
}
|
}
|
||||||
|
@@ -30,6 +30,20 @@ const (
|
|||||||
AwayAndReassigning = "away_and_reassigning"
|
AwayAndReassigning = "away_and_reassigning"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UserCompact struct {
|
||||||
|
ID int `db:"id" json:"id"`
|
||||||
|
Type string `db:"type" json:"type"`
|
||||||
|
FirstName string `db:"first_name" json:"first_name"`
|
||||||
|
LastName string `db:"last_name" json:"last_name"`
|
||||||
|
Email null.String `db:"email" json:"email"`
|
||||||
|
Enabled bool `db:"enabled" json:"enabled"`
|
||||||
|
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
|
||||||
|
Total int `db:"total" json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
@@ -48,7 +62,6 @@ type User struct {
|
|||||||
LastLoginAt null.Time `db:"last_login_at" json:"last_login_at"`
|
LastLoginAt null.Time `db:"last_login_at" json:"last_login_at"`
|
||||||
Roles pq.StringArray `db:"roles" json:"roles"`
|
Roles pq.StringArray `db:"roles" json:"roles"`
|
||||||
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
||||||
Meta pq.StringArray `db:"meta" json:"meta"`
|
|
||||||
CustomAttributes json.RawMessage `db:"custom_attributes" json:"custom_attributes"`
|
CustomAttributes json.RawMessage `db:"custom_attributes" json:"custom_attributes"`
|
||||||
Teams tmodels.Teams `db:"teams" json:"teams"`
|
Teams tmodels.Teams `db:"teams" json:"teams"`
|
||||||
ContactChannelID int `db:"contact_channel_id" json:"contact_channel_id,omitempty"`
|
ContactChannelID int `db:"contact_channel_id" json:"contact_channel_id,omitempty"`
|
||||||
@@ -62,8 +75,6 @@ type User struct {
|
|||||||
APIKey null.String `db:"api_key" json:"api_key"`
|
APIKey null.String `db:"api_key" json:"api_key"`
|
||||||
APIKeyLastUsedAt null.Time `db:"api_key_last_used_at" json:"api_key_last_used_at"`
|
APIKeyLastUsedAt null.Time `db:"api_key_last_used_at" json:"api_key_last_used_at"`
|
||||||
APISecret null.String `db:"api_secret" json:"-"`
|
APISecret null.String `db:"api_secret" json:"-"`
|
||||||
|
|
||||||
Total int `json:"total,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Note struct {
|
type Note struct {
|
||||||
|
@@ -26,12 +26,13 @@ func (u *Manager) GetNote(id int) (models.Note, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateNote creates a new note for a user.
|
// CreateNote creates a new note for a user.
|
||||||
func (u *Manager) CreateNote(userID, authorID int, note string) error {
|
func (u *Manager) CreateNote(userID, authorID int, note string) (models.Note, error) {
|
||||||
if _, err := u.q.InsertNote.Exec(userID, authorID, note); err != nil {
|
var createdNote models.Note
|
||||||
|
if err := u.q.InsertNote.Get(&createdNote, userID, authorID, note); err != nil {
|
||||||
u.lo.Error("error creating user note", "error", err)
|
u.lo.Error("error creating user note", "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", u.i18n.P("globals.terms.note")), nil)
|
return createdNote, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", u.i18n.P("globals.terms.note")), nil)
|
||||||
}
|
}
|
||||||
return nil
|
return createdNote, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteNote deletes a note for a user.
|
// DeleteNote deletes a note for a user.
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
-- name: get-users
|
-- name: get-users-compact
|
||||||
|
-- TODO: Remove hardcoded `type` of user in some queries in this file.
|
||||||
SELECT COUNT(*) OVER() as total, users.id, users.avatar_url, users.type, users.created_at, users.updated_at, users.first_name, users.last_name, users.email, users.enabled
|
SELECT COUNT(*) OVER() as total, users.id, users.avatar_url, users.type, users.created_at, users.updated_at, users.first_name, users.last_name, users.email, users.enabled
|
||||||
FROM users
|
FROM users
|
||||||
WHERE users.email != 'System' AND users.deleted_at IS NULL AND type = $1
|
WHERE users.email != 'System' AND users.deleted_at IS NULL AND type = ANY($1)
|
||||||
|
|
||||||
-- name: soft-delete-agent
|
-- name: soft-delete-agent
|
||||||
WITH soft_delete AS (
|
WITH soft_delete AS (
|
||||||
@@ -23,12 +24,6 @@ delete_user_roles AS (
|
|||||||
)
|
)
|
||||||
SELECT 1;
|
SELECT 1;
|
||||||
|
|
||||||
-- name: get-agents-compact
|
|
||||||
SELECT u.id, u.type, u.first_name, u.last_name, u.enabled, u.avatar_url
|
|
||||||
FROM users u
|
|
||||||
WHERE u.email != 'System' AND u.deleted_at IS NULL AND u.type = 'agent'
|
|
||||||
ORDER BY u.updated_at DESC;
|
|
||||||
|
|
||||||
-- name: get-user
|
-- name: get-user
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
@@ -37,8 +32,6 @@ SELECT
|
|||||||
u.email,
|
u.email,
|
||||||
u.password,
|
u.password,
|
||||||
u.type,
|
u.type,
|
||||||
u.created_at,
|
|
||||||
u.updated_at,
|
|
||||||
u.enabled,
|
u.enabled,
|
||||||
u.avatar_url,
|
u.avatar_url,
|
||||||
u.first_name,
|
u.first_name,
|
||||||
@@ -50,6 +43,7 @@ SELECT
|
|||||||
u.phone_number,
|
u.phone_number,
|
||||||
u.api_key,
|
u.api_key,
|
||||||
u.api_key_last_used_at,
|
u.api_key_last_used_at,
|
||||||
|
u.api_secret,
|
||||||
array_agg(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) AS roles,
|
array_agg(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) AS roles,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
(SELECT json_agg(json_build_object('id', t.id, 'name', t.name, 'emoji', t.emoji))
|
(SELECT json_agg(json_build_object('id', t.id, 'name', t.name, 'emoji', t.emoji))
|
||||||
@@ -135,7 +129,7 @@ WHERE id = $1 AND type = 'agent';
|
|||||||
-- name: set-password
|
-- name: set-password
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET password = $1, reset_password_token = NULL, reset_password_token_expiry = NULL
|
SET password = $1, reset_password_token = NULL, reset_password_token_expiry = NULL
|
||||||
WHERE reset_password_token = $2 AND reset_password_token_expiry > now() AND type = 'agent';
|
WHERE reset_password_token = $2 AND reset_password_token_expiry > now();
|
||||||
|
|
||||||
-- name: insert-agent
|
-- name: insert-agent
|
||||||
WITH inserted_user AS (
|
WITH inserted_user AS (
|
||||||
@@ -202,7 +196,8 @@ ORDER BY cn.created_at DESC;
|
|||||||
|
|
||||||
-- name: insert-note
|
-- name: insert-note
|
||||||
INSERT INTO contact_notes (contact_id, user_id, note)
|
INSERT INTO contact_notes (contact_id, user_id, note)
|
||||||
VALUES ($1, $2, $3);
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
-- name: delete-note
|
-- name: delete-note
|
||||||
DELETE FROM contact_notes
|
DELETE FROM contact_notes
|
||||||
@@ -229,6 +224,7 @@ SELECT
|
|||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at,
|
u.updated_at,
|
||||||
u.email,
|
u.email,
|
||||||
|
u.password,
|
||||||
u.type,
|
u.type,
|
||||||
u.enabled,
|
u.enabled,
|
||||||
u.avatar_url,
|
u.avatar_url,
|
||||||
@@ -239,6 +235,8 @@ SELECT
|
|||||||
u.last_login_at,
|
u.last_login_at,
|
||||||
u.phone_number_calling_code,
|
u.phone_number_calling_code,
|
||||||
u.phone_number,
|
u.phone_number,
|
||||||
|
u.api_key,
|
||||||
|
u.api_key_last_used_at,
|
||||||
u.api_secret,
|
u.api_secret,
|
||||||
array_agg(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) AS roles,
|
array_agg(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) AS roles,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
@@ -256,7 +254,7 @@ LEFT JOIN LATERAL unnest(r.permissions) AS p ON true
|
|||||||
WHERE u.api_key = $1 AND u.enabled = true AND u.deleted_at IS NULL
|
WHERE u.api_key = $1 AND u.enabled = true AND u.deleted_at IS NULL
|
||||||
GROUP BY u.id;
|
GROUP BY u.id;
|
||||||
|
|
||||||
-- name: generate-api-key
|
-- name: set-api-key
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET api_key = $2, api_secret = $3, api_key_last_used_at = NULL, updated_at = now()
|
SET api_key = $2, api_secret = $3, api_key_last_used_at = NULL, updated_at = now()
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/abhinavxd/libredesk/internal/user/models"
|
"github.com/abhinavxd/libredesk/internal/user/models"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/knadh/go-i18n"
|
"github.com/knadh/go-i18n"
|
||||||
|
"github.com/lib/pq"
|
||||||
"github.com/volatiletech/null/v9"
|
"github.com/volatiletech/null/v9"
|
||||||
"github.com/zerodha/logf"
|
"github.com/zerodha/logf"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -61,10 +62,9 @@ type Opts struct {
|
|||||||
// queries contains prepared SQL queries.
|
// queries contains prepared SQL queries.
|
||||||
type queries struct {
|
type queries struct {
|
||||||
GetUser *sqlx.Stmt `query:"get-user"`
|
GetUser *sqlx.Stmt `query:"get-user"`
|
||||||
GetUsers string `query:"get-users"`
|
|
||||||
GetNotes *sqlx.Stmt `query:"get-notes"`
|
GetNotes *sqlx.Stmt `query:"get-notes"`
|
||||||
GetNote *sqlx.Stmt `query:"get-note"`
|
GetNote *sqlx.Stmt `query:"get-note"`
|
||||||
GetAgentsCompact *sqlx.Stmt `query:"get-agents-compact"`
|
GetUsersCompact string `query:"get-users-compact"`
|
||||||
UpdateContact *sqlx.Stmt `query:"update-contact"`
|
UpdateContact *sqlx.Stmt `query:"update-contact"`
|
||||||
UpdateAgent *sqlx.Stmt `query:"update-agent"`
|
UpdateAgent *sqlx.Stmt `query:"update-agent"`
|
||||||
UpdateCustomAttributes *sqlx.Stmt `query:"update-custom-attributes"`
|
UpdateCustomAttributes *sqlx.Stmt `query:"update-custom-attributes"`
|
||||||
@@ -84,7 +84,7 @@ type queries struct {
|
|||||||
ToggleEnable *sqlx.Stmt `query:"toggle-enable"`
|
ToggleEnable *sqlx.Stmt `query:"toggle-enable"`
|
||||||
// API key queries
|
// API key queries
|
||||||
GetUserByAPIKey *sqlx.Stmt `query:"get-user-by-api-key"`
|
GetUserByAPIKey *sqlx.Stmt `query:"get-user-by-api-key"`
|
||||||
GenerateAPIKey *sqlx.Stmt `query:"generate-api-key"`
|
SetAPIKey *sqlx.Stmt `query:"set-api-key"`
|
||||||
RevokeAPIKey *sqlx.Stmt `query:"revoke-api-key"`
|
RevokeAPIKey *sqlx.Stmt `query:"revoke-api-key"`
|
||||||
UpdateAPIKeyLastUsed *sqlx.Stmt `query:"update-api-key-last-used"`
|
UpdateAPIKeyLastUsed *sqlx.Stmt `query:"update-api-key-last-used"`
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ type queries struct {
|
|||||||
func New(i18n *i18n.I18n, opts Opts) (*Manager, error) {
|
func New(i18n *i18n.I18n, opts Opts) (*Manager, error) {
|
||||||
var q queries
|
var q queries
|
||||||
if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
|
if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error scanning SQL file: %w", err)
|
||||||
}
|
}
|
||||||
return &Manager{
|
return &Manager{
|
||||||
q: q,
|
q: q,
|
||||||
@@ -121,7 +121,7 @@ func (u *Manager) VerifyPassword(email string, password []byte) (models.User, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAllUsers returns a list of all users.
|
// GetAllUsers returns a list of all users.
|
||||||
func (u *Manager) GetAllUsers(page, pageSize int, userType, order, orderBy string, filtersJSON string) ([]models.User, error) {
|
func (u *Manager) GetAllUsers(page, pageSize int, userType, order, orderBy string, filtersJSON string) ([]models.UserCompact, error) {
|
||||||
query, qArgs, err := u.makeUserListQuery(page, pageSize, userType, order, orderBy, filtersJSON)
|
query, qArgs, err := u.makeUserListQuery(page, pageSize, userType, order, orderBy, filtersJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.lo.Error("error creating user list query", "error", err)
|
u.lo.Error("error creating user list query", "error", err)
|
||||||
@@ -139,7 +139,7 @@ func (u *Manager) GetAllUsers(page, pageSize int, userType, order, orderBy strin
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
var users = make([]models.User, 0)
|
var users = make([]models.UserCompact, 0)
|
||||||
if err := tx.Select(&users, query, qArgs...); err != nil {
|
if err := tx.Select(&users, query, qArgs...); err != nil {
|
||||||
u.lo.Error("error fetching users", "error", err)
|
u.lo.Error("error fetching users", "error", err)
|
||||||
return nil, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil)
|
return nil, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil)
|
||||||
@@ -186,6 +186,7 @@ func (u *Manager) UpdateLastLoginAt(id int) error {
|
|||||||
|
|
||||||
// SetResetPasswordToken sets a reset password token for an user and returns the token.
|
// SetResetPasswordToken sets a reset password token for an user and returns the token.
|
||||||
func (u *Manager) SetResetPasswordToken(id int) (string, error) {
|
func (u *Manager) SetResetPasswordToken(id int) (string, error) {
|
||||||
|
// TODO: column `reset_password_token`, does not have a UNIQUE constraint. Add it in a future migration.
|
||||||
token, err := stringutil.RandomAlphanumeric(32)
|
token, err := stringutil.RandomAlphanumeric(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.lo.Error("error generating reset password token", "error", err)
|
u.lo.Error("error generating reset password token", "error", err)
|
||||||
@@ -198,7 +199,7 @@ func (u *Manager) SetResetPasswordToken(id int) (string, error) {
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetPassword sets a new password for an user.
|
// ResetPassword sets a password for a given user's reset password token.
|
||||||
func (u *Manager) ResetPassword(token, password string) error {
|
func (u *Manager) ResetPassword(token, password string) error {
|
||||||
if !IsStrongPassword(password) {
|
if !IsStrongPassword(password) {
|
||||||
return envelope.NewError(envelope.InputError, "Password is not strong enough, "+PasswordHint, nil)
|
return envelope.NewError(envelope.InputError, "Password is not strong enough, "+PasswordHint, nil)
|
||||||
@@ -255,44 +256,6 @@ func (u *Manager) UpdateCustomAttributes(id int, customAttributes map[string]any
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeUserListQuery generates a query to fetch users based on the provided filters.
|
|
||||||
func (u *Manager) makeUserListQuery(page, pageSize int, typ, order, orderBy, filtersJSON string) (string, []interface{}, error) {
|
|
||||||
var (
|
|
||||||
baseQuery = u.q.GetUsers
|
|
||||||
qArgs []any
|
|
||||||
)
|
|
||||||
// Set the type of user to fetch.
|
|
||||||
qArgs = append(qArgs, typ)
|
|
||||||
return dbutil.BuildPaginatedQuery(baseQuery, qArgs, dbutil.PaginationOptions{
|
|
||||||
Order: order,
|
|
||||||
OrderBy: orderBy,
|
|
||||||
Page: page,
|
|
||||||
PageSize: pageSize,
|
|
||||||
}, filtersJSON, dbutil.AllowedFields{
|
|
||||||
"users": {"email", "created_at", "updated_at"},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifyPassword compares the provided password with the stored password hash.
|
|
||||||
func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd); err != nil {
|
|
||||||
u.lo.Error("error verifying password", "error", err)
|
|
||||||
return fmt.Errorf("error verifying password: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generatePassword generates a random password and returns its bcrypt hash.
|
|
||||||
func (u *Manager) generatePassword() ([]byte, error) {
|
|
||||||
password, _ := stringutil.RandomAlphanumeric(70)
|
|
||||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
u.lo.Error("error generating bcrypt password", "error", err)
|
|
||||||
return nil, fmt.Errorf("generating bcrypt password: %w", err)
|
|
||||||
}
|
|
||||||
return bytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToggleEnabled toggles the enabled status of an user.
|
// ToggleEnabled toggles the enabled status of an user.
|
||||||
func (u *Manager) ToggleEnabled(id int, typ string, enabled bool) error {
|
func (u *Manager) ToggleEnabled(id int, typ string, enabled bool) error {
|
||||||
if _, err := u.q.ToggleEnable.Exec(id, typ, enabled); err != nil {
|
if _, err := u.q.ToggleEnable.Exec(id, typ, enabled); err != nil {
|
||||||
@@ -326,7 +289,7 @@ func (u *Manager) GenerateAPIKey(userID int) (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update user with API key.
|
// Update user with API key.
|
||||||
if _, err := u.q.GenerateAPIKey.Exec(userID, apiKey, string(secretHash)); err != nil {
|
if _, err := u.q.SetAPIKey.Exec(userID, apiKey, string(secretHash)); err != nil {
|
||||||
u.lo.Error("error saving API key", "error", err, "user_id", userID)
|
u.lo.Error("error saving API key", "error", err, "user_id", userID)
|
||||||
return "", "", envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorGenerating", "name", "{globals.terms.apiKey}"), nil)
|
return "", "", envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorGenerating", "name", "{globals.terms.apiKey}"), nil)
|
||||||
}
|
}
|
||||||
@@ -469,3 +432,37 @@ func updateSystemUserPassword(db *sqlx.DB, hashedPassword []byte) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeUserListQuery generates a query to fetch users based on the provided filters.
|
||||||
|
func (u *Manager) makeUserListQuery(page, pageSize int, typ, order, orderBy, filtersJSON string) (string, []interface{}, error) {
|
||||||
|
var qArgs []any
|
||||||
|
qArgs = append(qArgs, pq.Array([]string{typ}))
|
||||||
|
return dbutil.BuildPaginatedQuery(u.q.GetUsersCompact, qArgs, dbutil.PaginationOptions{
|
||||||
|
Order: order,
|
||||||
|
OrderBy: orderBy,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
}, filtersJSON, dbutil.AllowedFields{
|
||||||
|
"users": {"email", "created_at", "updated_at"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyPassword compares the provided password with the stored password hash.
|
||||||
|
func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd); err != nil {
|
||||||
|
u.lo.Error("error verifying password", "error", err)
|
||||||
|
return fmt.Errorf("error verifying password: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePassword generates a random password and returns its bcrypt hash.
|
||||||
|
func (u *Manager) generatePassword() ([]byte, error) {
|
||||||
|
password, _ := stringutil.RandomAlphanumeric(70)
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
u.lo.Error("error generating bcrypt password", "error", err)
|
||||||
|
return nil, fmt.Errorf("generating bcrypt password: %w", err)
|
||||||
|
}
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
@@ -37,10 +36,3 @@ const (
|
|||||||
// Test event
|
// Test event
|
||||||
EventWebhookTest WebhookEvent = "webhook.test"
|
EventWebhookTest WebhookEvent = "webhook.test"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebhookPayload represents the payload sent to a webhook
|
|
||||||
type WebhookPayload struct {
|
|
||||||
Event WebhookEvent `json:"event"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
Data json.RawMessage `json:",inline"`
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user