Skip to content

Commit 91445d7

Browse files
committed
Merge branch 'main' into feat-notifications
2 parents 1da8ff5 + 96559fb commit 91445d7

File tree

17 files changed

+230
-74
lines changed

17 files changed

+230
-74
lines changed

cmd/handlers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
2626
// Public config for app initialization.
2727
g.GET("/api/v1/config", handleGetConfig)
2828

29-
// Media.
30-
g.GET("/uploads/{uuid}", auth(handleServeMedia))
29+
// Media - supports both authenticated access and signed URLs.
30+
g.GET("/uploads/{uuid}", authOrSignedURL(handleServeMedia))
3131
g.POST("/api/v1/media", auth(handleMediaUpload))
3232

3333
// Settings.

cmd/init.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,11 @@ func initMedia(db *sqlx.DB, i18n *i18n.I18n, settings *setting.Manager) *media.M
480480
log.Fatalf("error initializing s3 media store: %v", err)
481481
}
482482
case "fs":
483+
// Default expiry to 1h if not set.
484+
fsExpiry := ko.Duration("upload.fs.expiry")
485+
if fsExpiry == 0 {
486+
fsExpiry = 1 * time.Hour
487+
}
483488
store, err = fs.New(fs.Opts{
484489
UploadURI: "/uploads",
485490
UploadPath: filepath.Clean(ko.String("upload.fs.upload_path")),
@@ -491,6 +496,8 @@ func initMedia(db *sqlx.DB, i18n *i18n.I18n, settings *setting.Manager) *media.M
491496
}
492497
return rootURL
493498
},
499+
SigningKey: ko.MustString("app.encryption_key"),
500+
Expiry: fsExpiry,
494501
})
495502
if err != nil {
496503
log.Fatalf("error initializing fs media store: %v", err)

cmd/media.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
1414
"github.com/abhinavxd/libredesk/internal/envelope"
1515
"github.com/abhinavxd/libredesk/internal/image"
16+
mmodels "github.com/abhinavxd/libredesk/internal/media/models"
1617
"github.com/abhinavxd/libredesk/internal/stringutil"
1718
"github.com/google/uuid"
1819
"github.com/valyala/fasthttp"
@@ -148,20 +149,29 @@ func handleMediaUpload(r *fastglue.Request) error {
148149
}
149150

150151
// handleServeMedia serves uploaded media.
152+
// Supports both authenticated access (with permission checks) and signed URL access (no permission checks).
151153
func handleServeMedia(r *fastglue.Request) error {
152154
var (
153-
app = r.Context.(*App)
154-
auser = r.RequestCtx.UserValue("user").(amodels.User)
155-
uuid = r.RequestCtx.UserValue("uuid").(string)
155+
app = r.Context.(*App)
156+
uuid = r.RequestCtx.UserValue("uuid").(string)
157+
authMethod = r.RequestCtx.UserValue("auth_method")
156158
)
157159

160+
// If accessed via signed URL, skip permission checks and serve file directly.
161+
if authMethod == "signed_url" {
162+
return serveMediaFile(r, app, uuid, nil)
163+
}
164+
165+
// Session/API key authenticated - perform full permission check.
166+
auser := r.RequestCtx.UserValue("user").(amodels.User)
167+
158168
user, err := app.user.GetAgent(auser.ID, "")
159169
if err != nil {
160170
return sendErrorEnvelope(r, err)
161171
}
162172

163173
// Fetch media from DB.
164-
media, err := app.media.Get(0, strings.TrimPrefix(uuid, image.ThumbPrefix))
174+
media, err := getMediaByUUID(app, uuid)
165175
if err != nil {
166176
return sendErrorEnvelope(r, err)
167177
}
@@ -187,6 +197,22 @@ func handleServeMedia(r *fastglue.Request) error {
187197
if !allowed {
188198
return r.SendErrorEnvelope(http.StatusUnauthorized, app.i18n.Ts("globals.messages.denied", "name", "{globals.terms.permission}"), nil, envelope.UnauthorizedError)
189199
}
200+
201+
return serveMediaFile(r, app, uuid, &media)
202+
}
203+
204+
// serveMediaFile serves the actual file content based on the storage provider.
205+
// If media is nil, it will be fetched from DB.
206+
func serveMediaFile(r *fastglue.Request, app *App, uuid string, media *mmodels.Media) error {
207+
// Fetch media metadata from DB if not provided.
208+
if media == nil {
209+
m, err := getMediaByUUID(app, uuid)
210+
if err != nil {
211+
return sendErrorEnvelope(r, err)
212+
}
213+
media = &m
214+
}
215+
190216
consts := app.consts.Load().(*constants)
191217
switch consts.UploadProvider {
192218
case "fs":
@@ -213,3 +239,8 @@ func handleServeMedia(r *fastglue.Request) error {
213239
func bytesToMegabytes(bytes int64) float64 {
214240
return float64(bytes) / 1024 / 1024
215241
}
242+
243+
// getMediaByUUID fetches media metadata from DB, handling thumbnail prefix.
244+
func getMediaByUUID(app *App, uuid string) (mmodels.Media, error) {
245+
return app.media.Get(0, strings.TrimPrefix(uuid, image.ThumbPrefix))
246+
}

cmd/messages.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,27 @@ type messageReq struct {
2828
// handleGetMessages returns messages for a conversation.
2929
func handleGetMessages(r *fastglue.Request) error {
3030
var (
31-
app = r.Context.(*App)
32-
uuid = r.RequestCtx.UserValue("uuid").(string)
33-
auser = r.RequestCtx.UserValue("user").(amodels.User)
34-
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
35-
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
36-
total = 0
31+
app = r.Context.(*App)
32+
uuid = r.RequestCtx.UserValue("uuid").(string)
33+
auser = r.RequestCtx.UserValue("user").(amodels.User)
34+
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
35+
pageSize = r.RequestCtx.QueryArgs().GetUintOrZero("page_size")
36+
total = 0
37+
private *bool
3738
)
3839

40+
// Parse optional private filter (null = no filter)
41+
if r.RequestCtx.QueryArgs().Has("private") {
42+
p := r.RequestCtx.QueryArgs().GetBool("private")
43+
private = &p
44+
}
45+
46+
// Parse repeated type params: ?type=incoming&type=outgoing
47+
var msgTypes []string
48+
for _, v := range r.RequestCtx.QueryArgs().PeekMulti("type") {
49+
msgTypes = append(msgTypes, string(v))
50+
}
51+
3952
user, err := app.user.GetAgent(auser.ID, "")
4053
if err != nil {
4154
return sendErrorEnvelope(r, err)
@@ -47,7 +60,7 @@ func handleGetMessages(r *fastglue.Request) error {
4760
return sendErrorEnvelope(r, err)
4861
}
4962

50-
messages, pageSize, err := app.conversation.GetConversationMessages(uuid, page, pageSize)
63+
messages, pageSize, err := app.conversation.GetConversationMessages(uuid, page, pageSize, private, msgTypes)
5164
if err != nil {
5265
return sendErrorEnvelope(r, err)
5366
}

cmd/middlewares.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"net/http"
5+
"strconv"
56
"strings"
67

78
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
@@ -220,3 +221,61 @@ func notAuthPage(handler fastglue.FastRequestHandler) fastglue.FastRequestHandle
220221
return handler(r)
221222
}
222223
}
224+
225+
// authOrSignedURL allows access if user is authenticated OR if URL has valid signature.
226+
// Used for media endpoints that support both access methods.
227+
func authOrSignedURL(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
228+
return func(r *fastglue.Request) error {
229+
app := r.Context.(*App)
230+
231+
// First, try to authenticate normally.
232+
user, err := authenticateUser(r, app)
233+
if err == nil && user.ID > 0 {
234+
// User is authenticated, set user context and proceed.
235+
r.RequestCtx.SetUserValue("user", amodels.User{
236+
ID: user.ID,
237+
Email: user.Email.String,
238+
FirstName: user.FirstName,
239+
LastName: user.LastName,
240+
})
241+
r.RequestCtx.SetUserValue("auth_method", "session")
242+
return handler(r)
243+
}
244+
245+
// Authentication failed, check for signed URL.
246+
validator := app.media.SignedURLValidator()
247+
if validator == nil {
248+
// Store doesn't support signed URLs, require auth.
249+
return r.SendErrorEnvelope(http.StatusUnauthorized,
250+
app.i18n.T("auth.invalidOrExpiredSession"), nil, envelope.GeneralError)
251+
}
252+
253+
// Parse signature and expiry from query params.
254+
sig := string(r.RequestCtx.QueryArgs().Peek("sig"))
255+
expStr := string(r.RequestCtx.QueryArgs().Peek("exp"))
256+
257+
if sig == "" || expStr == "" {
258+
return r.SendErrorEnvelope(http.StatusUnauthorized,
259+
app.i18n.T("auth.invalidOrExpiredSession"), nil, envelope.GeneralError)
260+
}
261+
262+
exp, err := strconv.ParseInt(expStr, 10, 64)
263+
if err != nil {
264+
return r.SendErrorEnvelope(http.StatusBadRequest,
265+
app.i18n.Ts("globals.messages.invalid", "name", "expiry"), nil, envelope.InputError)
266+
}
267+
268+
// Get the UUID from the route.
269+
uuid := r.RequestCtx.UserValue("uuid").(string)
270+
271+
// Validate signature.
272+
if !validator(uuid, sig, exp) {
273+
return r.SendErrorEnvelope(http.StatusForbidden,
274+
app.i18n.T("media.invalidOrExpiredURL"), nil, envelope.PermissionError)
275+
}
276+
277+
// Mark as signed URL access (no user context).
278+
r.RequestCtx.SetUserValue("auth_method", "signed_url")
279+
return handler(r)
280+
}
281+
}

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"lucide-vue-next": "^0.378.0",
4949
"mitt": "^3.0.1",
5050
"pinia": "^2.1.7",
51-
"qs": "^6.12.1",
51+
"qs": "^6.14.1",
5252
"radix-vue": "^1.9.17",
5353
"reka-ui": "^2.2.0",
5454
"tailwind-merge": "^2.3.0",

0 commit comments

Comments
 (0)