- show thumbnail image in widget thread instead of the entire image

- update file imports to use shared-ui utils and remove redundant file.js
- Implement SignedURLStore interface for fs store
This commit is contained in:
Abhinav Raut
2025-08-19 03:01:21 +05:30
parent 1962abdc16
commit f72f158cf0
8 changed files with 79 additions and 19 deletions
@@ -42,7 +42,7 @@
<script setup>
import { computed } from 'vue'
import { formatBytes } from '../../../../utils/file.js'
import { formatBytes } from '@shared-ui/utils/file'
import { X, Paperclip as PaperclipIcon } from 'lucide-vue-next'
import { DotLoader } from '@shared-ui/components/ui/loader'
import { Tooltip, TooltipContent, TooltipTrigger } from '@shared-ui/components/ui/tooltip'
@@ -22,7 +22,7 @@
</template>
<script setup>
import { formatBytes } from '../../../../utils/file.js'
import { formatBytes } from '@shared-ui/utils/file'
import { Download } from 'lucide-vue-next';
const props = defineProps({
@@ -18,7 +18,7 @@
</template>
<script setup>
import { formatBytes, getThumbFilepath } from '../../../../utils/file.js'
import { formatBytes, getThumbFilepath } from '@shared-ui/utils/file'
import { Download } from 'lucide-vue-next';
const props = defineProps({
-13
View File
@@ -1,13 +0,0 @@
export function formatBytes(bytes) {
if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + ' KB'
} else {
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
}
}
export function getThumbFilepath (filepath) {
const urlParts = filepath.split('/')
const filename = urlParts.pop()
return `/uploads/thumb_${filename}`
}
@@ -8,7 +8,7 @@
<!-- Image preview -->
<div v-if="isImage(attachment)" class="relative">
<img
:src="attachment.url"
:src="getThumbnailUrl(attachment)"
:alt="attachment.name"
class="max-w-48 max-h-32 rounded-lg object-cover"
@click="openImage(attachment.url)"
@@ -35,7 +35,7 @@
<script setup>
import { File } from 'lucide-vue-next';
import { formatBytes } from '@shared-ui/utils/file';
import { formatBytes, getThumbFilepath } from '@shared-ui/utils/file';
defineProps({
attachments: {
type: Array,
@@ -47,6 +47,11 @@ const isImage = (attachment) => {
return attachment.content_type && attachment.content_type.startsWith('image/')
}
const getThumbnailUrl = (attachment) => {
if (!isImage(attachment)) return attachment.url
return getThumbFilepath(attachment.url)
}
const openImage = (url) => {
window.open(url, '_blank')
}
+6
View File
@@ -5,3 +5,9 @@ export function formatBytes (bytes) {
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
}
}
export function getThumbFilepath (filepath) {
const urlParts = filepath.split('/')
const filename = urlParts.pop()
return `/uploads/thumb_${filename}`
}
+4 -1
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"os"
"strings"
"time"
"github.com/abhinavxd/libredesk/internal/dbutil"
@@ -266,7 +267,9 @@ func (m *Manager) VerifySignature(r *fastglue.Request) error {
// Check if store supports signature verification
if signedStore, ok := m.store.(SignedURLStore); ok {
if !signedStore.VerifySignature(uuid.(string), signature, expiresAt, []byte(m.secret)) {
// Strip thumb_ prefix for signature verification to match the base UUID
verificationName := strings.TrimPrefix(uuid.(string), "thumb_")
if !signedStore.VerifySignature(verificationName, signature, expiresAt, []byte(m.secret)) {
return fmt.Errorf("signature verification failed")
}
return nil
+59
View File
@@ -1,10 +1,17 @@
package fs
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strconv"
"time"
"github.com/abhinavxd/libredesk/internal/media"
)
@@ -73,6 +80,58 @@ func (c *Client) Name() string {
return "fs"
}
// GetSignedURL generates a signed URL for the file with expiration.
// This implements the SignedURLStore interface for secure public access.
func (c *Client) GetSignedURL(name string, expiresAt time.Time, secret []byte) string {
// Generate base URL
baseURL := c.GetURL(name)
// Create the signature payload: name + expires timestamp
expires := expiresAt.Unix()
payload := name + strconv.FormatInt(expires, 10)
// Generate HMAC-SHA256 signature
h := hmac.New(sha256.New, secret)
h.Write([]byte(payload))
signature := base64.URLEncoding.EncodeToString(h.Sum(nil))
// Parse base URL and add query parameters
u, err := url.Parse(baseURL)
if err != nil {
// Fallback to base URL if parsing fails
return baseURL
}
// Add signature and expires parameters
query := u.Query()
query.Set("signature", signature)
query.Set("expires", strconv.FormatInt(expires, 10))
u.RawQuery = query.Encode()
return u.String()
}
// VerifySignature verifies that a signature is valid for the given parameters.
// This implements the SignedURLStore interface for secure public access.
func (c *Client) VerifySignature(name, signature string, expiresAt time.Time, secret []byte) bool {
// Check if URL has expired
if time.Now().After(expiresAt) {
return false
}
// Recreate the signature payload: name + expires timestamp
expires := expiresAt.Unix()
payload := name + strconv.FormatInt(expires, 10)
// Generate expected HMAC-SHA256 signature
h := hmac.New(sha256.New, secret)
h.Write([]byte(payload))
expectedSignature := base64.URLEncoding.EncodeToString(h.Sum(nil))
// Use constant-time comparison to prevent timing attacks
return subtle.ConstantTimeCompare([]byte(signature), []byte(expectedSignature)) == 1
}
// getDir returns the current working directory path if no directory is specified,
// else returns the directory path specified itself.
func getDir(dir string) string {