mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-11 13:28:57 +00:00
dad99bf0ee
Replies from the AI agent are now converted from markdown to HTML with goldmark before queueing, so bold, links, and lists render properly in the widget, agent app, and email. The prompt now allows simple markdown. Raw HTML in model output is escaped by goldmark, and both frontends sanitize on render anyway. Other fixes bundled in: - validate avatar type and size before creating or updating an assistant, and roll back the assistant if the avatar upload fails after create - return 404 from agent update and API key endpoints for AI assistant identity users, and hide assistants from mention and SLA user pickers - reserve the autonomous assistant's built-in tool names so custom tools cannot shadow them - apply resolve after the reply is posted so the CSAT survey follows the answer instead of preceding it - unassign the assistant on handoff even when the fallback team is the same team - count reopens by status category instead of status name, and exclude CSAT messages from the turn cap - split oversized wrapper divs into child blocks when chunking KB HTML instead of truncating them - return 404 when soft-deleting an already-deleted agent, and keep AI assistants (which have no email) visible in the compact users list
637 lines
19 KiB
Go
637 lines
19 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 (
|
|
maxAvatarSizeMB = 2
|
|
)
|
|
|
|
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 all agents in a compact format.
|
|
func handleGetAgentsCompact(r *fastglue.Request) error {
|
|
var app = r.Context.(*App)
|
|
agents, err := app.user.GetAgentsCompact()
|
|
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)
|
|
}
|
|
|
|
// Validate agent request
|
|
if err := validateAgentRequest(r, &req); err != nil {
|
|
return 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)
|
|
}
|
|
|
|
// Validate agent request
|
|
if err := validateAgentRequest(r, &req); err != nil {
|
|
return 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")
|
|
srcFileSize := fileHeader.Size
|
|
|
|
// Reset ptr.
|
|
file.Seek(0, 0)
|
|
linkedModel := null.StringFrom(mmodels.ModelUser)
|
|
linkedID := null.IntFrom(user.ID)
|
|
disposition := null.NewString("", false)
|
|
contentID := ""
|
|
meta := []byte("{}")
|
|
media, err := app.media.UploadAndInsert(srcFileName, srcContentType, contentID, linkedModel, linkedID, file, int(srcFileSize), disposition, meta)
|
|
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)
|
|
}
|
|
|
|
// 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.T("validation.invalidEmail"), 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
|
|
}
|