fix: hide passwords in API response

- Minor refactors.
- Fixes bugs in SelectTag component.
- Update vite.
This commit is contained in:
Abhinav Raut
2024-10-23 05:25:10 +05:30
parent 268c0b9b3b
commit ee7be54c0d
34 changed files with 655 additions and 509 deletions

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"mime" "mime"
"net/http" "net/http"
"path"
"path/filepath" "path/filepath"
"github.com/abhinavxd/artemis/internal/envelope" "github.com/abhinavxd/artemis/internal/envelope"
@@ -132,7 +133,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
g.DELETE("/api/templates/{id}", authPerm(handleDeleteTemplate, "templates", "delete")) g.DELETE("/api/templates/{id}", authPerm(handleDeleteTemplate, "templates", "delete"))
// WebSocket. // WebSocket.
g.GET("/api/ws", auth(func(r *fastglue.Request) error { g.GET("/ws", auth(func(r *fastglue.Request) error {
return handleWS(r, hub) return handleWS(r, hub)
})) }))
@@ -157,14 +158,14 @@ func serveIndexPage(r *fastglue.Request) error {
r.RequestCtx.Response.Header.Add("Expires", "-1") r.RequestCtx.Response.Header.Add("Expires", "-1")
// Serve the index.html file from the embedded filesystem. // Serve the index.html file from the embedded filesystem.
file, err := app.fs.Get("/frontend/dist/index.html") file, err := app.fs.Get(path.Join(frontendDir, "index.html"))
if err != nil { if err != nil {
return r.SendErrorEnvelope(http.StatusNotFound, "Page not found", nil, "InputException") return r.SendErrorEnvelope(http.StatusNotFound, "Page not found", nil, envelope.NotFoundError)
} }
r.RequestCtx.Response.Header.Set("Content-Type", "text/html") r.RequestCtx.Response.Header.Set("Content-Type", "text/html")
r.RequestCtx.SetBody(file.ReadBytes()) r.RequestCtx.SetBody(file.ReadBytes())
// Set csrf cookie if not already set. // Set CSRF cookie if not already set.
if err := app.auth.SetCSRFCookie(r); err != nil { if err := app.auth.SetCSRFCookie(r); err != nil {
app.lo.Error("error setting csrf cookie", "error", err) app.lo.Error("error setting csrf cookie", "error", err)
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.errorAcquiringSession"), nil)) return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.errorAcquiringSession"), nil))
@@ -180,10 +181,10 @@ func serveStaticFiles(r *fastglue.Request) error {
filePath := string(r.RequestCtx.Path()) filePath := string(r.RequestCtx.Path())
// Fetch and serve the file from the embedded filesystem. // Fetch and serve the file from the embedded filesystem.
finalPath := filepath.Join("frontend/dist", filePath) finalPath := filepath.Join(frontendDir, filePath)
file, err := app.fs.Get(finalPath) file, err := app.fs.Get(finalPath)
if err != nil { if err != nil {
return r.SendErrorEnvelope(http.StatusNotFound, "File not found", nil, "InputException") return r.SendErrorEnvelope(http.StatusNotFound, "File not found", nil, envelope.NotFoundError)
} }
// Set the appropriate Content-Type based on the file extension. // Set the appropriate Content-Type based on the file extension.

View File

@@ -12,9 +12,8 @@ import (
func handleGetInboxes(r *fastglue.Request) error { func handleGetInboxes(r *fastglue.Request) error {
var app = r.Context.(*App) var app = r.Context.(*App)
inboxes, err := app.inbox.GetAll() inboxes, err := app.inbox.GetAll()
// TODO: Clear out passwords.
if err != nil { if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Could not fetch inboxes", nil, envelope.GeneralError) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope(inboxes) return r.SendEnvelope(inboxes)
} }
@@ -25,9 +24,12 @@ func handleGetInbox(r *fastglue.Request) error {
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
) )
inbox, err := app.inbox.GetByID(id) inbox, err := app.inbox.GetByID(id)
// TODO: Clear out passwords.
if err != nil { if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Could not fetch inboxes", nil, envelope.GeneralError) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error fetching inbox", nil, envelope.GeneralError)
}
if err := inbox.ClearPasswords(); err != nil {
app.lo.Error("error clearing out passwords", "error", err)
return envelope.NewError(envelope.GeneralError, "Error fetching inbox", nil)
} }
return r.SendEnvelope(inbox) return r.SendEnvelope(inbox)
} }

View File

@@ -22,6 +22,11 @@ func handleLogin(r *fastglue.Request) error {
app.lo.Error("error saving session", "error", err) app.lo.Error("error saving session", "error", err)
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.errorAcquiringSession"), nil)) return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.errorAcquiringSession"), nil))
} }
// Set CSRF cookie if not already set.
if err := app.auth.SetCSRFCookie(r); err != nil {
app.lo.Error("error setting csrf cookie", "error", err)
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.errorAcquiringSession"), nil))
}
return r.SendEnvelope(user) return r.SendEnvelope(user)
} }

View File

@@ -33,7 +33,10 @@ import (
"github.com/zerodha/logf" "github.com/zerodha/logf"
) )
var ko = koanf.New(".") var (
ko = koanf.New(".")
frontendDir = "frontend/dist"
)
// App is the global app context which is passed and injected in the http handlers. // App is the global app context which is passed and injected in the http handlers.
type App struct { type App struct {
@@ -176,7 +179,7 @@ func main() {
initHandlers(g, wsHub) initHandlers(g, wsHub)
s := &fasthttp.Server{ s := &fasthttp.Server{
Name: ko.MustString("app.server.name"), Name: "server",
ReadTimeout: ko.MustDuration("app.server.read_timeout"), ReadTimeout: ko.MustDuration("app.server.read_timeout"),
WriteTimeout: ko.MustDuration("app.server.write_timeout"), WriteTimeout: ko.MustDuration("app.server.write_timeout"),
MaxRequestBodySize: ko.MustInt("app.server.max_body_size"), MaxRequestBodySize: ko.MustInt("app.server.max_body_size"),

View File

@@ -87,7 +87,8 @@ func handleMediaUpload(r *fastglue.Request) error {
} }
}() }()
// Generate and upload thumbnail if it's an image. // Generate and upload thumbnail and save it's dimensions if it's an image.
var meta = []byte("{}")
if slices.Contains(image.Exts, srcExt) { if slices.Contains(image.Exts, srcExt) {
file.Seek(0, 0) file.Seek(0, 0)
thumbFile, err := image.CreateThumb(thumbnailSize, file) thumbFile, err := image.CreateThumb(thumbnailSize, file)
@@ -100,20 +101,21 @@ func handleMediaUpload(r *fastglue.Request) error {
app.lo.Error("error uploading thumbnail", "error", err) app.lo.Error("error uploading thumbnail", "error", err)
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
}
// Store image dimensions in the media meta. // Store image dimensions in the media meta.
file.Seek(0, 0) file.Seek(0, 0)
width, height, err := image.GetDimensions(file) width, height, err := image.GetDimensions(file)
if err != nil { if err != nil {
cleanUp = true cleanUp = true
app.lo.Error("error getting image dimensions", "error", err) app.lo.Error("error getting image dimensions", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error uploading file", nil, envelope.GeneralError) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error uploading file", nil, envelope.GeneralError)
}
meta, _ = json.Marshal(map[string]interface{}{
"width": width,
"height": height,
})
} }
meta, _ := json.Marshal(map[string]interface{}{
"width": width,
"height": height,
})
file.Seek(0, 0) file.Seek(0, 0)
_, err = app.media.Upload(uuid.String(), srcContentType, file) _, err = app.media.Upload(uuid.String(), srcContentType, file)

View File

@@ -1,7 +1,12 @@
package main package main
import ( import (
"encoding/json"
"strings"
"github.com/abhinavxd/artemis/internal/envelope"
"github.com/abhinavxd/artemis/internal/setting/models" "github.com/abhinavxd/artemis/internal/setting/models"
"github.com/abhinavxd/artemis/internal/stringutil"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
) )
@@ -35,30 +40,48 @@ func handleUpdateGeneralSettings(r *fastglue.Request) error {
func handleGetEmailNotificationSettings(r *fastglue.Request) error { func handleGetEmailNotificationSettings(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
req = models.EmailNotification{} notif = models.EmailNotification{}
) )
if err := r.Decode(&req, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Bad request", nil, "")
}
out, err := app.setting.GetByPrefix("notification.email") out, err := app.setting.GetByPrefix("notification.email")
if err != nil { if err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)
} }
return r.SendEnvelope(out)
// Unmarshal and filter out password.
if err := json.Unmarshal(out, &notif); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, "Error fetching settings", nil))
}
if notif.Password != "" {
notif.Password = strings.Repeat(stringutil.PasswordDummy, 10)
}
return r.SendEnvelope(notif)
} }
func handleUpdateEmailNotificationSettings(r *fastglue.Request) error { func handleUpdateEmailNotificationSettings(r *fastglue.Request) error {
var ( var (
app = r.Context.(*App) app = r.Context.(*App)
req = models.EmailNotification{} req = models.EmailNotification{}
cur = models.EmailNotification{}
) )
if err := r.Decode(&req, "json"); err != nil { if err := r.Decode(&req, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Bad request", nil, "") return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Bad request", nil, envelope.InputError)
} }
out, err := app.setting.GetByPrefix("notification.email")
if err != nil {
return sendErrorEnvelope(r, err)
}
if err := json.Unmarshal(out, &cur); err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, "Error updating settings", nil))
}
if req.Password == "" {
req.Password = cur.Password
}
if err := app.setting.Update(req); err != nil { if err := app.setting.Update(req); err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)

View File

@@ -5,7 +5,6 @@ env = "dev"
# HTTP server. # HTTP server.
[app.server] [app.server]
name = ""
address = "0.0.0.0:9009" address = "0.0.0.0:9009"
socket = "" socket = ""
read_timeout = "5s" read_timeout = "5s"

View File

@@ -73,7 +73,7 @@
"sass": "^1.70.0", "sass": "^1.70.0",
"start-server-and-test": "^2.0.3", "start-server-and-test": "^2.0.3",
"tailwindcss": "latest", "tailwindcss": "latest",
"vite": "^5.0.11" "vite": "^5.4.9"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@@ -10,7 +10,7 @@
<div class="space-y-5"> <div class="space-y-5">
<FormField v-slot="{ field }" name="name"> <FormField v-slot="{ field }" name="name">
<FormItem v-auto-animate> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input type="text" placeholder="My new rule" v-bind="field" /> <Input type="text" placeholder="My new rule" v-bind="field" />
@@ -21,7 +21,7 @@
</FormField> </FormField>
<FormField v-slot="{ field }" name="description"> <FormField v-slot="{ field }" name="description">
<FormItem v-auto-animate> <FormItem>
<FormLabel>Description</FormLabel> <FormLabel>Description</FormLabel>
<FormControl> <FormControl>
<Input type="text" placeholder="Description for new rule" v-bind="field" /> <Input type="text" placeholder="Description for new rule" v-bind="field" />
@@ -32,7 +32,7 @@
</FormField> </FormField>
<FormField v-slot="{ componentField }" name="type"> <FormField v-slot="{ componentField }" name="type">
<FormItem v-auto-animate> <FormItem>
<FormLabel>Type</FormLabel> <FormLabel>Type</FormLabel>
<FormControl> <FormControl>
<Select v-bind="componentField"> <Select v-bind="componentField">
@@ -57,9 +57,9 @@
<p class="font-semibold">Match these rules</p> <p class="font-semibold">Match these rules</p>
<RuleBox :ruleGroup="firstRuleGroup" @update-group="handleUpdateGroup" @add-condition="handleAddCondition" <RuleBox :ruleGroup="firstRuleGroup" @update-group="handleUpdateGroup" @add-condition="handleAddCondition"
@remove-condition="handleRemoveCondition" :groupIndex="0" v-auto-animate /> @remove-condition="handleRemoveCondition" :groupIndex="0" />
<div class="flex justify-center" v-auto-animate> <div class="flex justify-center">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Button :class="[groupOperator === 'AND' ? 'bg-black' : 'bg-gray-100 text-black']" <Button :class="[groupOperator === 'AND' ? 'bg-black' : 'bg-gray-100 text-black']"
@click.prevent="toggleGroupOperator('AND')"> @click.prevent="toggleGroupOperator('AND')">
@@ -73,11 +73,11 @@
</div> </div>
<RuleBox :ruleGroup="secondRuleGroup" @update-group="handleUpdateGroup" @add-condition="handleAddCondition" <RuleBox :ruleGroup="secondRuleGroup" @update-group="handleUpdateGroup" @add-condition="handleAddCondition"
@remove-condition="handleRemoveCondition" :groupIndex="1" v-auto-animate /> @remove-condition="handleRemoveCondition" :groupIndex="1" />
<p class="font-semibold">Perform these actions</p> <p class="font-semibold">Perform these actions</p>
<ActionBox :actions="getActions()" :update-actions="handleUpdateActions" @add-action="handleAddAction" <ActionBox :actions="getActions()" :update-actions="handleUpdateActions" @add-action="handleAddAction"
@remove-action="handleRemoveAction" v-auto-animate /> @remove-action="handleRemoveAction" />
<Button type="submit" :isLoading="isLoading" size="sm">Save</Button> <Button type="submit" :isLoading="isLoading" size="sm">Save</Button>
</div> </div>
</form> </form>
@@ -86,7 +86,6 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue' import { onMounted, ref, computed } from 'vue'
import { vAutoAnimate } from '@formkit/auto-animate/vue'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import RuleBox from './RuleBox.vue' import RuleBox from './RuleBox.vue'
@@ -307,14 +306,14 @@ onMounted(async () => {
firstRuleGroup.value = getFirstGroup() firstRuleGroup.value = getFirstGroup()
// Convert multi tag select values separated by commas to an array // Convert multi tag select values separated by commas to an array
firstRuleGroup.value.rules.forEach(rule => { firstRuleGroup.value.rules.forEach(rule => {
if (!Array.isArray(rule.value)) if (!Array.isArray(rule.value))
if (["contains", "not contains"].includes(rule.operator)) { if (["contains", "not contains"].includes(rule.operator)) {
rule.value = rule.value ? rule.value.split(',') : [] rule.value = rule.value ? rule.value.split(',') : []
} }
}) })
secondRuleGroup.value = getSecondGroup() secondRuleGroup.value = getSecondGroup()
secondRuleGroup.value?.rules?.forEach(rule => { secondRuleGroup.value?.rules?.forEach(rule => {
if (!Array.isArray(rule.value)) if (!Array.isArray(rule.value))
if (["contains", "not contains"].includes(rule.operator)) { if (["contains", "not contains"].includes(rule.operator)) {
rule.value = rule.value ? rule.value.split(',') : [] rule.value = rule.value ? rule.value.split(',') : []
} }

View File

@@ -32,13 +32,25 @@ const submitForm = (values) => {
from: values.from, from: values.from,
channel: channelName, channel: channelName,
config: { config: {
imap: [values.imap], imap: [{ ...values.imap }],
smtp: values.smtp smtp: [...values.smtp]
} }
} }
// Set dummy IMAP password to empty string
if (payload.config.imap[0].password?.includes('•')) {
payload.config.imap[0].password = ''
}
// Set dummy SMTP passwords to empty strings
payload.config.smtp.forEach(smtp => {
if (smtp.password?.includes('•')) {
smtp.password = ''
}
})
updateInbox(payload) updateInbox(payload)
} }
const updateInbox = async (payload) => { const updateInbox = async (payload) => {
try { try {
isLoading.value = true isLoading.value = true

View File

@@ -1,7 +1,7 @@
<template> <template>
<AutoForm <AutoForm
class="w-11/12 space-y-6" class="space-y-6"
:schema="emailChannelFormSchema" :schema="formSchema"
:form="form" :form="form"
:field-config="{ :field-config="{
name: { name: {
@@ -68,7 +68,7 @@ import { watch } from 'vue'
import { AutoForm } from '@/components/ui/auto-form' import { AutoForm } from '@/components/ui/auto-form'
import { useForm } from 'vee-validate' import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import { emailChannelFormSchema } from './emailChannelFormSchema.js' import { formSchema } from './formSchema.js'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
const props = defineProps({ const props = defineProps({
@@ -92,7 +92,7 @@ const props = defineProps({
}) })
const form = useForm({ const form = useForm({
validationSchema: toTypedSchema(emailChannelFormSchema), validationSchema: toTypedSchema(formSchema),
initialValues: props.initialValues initialValues: props.initialValues
}) })

View File

@@ -1,7 +1,7 @@
import * as z from 'zod' import * as z from 'zod'
import { isGoDuration } from '@/utils/strings' import { isGoDuration } from '@/utils/strings'
export const emailChannelFormSchema = z.object({ export const formSchema = z.object({
name: z.string().describe('Name').default(''), name: z.string().describe('Name').default(''),
from: z.string().describe('From address').default(''), from: z.string().describe('From address').default(''),
imap: z imap: z
@@ -46,38 +46,22 @@ export const emailChannelFormSchema = z.object({
.object({ .object({
host: z.string().describe('Host').default('smtp.yourmailserver.com'), host: z.string().describe('Host').default('smtp.yourmailserver.com'),
port: z port: z
.number({ .number({ invalid_type_error: 'Port must be a number.' })
invalid_type_error: 'Port must be a number.' .min(1, { message: 'Port must be at least 1.' })
}) .max(65535, { message: 'Port must be at most 65535.' })
.min(1, {
message: 'Port must be at least 1.'
})
.max(65535, {
message: 'Port must be at most 65535.'
})
.describe('Port') .describe('Port')
.default(25), .default(25),
username: z.string().describe('Username'), username: z.string().describe('Username'),
password: z.string().describe('Password'), password: z.string().describe('Password'),
max_conns: z max_conns: z
.number({ .number({ invalid_type_error: 'Must be a number.' })
invalid_type_error: 'Must be a number.' .min(1, { message: 'Must be at least 1.' })
})
.min(1, {
message: 'Must be at least 1.'
})
.describe('Maximum concurrent connections to the server.') .describe('Maximum concurrent connections to the server.')
.default(10), .default(10),
max_msg_retries: z max_msg_retries: z
.number({ .number({ invalid_type_error: 'Must be a number.' })
invalid_type_error: 'Must be a number.' .min(0, { message: 'Must be at least 0.' })
}) .max(100, { message: 'Max retries allowed are 100.' })
.min(0, {
message: 'Must be at least 0.'
})
.max(100, {
message: 'Max retries allowed are 100.'
})
.describe('Number of times to retry when a message fails.') .describe('Number of times to retry when a message fails.')
.default(2), .default(2),
idle_timeout: z idle_timeout: z
@@ -100,14 +84,14 @@ export const emailChannelFormSchema = z.object({
'Invalid duration format. Should be a number followed by s (seconds), m (minutes), or h (hours).' 'Invalid duration format. Should be a number followed by s (seconds), m (minutes), or h (hours).'
}) })
.default('5s'), .default('5s'),
auth_protocol: z.enum(['login', 'cram', 'plain', 'none']).default('none').optional() auth_protocol: z.enum(['login', 'cram', 'plain', 'none']).default('none').optional(),
}) })
.describe('SMTP') .describe('SMTP')
) )
.describe('SMTP servers') .describe('SMTP servers')
.default([ .default([
{ {
host: 'smtp.yourmailserver.com', host: 'smtp.gmail.com',
port: 25, port: 25,
username: '', username: '',
password: '', password: '',
@@ -115,7 +99,18 @@ export const emailChannelFormSchema = z.object({
max_msg_retries: 2, max_msg_retries: 2,
idle_timeout: '5s', idle_timeout: '5s',
wait_timeout: '5s', wait_timeout: '5s',
auth_protocol: 'plain' auth_protocol: 'plain',
} },
]) {
host: 'smtp.gmail.com',
port: 25,
username: '',
password: '',
max_conns: 10,
max_msg_retries: 2,
idle_timeout: '5s',
wait_timeout: '5s',
auth_protocol: 'plain',
},
]),
}) })

View File

@@ -35,7 +35,7 @@ const getNotificationSettings = async () => {
) )
} catch (error) { } catch (error) {
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Could not save', title: 'Could not fetch',
variant: 'destructive', variant: 'destructive',
description: handleHTTPError(error).message description: handleHTTPError(error).message
}) })
@@ -48,11 +48,16 @@ const submitForm = async (values) => {
try { try {
formLoading.value = true formLoading.value = true
const updatedValues = Object.fromEntries( const updatedValues = Object.fromEntries(
Object.entries(values).map(([key, value]) => [`notification.email.${key}`, value]) Object.entries(values).map(([key, value]) => {
if (key === 'password' && value.includes('•')) {
return [`notification.email.${key}`, '']
}
return [`notification.email.${key}`, value]
})
); );
await api.updateEmailNotificationSettings(updatedValues) await api.updateEmailNotificationSettings(updatedValues)
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
description: "Saved" description: "Saved successfully"
}) })
} catch (error) { } catch (error) {
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {

View File

@@ -35,6 +35,9 @@ const submitForm = async (values) => {
formLoading.value = true formLoading.value = true
let toastDescription = '' let toastDescription = ''
if (props.id) { if (props.id) {
if (values.client_secret.includes('•')) {
values.client_secret = ''
}
await api.updateOIDC(props.id, values) await api.updateOIDC(props.id, values)
toastDescription = 'Updated successfully' toastDescription = 'Updated successfully'
} else { } else {

View File

@@ -34,8 +34,7 @@
<FormItem v-auto-animate> <FormItem v-auto-animate>
<FormLabel>Teams</FormLabel> <FormLabel>Teams</FormLabel>
<FormControl> <FormControl>
<SelectTag v-model="selectedTeams" :items="teamNames" :initialValue="initialTeamNames" <SelectTag v-model="selectedTeams" :items="teamNames" placeholder="Select teams"></SelectTag>
placeHolder="Select teams"></SelectTag>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -45,8 +44,7 @@
<FormItem v-auto-animate> <FormItem v-auto-animate>
<FormLabel>Roles</FormLabel> <FormLabel>Roles</FormLabel>
<FormControl> <FormControl>
<SelectTag v-model="selectedRoles" :items="roleNames" :initialValue="initialValues.roles" <SelectTag v-model="selectedRoles" :items="roleNames" placeholder="Select roles"></SelectTag>
placeHolder="Select roles"></SelectTag>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -92,12 +90,6 @@ import { SelectTag } from '@/components/ui/select'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import api from '@/api' import api from '@/api'
const teams = ref([])
const roles = ref([])
const selectedRoles = ref([])
const selectedTeams = ref([])
const initialTeamNames = computed(() => props.initialValues.teams?.map(team => team.name) || [])
const props = defineProps({ const props = defineProps({
initialValues: { initialValues: {
type: Object, type: Object,
@@ -123,6 +115,11 @@ const props = defineProps({
} }
}) })
const teams = ref([])
const roles = ref([])
const selectedRoles = ref(props.initialValues.roles)
const selectedTeams = ref(props.initialValues.teams?.map(team => team.name) || [])
onMounted(async () => { onMounted(async () => {
try { try {
const [teamsResp, rolesResp] = await Promise.all([api.getTeams(), api.getRoles()]) const [teamsResp, rolesResp] = await Promise.all([api.getTeams(), api.getRoles()])

View File

@@ -27,7 +27,6 @@
<script setup> <script setup>
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { defineProps } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
const route = useRoute() const route = useRoute()

View File

@@ -32,7 +32,7 @@
<div class="pt-2 pr-3"> <div class="pt-2 pr-3">
<div class="flex justify-between"> <div class="flex justify-between">
<p class="text-gray-800 max-w-xs text-sm dark:text-white text-ellipsis flex gap-1"> <p class="text-gray-800 max-w-xs text-sm dark:text-white text-ellipsis flex gap-1">
<CheckCheck :size="14" /> {{ conversation.last_message }} <CheckCheck :size="14" /> {{ trimmedLastMessage }}
</p> </p>
<div class="flex items-center justify-center bg-green-500 rounded-full w-[20px] h-[20px]" <div class="flex items-center justify-center bg-green-500 rounded-full w-[20px] h-[20px]"
v-if="conversation.unread_message_count > 0"> v-if="conversation.unread_message_count > 0">
@@ -47,15 +47,21 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { formatTime } from '@/utils/datetime' import { formatTime } from '@/utils/datetime'
import { Mail, CheckCheck } from 'lucide-vue-next' import { Mail, CheckCheck } from 'lucide-vue-next'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
const router = useRouter() const router = useRouter()
defineProps({ const props = defineProps({
conversation: Object, conversation: Object,
currentConversation: Object, currentConversation: Object,
contactFullName: String contactFullName: String
}) })
const trimmedLastMessage = computed(() => {
const message = props.conversation.last_message || ''
return message.length > 45 ? message.slice(0, 45) + "..." : message
})
</script> </script>

View File

@@ -19,8 +19,7 @@
<PriorityChange :priorities="priorities" :conversation="conversationStore.current" <PriorityChange :priorities="priorities" :conversation="conversationStore.current"
:selectPriority="selectPriority"></PriorityChange> :selectPriority="selectPriority"></PriorityChange>
<!-- Tags --> <!-- Tags -->
<SelectTag :initialValue="conversationStore.current.tags" v-model="selectedTags" :items="tags" <SelectTag v-model="conversationStore.current.tags" :items="tags" placeHolder="Select tags"></SelectTag>
placeHolder="Select tags"></SelectTag>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem value="Information"> <AccordionItem value="Information">
@@ -57,28 +56,42 @@ import { SelectTag } from '@/components/ui/select'
import { useToast } from '@/components/ui/toast/use-toast' import { useToast } from '@/components/ui/toast/use-toast'
import { handleHTTPError } from '@/utils/http' import { handleHTTPError } from '@/utils/http'
const priorities = ref([])
const { toast } = useToast() const { toast } = useToast()
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const priorities = ref([])
const agents = ref([]) const agents = ref([])
const teams = ref([]) const teams = ref([])
const selectedTags = ref([])
const tags = ref([]) const tags = ref([])
const tagIDMap = {} const tagIDMap = {}
const filteredAgents = ref([])
onMounted(() => { onMounted(async () => {
fetchUsers() await Promise.all([
fetchTeams() fetchUsers(),
fetchTags() fetchTeams(),
getPrioritites() fetchTags(),
getPrioritites()
])
}) })
watch(() => conversationStore.current.tags, () => {
handleUpsertTags()
}, { deep: true })
const handleUpsertTags = () => {
let tagIDs = conversationStore.current.tags.map((tag) => {
if (tag in tagIDMap) {
return tagIDMap[tag]
}
})
conversationStore.upsertTags({
tag_ids: JSON.stringify(tagIDs)
})
}
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const resp = await api.getUsersCompact() const resp = await api.getUsersCompact()
agents.value = resp.data.data agents.value = resp.data.data
filteredAgents.value = resp.data.data
} catch (error) { } catch (error) {
toast({ toast({
title: 'Could not fetch users', title: 'Could not fetch users',
@@ -138,21 +151,6 @@ const handlePriorityChange = (priority) => {
conversationStore.updatePriority(priority) conversationStore.updatePriority(priority)
} }
watch(selectedTags, () => {
handleUpsertTags()
}, { deep: true })
const handleUpsertTags = () => {
let tagIDs = selectedTags.value.map((tag) => {
if (tag in tagIDMap) {
return tagIDMap[tag]
}
})
conversationStore.upsertTags({
tag_ids: JSON.stringify(tagIDs)
})
}
const selectAgent = (id) => { const selectAgent = (id) => {
conversationStore.current.assigned_user_id = id conversationStore.current.assigned_user_id = id
handleAssignedUserChange(id) handleAssignedUserChange(id)

View File

@@ -21,7 +21,6 @@ import {
BreadcrumbList, BreadcrumbList,
BreadcrumbSeparator BreadcrumbSeparator
} from '@/components/ui/breadcrumb' } from '@/components/ui/breadcrumb'
import { defineProps } from 'vue'
defineProps({ defineProps({
links: { links: {

View File

@@ -1,98 +1,72 @@
<template> <template>
<div ref="dropdownRef"> <TagsInput v-model="tags" class="px-0 gap-0">
<TagsInput v-model="selectedItems" class="px-0 gap-0 shadow-sm"> <div class="flex gap-2 flex-wrap items-center px-3">
<div class="flex gap-2 flex-wrap items-center px-3"> <TagsInputItem v-for="tag in tags" :key="tag" :value="tag">
<TagsInputItem v-for="item in selectedItems" :key="item" :value="item"> <TagsInputItemText>{{ tag }}</TagsInputItemText>
<TagsInputItemText>{{ item }}</TagsInputItemText> <TagsInputItemDelete />
<TagsInputItemDelete /> </TagsInputItem>
</TagsInputItem> </div>
</div> <ComboboxRoot :model-value="tags" v-model:open="open" v-model:search-term="searchTerm" class="w-full">
<ComboboxAnchor as-child>
<ComboboxRoot v-model:open="isOpen" class="w-full"> <ComboboxInput :placeholder="placeholder" as-child>
<ComboboxAnchor as-child> <TagsInputInput class="w-full px-3" :class="tags.length > 0 ? 'mt-2' : ''" @keydown.enter.prevent />
<ComboboxInput :placeholder="placeHolder" as-child> </ComboboxInput>
<TagsInputInput class="w-full px-3" :class="selectedItems.length > 0 ? 'mt-2' : ''" </ComboboxAnchor>
@keydown.enter.prevent /> <ComboboxPortal>
</ComboboxInput> <ComboboxContent>
</ComboboxAnchor> <CommandList position="popper"
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2">
<ComboboxPortal> <CommandEmpty />
<CommandList position="popper" <CommandGroup>
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"> <CommandItem v-for="item in filteredOptions" :key="item" :value="item" @select="handleSelect">
<CommandEmpty /> {{ item }}
<CommandGroup> </CommandItem>
<CommandItem v-for="item in filteredItems" :key="item" :value="item" @select.prevent="selectItem(item)"> </CommandGroup>
{{ item }} </CommandList>
</CommandItem> </ComboboxContent>
</CommandGroup> </ComboboxPortal>
</CommandList> </ComboboxRoot>
</ComboboxPortal> </TagsInput>
</ComboboxRoot> </template>
</TagsInput>
</div> <script setup>
</template> import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/components/ui/tags-input'
<script setup> import { ComboboxAnchor, ComboboxContent, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue'
import { ref, computed, onMounted } from 'vue' import { computed, ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
import { ComboboxAnchor, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue' const tags = defineModel({
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command' required: true,
import { default: () => []
TagsInput, })
TagsInputInput,
TagsInputItem, const props = defineProps({
TagsInputItemDelete, placeholder: {
TagsInputItemText type: String,
} from '@/components/ui/tags-input' default: 'Select...'
},
const props = defineProps({ items: {
items: { type: Array,
type: Array, required: true
required: true, }
default: () => [] })
},
placeHolder: { const open = ref(false)
type: String, const searchTerm = ref('')
required: false,
default: () => '' const filteredOptions = computed(() =>
}, props.items.filter(item => !tags.value.includes(item))
initialValue: { )
type: Array,
required: false, const handleSelect = (event) => {
default: () => [] if (event.detail.value) {
} searchTerm.value = ''
}) const newTags = Array.isArray(tags.value) ? [...tags.value] : []
newTags.push(event.detail.value)
const selectedItems = defineModel({ default: [] }) tags.value = newTags
const isOpen = ref(false) }
const searchTerm = ref('') if (filteredOptions.value.length === 0) {
const dropdownRef = ref(null) open.value = false
}
onClickOutside(dropdownRef, () => { }
isOpen.value = false </script>
})
onMounted(() => {
selectedItems.value = props.initialValue
})
const filteredItems = computed(() => {
if (searchTerm.value) {
return props.items.filter(
(item) =>
item.toLowerCase().includes(searchTerm.value.toLowerCase()) &&
!selectedItems.value.includes(item)
)
}
return props.items.filter((item) => !selectedItems.value.includes(item))
})
function selectItem (item) {
if (!selectedItems.value.includes(item)) {
selectedItems.value.push(item)
}
if (filteredItems.value.length === 0) {
isOpen.value = false
}
}
</script>

View File

@@ -78,8 +78,7 @@ export const useConversationStore = defineStore('conversation', () => {
return [...messages.data].sort((a, b) => new Date(a.created_at) - new Date(b.created_at)) return [...messages.data].sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
}) })
// Marks a conversation as read. function markConversationAsRead (uuid) {
function markAsRead (uuid) {
const index = conversations.data.findIndex((conv) => conv.uuid === uuid) const index = conversations.data.findIndex((conv) => conv.uuid === uuid)
if (index !== -1) { if (index !== -1) {
conversations.data[index].unread_message_count = 0 conversations.data[index].unread_message_count = 0
@@ -109,7 +108,7 @@ export const useConversationStore = defineStore('conversation', () => {
const resp = await api.getConversation(uuid) const resp = await api.getConversation(uuid)
conversation.data = resp.data.data conversation.data = resp.data.data
// Mark this conversation as read. // Mark this conversation as read.
markAsRead(uuid) markConversationAsRead(uuid)
// Reset messages state. // Reset messages state.
resetMessages() resetMessages()
} catch (error) { } catch (error) {

View File

@@ -10,8 +10,7 @@ let manualClose = false;
let convStore; let convStore;
function initializeWebSocket () { function initializeWebSocket () {
// TODO: Update URL. socket = new WebSocket('/ws');
socket = new WebSocket('ws://localhost:9009/api/ws')
socket.addEventListener('open', handleOpen) socket.addEventListener('open', handleOpen)
socket.addEventListener('message', handleMessage) socket.addEventListener('message', handleMessage)
socket.addEventListener('error', handleError) socket.addEventListener('error', handleError)

View File

@@ -6,8 +6,16 @@ import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
server: { server: {
port: 8000,
proxy: { proxy: {
'/api': 'http://127.0.0.1:9009', '/api': {
target: 'http://127.0.0.1:9000',
changeOrigin: true,
},
'/ws': {
target: 'ws://127.0.0.1:9000',
ws: true,
},
}, },
}, },
plugins: [ plugins: [

View File

@@ -481,120 +481,120 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6"
integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==
"@esbuild/aix-ppc64@0.20.2": "@esbuild/aix-ppc64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
"@esbuild/android-arm64@0.20.2": "@esbuild/android-arm64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
"@esbuild/android-arm@0.20.2": "@esbuild/android-arm@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
"@esbuild/android-x64@0.20.2": "@esbuild/android-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
"@esbuild/darwin-arm64@0.20.2": "@esbuild/darwin-arm64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
"@esbuild/darwin-x64@0.20.2": "@esbuild/darwin-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
"@esbuild/freebsd-arm64@0.20.2": "@esbuild/freebsd-arm64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
"@esbuild/freebsd-x64@0.20.2": "@esbuild/freebsd-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
"@esbuild/linux-arm64@0.20.2": "@esbuild/linux-arm64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
"@esbuild/linux-arm@0.20.2": "@esbuild/linux-arm@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
"@esbuild/linux-ia32@0.20.2": "@esbuild/linux-ia32@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
"@esbuild/linux-loong64@0.20.2": "@esbuild/linux-loong64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
"@esbuild/linux-mips64el@0.20.2": "@esbuild/linux-mips64el@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
"@esbuild/linux-ppc64@0.20.2": "@esbuild/linux-ppc64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
"@esbuild/linux-riscv64@0.20.2": "@esbuild/linux-riscv64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
"@esbuild/linux-s390x@0.20.2": "@esbuild/linux-s390x@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
"@esbuild/linux-x64@0.20.2": "@esbuild/linux-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
"@esbuild/netbsd-x64@0.20.2": "@esbuild/netbsd-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
"@esbuild/openbsd-x64@0.20.2": "@esbuild/openbsd-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
"@esbuild/sunos-x64@0.20.2": "@esbuild/sunos-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
"@esbuild/win32-arm64@0.20.2": "@esbuild/win32-arm64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
"@esbuild/win32-ia32@0.20.2": "@esbuild/win32-ia32@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
"@esbuild/win32-x64@0.20.2": "@esbuild/win32-x64@0.21.5":
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0" version "4.4.0"
@@ -1090,85 +1090,85 @@
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a" resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a"
integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ== integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==
"@rollup/rollup-android-arm-eabi@4.17.2": "@rollup/rollup-android-arm-eabi@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz#1a32112822660ee104c5dd3a7c595e26100d4c2d" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz#1661ff5ea9beb362795304cb916049aba7ac9c54"
integrity sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ== integrity sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==
"@rollup/rollup-android-arm64@4.17.2": "@rollup/rollup-android-arm64@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz#5aeef206d65ff4db423f3a93f71af91b28662c5b" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz#2ffaa91f1b55a0082b8a722525741aadcbd3971e"
integrity sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw== integrity sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==
"@rollup/rollup-darwin-arm64@4.17.2": "@rollup/rollup-darwin-arm64@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz#6b66aaf003c70454c292cd5f0236ebdc6ffbdf1a" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz#627007221b24b8cc3063703eee0b9177edf49c1f"
integrity sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw== integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==
"@rollup/rollup-darwin-x64@4.17.2": "@rollup/rollup-darwin-x64@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz#f64fc51ed12b19f883131ccbcea59fc68cbd6c0b" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz#0605506142b9e796c370d59c5984ae95b9758724"
integrity sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ== integrity sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==
"@rollup/rollup-linux-arm-gnueabihf@4.17.2": "@rollup/rollup-linux-arm-gnueabihf@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz#1a7641111be67c10111f7122d1e375d1226cbf14" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz#62dfd196d4b10c0c2db833897164d2d319ee0cbb"
integrity sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A== integrity sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==
"@rollup/rollup-linux-arm-musleabihf@4.17.2": "@rollup/rollup-linux-arm-musleabihf@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz#c93fd632923e0fee25aacd2ae414288d0b7455bb" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz#53ce72aeb982f1f34b58b380baafaf6a240fddb3"
integrity sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg== integrity sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==
"@rollup/rollup-linux-arm64-gnu@4.17.2": "@rollup/rollup-linux-arm64-gnu@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz#fa531425dd21d058a630947527b4612d9d0b4a4a" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz#1632990f62a75c74f43e4b14ab3597d7ed416496"
integrity sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A== integrity sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==
"@rollup/rollup-linux-arm64-musl@4.17.2": "@rollup/rollup-linux-arm64-musl@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz#8acc16f095ceea5854caf7b07e73f7d1802ac5af" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz#8c03a996efb41e257b414b2e0560b7a21f2d9065"
integrity sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA== integrity sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==
"@rollup/rollup-linux-powerpc64le-gnu@4.17.2": "@rollup/rollup-linux-powerpc64le-gnu@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz#94e69a8499b5cf368911b83a44bb230782aeb571" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz#5b98729628d5bcc8f7f37b58b04d6845f85c7b5d"
integrity sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ== integrity sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==
"@rollup/rollup-linux-riscv64-gnu@4.17.2": "@rollup/rollup-linux-riscv64-gnu@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz#7ef1c781c7e59e85a6ce261cc95d7f1e0b56db0f" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz#48e42e41f4cabf3573cfefcb448599c512e22983"
integrity sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg== integrity sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==
"@rollup/rollup-linux-s390x-gnu@4.17.2": "@rollup/rollup-linux-s390x-gnu@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz#f15775841c3232fca9b78cd25a7a0512c694b354" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz#e0b4f9a966872cb7d3e21b9e412a4b7efd7f0b58"
integrity sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g== integrity sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==
"@rollup/rollup-linux-x64-gnu@4.17.2": "@rollup/rollup-linux-x64-gnu@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz#b521d271798d037ad70c9f85dd97d25f8a52e811" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz#78144741993100f47bd3da72fce215e077ae036b"
integrity sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ== integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==
"@rollup/rollup-linux-x64-musl@4.17.2": "@rollup/rollup-linux-x64-musl@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz#9254019cc4baac35800991315d133cc9fd1bf385" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz#d9fe32971883cd1bd858336bd33a1c3ca6146127"
integrity sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q== integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==
"@rollup/rollup-win32-arm64-msvc@4.17.2": "@rollup/rollup-win32-arm64-msvc@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz#27f65a89f6f52ee9426ec11e3571038e4671790f" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz#71fa3ea369316db703a909c790743972e98afae5"
integrity sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA== integrity sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==
"@rollup/rollup-win32-ia32-msvc@4.17.2": "@rollup/rollup-win32-ia32-msvc@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz#a2fbf8246ed0bb014f078ca34ae6b377a90cb411" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz#653f5989a60658e17d7576a3996deb3902e342e2"
integrity sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ== integrity sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==
"@rollup/rollup-win32-x64-msvc@4.17.2": "@rollup/rollup-win32-x64-msvc@4.24.0":
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz#5a2d08b81e8064b34242d5cc9973ef8dd1e60503" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz#0574d7e87b44ee8511d08cc7f914bcb802b70818"
integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w== integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==
"@rushstack/eslint-patch@^1.3.3": "@rushstack/eslint-patch@^1.3.3":
version "1.10.2" version "1.10.2"
@@ -1734,10 +1734,10 @@
resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.52.tgz#edbf0bca6922cd0ad1936a7486f9d03523d7565a" resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.52.tgz#edbf0bca6922cd0ad1936a7486f9d03523d7565a"
integrity sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw== integrity sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==
"@types/estree@1.0.5": "@types/estree@1.0.6":
version "1.0.5" version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.8": "@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.8":
version "7946.0.14" version "7946.0.14"
@@ -3930,34 +3930,34 @@ es6-promisify@^5.0.0:
dependencies: dependencies:
es6-promise "^4.0.3" es6-promise "^4.0.3"
esbuild@^0.20.1: esbuild@^0.21.3:
version "0.20.2" version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
optionalDependencies: optionalDependencies:
"@esbuild/aix-ppc64" "0.20.2" "@esbuild/aix-ppc64" "0.21.5"
"@esbuild/android-arm" "0.20.2" "@esbuild/android-arm" "0.21.5"
"@esbuild/android-arm64" "0.20.2" "@esbuild/android-arm64" "0.21.5"
"@esbuild/android-x64" "0.20.2" "@esbuild/android-x64" "0.21.5"
"@esbuild/darwin-arm64" "0.20.2" "@esbuild/darwin-arm64" "0.21.5"
"@esbuild/darwin-x64" "0.20.2" "@esbuild/darwin-x64" "0.21.5"
"@esbuild/freebsd-arm64" "0.20.2" "@esbuild/freebsd-arm64" "0.21.5"
"@esbuild/freebsd-x64" "0.20.2" "@esbuild/freebsd-x64" "0.21.5"
"@esbuild/linux-arm" "0.20.2" "@esbuild/linux-arm" "0.21.5"
"@esbuild/linux-arm64" "0.20.2" "@esbuild/linux-arm64" "0.21.5"
"@esbuild/linux-ia32" "0.20.2" "@esbuild/linux-ia32" "0.21.5"
"@esbuild/linux-loong64" "0.20.2" "@esbuild/linux-loong64" "0.21.5"
"@esbuild/linux-mips64el" "0.20.2" "@esbuild/linux-mips64el" "0.21.5"
"@esbuild/linux-ppc64" "0.20.2" "@esbuild/linux-ppc64" "0.21.5"
"@esbuild/linux-riscv64" "0.20.2" "@esbuild/linux-riscv64" "0.21.5"
"@esbuild/linux-s390x" "0.20.2" "@esbuild/linux-s390x" "0.21.5"
"@esbuild/linux-x64" "0.20.2" "@esbuild/linux-x64" "0.21.5"
"@esbuild/netbsd-x64" "0.20.2" "@esbuild/netbsd-x64" "0.21.5"
"@esbuild/openbsd-x64" "0.20.2" "@esbuild/openbsd-x64" "0.21.5"
"@esbuild/sunos-x64" "0.20.2" "@esbuild/sunos-x64" "0.21.5"
"@esbuild/win32-arm64" "0.20.2" "@esbuild/win32-arm64" "0.21.5"
"@esbuild/win32-ia32" "0.20.2" "@esbuild/win32-ia32" "0.21.5"
"@esbuild/win32-x64" "0.20.2" "@esbuild/win32-x64" "0.21.5"
escalade@^3.1.2: escalade@^3.1.2:
version "3.1.2" version "3.1.2"
@@ -7135,6 +7135,11 @@ picocolors@^1.0.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
picocolors@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
@@ -7242,6 +7247,15 @@ postcss@^8.4.40:
picocolors "^1.0.1" picocolors "^1.0.1"
source-map-js "^1.2.0" source-map-js "^1.2.0"
postcss@^8.4.43:
version "8.4.47"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
dependencies:
nanoid "^3.3.7"
picocolors "^1.1.0"
source-map-js "^1.2.1"
potpack@^1.0.2: potpack@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14" resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14"
@@ -8010,29 +8024,29 @@ robust-predicates@^3.0.2:
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
rollup@^4.13.0: rollup@^4.20.0:
version "4.17.2" version "4.24.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.17.2.tgz#26d1785d0144122277fdb20ab3a24729ae68301f" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.0.tgz#c14a3576f20622ea6a5c9cad7caca5e6e9555d05"
integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ== integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==
dependencies: dependencies:
"@types/estree" "1.0.5" "@types/estree" "1.0.6"
optionalDependencies: optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.17.2" "@rollup/rollup-android-arm-eabi" "4.24.0"
"@rollup/rollup-android-arm64" "4.17.2" "@rollup/rollup-android-arm64" "4.24.0"
"@rollup/rollup-darwin-arm64" "4.17.2" "@rollup/rollup-darwin-arm64" "4.24.0"
"@rollup/rollup-darwin-x64" "4.17.2" "@rollup/rollup-darwin-x64" "4.24.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.17.2" "@rollup/rollup-linux-arm-gnueabihf" "4.24.0"
"@rollup/rollup-linux-arm-musleabihf" "4.17.2" "@rollup/rollup-linux-arm-musleabihf" "4.24.0"
"@rollup/rollup-linux-arm64-gnu" "4.17.2" "@rollup/rollup-linux-arm64-gnu" "4.24.0"
"@rollup/rollup-linux-arm64-musl" "4.17.2" "@rollup/rollup-linux-arm64-musl" "4.24.0"
"@rollup/rollup-linux-powerpc64le-gnu" "4.17.2" "@rollup/rollup-linux-powerpc64le-gnu" "4.24.0"
"@rollup/rollup-linux-riscv64-gnu" "4.17.2" "@rollup/rollup-linux-riscv64-gnu" "4.24.0"
"@rollup/rollup-linux-s390x-gnu" "4.17.2" "@rollup/rollup-linux-s390x-gnu" "4.24.0"
"@rollup/rollup-linux-x64-gnu" "4.17.2" "@rollup/rollup-linux-x64-gnu" "4.24.0"
"@rollup/rollup-linux-x64-musl" "4.17.2" "@rollup/rollup-linux-x64-musl" "4.24.0"
"@rollup/rollup-win32-arm64-msvc" "4.17.2" "@rollup/rollup-win32-arm64-msvc" "4.24.0"
"@rollup/rollup-win32-ia32-msvc" "4.17.2" "@rollup/rollup-win32-ia32-msvc" "4.24.0"
"@rollup/rollup-win32-x64-msvc" "4.17.2" "@rollup/rollup-win32-x64-msvc" "4.24.0"
fsevents "~2.3.2" fsevents "~2.3.2"
rope-sequence@^1.3.0: rope-sequence@^1.3.0:
@@ -8326,6 +8340,11 @@ sorted-union-stream@~2.1.3:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map@^0.5.7: source-map@^0.5.7:
version "0.5.7" version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -9195,14 +9214,14 @@ verror@1.10.0:
core-util-is "1.0.2" core-util-is "1.0.2"
extsprintf "^1.2.0" extsprintf "^1.2.0"
vite@^5.0.11: vite@^5.4.9:
version "5.2.11" version "5.4.9"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.9.tgz#215c80cbebfd09ccbb9ceb8c0621391c9abdc19c"
integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== integrity sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==
dependencies: dependencies:
esbuild "^0.20.1" esbuild "^0.21.3"
postcss "^8.4.38" postcss "^8.4.43"
rollup "^4.13.0" rollup "^4.20.0"
optionalDependencies: optionalDependencies:
fsevents "~2.3.3" fsevents "~2.3.3"

View File

@@ -89,12 +89,11 @@ func (e *Engine) evaluateGroup(rules []models.RuleDetail, operator string, conve
return false return false
} }
// evaluateRule evaluates a single rule against a given conversation.
func (e *Engine) evaluateRule(rule models.RuleDetail, conversation cmodels.Conversation) bool { func (e *Engine) evaluateRule(rule models.RuleDetail, conversation cmodels.Conversation) bool {
var ( var (
valueToCompare string valueToCompare string
valuesToCompare []string ruleValues []string
conditionMet bool conditionMet bool
) )
// Extract the value from the conversation based on the rule's field // Extract the value from the conversation based on the rule's field
@@ -102,7 +101,7 @@ func (e *Engine) evaluateRule(rule models.RuleDetail, conversation cmodels.Conve
case models.ConversationFieldSubject: case models.ConversationFieldSubject:
valueToCompare = conversation.Subject valueToCompare = conversation.Subject
case models.ConversationFieldContent: case models.ConversationFieldContent:
valueToCompare = conversation.FirstMessage valueToCompare = conversation.LastMessage
case models.ConversationFieldStatus: case models.ConversationFieldStatus:
valueToCompare = conversation.Status.String valueToCompare = conversation.Status.String
case models.ConversationFieldPriority: case models.ConversationFieldPriority:
@@ -120,30 +119,35 @@ func (e *Engine) evaluateRule(rule models.RuleDetail, conversation cmodels.Conve
return false return false
} }
// Case sensitivity handling
if !rule.CaseSensitiveMatch { if !rule.CaseSensitiveMatch {
valueToCompare = strings.ToLower(valueToCompare) valueToCompare = strings.ToLower(valueToCompare)
rule.Value = strings.ToLower(rule.Value) rule.Value = strings.ToLower(rule.Value)
} }
// RuleContains and NotContains have all values comma-separated. // Split and trim values for Contains/NotContains operations
if rule.Operator == models.RuleContains || rule.Operator == models.RuleNotContains { if rule.Operator == models.RuleContains || rule.Operator == models.RuleNotContains {
valuesToCompare = strings.Split(rule.Value, ",") ruleValues = strings.Split(rule.Value, ",")
// Trim whitespace from each value for i := range ruleValues {
for i := range valuesToCompare { ruleValues[i] = strings.TrimSpace(ruleValues[i])
valuesToCompare[i] = strings.TrimSpace(valuesToCompare[i]) if !rule.CaseSensitiveMatch {
ruleValues[i] = strings.ToLower(ruleValues[i])
}
} }
} }
e.lo.Debug("evaluating rule", "rule_field", rule.Field, "rule_operator", rule.Operator, "rule_value", rule.Value, "values_to_compare", valuesToCompare, "value_to_compare", valueToCompare, "conversation_uuid", conversation.UUID) e.lo.Debug("evaluating rule", "rule_field", rule.Field, "rule_operator", rule.Operator,
"rule_value", rule.Value, "rule_values", ruleValues, "value_to_compare",
valueToCompare, "conversation_uuid", conversation.UUID)
// Compare with set operator. // Compare with set operator
switch rule.Operator { switch rule.Operator {
case models.RuleEquals: case models.RuleEquals:
conditionMet = valueToCompare == rule.Value conditionMet = valueToCompare == rule.Value
case models.RuleNotEqual: case models.RuleNotEqual:
conditionMet = valueToCompare != rule.Value conditionMet = valueToCompare != rule.Value
case models.RuleContains: case models.RuleContains:
for _, val := range valuesToCompare { for _, val := range ruleValues {
if strings.Contains(valueToCompare, val) { if strings.Contains(valueToCompare, val) {
conditionMet = true conditionMet = true
break break
@@ -151,7 +155,7 @@ func (e *Engine) evaluateRule(rule models.RuleDetail, conversation cmodels.Conve
} }
case models.RuleNotContains: case models.RuleNotContains:
conditionMet = true conditionMet = true
for _, val := range valuesToCompare { for _, val := range ruleValues {
if strings.Contains(valueToCompare, val) { if strings.Contains(valueToCompare, val) {
conditionMet = false conditionMet = false
break break
@@ -165,7 +169,8 @@ func (e *Engine) evaluateRule(rule models.RuleDetail, conversation cmodels.Conve
e.lo.Error("unrecognized rule logical operator", "operator", rule.Operator) e.lo.Error("unrecognized rule logical operator", "operator", rule.Operator)
return false return false
} }
e.lo.Debug("rule conditions met status", "met", conditionMet, "conversation_uuid", conversation.UUID) e.lo.Debug("rule conditions met status", "met", conditionMet,
"conversation_uuid", conversation.UUID)
return conditionMet return conditionMet
} }

View File

@@ -163,8 +163,7 @@ type queries struct {
UpdateConversationMeta *sqlx.Stmt `query:"update-conversation-meta"` UpdateConversationMeta *sqlx.Stmt `query:"update-conversation-meta"`
InsertConverstionParticipant *sqlx.Stmt `query:"insert-conversation-participant"` InsertConverstionParticipant *sqlx.Stmt `query:"insert-conversation-participant"`
InsertConversation *sqlx.Stmt `query:"insert-conversation"` InsertConversation *sqlx.Stmt `query:"insert-conversation"`
AddConversationTag *sqlx.Stmt `query:"add-conversation-tag"` UpsertConversationTags *sqlx.Stmt `query:"upsert-conversation-tags"`
DeleteConversationTags *sqlx.Stmt `query:"delete-conversation-tags"`
// Message queries. // Message queries.
GetMessage *sqlx.Stmt `query:"get-message"` GetMessage *sqlx.Stmt `query:"get-message"`
@@ -544,17 +543,11 @@ func (c *Manager) GetDashboardChart(userID, teamID int) (json.RawMessage, error)
return stats, nil return stats, nil
} }
// UpsertConversationTags updates the tags associated with a conversation. // UpsertConversationTags upserts the tags associated with a conversation.
func (t *Manager) UpsertConversationTags(uuid string, tagIDs []int) error { func (t *Manager) UpsertConversationTags(uuid string, tagIDs []int) error {
if _, err := t.q.DeleteConversationTags.Exec(uuid, pq.Array(tagIDs)); err != nil { if _, err := t.q.UpsertConversationTags.Exec(uuid, pq.Array(tagIDs)); err != nil {
t.lo.Error("error deleting conversation tags", "error", err) t.lo.Error("error upserting conversation tags", "error", err)
return envelope.NewError(envelope.GeneralError, "Error adding tags", nil) return envelope.NewError(envelope.GeneralError, "Error upserting tags", nil)
}
for _, tagID := range tagIDs {
if _, err := t.q.AddConversationTag.Exec(uuid, tagID); err != nil {
t.lo.Error("error adding tags to conversation", "error", err)
return envelope.NewError(envelope.GeneralError, "Error adding tags", nil)
}
} }
return nil return nil
} }

View File

@@ -308,11 +308,11 @@ func (m *Manager) InsertMessage(message *models.Message) error {
} }
// Update conversation meta with the last message details. // Update conversation meta with the last message details.
trimmedMessage := stringutil.SanitizeAndTruncate(message.Content, 45) plainTextContent := stringutil.HTML2Text(message.Content)
m.UpdateConversationLastMessage(0, message.ConversationUUID, trimmedMessage, message.CreatedAt) m.UpdateConversationLastMessage(0, message.ConversationUUID, plainTextContent, message.CreatedAt)
// Broadcast new message to all conversation subscribers. // Broadcast new message to all conversation subscribers.
m.BroadcastNewConversationMessage(message.ConversationUUID, trimmedMessage, message.UUID, message.CreatedAt.Format(time.RFC3339), message.Type, message.Private) m.BroadcastNewConversationMessage(message.ConversationUUID, plainTextContent, message.UUID, message.CreatedAt.Format(time.RFC3339), message.Type, message.Private)
return nil return nil
} }
@@ -555,9 +555,10 @@ func (m *Manager) findOrCreateConversation(in *models.Message, inboxID int, cont
new = true new = true
// Put subject & last message details in meta. // Put subject & last message details in meta.
plainTextContent := stringutil.HTML2Text(in.Content)
conversationMeta, err := json.Marshal(map[string]string{ conversationMeta, err := json.Marshal(map[string]string{
"subject": in.Subject, "subject": in.Subject,
"last_message": stringutil.SanitizeAndTruncate(in.Content, maxLastMessageLen), "last_message": plainTextContent,
"last_message_at": time.Now().Format(time.RFC3339), "last_message_at": time.Now().Format(time.RFC3339),
}) })
if err != nil { if err != nil {

View File

@@ -45,7 +45,6 @@ type Conversation struct {
ContactAvatarURL null.String `db:"contact_avatar_url" json:"contact_avatar_url"` ContactAvatarURL null.String `db:"contact_avatar_url" json:"contact_avatar_url"`
LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"` LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"`
LastMessage string `db:"last_message" json:"last_message"` LastMessage string `db:"last_message" json:"last_message"`
FirstMessage string `json:"-"`
} }
type ConversationParticipant struct { type ConversationParticipant struct {

View File

@@ -62,6 +62,7 @@ SELECT
ct.email as email, ct.email as email,
ct.phone_number as phone_number, ct.phone_number as phone_number,
ct.avatar_url as avatar_url, ct.avatar_url as avatar_url,
COALESCE(c.meta->>'last_message', '') as last_message,
(SELECT COALESCE( (SELECT COALESCE(
(SELECT json_agg(t.name) (SELECT json_agg(t.name)
FROM tags t FROM tags t
@@ -102,8 +103,6 @@ SELECT
)) AS tags )) AS tags
FROM conversations c FROM conversations c
JOIN contacts ct ON c.contact_id = ct.id JOIN contacts ct ON c.contact_id = ct.id
LEFT JOIN users u ON u.id = c.assigned_user_id
LEFT JOIN teams at ON at.id = c.assigned_team_id
LEFT JOIN status s ON c.status_id = s.id LEFT JOIN status s ON c.status_id = s.id
LEFT JOIN priority p ON c.priority_id = p.id LEFT JOIN priority p ON c.priority_id = p.id
WHERE c.created_at > $1; WHERE c.created_at > $1;
@@ -249,24 +248,19 @@ UPDATE conversations
SET first_reply_at = $2 SET first_reply_at = $2
WHERE first_reply_at IS NULL AND id = $1; WHERE first_reply_at IS NULL AND id = $1;
-- name: add-conversation-tag -- name: upsert-conversation-tags
INSERT INTO conversation_tags (conversation_id, tag_id) WITH conversation_id AS (
VALUES( SELECT id FROM conversations WHERE uuid = $1
( ),
SELECT id inserted AS (
FROM conversations INSERT INTO conversation_tags (conversation_id, tag_id)
WHERE uuid = $1 SELECT conversation_id.id, unnest($2::int[])
), FROM conversation_id
$2 ON CONFLICT (conversation_id, tag_id) DO UPDATE SET tag_id = EXCLUDED.tag_id
) ON CONFLICT DO NOTHING; )
-- name: delete-conversation-tags
DELETE FROM conversation_tags DELETE FROM conversation_tags
WHERE conversation_id = ( WHERE conversation_id = (SELECT id FROM conversation_id)
SELECT id AND tag_id NOT IN (SELECT unnest($2::int[]));
FROM conversations
WHERE uuid = $1
) AND tag_id NOT IN (SELECT unnest($2::int[]));
-- name: get-to-address -- name: get-to-address
SELECT cm.source_id SELECT cm.source_id

View File

@@ -4,6 +4,7 @@ package inbox
import ( import (
"context" "context"
"embed" "embed"
"encoding/json"
"errors" "errors"
"sync" "sync"
@@ -120,8 +121,8 @@ func (m *Manager) Get(id int) (Inbox, error) {
func (m *Manager) GetByID(id int) (imodels.Inbox, error) { func (m *Manager) GetByID(id int) (imodels.Inbox, error) {
var inbox imodels.Inbox var inbox imodels.Inbox
if err := m.queries.GetByID.Get(&inbox, id); err != nil { if err := m.queries.GetByID.Get(&inbox, id); err != nil {
m.lo.Error("fetching inbox by ID", "error", err) m.lo.Error("error fetching inbox", "error", err)
return inbox, err return inbox, envelope.NewError(envelope.GeneralError, "Error fetching inbox", nil)
} }
return inbox, nil return inbox, nil
} }
@@ -157,11 +158,65 @@ func (m *Manager) Create(inbox imodels.Inbox) error {
// Update updates an inbox in the DB. // Update updates an inbox in the DB.
func (m *Manager) Update(id int, inbox imodels.Inbox) error { func (m *Manager) Update(id int, inbox imodels.Inbox) error {
if _, err := m.queries.Update.Exec(id, inbox.Channel, inbox.Config, inbox.Name, inbox.From); err != nil { current, err := m.GetByID(id)
m.lo.Error("error updating inbox", "error", err) if err != nil {
return envelope.NewError(envelope.GeneralError, "Error updating inbox", nil) return err
} }
return nil
switch current.Channel {
case "email":
var currentCfg struct {
IMAP []map[string]interface{} `json:"imap"`
SMTP []map[string]interface{} `json:"smtp"`
}
var updateCfg struct {
IMAP []map[string]interface{} `json:"imap"`
SMTP []map[string]interface{} `json:"smtp"`
}
if err := json.Unmarshal(current.Config, &currentCfg); err != nil {
m.lo.Error("error unmarshalling current config", "id", id, "error", err)
return envelope.NewError(envelope.GeneralError, "Error unmarshalling config", nil)
}
if len(inbox.Config) == 0 {
return envelope.NewError(envelope.InputError, "Empty config provided", nil)
}
if err := json.Unmarshal(inbox.Config, &updateCfg); err != nil {
m.lo.Error("error unmarshalling update config", "id", id, "error", err)
return envelope.NewError(envelope.GeneralError, "Error unmarshalling config", nil)
}
if len(updateCfg.IMAP) == 0 || len(updateCfg.SMTP) == 0 {
return envelope.NewError(envelope.InputError, "Invalid email config", nil)
}
// Preserve existing IMAP passwords if update has empty password
for i := range updateCfg.IMAP {
if updateCfg.IMAP[i]["password"] == "" && i < len(currentCfg.IMAP) {
updateCfg.IMAP[i]["password"] = currentCfg.IMAP[i]["password"]
}
}
// Preserve existing SMTP passwords if update has empty password
for i := range updateCfg.SMTP {
if updateCfg.SMTP[i]["password"] == "" && i < len(currentCfg.SMTP) {
updateCfg.SMTP[i]["password"] = currentCfg.SMTP[i]["password"]
}
}
updatedConfig, err := json.Marshal(updateCfg)
if err != nil {
m.lo.Error("error marshalling updated config", "id", id, "error", err)
return err
}
inbox.Config = updatedConfig
}
if _, err := m.queries.Update.Exec(id, inbox.Channel, inbox.Config, inbox.Name, inbox.From); err != nil {
m.lo.Error("error updating inbox", "error", err)
return envelope.NewError(envelope.GeneralError, "Error updating inbox", nil)
}
return nil
} }
// Toggle toggles the status of an inbox in the DB. // Toggle toggles the status of an inbox in the DB.

View File

@@ -2,7 +2,10 @@ package models
import ( import (
"encoding/json" "encoding/json"
"strings"
"time" "time"
"github.com/abhinavxd/artemis/internal/stringutil"
) )
// Inbox represents a inbox record in DB. // Inbox represents a inbox record in DB.
@@ -16,3 +19,40 @@ type Inbox struct {
From string `db:"from" json:"from"` From string `db:"from" json:"from"`
Config json.RawMessage `db:"config" json:"config"` Config json.RawMessage `db:"config" json:"config"`
} }
// ClearPasswords masks all config passwords
func (m *Inbox) ClearPasswords() error {
switch m.Channel {
case "email":
var cfg struct {
IMAP []map[string]interface{} `json:"imap"`
SMTP []map[string]interface{} `json:"smtp"`
}
if err := json.Unmarshal(m.Config, &cfg); err != nil {
return err
}
dummyPassword := strings.Repeat(stringutil.PasswordDummy, 10)
for i := range cfg.IMAP {
cfg.IMAP[i]["password"] = dummyPassword
}
for i := range cfg.SMTP {
cfg.SMTP[i]["password"] = dummyPassword
}
clearedConfig, err := json.Marshal(cfg)
if err != nil {
return err
}
m.Config = clearedConfig
default:
return nil
}
return nil
}

View File

@@ -2,7 +2,7 @@
SELECT * from inboxes where disabled is NOT TRUE and soft_delete is false; SELECT * from inboxes where disabled is NOT TRUE and soft_delete is false;
-- name: get-all-inboxes -- name: get-all-inboxes
SELECT * from inboxes where soft_delete is false; SELECT id, name, channel, disabled, updated_at from inboxes where soft_delete is false;
-- name: insert-inbox -- name: insert-inbox
INSERT INTO inboxes INSERT INTO inboxes

View File

@@ -2,10 +2,12 @@ package oidc
import ( import (
"embed" "embed"
"strings"
"github.com/abhinavxd/artemis/internal/dbutil" "github.com/abhinavxd/artemis/internal/dbutil"
"github.com/abhinavxd/artemis/internal/envelope" "github.com/abhinavxd/artemis/internal/envelope"
"github.com/abhinavxd/artemis/internal/oidc/models" "github.com/abhinavxd/artemis/internal/oidc/models"
"github.com/abhinavxd/artemis/internal/stringutil"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/zerodha/logf" "github.com/zerodha/logf"
) )
@@ -58,6 +60,9 @@ func (o *Manager) Get(id int) (models.OIDC, error) {
return oidc, envelope.NewError(envelope.GeneralError, "Error fetching OIDC", nil) return oidc, envelope.NewError(envelope.GeneralError, "Error fetching OIDC", nil)
} }
oidc.SetProviderLogo() oidc.SetProviderLogo()
if oidc.ClientSecret != "" {
oidc.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10)
}
return oidc, nil return oidc, nil
} }
@@ -85,6 +90,13 @@ func (o *Manager) Create(oidc models.OIDC) error {
// Create updates a oidc by id. // Create updates a oidc by id.
func (o *Manager) Update(id int, oidc models.OIDC) error { func (o *Manager) Update(id int, oidc models.OIDC) error {
current, err := o.Get(id)
if err != nil {
return err
}
if oidc.ClientSecret == "" {
oidc.ClientSecret = current.ClientSecret
}
if _, err := o.q.UpdateOIDC.Exec(id, oidc.Name, oidc.Provider, oidc.ProviderURL, oidc.ClientID, oidc.ClientSecret, oidc.Disabled); err != nil { if _, err := o.q.UpdateOIDC.Exec(id, oidc.Name, oidc.Provider, oidc.ProviderURL, oidc.ClientID, oidc.ClientSecret, oidc.Disabled); err != nil {
o.lo.Error("error updating oidc", "error", err) o.lo.Error("error updating oidc", "error", err)
return envelope.NewError(envelope.GeneralError, "Error updating OIDC", nil) return envelope.NewError(envelope.GeneralError, "Error updating OIDC", nil)

View File

@@ -11,18 +11,18 @@ import (
"github.com/k3a/html2text" "github.com/k3a/html2text"
) )
const (
PasswordDummy = "•"
)
var ( var (
regexpNonAlNum = regexp.MustCompile(`[^a-zA-Z0-9\-_\.]+`) regexpNonAlNum = regexp.MustCompile(`[^a-zA-Z0-9\-_\.]+`)
regexpSpaces = regexp.MustCompile(`[\s]+`) regexpSpaces = regexp.MustCompile(`[\s]+`)
) )
// SanitizeAndTruncate removes HTML tags, trims whitespace, makes the text human-readable, and shortens the content to a specified maximum length, appending "..." if truncated. // HTML2Text converts HTML to text.
func SanitizeAndTruncate(content string, maxLen int) string { func HTML2Text(html string) string {
plain := strings.TrimSpace(html2text.HTML2Text(content)) return strings.TrimSpace(html2text.HTML2Text(html))
if len(plain) > maxLen {
plain = plain[:maxLen] + "..."
}
return plain
} }
// SanitizeFilename sanitizes the provided filename. // SanitizeFilename sanitizes the provided filename.
@@ -81,4 +81,4 @@ func GetPathFromURL(rawURL string) (string, error) {
return "", err return "", err
} }
return parsedURL.Path, nil return parsedURL.Path, nil
} }