Compare commits

..

8 Commits

Author SHA1 Message Date
Abhinav Raut
e0dc0285a4 fix: agents availability status changing to online after doing an email password login even after being Away or in Reassinging Replies status.
This was not affecting OIDC login just email password login
2025-08-19 16:34:15 +05:30
Abhinav Raut
b971619ea6 use tabs for search results seperation also looks better now. 2025-08-14 16:12:29 +05:30
Abhinav Raut
69accaebef fix: conversations not reopening on reply from contacts (only when there's an attachment in the reply) else the convo would reopen, the conversation_uuid field was empty as it wasn't part of the get-messages query. 2025-08-14 16:11:27 +05:30
Abhinav Raut
27de73536e Update confirmed-bug.md 2025-07-23 00:18:36 +05:30
Abhinav Raut
df108a3363 feat: add confirmed and possible bug report templates 2025-07-23 00:14:34 +05:30
Abhinav Raut
266c3dab72 Merge pull request #121 from abhinavxd/dependabot/go_modules/golang.org/x/oauth2-0.27.0
chore(deps): bump golang.org/x/oauth2 from 0.21.0 to 0.27.0
2025-07-20 14:42:29 +05:30
dependabot[bot]
bf2c1fff6f chore(deps): bump golang.org/x/oauth2 from 0.21.0 to 0.27.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.21.0 to 0.27.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.21.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.27.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-20 09:11:03 +00:00
Abhinav Raut
2930af0c4f feat: add API getting started guide and update navigation 2025-07-07 01:06:28 +05:30
10 changed files with 181 additions and 90 deletions

16
.github/ISSUE_TEMPLATE/confirmed-bug.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Confirmed Bug Report
about: Report a confirmed bug in Libredesk
title: "[Bug] <brief summary>"
labels: bug
assignees: ""
---
**Version:**
- libredesk: [eg: v0.7.0]
**Description of the bug and steps to reproduce:**
A clear and concise description of what the bug is.
**Logs / Screenshots:**
Attach any relevant logs or screenshots to help diagnose the issue.

16
.github/ISSUE_TEMPLATE/possible-bug.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Possible Bug Report
about: Something in Libredesk might be broken but needs confirmation
title: "[Possible Bug] <brief summary>"
labels: bug, needs-investigation
assignees: ""
---
**Version:**
- libredesk: [eg: v0.7.0]
**Description of the bug and steps to reproduce:**
A clear and concise description of what the bug is.
**Logs / Screenshots:**
Attach any relevant logs or screenshots to help diagnose the issue.

View File

@@ -3,7 +3,6 @@ package main
import ( import (
amodels "github.com/abhinavxd/libredesk/internal/auth/models" amodels "github.com/abhinavxd/libredesk/internal/auth/models"
"github.com/abhinavxd/libredesk/internal/envelope" "github.com/abhinavxd/libredesk/internal/envelope"
umodels "github.com/abhinavxd/libredesk/internal/user/models"
realip "github.com/ferluci/fast-realip" realip "github.com/ferluci/fast-realip"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
@@ -42,12 +41,6 @@ func handleLogin(r *fastglue.Request) error {
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.accountDisabled"), nil)) return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.accountDisabled"), nil))
} }
// Set user availability status to online.
if err := app.user.UpdateAvailability(user.ID, umodels.Online); err != nil {
return sendErrorEnvelope(r, err)
}
user.AvailabilityStatus = umodels.Online
if err := app.auth.SaveSession(amodels.User{ if err := app.auth.SaveSession(amodels.User{
ID: user.ID, ID: user.ID,
Email: user.Email.String, Email: user.Email.String,

View File

@@ -0,0 +1,30 @@
# API getting started
You can access the Libredesk API to interact with your instance programmatically.
## Generating API keys
1. **Edit agent**: Go to Admin → Teammate → Agent → Edit
2. **Generate new API key**: An API Key and API Secret will be generated for the agent
3. **Save the credentials**: Keep both the API Key and API Secret secure
4. **Key management**: You can revoke / regenerate API keys at any time from the same page
## Using the API
LibreDesk supports two authentication schemes:
### Basic authentication
```bash
curl -X GET "https://your-libredesk-instance.com/api/endpoint" \
-H "Authorization: Basic <base64_encoded_key:secret>"
```
### Token authentication
```bash
curl -X GET "https://your-libredesk-instance.com/api/endpoint" \
-H "Authorization: token your_api_key:your_api_secret"
```
## API Documentation
Complete API documentation with available endpoints and examples coming soon.

View File

@@ -32,6 +32,7 @@ nav:
- Email Templates: templating.md - Email Templates: templating.md
- SSO Setup: sso.md - SSO Setup: sso.md
- Webhooks: webhooks.md - Webhooks: webhooks.md
- API Getting Started: api-getting-started.md
- Contributions: - Contributions:
- Developer Setup: developer-setup.md - Developer Setup: developer-setup.md
- Translate Libredesk: translations.md - Translate Libredesk: translations.md

View File

@@ -1,105 +1,139 @@
<template> <template>
<div class="max-w-5xl mx-auto p-6 min-h-screen"> <div class="max-w-5xl mx-auto p-6 min-h-screen">
<div class="space-y-8"> <Tabs :default-value="defaultTab" v-model="activeTab">
<div <TabsList class="grid w-full mb-6" :class="tabsGridClass">
v-for="(items, type) in results" <TabsTrigger v-for="(items, type) in results" :key="type" :value="type" class="capitalize">
:key="type" {{ type }} ({{ items.length }})
class="bg-card rounded shadow overflow-hidden" </TabsTrigger>
> </TabsList>
<!-- Header for each section -->
<h2
class="bg-primary dark:bg-primary text-lg font-bold text-white dark:text-primary-foreground py-2 px-6 capitalize"
>
{{ type }}
</h2>
<!-- No results message --> <TabsContent v-for="(items, type) in results" :key="type" :value="type" class="mt-0">
<div v-if="items.length === 0" class="p-6 text-gray-500 dark:text-muted-foreground"> <div class="bg-background rounded border overflow-hidden">
{{ <!-- No results message -->
$t('globals.messages.noResults', { <div v-if="items.length === 0" class="p-8 text-center text-muted-foreground">
name: type <div class="text-lg font-medium mb-2">
}) {{
}} $t('globals.messages.noResults', {
</div> name: type
})
}}
</div>
<div class="text-sm">{{ $t('search.adjustSearchTerms') }}</div>
</div>
<!-- Results list --> <!-- Results list -->
<div class="divide-y divide-gray-200 dark:divide-border"> <div v-else class="divide-y divide-border">
<div <div
v-for="item in items" v-for="item in items"
:key="item.id || item.uuid" :key="item.id || item.uuid"
class="p-6 hover:bg-gray-100 dark:hover:bg-accent transition duration-300 ease-in-out group" class="p-6 hover:bg-accent/50 transition duration-200 ease-in-out group"
>
<router-link
:to="{
name: 'inbox-conversation',
params: {
uuid: type === 'conversations' ? item.uuid : item.conversation_uuid,
type: 'assigned'
}
}"
class="block"
> >
<div class="flex justify-between items-start"> <router-link
<div class="flex-grow"> :to="{
<!-- Reference number --> name: 'inbox-conversation',
<div params: {
class="text-sm font-semibold mb-2 group-hover:text-primary dark:group-hover:text-primary transition duration-300" uuid: type === 'conversations' ? item.uuid : item.conversation_uuid,
> type: 'assigned'
#{{ }
type === 'conversations' }"
? item.reference_number class="block"
: item.conversation_reference_number >
}} <div class="flex justify-between items-start">
<div class="flex-grow">
<!-- Reference number -->
<div
class="text-sm font-semibold mb-2 text-muted-foreground group-hover:text-primary transition duration-200"
>
#{{
type === 'conversations'
? item.reference_number
: item.conversation_reference_number
}}
</div>
<!-- Content -->
<div
class="text-foreground font-medium mb-2 text-lg group-hover:text-primary transition duration-200"
>
{{
truncateText(
type === 'conversations' ? item.subject : item.text_content,
100
)
}}
</div>
<!-- Timestamp -->
<div class="text-sm text-muted-foreground flex items-center">
<ClockIcon class="h-4 w-4 mr-1" />
{{
formatDate(
type === 'conversations' ? item.created_at : item.conversation_created_at
)
}}
</div>
</div> </div>
<!-- Content --> <!-- Right arrow icon -->
<div <div
class="text-gray-900 dark:text-card-foreground font-medium mb-2 text-lg group-hover:text-gray-950 dark:group-hover:text-foreground transition duration-300" class="bg-secondary rounded-full p-2 group-hover:bg-primary transition duration-200"
> >
{{ <ChevronRightIcon
truncateText(type === 'conversations' ? item.subject : item.text_content, 100) class="h-5 w-5 text-secondary-foreground group-hover:text-primary-foreground"
}} aria-hidden="true"
</div> />
<!-- Timestamp -->
<div class="text-sm text-gray-500 dark:text-muted-foreground flex items-center">
<ClockIcon class="h-4 w-4 mr-1" />
{{
formatDate(
type === 'conversations' ? item.created_at : item.conversation_created_at
)
}}
</div> </div>
</div> </div>
</router-link>
<!-- Right arrow icon --> </div>
<div
class="bg-gray-200 dark:bg-secondary rounded-full p-2 group-hover:bg-primary dark:group-hover:bg-primary transition duration-300"
>
<ChevronRightIcon
class="h-5 w-5 text-gray-700 dark:text-secondary-foreground group-hover:text-white dark:group-hover:text-primary-foreground"
aria-hidden="true"
/>
</div>
</div>
</router-link>
</div> </div>
</div> </div>
</div> </TabsContent>
</div> </Tabs>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, ref, watch } from 'vue'
import { ChevronRightIcon, ClockIcon } from 'lucide-vue-next' import { ChevronRightIcon, ClockIcon } from 'lucide-vue-next'
import { format, parseISO } from 'date-fns' import { format, parseISO } from 'date-fns'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
defineProps({ const props = defineProps({
results: { results: {
type: Object, type: Object,
required: true required: true
} }
}) })
// Get the first available tab as default
const defaultTab = computed(() => {
const types = Object.keys(props.results)
return types.length > 0 ? types[0] : ''
})
const activeTab = ref('')
// Watch for changes in results and set the first tab as active
watch(
() => props.results,
(newResults) => {
const types = Object.keys(newResults)
if (types.length > 0 && !activeTab.value) {
activeTab.value = types[0]
}
},
{ immediate: true }
)
// Dynamic grid class based on number of tabs
const tabsGridClass = computed(() => {
const tabCount = Object.keys(props.results).length
if (tabCount <= 2) return 'grid-cols-2'
if (tabCount <= 3) return 'grid-cols-3'
if (tabCount <= 4) return 'grid-cols-4'
return 'grid-cols-5'
})
const formatDate = (dateString) => { const formatDate = (dateString) => {
const date = parseISO(dateString) const date = parseISO(dateString)
return format(date, 'MMM d, yyyy HH:mm') return format(date, 'MMM d, yyyy HH:mm')

2
go.mod
View File

@@ -38,7 +38,7 @@ require (
github.com/zerodha/simplesessions/v3 v3.0.0 github.com/zerodha/simplesessions/v3 v3.0.0
golang.org/x/crypto v0.38.0 golang.org/x/crypto v0.38.0
golang.org/x/mod v0.17.0 golang.org/x/mod v0.17.0
golang.org/x/oauth2 v0.21.0 golang.org/x/oauth2 v0.27.0
) )
require ( require (

6
go.sum
View File

@@ -140,8 +140,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM= github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM=
github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rhnvrm/simples3 v0.9.0 h1:It6/glyqRTRooRzXcYOuqpKwjGg3lsXgNmeGgxpBtjA=
github.com/rhnvrm/simples3 v0.9.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rhnvrm/simples3 v0.9.1 h1:pYfEe2wTjx8B2zFzUdy4kZn3I3Otd9ZvzIhHkFR85kE= github.com/rhnvrm/simples3 v0.9.1 h1:pYfEe2wTjx8B2zFzUdy4kZn3I3Otd9ZvzIhHkFR85kE=
github.com/rhnvrm/simples3 v0.9.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rhnvrm/simples3 v0.9.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -211,8 +209,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -568,6 +568,7 @@
"search.noResultsForQuery": "No results found for query `{query}`. Try a different search term.", "search.noResultsForQuery": "No results found for query `{query}`. Try a different search term.",
"search.minQueryLength": " Please enter at least {length} characters to search.", "search.minQueryLength": " Please enter at least {length} characters to search.",
"search.searchBy": "Search by reference number, contact email address or messages in conversations.", "search.searchBy": "Search by reference number, contact email address or messages in conversations.",
"search.adjustSearchTerms": "Try adjusting your search terms or filters.",
"sla.overdueBy": "Overdue by", "sla.overdueBy": "Overdue by",
"sla.met": "SLA met", "sla.met": "SLA met",
"view.form.description": "Create and save custom filter views for quick access to your conversations.", "view.form.description": "Create and save custom filter views for quick access to your conversations.",

View File

@@ -438,6 +438,7 @@ SELECT
m.sender_type, m.sender_type,
m.sender_id, m.sender_id,
m.meta, m.meta,
c.uuid as conversation_uuid,
COALESCE( COALESCE(
json_agg( json_agg(
json_build_object( json_build_object(
@@ -452,10 +453,11 @@ SELECT
'[]'::json '[]'::json
) AS attachments ) AS attachments
FROM conversation_messages m FROM conversation_messages m
INNER JOIN conversations c ON c.id = m.conversation_id
LEFT JOIN media ON media.model_type = 'messages' AND media.model_id = m.id LEFT JOIN media ON media.model_type = 'messages' AND media.model_id = m.id
WHERE m.uuid = $1 WHERE m.uuid = $1
GROUP BY GROUP BY
m.id, m.created_at, m.updated_at, m.status, m.type, m.content, m.uuid, m.private, m.sender_type m.id, m.created_at, m.updated_at, m.status, m.type, m.content, m.uuid, m.private, m.sender_type, c.uuid
ORDER BY m.created_at; ORDER BY m.created_at;
-- name: get-messages -- name: get-messages