diff --git a/b0esche_cloud/lib/widgets/share_file_dialog.dart b/b0esche_cloud/lib/widgets/share_file_dialog.dart index ac19d72..3b4eb52 100644 --- a/b0esche_cloud/lib/widgets/share_file_dialog.dart +++ b/b0esche_cloud/lib/widgets/share_file_dialog.dart @@ -40,9 +40,10 @@ class _ShareFileDialogState extends State { try { final apiClient = getIt(); - final response = await apiClient.getRaw( - '/orgs/${widget.orgId}/files/${widget.fileId}/share', - ); + final path = widget.orgId == 'files' + ? '/user/files/${widget.fileId}/share' + : '/orgs/${widget.orgId}/files/${widget.fileId}/share'; + final response = await apiClient.getRaw(path); if (response['exists'] == true) { setState(() { @@ -67,10 +68,10 @@ class _ShareFileDialogState extends State { try { final apiClient = getIt(); - final response = await apiClient.postRaw( - '/orgs/${widget.orgId}/files/${widget.fileId}/share', - data: {}, - ); + final path = widget.orgId == 'files' + ? '/user/files/${widget.fileId}/share' + : '/orgs/${widget.orgId}/files/${widget.fileId}/share'; + final response = await apiClient.postRaw(path, data: {}); setState(() { _shareUrl = response['url']; @@ -92,9 +93,10 @@ class _ShareFileDialogState extends State { try { final apiClient = getIt(); - await apiClient.delete( - '/orgs/${widget.orgId}/files/${widget.fileId}/share', - ); + final path = widget.orgId == 'files' + ? '/user/files/${widget.fileId}/share' + : '/orgs/${widget.orgId}/files/${widget.fileId}/share'; + await apiClient.delete(path); setState(() { _shareUrl = null; diff --git a/go_cloud/api b/go_cloud/api index 0e5feea..879a194 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 37267d3..95c660e 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -210,6 +210,16 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut r.Get("/user/files/{fileId}/collabora-proxy", func(w http.ResponseWriter, req *http.Request) { collaboraProxyHandler(w, req, db, jwtManager, "https://of.b0esche.cloud") }) + // Share link management for user files + r.Get("/user/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) { + getUserFileShareLinkHandler(w, req, db) + }) + r.Post("/user/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) { + createUserFileShareLinkHandler(w, req, db) + }) + r.Delete("/user/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) { + revokeUserFileShareLinkHandler(w, req, db) + }) // Org routes r.Get("/orgs", func(w http.ResponseWriter, req *http.Request) { @@ -3028,3 +3038,156 @@ func publicFileDownloadHandler(w http.ResponseWriter, r *http.Request, db *datab // Copy body io.Copy(w, resp.Body) } + +func getUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { + userIDStr, _ := middleware.GetUserID(r.Context()) + userID, _ := uuid.Parse(userIDStr) + fileId := chi.URLParam(r, "fileId") + + fileUUID, err := uuid.Parse(fileId) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest) + return + } + + // Check if file exists and belongs to user + file, err := db.GetFileByID(r.Context(), fileUUID) + if err != nil { + errors.LogError(r, err, "Failed to get file") + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + if file.UserID == nil || *file.UserID != userID { + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + + link, err := db.GetFileShareLinkByFileID(r.Context(), fileUUID) + if err != nil { + if err == sql.ErrNoRows { + // No share link exists + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "exists": false, + }) + return + } + errors.LogError(r, err, "Failed to get share link") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + // Build full URL + scheme := "https" + if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { + scheme = proto + } else if r.TLS == nil { + scheme = "http" + } + host := r.Host + if host == "" { + host = "go.b0esche.cloud" + } + fullURL := fmt.Sprintf("%s://%s/public/share/%s", scheme, host, link.Token) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "exists": true, + "url": fullURL, + "token": link.Token, + }) +} + +func createUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { + userIDStr, _ := middleware.GetUserID(r.Context()) + userID, _ := uuid.Parse(userIDStr) + fileId := chi.URLParam(r, "fileId") + + fileUUID, err := uuid.Parse(fileId) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest) + return + } + + // Check if file exists and belongs to user + file, err := db.GetFileByID(r.Context(), fileUUID) + if err != nil { + errors.LogError(r, err, "Failed to get file") + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + if file.UserID == nil || *file.UserID != userID { + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + + // Revoke existing link if any + db.RevokeFileShareLink(r.Context(), fileUUID) // Ignore error + + // Generate token + token, err := storage.GenerateSecurePassword(32) + if err != nil { + errors.LogError(r, err, "Failed to generate token") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + link, err := db.CreateFileShareLink(r.Context(), token, fileUUID, userID, userID) + if err != nil { + errors.LogError(r, err, "Failed to create share link") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + // Build full URL + scheme := "https" + if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { + scheme = proto + } else if r.TLS == nil { + scheme = "http" + } + host := r.Host + if host == "" { + host = "go.b0esche.cloud" + } + fullURL := fmt.Sprintf("%s://%s/public/share/%s", scheme, host, link.Token) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "url": fullURL, + "token": link.Token, + }) +} + +func revokeUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { + userIDStr, _ := middleware.GetUserID(r.Context()) + userID, _ := uuid.Parse(userIDStr) + fileId := chi.URLParam(r, "fileId") + + fileUUID, err := uuid.Parse(fileId) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest) + return + } + + // Check if file exists and belongs to user + file, err := db.GetFileByID(r.Context(), fileUUID) + if err != nil { + errors.LogError(r, err, "Failed to get file") + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + if file.UserID == nil || *file.UserID != userID { + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + + err = db.RevokeFileShareLink(r.Context(), fileUUID) + if err != nil { + errors.LogError(r, err, "Failed to revoke share link") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +}