FIX: Extend JWT token expiration to 24 hours for document viewer sessions

- Add GenerateWithDuration method to JWT manager to support custom expiration times
- Update viewerHandler and userViewerHandler to generate viewer-specific tokens with 24-hour expiration
- This fixes the issue where PDF viewer fails due to token expiration within 15 minutes
- Add [VIEWER-SESSION] logging to track viewer session creation
- Tokens now remain valid long enough for users to view, navigate, and interact with PDFs
This commit is contained in:
Leon Bösche
2026-01-11 17:39:12 +01:00
parent 3d80072e7b
commit 2129d72a1f
3 changed files with 59 additions and 19 deletions

Binary file not shown.

View File

@@ -138,7 +138,7 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
})
// User file viewer
r.Get("/user/files/{fileId}/view", func(w http.ResponseWriter, req *http.Request) {
userViewerHandler(w, req, db, auditLogger)
userViewerHandler(w, req, db, jwtManager, auditLogger)
})
// Download user file
r.Get("/user/files/download", func(w http.ResponseWriter, req *http.Request) {
@@ -192,7 +192,7 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
})
r.Route("/files/{fileId}", func(r chi.Router) {
r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/view", func(w http.ResponseWriter, req *http.Request) {
viewerHandler(w, req, db, auditLogger)
viewerHandler(w, req, db, jwtManager, auditLogger)
})
r.With(middleware.Permission(db, auditLogger, permission.DocumentEdit)).Get("/edit", func(w http.ResponseWriter, req *http.Request) {
editorHandler(w, req, db, auditLogger)
@@ -407,10 +407,11 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
json.NewEncoder(w).Encode(out)
}
func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
userIDStr, _ := middleware.GetUserID(r.Context())
userID, _ := uuid.Parse(userIDStr)
orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
sessionObj, _ := middleware.GetSession(r.Context())
fileId := chi.URLParam(r, "fileId")
// Get file metadata to determine path and type
@@ -441,14 +442,29 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi
if host == "" {
host = "go.b0esche.cloud"
}
// Get JWT token from context (used for header or query fallback)
token, _ := middleware.GetToken(r.Context())
downloadPath := fmt.Sprintf("%s://%s/orgs/%s/files/download?path=%s&token=%s", scheme, host, orgID.String(), url.QueryEscape(file.Path), url.QueryEscape(token))
// Generate a long-lived token specifically for this viewer session (24 hours)
orgs, err := db.GetUserOrganizations(r.Context(), userID)
if err != nil {
errors.LogError(r, err, "Failed to get user organizations")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return
}
orgIDs := make([]string, len(orgs))
for i, o := range orgs {
orgIDs[i] = o.ID.String()
}
viewerToken, err := jwtManager.GenerateWithDuration(userID.String(), orgIDs, sessionObj.ID.String(), 24*time.Hour)
if err != nil {
errors.LogError(r, err, "Failed to generate viewer token")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return
}
downloadPath := fmt.Sprintf("%s://%s/orgs/%s/files/download?path=%s&token=%s", scheme, host, orgID.String(), url.QueryEscape(file.Path), url.QueryEscape(viewerToken))
// Determine if it's a PDF based on file extension
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
session := struct {
viewerSession := struct {
ViewUrl string `json:"viewUrl"`
Token string `json:"token"`
Capabilities struct {
@@ -459,23 +475,26 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi
ExpiresAt string `json:"expiresAt"`
}{
ViewUrl: downloadPath,
Token: token, // JWT token for authenticating file download
Token: viewerToken, // Long-lived JWT token for authenticating file download
Capabilities: struct {
CanEdit bool `json:"canEdit"`
CanAnnotate bool `json:"canAnnotate"`
IsPdf bool `json:"isPdf"`
}{CanEdit: false, CanAnnotate: isPdf, IsPdf: isPdf},
ExpiresAt: time.Now().Add(15 * time.Minute).UTC().Format(time.RFC3339),
ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
}
fmt.Printf("[VIEWER-SESSION] orgId=%s, fileId=%s, token_included=yes, isPdf=%v\n", orgID.String(), fileId, isPdf)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(session)
json.NewEncoder(w).Encode(viewerSession)
}
// userViewerHandler serves a viewer session for personal workspace files
func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
userIDStr, _ := middleware.GetUserID(r.Context())
userID, _ := uuid.Parse(userIDStr)
sessionObj, _ := middleware.GetSession(r.Context())
fileId := chi.URLParam(r, "fileId")
// Get file metadata to determine path and type
@@ -506,14 +525,29 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
if host == "" {
host = "go.b0esche.cloud"
}
// Get JWT token from context
token, _ := middleware.GetToken(r.Context())
downloadPath := fmt.Sprintf("%s://%s/user/files/download?path=%s&token=%s", scheme, host, url.QueryEscape(file.Path), url.QueryEscape(token))
// Generate a long-lived token specifically for this viewer session (24 hours)
orgs, err := db.GetUserOrganizations(r.Context(), userID)
if err != nil {
errors.LogError(r, err, "Failed to get user organizations")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return
}
orgIDs := make([]string, len(orgs))
for i, o := range orgs {
orgIDs[i] = o.ID.String()
}
viewerToken, err := jwtManager.GenerateWithDuration(userID.String(), orgIDs, sessionObj.ID.String(), 24*time.Hour)
if err != nil {
errors.LogError(r, err, "Failed to generate viewer token")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return
}
downloadPath := fmt.Sprintf("%s://%s/user/files/download?path=%s&token=%s", scheme, host, url.QueryEscape(file.Path), url.QueryEscape(viewerToken))
// Determine if it's a PDF based on file extension
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
session := struct {
viewerSession := struct {
ViewUrl string `json:"viewUrl"`
Token string `json:"token"`
Capabilities struct {
@@ -524,7 +558,7 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
ExpiresAt string `json:"expiresAt"`
}{
ViewUrl: downloadPath,
Token: token, // JWT token for authenticating file download
Token: viewerToken, // Long-lived JWT token for authenticating file download
Capabilities: struct {
CanEdit bool `json:"canEdit"`
CanAnnotate bool `json:"canAnnotate"`
@@ -534,11 +568,13 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
CanAnnotate: isPdf,
IsPdf: isPdf,
},
ExpiresAt: time.Now().Add(15 * time.Minute).UTC().Format(time.RFC3339),
ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
}
fmt.Printf("[VIEWER-SESSION] userId=%s, fileId=%s, token_included=yes, isPdf=%v\n", userID.String(), fileId, isPdf)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(session)
json.NewEncoder(w).Encode(viewerSession)
}
func editorHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {

View File

@@ -27,12 +27,16 @@ func NewManager(secret string) *Manager {
}
func (m *Manager) Generate(userID string, orgIDs []string, sessionID string) (string, error) {
return m.GenerateWithDuration(userID, orgIDs, sessionID, 15*time.Minute)
}
func (m *Manager) GenerateWithDuration(userID string, orgIDs []string, sessionID string, duration time.Duration) (string, error) {
claims := Claims{
UserID: userID,
OrgIDs: orgIDs,
SessionID: sessionID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}