diff --git a/go_cloud/api b/go_cloud/api index b65f6cc..70675ae 100755 Binary files a/go_cloud/api and b/go_cloud/api differ diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index 6512894..3a7b67f 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -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) { diff --git a/go_cloud/pkg/jwt/jwt.go b/go_cloud/pkg/jwt/jwt.go index 0c030f4..8d2fc21 100644 --- a/go_cloud/pkg/jwt/jwt.go +++ b/go_cloud/pkg/jwt/jwt.go @@ -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()), }, }