mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 14:15:42 +00:00
664 lines
21 KiB
Go
664 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"mime/multipart"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
|
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
|
"github.com/abhinavxd/libredesk/internal/image"
|
|
mmodels "github.com/abhinavxd/libredesk/internal/media/models"
|
|
notifier "github.com/abhinavxd/libredesk/internal/notification"
|
|
"github.com/abhinavxd/libredesk/internal/stringutil"
|
|
tmpl "github.com/abhinavxd/libredesk/internal/template"
|
|
"github.com/abhinavxd/libredesk/internal/user/models"
|
|
realip "github.com/ferluci/fast-realip"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/volatiletech/null/v9"
|
|
"github.com/zerodha/fastglue"
|
|
)
|
|
|
|
const (
|
|
// Bounds the request body only; the stored file is capped by image.AvatarMaxDim regardless.
|
|
maxAvatarSizeMB = 10
|
|
)
|
|
|
|
type resetPasswordRequest struct {
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
type setPasswordRequest struct {
|
|
Token string `json:"token"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type availabilityRequest struct {
|
|
Status string `json:"status"`
|
|
Source string `json:"source"`
|
|
}
|
|
|
|
const availabilitySourceIdle = "idle"
|
|
|
|
type agentReq struct {
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
SendWelcomeEmail bool `json:"send_welcome_email"`
|
|
Teams []string `json:"teams"`
|
|
Roles []string `json:"roles"`
|
|
Enabled bool `json:"enabled"`
|
|
AvailabilityStatus string `json:"availability_status"`
|
|
NewPassword string `json:"new_password,omitempty"`
|
|
}
|
|
|
|
// handleGetAgents returns all agents.
|
|
func handleGetAgents(r *fastglue.Request) error {
|
|
var app = r.Context.(*App)
|
|
agents, err := app.user.GetAgents()
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(agents)
|
|
}
|
|
|
|
// handleGetAgentsCompact returns agents in a compact format, all of them without page params.
|
|
func handleGetAgentsCompact(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
query = string(r.RequestCtx.QueryArgs().Peek("q"))
|
|
userType = string(r.RequestCtx.QueryArgs().Peek("type"))
|
|
enabledOnly = string(r.RequestCtx.QueryArgs().Peek("enabled")) == "true"
|
|
)
|
|
switch userType {
|
|
case "", models.UserTypeAgent, models.UserTypeAIAssistant:
|
|
default:
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if ids := getIDsParam(r, "ids"); len(ids) > 0 {
|
|
agents, err := app.user.GetAgentsCompactByIDs(ids)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(agents)
|
|
}
|
|
page, pageSize := getOptionalPagination(r)
|
|
agents, err := app.user.GetAgentsCompact(query, userType, enabledOnly, page, pageSize)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(agents)
|
|
}
|
|
|
|
// handleGetAgent returns an agent.
|
|
func handleGetAgent(r *fastglue.Request) error {
|
|
var app = r.Context.(*App)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil || id <= 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
agent, err := app.user.GetAgent(id, "")
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(agent)
|
|
}
|
|
|
|
// handleUpdateAgentAvailability updates the current agent availability.
|
|
func handleUpdateAgentAvailability(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
ip = realip.FromRequest(r.RequestCtx)
|
|
availReq availabilityRequest
|
|
)
|
|
|
|
if err := r.Decode(&availReq, "json"); err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), nil, envelope.InputError)
|
|
}
|
|
|
|
agent, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
if agent.AvailabilityStatus == availReq.Status {
|
|
return r.SendEnvelope(agent)
|
|
}
|
|
|
|
// Idle-driven transitions must never overwrite manual-away states.
|
|
if availReq.Source == availabilitySourceIdle &&
|
|
(agent.AvailabilityStatus == models.AwayManual || agent.AvailabilityStatus == models.AwayAndReassigning) {
|
|
return r.SendEnvelope(agent)
|
|
}
|
|
|
|
if err := app.user.UpdateAvailability(auser.ID, availReq.Status); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
app.user.InvalidateAgentCache(auser.ID)
|
|
|
|
go app.conversation.BroadcastAgentAvailability(auser.ID, availReq.Status)
|
|
|
|
// Skip activity log when returning online from idle-away to avoid log spam.
|
|
if !(agent.AvailabilityStatus == models.Away && availReq.Status == models.Online) {
|
|
if err := app.activityLog.UserAvailability(auser.ID, auser.Email, availReq.Status, ip, "", 0); err != nil {
|
|
app.lo.Error("error creating activity log", "error", err)
|
|
}
|
|
}
|
|
|
|
agent, err = app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
return r.SendEnvelope(agent)
|
|
}
|
|
|
|
// handleGetCurrentAgentTeams returns the teams of current agent.
|
|
func handleGetCurrentAgentTeams(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
)
|
|
teams, err := app.team.GetUserTeams(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(teams)
|
|
}
|
|
|
|
// handleUpdateCurrentAgent updates the current agent.
|
|
func handleUpdateCurrentAgent(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
)
|
|
form, err := r.RequestCtx.MultipartForm()
|
|
if err != nil {
|
|
app.lo.Error("error parsing form data", "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("errors.parsingRequest"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
files, ok := form.File["files"]
|
|
|
|
// Upload avatar?
|
|
if ok && len(files) > 0 {
|
|
agent, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if err := uploadUserAvatar(r, agent, files); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
}
|
|
|
|
// Fetch updated agent and return.
|
|
agent, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
return r.SendEnvelope(agent)
|
|
}
|
|
|
|
// handleCreateAgent creates a new agent.
|
|
func handleCreateAgent(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req = agentReq{}
|
|
)
|
|
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), nil, envelope.InputError)
|
|
}
|
|
|
|
if err := validateAgentRequest(app, &req); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
agent, err := app.user.CreateAgent(req.FirstName, req.LastName, req.Email, req.Roles)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Upsert user teams.
|
|
if len(req.Teams) > 0 {
|
|
app.team.UpsertUserTeams(agent.ID, req.Teams)
|
|
}
|
|
|
|
if req.SendWelcomeEmail {
|
|
// Generate reset token.
|
|
resetToken, err := app.user.SetResetPasswordToken(agent.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Render template and send email.
|
|
content, err := app.tmpl.RenderInMemoryTemplate(tmpl.TmplWelcome, map[string]any{
|
|
"ResetToken": resetToken,
|
|
"Email": req.Email,
|
|
})
|
|
if err != nil {
|
|
app.lo.Error("error rendering template", "error", err)
|
|
}
|
|
|
|
if err := app.notifier.Send(notifier.Message{
|
|
RecipientEmails: []string{req.Email},
|
|
Subject: app.i18n.T("globals.messages.welcomeToLibredesk"),
|
|
Content: content,
|
|
Provider: notifier.ProviderEmail,
|
|
}); err != nil {
|
|
app.lo.Error("error sending notification message", "error", err)
|
|
}
|
|
}
|
|
|
|
// Refetch agent as other details might've changed.
|
|
agent, err = app.user.GetAgent(agent.ID, "")
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
return r.SendEnvelope(agent)
|
|
}
|
|
|
|
// handleUpdateAgent updates an agent.
|
|
func handleUpdateAgent(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req = agentReq{}
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
ip = realip.FromRequest(r.RequestCtx)
|
|
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
)
|
|
if id == 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`id`"), nil, envelope.InputError)
|
|
}
|
|
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), nil, envelope.InputError)
|
|
}
|
|
|
|
if err := validateAgentRequest(app, &req); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
agent, err := app.user.GetAgent(id, "")
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
// AI assistant identity users are managed via the AI assistant endpoints only.
|
|
if agent.Type != models.UserTypeAgent {
|
|
return r.SendErrorEnvelope(fasthttp.StatusNotFound, app.i18n.Ts("globals.messages.notFound", "name", app.i18n.T("globals.terms.agent")), nil, envelope.NotFoundError)
|
|
}
|
|
oldAvailabilityStatus := agent.AvailabilityStatus
|
|
|
|
// Update agent with individual fields
|
|
if err = app.user.UpdateAgent(id, req.FirstName, req.LastName, req.Email, req.Roles, req.Enabled, req.AvailabilityStatus, req.NewPassword); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
app.user.InvalidateAgentCache(id)
|
|
app.wsHub.KickUser(id)
|
|
|
|
// Create activity log if user availability status changed.
|
|
if oldAvailabilityStatus != req.AvailabilityStatus {
|
|
if err := app.activityLog.UserAvailability(auser.ID, auser.Email, req.AvailabilityStatus, ip, req.Email, id); err != nil {
|
|
app.lo.Error("error creating activity log", "error", err)
|
|
}
|
|
}
|
|
|
|
// Log activity if password was changed.
|
|
if req.NewPassword != "" {
|
|
if err := app.activityLog.PasswordSet(auser.ID, auser.Email, ip, id, req.Email); err != nil {
|
|
app.lo.Error("error creating activity log", "error", err)
|
|
}
|
|
}
|
|
|
|
// Upsert agent teams.
|
|
if err := app.team.UpsertUserTeams(id, req.Teams); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// 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.
|
|
func handleDeleteAgent(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
id, err = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
)
|
|
if err != nil || id == 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "{globals.terms.user} `id`"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Disallow if self-deleting.
|
|
if id == auser.ID {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userCannotDeleteSelf"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Soft delete user.
|
|
if err = app.user.SoftDeleteAgent(id); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
defer app.wsHub.KickUser(id)
|
|
defer app.user.InvalidateAgentCache(id)
|
|
|
|
// Unassign all open conversations assigned to the user.
|
|
if err := app.conversation.UnassignOpen(id); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleGetCurrentAgent returns the current logged in agent.
|
|
func handleGetCurrentAgent(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
)
|
|
u, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(u)
|
|
}
|
|
|
|
// handleDeleteCurrentAgentAvatar deletes the current agent's avatar.
|
|
func handleDeleteCurrentAgentAvatar(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
)
|
|
|
|
// Get user
|
|
agent, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Valid str?
|
|
if agent.AvatarURL.String == "" {
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
fileName := filepath.Base(agent.AvatarURL.String)
|
|
|
|
// Delete file from the store.
|
|
if err := app.media.Delete(fileName); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
if err = app.user.UpdateAvatar(agent.ID, ""); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
app.user.InvalidateAgentCache(agent.ID)
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleResetPassword generates a reset password token and sends an email to the agent.
|
|
func handleResetPassword(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser, ok = r.RequestCtx.UserValue("user").(amodels.User)
|
|
resetReq resetPasswordRequest
|
|
)
|
|
if ok && auser.ID > 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userAlreadyLoggedIn"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Decode JSON request
|
|
if err := r.Decode(&resetReq, "json"); err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), nil, envelope.InputError)
|
|
}
|
|
|
|
if resetReq.Email == "" {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil, envelope.InputError)
|
|
}
|
|
|
|
agent, err := app.user.GetAgent(0, resetReq.Email)
|
|
if err != nil {
|
|
// Send 200 even if user not found, to prevent email enumeration.
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
token, err := app.user.SetResetPasswordToken(agent.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Send email.
|
|
content, err := app.tmpl.RenderInMemoryTemplate(tmpl.TmplResetPassword, map[string]string{
|
|
"ResetToken": token,
|
|
})
|
|
if err != nil {
|
|
app.lo.Error("error rendering template", "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.errorSendingPasswordResetEmail"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
if err := app.notifier.Send(notifier.Message{
|
|
RecipientEmails: []string{agent.Email.String},
|
|
Subject: "Reset Password",
|
|
Content: content,
|
|
Provider: notifier.ProviderEmail,
|
|
}); err != nil {
|
|
app.lo.Error("error sending password reset email", "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.errorSendingPasswordResetEmail"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleSetPassword resets the password with the provided token.
|
|
func handleSetPassword(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
agent, ok = r.RequestCtx.UserValue("user").(amodels.User)
|
|
req setPasswordRequest
|
|
)
|
|
|
|
if ok && agent.ID > 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userAlreadyLoggedIn"), nil, envelope.InputError)
|
|
}
|
|
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
|
|
if req.Password == "" {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "{globals.terms.password}"), nil, envelope.InputError)
|
|
}
|
|
|
|
id, err := app.user.ResetPassword(req.Token, req.Password)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
app.user.InvalidateAgentCache(id)
|
|
app.wsHub.KickUser(id)
|
|
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// validateAvatarFile checks avatar type and size without side effects.
|
|
func validateAvatarFile(r *fastglue.Request, files []*multipart.FileHeader) error {
|
|
var app = r.Context.(*App)
|
|
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
fileHeader := files[0]
|
|
srcExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(stringutil.SanitizeFilename(fileHeader.Filename))), ".")
|
|
if !slices.Contains(image.Exts, srcExt) {
|
|
return envelope.NewError(envelope.InputError, app.i18n.T("globals.messages.fileTypeisNotAnImage"), nil)
|
|
}
|
|
if bytesToMegabytes(fileHeader.Size) > maxAvatarSizeMB {
|
|
return envelope.NewError(
|
|
envelope.InputError,
|
|
app.i18n.Ts("media.fileSizeTooLarge", "size", fmt.Sprintf("%dMB", maxAvatarSizeMB)),
|
|
nil,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// uploadUserAvatar uploads the user avatar.
|
|
func uploadUserAvatar(r *fastglue.Request, user models.User, files []*multipart.FileHeader) error {
|
|
var app = r.Context.(*App)
|
|
|
|
if err := validateAvatarFile(r, files); err != nil {
|
|
return err
|
|
}
|
|
|
|
fileHeader := files[0]
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
app.lo.Error("error opening uploaded file", "user_id", user.ID, "error", err)
|
|
return envelope.NewError(envelope.GeneralError, app.i18n.T("globals.messages.errorUploadingFile"), nil)
|
|
}
|
|
defer file.Close()
|
|
|
|
srcFileName := stringutil.SanitizeFilename(fileHeader.Filename)
|
|
srcContentType := fileHeader.Header.Get("Content-Type")
|
|
|
|
resized, err := image.Downscale(image.AvatarMaxDim, file)
|
|
if err != nil {
|
|
app.lo.Error("error downscaling avatar", "user_id", user.ID, "error", err)
|
|
return envelope.NewError(envelope.InputError, app.i18n.T("globals.messages.fileTypeisNotAnImage"), nil)
|
|
}
|
|
srcFileSize := resized.Len()
|
|
|
|
linkedModel := null.StringFrom(mmodels.ModelUser)
|
|
linkedID := null.IntFrom(user.ID)
|
|
disposition := null.NewString("", false)
|
|
contentID := ""
|
|
meta := []byte("{}")
|
|
// Agent and AI assistant avatars are public: both appear as authors on the help center.
|
|
private := user.Type != models.UserTypeAgent && user.Type != models.UserTypeAIAssistant
|
|
media, err := app.media.UploadAndInsert(srcFileName, srcContentType, contentID, linkedModel, linkedID, resized, srcFileSize, disposition, meta, private)
|
|
if err != nil {
|
|
app.lo.Error("error uploading file", "user_id", user.ID, "error", err)
|
|
return envelope.NewError(envelope.GeneralError, app.i18n.T("globals.messages.errorUploadingFile"), nil)
|
|
}
|
|
|
|
// Delete current avatar.
|
|
if user.AvatarURL.Valid {
|
|
fileName := filepath.Base(user.AvatarURL.String)
|
|
if err := app.media.Delete(fileName); err != nil {
|
|
app.lo.Error("error deleting user avatar", "user_id", user.ID, "error", err)
|
|
}
|
|
}
|
|
|
|
if err := app.user.UpdateAvatar(user.ID, "/uploads/"+media.UUID); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
app.user.InvalidateAgentCache(user.ID)
|
|
return nil
|
|
}
|
|
|
|
// handleGenerateAPIKey generates a new API key for a user
|
|
func handleGenerateAPIKey(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
)
|
|
if id <= 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Check if user exists
|
|
user, err := app.user.GetAgent(id, "")
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if user.Type != models.UserTypeAgent {
|
|
return r.SendErrorEnvelope(fasthttp.StatusNotFound, app.i18n.Ts("globals.messages.notFound", "name", app.i18n.T("globals.terms.agent")), nil, envelope.NotFoundError)
|
|
}
|
|
|
|
// Generate API key and secret
|
|
apiKey, apiSecret, err := app.user.GenerateAPIKey(user.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
app.user.InvalidateAgentCache(user.ID)
|
|
|
|
// Return the API key and secret (only shown once)
|
|
response := struct {
|
|
APIKey string `json:"api_key"`
|
|
APISecret string `json:"api_secret"`
|
|
}{
|
|
APIKey: apiKey,
|
|
APISecret: apiSecret,
|
|
}
|
|
|
|
return r.SendEnvelope(response)
|
|
}
|
|
|
|
// handleRevokeAPIKey revokes a user's API key
|
|
func handleRevokeAPIKey(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
)
|
|
if id <= 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Check if user exists
|
|
user, err := app.user.GetAgent(id, "")
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if user.Type != models.UserTypeAgent {
|
|
return r.SendErrorEnvelope(fasthttp.StatusNotFound, app.i18n.Ts("globals.messages.notFound", "name", app.i18n.T("globals.terms.agent")), nil, envelope.NotFoundError)
|
|
}
|
|
|
|
// Revoke API key
|
|
if err := app.user.RevokeAPIKey(id); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
app.user.InvalidateAgentCache(id)
|
|
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
func validateAgentRequest(app *App, req *agentReq) error {
|
|
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
|
|
req.FirstName = strings.TrimSpace(req.FirstName)
|
|
req.LastName = strings.TrimSpace(req.LastName)
|
|
if req.Email == "" {
|
|
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil)
|
|
}
|
|
|
|
if !stringutil.ValidEmail(req.Email) {
|
|
return envelope.NewError(envelope.InputError, app.i18n.T("validation.invalidEmail"), nil)
|
|
}
|
|
|
|
if req.Roles == nil {
|
|
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`role`"), nil)
|
|
}
|
|
|
|
if req.FirstName == "" {
|
|
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`first_name`"), nil)
|
|
}
|
|
|
|
switch req.AvailabilityStatus {
|
|
case "", models.Online, models.Offline, models.Away, models.AwayManual, models.AwayAndReassigning:
|
|
default:
|
|
return envelope.NewError(envelope.InputError, app.i18n.T("validation.invalidAvailabilityStatus"), nil)
|
|
}
|
|
|
|
return nil
|
|
}
|