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:
BIN
go_cloud/api
BIN
go_cloud/api
Binary file not shown.
@@ -138,7 +138,7 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
|||||||
})
|
})
|
||||||
// User file viewer
|
// User file viewer
|
||||||
r.Get("/user/files/{fileId}/view", func(w http.ResponseWriter, req *http.Request) {
|
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
|
// Download user file
|
||||||
r.Get("/user/files/download", func(w http.ResponseWriter, req *http.Request) {
|
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.Route("/files/{fileId}", func(r chi.Router) {
|
||||||
r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/view", func(w http.ResponseWriter, req *http.Request) {
|
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) {
|
r.With(middleware.Permission(db, auditLogger, permission.DocumentEdit)).Get("/edit", func(w http.ResponseWriter, req *http.Request) {
|
||||||
editorHandler(w, req, db, auditLogger)
|
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)
|
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())
|
userIDStr, _ := middleware.GetUserID(r.Context())
|
||||||
userID, _ := uuid.Parse(userIDStr)
|
userID, _ := uuid.Parse(userIDStr)
|
||||||
orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
|
orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
|
||||||
|
sessionObj, _ := middleware.GetSession(r.Context())
|
||||||
fileId := chi.URLParam(r, "fileId")
|
fileId := chi.URLParam(r, "fileId")
|
||||||
|
|
||||||
// Get file metadata to determine path and type
|
// 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 == "" {
|
if host == "" {
|
||||||
host = "go.b0esche.cloud"
|
host = "go.b0esche.cloud"
|
||||||
}
|
}
|
||||||
// Get JWT token from context (used for header or query fallback)
|
// Generate a long-lived token specifically for this viewer session (24 hours)
|
||||||
token, _ := middleware.GetToken(r.Context())
|
orgs, err := db.GetUserOrganizations(r.Context(), userID)
|
||||||
downloadPath := fmt.Sprintf("%s://%s/orgs/%s/files/download?path=%s&token=%s", scheme, host, orgID.String(), url.QueryEscape(file.Path), url.QueryEscape(token))
|
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
|
// Determine if it's a PDF based on file extension
|
||||||
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
|
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
|
||||||
|
|
||||||
session := struct {
|
viewerSession := struct {
|
||||||
ViewUrl string `json:"viewUrl"`
|
ViewUrl string `json:"viewUrl"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Capabilities struct {
|
Capabilities struct {
|
||||||
@@ -459,23 +475,26 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi
|
|||||||
ExpiresAt string `json:"expiresAt"`
|
ExpiresAt string `json:"expiresAt"`
|
||||||
}{
|
}{
|
||||||
ViewUrl: downloadPath,
|
ViewUrl: downloadPath,
|
||||||
Token: token, // JWT token for authenticating file download
|
Token: viewerToken, // Long-lived JWT token for authenticating file download
|
||||||
Capabilities: struct {
|
Capabilities: struct {
|
||||||
CanEdit bool `json:"canEdit"`
|
CanEdit bool `json:"canEdit"`
|
||||||
CanAnnotate bool `json:"canAnnotate"`
|
CanAnnotate bool `json:"canAnnotate"`
|
||||||
IsPdf bool `json:"isPdf"`
|
IsPdf bool `json:"isPdf"`
|
||||||
}{CanEdit: false, CanAnnotate: isPdf, IsPdf: 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")
|
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
|
// 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())
|
userIDStr, _ := middleware.GetUserID(r.Context())
|
||||||
userID, _ := uuid.Parse(userIDStr)
|
userID, _ := uuid.Parse(userIDStr)
|
||||||
|
sessionObj, _ := middleware.GetSession(r.Context())
|
||||||
fileId := chi.URLParam(r, "fileId")
|
fileId := chi.URLParam(r, "fileId")
|
||||||
|
|
||||||
// Get file metadata to determine path and type
|
// 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 == "" {
|
if host == "" {
|
||||||
host = "go.b0esche.cloud"
|
host = "go.b0esche.cloud"
|
||||||
}
|
}
|
||||||
// Get JWT token from context
|
// Generate a long-lived token specifically for this viewer session (24 hours)
|
||||||
token, _ := middleware.GetToken(r.Context())
|
orgs, err := db.GetUserOrganizations(r.Context(), userID)
|
||||||
downloadPath := fmt.Sprintf("%s://%s/user/files/download?path=%s&token=%s", scheme, host, url.QueryEscape(file.Path), url.QueryEscape(token))
|
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
|
// Determine if it's a PDF based on file extension
|
||||||
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
|
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
|
||||||
|
|
||||||
session := struct {
|
viewerSession := struct {
|
||||||
ViewUrl string `json:"viewUrl"`
|
ViewUrl string `json:"viewUrl"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Capabilities struct {
|
Capabilities struct {
|
||||||
@@ -524,7 +558,7 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
|||||||
ExpiresAt string `json:"expiresAt"`
|
ExpiresAt string `json:"expiresAt"`
|
||||||
}{
|
}{
|
||||||
ViewUrl: downloadPath,
|
ViewUrl: downloadPath,
|
||||||
Token: token, // JWT token for authenticating file download
|
Token: viewerToken, // Long-lived JWT token for authenticating file download
|
||||||
Capabilities: struct {
|
Capabilities: struct {
|
||||||
CanEdit bool `json:"canEdit"`
|
CanEdit bool `json:"canEdit"`
|
||||||
CanAnnotate bool `json:"canAnnotate"`
|
CanAnnotate bool `json:"canAnnotate"`
|
||||||
@@ -534,11 +568,13 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
|||||||
CanAnnotate: isPdf,
|
CanAnnotate: isPdf,
|
||||||
IsPdf: 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")
|
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) {
|
func editorHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
|
||||||
|
|||||||
@@ -27,12 +27,16 @@ func NewManager(secret string) *Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Generate(userID string, orgIDs []string, sessionID string) (string, error) {
|
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{
|
claims := Claims{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
OrgIDs: orgIDs,
|
OrgIDs: orgIDs,
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user