Compare commits

...

7 Commits

Author SHA1 Message Date
Abhinav Raut
59951f0829 fix: private message sent as reply 2025-02-28 21:44:02 +05:30
Abhinav Raut
461ae3cf22 fix: sla badge not visible in conversation info sidebar. 2025-02-28 21:32:08 +05:30
Abhinav Raut
da5dfdbcde fix: prevent email enumeration in reset password flow. 2025-02-28 20:57:47 +05:30
Abhinav Raut
9c67c02b08 fix: ensure navigation to SSO list only after creating SSO provider and not while updating SSO provider. 2025-02-27 23:46:31 +05:30
Abhinav Raut
15b200b0db fix: add descriptions for notification settings SMTP config for better clarity 2025-02-27 23:02:14 +05:30
Abhinav Raut
f4617c599c fix: correct Zod schema for email address validation 2025-02-27 23:01:49 +05:30
Abhinav Raut
341d0b7e47 Update README.md 2025-02-27 21:37:07 +05:30
10 changed files with 71 additions and 42 deletions

View File

@@ -74,7 +74,7 @@ __________________
- Run `./libredesk --set-system-user-password` to set the password for the System user. - Run `./libredesk --set-system-user-password` to set the password for the System user.
- Run `./libredesk` and visit `http://localhost:9000` and login with username `System` and the password you set using the --set-system-user-password command. - Run `./libredesk` and visit `http://localhost:9000` and login with username `System` and the password you set using the --set-system-user-password command.
See [installation docs](https://libredesk.app/docs/installation) See [installation docs](https://libredesk.io/docs/installation)
__________________ __________________

View File

@@ -338,7 +338,7 @@ func handleDeleteAvatar(r *fastglue.Request) error {
// Valid str? // Valid str?
if user.AvatarURL.String == "" { if user.AvatarURL.String == "" {
return r.SendEnvelope(true) return r.SendEnvelope("Avatar deleted successfully.")
} }
fileName := filepath.Base(user.AvatarURL.String) fileName := filepath.Base(user.AvatarURL.String)
@@ -347,8 +347,8 @@ func handleDeleteAvatar(r *fastglue.Request) error {
if err := app.media.Delete(fileName); err != nil { if err := app.media.Delete(fileName); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
err = app.user.UpdateAvatar(user.ID, "")
if err != nil { if err = app.user.UpdateAvatar(user.ID, ""); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope("Avatar deleted successfully.") return r.SendEnvelope("Avatar deleted successfully.")
@@ -363,7 +363,7 @@ func handleResetPassword(r *fastglue.Request) error {
email = string(p.Peek("email")) email = string(p.Peek("email"))
) )
if ok && auser.ID > 0 { if ok && auser.ID > 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "User is already logged in", nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "User is already logged in, Please logout to reset password.", nil, envelope.InputError)
} }
if email == "" { if email == "" {
@@ -372,7 +372,8 @@ func handleResetPassword(r *fastglue.Request) error {
user, err := app.user.GetByEmail(email) user, err := app.user.GetByEmail(email)
if err != nil { if err != nil {
return sendErrorEnvelope(r, err) // Send 200 even if user not found, to prevent email enumeration.
return r.SendEnvelope("Reset password email sent successfully.")
} }
token, err := app.user.SetResetPasswordToken(user.ID) token, err := app.user.SetResetPasswordToken(user.ID)
@@ -396,8 +397,8 @@ func handleResetPassword(r *fastglue.Request) error {
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 password reset email", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error sending notification message", nil, envelope.GeneralError) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error sending password reset email", nil, envelope.GeneralError)
} }
return r.SendEnvelope("Reset password email sent successfully.") return r.SendEnvelope("Reset password email sent successfully.")

View File

@@ -18,5 +18,5 @@ export function useSla (dueAt, actualAt) {
clearInterval(intervalId) clearInterval(intervalId)
}) })
}) })
return { sla, updateSla } return sla
} }

View File

@@ -65,6 +65,7 @@
<Input type="number" placeholder="2" v-bind="componentField" /> <Input type="number" placeholder="2" v-bind="componentField" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> Maximum concurrent connections to the server. </FormDescription>
</FormItem> </FormItem>
</FormField> </FormField>
@@ -76,6 +77,10 @@
<Input type="text" placeholder="15s" v-bind="componentField" /> <Input type="text" placeholder="15s" v-bind="componentField" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription>
Time to wait for new activity on a connection before closing it and removing it from the
pool (s for second, m for minute)
</FormDescription>
</FormItem> </FormItem>
</FormField> </FormField>
@@ -87,6 +92,10 @@
<Input type="text" placeholder="5s" v-bind="componentField" /> <Input type="text" placeholder="5s" v-bind="componentField" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription>
Time to wait for new activity on a connection before closing it and removing it from the
pool (s for second, m for minute, h for hour).
</FormDescription>
</FormItem> </FormItem>
</FormField> </FormField>
@@ -139,6 +148,7 @@
<Input type="number" placeholder="2" v-bind="componentField" /> <Input type="number" placeholder="2" v-bind="componentField" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> Number of times to retry when a message fails. </FormDescription>
</FormItem> </FormItem>
</FormField> </FormField>

View File

@@ -51,8 +51,8 @@ export const smtpConfigSchema = z.object({
auth_protocol: z auth_protocol: z
.enum(['plain', 'login', 'cram', 'none']) .enum(['plain', 'login', 'cram', 'none'])
.describe('Authentication protocol'), .describe('Authentication protocol'),
email_address: z.string().describe('Email address').email().nonempty({ email_address: z.string().describe('From email address with name (e.g., "Name <email@example.com>")').nonempty({
message: "Email address is required" message: "From email address is required"
}), }),
max_msg_retries: z max_msg_retries: z
.number({ .number({

View File

@@ -267,7 +267,7 @@ const processSend = async () => {
) )
await api.sendMessage(conversationStore.current.uuid, { await api.sendMessage(conversationStore.current.uuid, {
private: messageType.value === 'private', private: messageType.value === 'private_note',
message: message, message: message,
attachments: conversationStore.conversation.mediaFiles.map((file) => file.id), attachments: conversationStore.conversation.mediaFiles.map((file) => file.id),
// Convert email addresses to array and remove empty strings. // Convert email addresses to array and remove empty strings.

View File

@@ -57,16 +57,18 @@
<div class="flex items-center mt-2 space-x-2"> <div class="flex items-center mt-2 space-x-2">
<SlaBadge <SlaBadge
v-if="conversation.first_response_due_at"
:dueAt="conversation.first_response_due_at" :dueAt="conversation.first_response_due_at"
:actualAt="conversation.first_reply_at" :actualAt="conversation.first_reply_at"
:label="'FRD'" :label="'FRD'"
:showSLAMet="false" :showExtra="false"
/> />
<SlaBadge <SlaBadge
v-if="conversation.resolution_due_at"
:dueAt="conversation.resolution_due_at" :dueAt="conversation.resolution_due_at"
:actualAt="conversation.resolved_at" :actualAt="conversation.resolved_at"
:label="'RD'" :label="'RD'"
:showSLAMet="false" :showExtra="false"
/> />
</div> </div>
</div> </div>

View File

@@ -27,8 +27,10 @@
<div class="flex justify-start items-center space-x-2"> <div class="flex justify-start items-center space-x-2">
<p class="font-medium">First reply at</p> <p class="font-medium">First reply at</p>
<SlaBadge <SlaBadge
v-if="conversation.first_response_due_at"
:dueAt="conversation.first_response_due_at" :dueAt="conversation.first_response_due_at"
:actualAt="conversation.first_reply_at" :actualAt="conversation.first_reply_at"
:key="conversation.uuid"
/> />
</div> </div>
<Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" /> <Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" />
@@ -43,7 +45,12 @@
<div class="flex flex-col gap-1 mb-5"> <div class="flex flex-col gap-1 mb-5">
<div class="flex justify-start items-center space-x-2"> <div class="flex justify-start items-center space-x-2">
<p class="font-medium">Resolved at</p> <p class="font-medium">Resolved at</p>
<SlaBadge :dueAt="conversation.resolution_due_at" :actualAt="conversation.resolved_at" /> <SlaBadge
v-if="conversation.resolution_due_at"
:dueAt="conversation.resolution_due_at"
:actualAt="conversation.resolved_at"
:key="conversation.uuid"
/>
</div> </div>
<Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" /> <Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" />
<div v-else> <div v-else>

View File

@@ -1,32 +1,33 @@
<template> <template>
<div v-if="dueAt" class="flex justify-start items-center space-x-2"> <div v-if="dueAt" class="flex justify-start items-center space-x-2">
<TransitionGroup name="fade"> <!-- Overdue-->
<!-- Overdue--> <span v-if="sla?.status === 'overdue'" key="overdue" class="sla-badge box sla-overdue">
<span v-if="sla?.status === 'overdue'" key="overdue" class="sla-badge box sla-overdue"> <AlertCircle size="12" class="text-red-800" />
<AlertCircle size="10" class="text-red-800" /> <span class="sla-text text-red-800"
<span class="text-xs text-red-800">{{ label }} Overdue</span> >{{ label }} Overdue
<span v-if="showExtra">by {{ sla.value }}</span>
</span> </span>
</span>
<!-- SLA Hit --> <!-- SLA Hit -->
<span <span
v-else-if="sla?.status === 'hit' && showSLAMet" v-else-if="sla?.status === 'hit' && showExtra"
key="sla-hit" key="sla-hit"
class="sla-badge box sla-hit" class="sla-badge box sla-hit"
> >
<CheckCircle size="10" /> <CheckCircle size="12" />
<span class="sla-text">{{ label }} SLA met</span> <span class="sla-text">{{ label }} SLA met</span>
</span> </span>
<!-- Remaining --> <!-- Remaining -->
<span <span
v-else-if="sla?.status === 'remaining'" v-else-if="sla?.status === 'remaining'"
key="remaining" key="remaining"
class="sla-badge box sla-remaining" class="sla-badge box sla-remaining"
> >
<Clock size="10" /> <Clock size="12" />
<span class="sla-text">{{ label }} {{ sla.value }}</span> <span class="sla-text">{{ label }} {{ sla.value }}</span>
</span> </span>
</TransitionGroup>
</div> </div>
</template> </template>
@@ -38,12 +39,16 @@ const props = defineProps({
dueAt: String, dueAt: String,
actualAt: String, actualAt: String,
label: String, label: String,
showSLAMet: { showExtra: {
type: Boolean, type: Boolean,
default: true default: true
} }
}) })
const { sla } = useSla(ref(props.dueAt), ref(props.actualAt))
let sla = null
if (props.dueAt) {
sla = useSla(ref(props.dueAt), ref(props.actualAt))
}
</script> </script>
<style scoped> <style scoped>
@@ -62,4 +67,8 @@ const { sla } = useSla(ref(props.dueAt), ref(props.actualAt))
.sla-remaining { .sla-remaining {
@apply bg-yellow-100 text-yellow-800; @apply bg-yellow-100 text-yellow-800;
} }
.sla-text {
@apply text-[0.65rem];
}
</style> </style>

View File

@@ -62,9 +62,9 @@ const submitForm = async (values) => {
} }
await api.updateOIDC(props.id, values) await api.updateOIDC(props.id, values)
toastDescription = 'Provider updated successfully' toastDescription = 'Provider updated successfully'
router.push({ name: 'sso-list' })
} else { } else {
await api.createOIDC(values) await api.createOIDC(values)
router.push({ name: 'sso-list' })
toastDescription = 'Provider created successfully' toastDescription = 'Provider created successfully'
} }
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {