From bd6dd68f0bc2f5832285b4046c7026e82789d6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Sun, 11 Jan 2026 03:40:44 +0100 Subject: [PATCH] Fix file browser state persistence and PDF viewer loading - Clear file lists in ResetFileBrowser to prevent org files showing in personal workspace - Include JWT token as query parameter in PDF download URL for viewer compatibility - Remove Authorization header from SfPdfViewer (browser security restrictions) - Fix mock repository EditorSession to include required token parameter --- .../blocs/file_browser/file_browser_bloc.dart | 2 + b0esche_cloud/lib/pages/document_viewer.dart | 3 +- .../repositories/mock_file_repository.dart | 292 ++++++++++++++++++ go_cloud/internal/http/routes.go | 10 +- 4 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 b0esche_cloud/lib/repositories/mock_file_repository.dart diff --git a/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart b/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart index 753307f..39502c6 100644 --- a/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart +++ b/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart @@ -194,6 +194,8 @@ class FileBrowserBloc extends Bloc { emit(DirectoryInitial()); _currentOrgId = ''; _currentPath = '/'; + _currentFiles = []; + _filteredFiles = []; _currentFilter = ''; _currentPage = 1; _pageSize = 20; diff --git a/b0esche_cloud/lib/pages/document_viewer.dart b/b0esche_cloud/lib/pages/document_viewer.dart index 43b89f7..ee4f63c 100644 --- a/b0esche_cloud/lib/pages/document_viewer.dart +++ b/b0esche_cloud/lib/pages/document_viewer.dart @@ -180,10 +180,9 @@ class _DocumentViewerModalState extends State { if (state.caps.isPdf) { // Log the URL being used for debugging print('Loading PDF from: ${state.viewUrl}'); - print('Using token: ${state.token.substring(0, 20)}...'); + // Token is already included in the URL query parameter return SfPdfViewer.network( state.viewUrl.toString(), - headers: {'Authorization': 'Bearer ${state.token}'}, onDocumentLoadFailed: (details) { print('PDF load failed: ${details.error}'); print('Description: ${details.description}'); diff --git a/b0esche_cloud/lib/repositories/mock_file_repository.dart b/b0esche_cloud/lib/repositories/mock_file_repository.dart new file mode 100644 index 0000000..6adde7e --- /dev/null +++ b/b0esche_cloud/lib/repositories/mock_file_repository.dart @@ -0,0 +1,292 @@ +import '../models/file_item.dart'; +import '../models/viewer_session.dart'; +import '../models/editor_session.dart'; +import '../models/annotation.dart'; +import '../models/document_capabilities.dart'; +import '../models/api_error.dart'; +import '../repositories/file_repository.dart'; +import 'package:path/path.dart' as p; +import 'package:uuid/uuid.dart'; + +class MockFileRepository implements FileRepository { + final Map> _orgFiles = {}; + final _uuid = const Uuid(); + + List _getFilesForOrg(String orgId) { + if (!_orgFiles.containsKey(orgId)) { + // Initialize with different files per org + if (orgId == 'org1') { + _orgFiles[orgId] = [ + FileItem( + id: _uuid.v4(), + name: 'Personal Documents', + path: '/Personal Documents', + type: FileType.folder, + lastModified: DateTime.now(), + ), + FileItem( + id: _uuid.v4(), + name: 'Photos', + path: '/Photos', + type: FileType.folder, + lastModified: DateTime.now(), + ), + FileItem( + id: _uuid.v4(), + name: 'resume.pdf', + path: '/resume.pdf', + type: FileType.file, + size: 1024, + lastModified: DateTime.now(), + ), + FileItem( + id: _uuid.v4(), + name: 'notes.txt', + path: '/notes.txt', + type: FileType.file, + size: 256, + lastModified: DateTime.now(), + ), + ]; + } else if (orgId == 'org2') { + _orgFiles[orgId] = [ + FileItem( + id: _uuid.v4(), + name: 'Company Reports', + path: '/Company Reports', + type: FileType.folder, + lastModified: DateTime.now(), + ), + FileItem( + id: _uuid.v4(), + name: 'annual_report.pdf', + path: '/annual_report.pdf', + type: FileType.file, + size: 2048, + lastModified: DateTime.now(), + ), + FileItem( + id: _uuid.v4(), + name: 'presentation.pptx', + path: '/presentation.pptx', + type: FileType.file, + size: 4096, + lastModified: DateTime.now(), + ), + ]; + } else if (orgId == 'org3') { + _orgFiles[orgId] = [ + FileItem( + id: _uuid.v4(), + name: 'Project Code', + path: '/Project Code', + type: FileType.folder, + lastModified: DateTime.now(), + ), + FileItem( + id: _uuid.v4(), + name: 'side_project.dart', + path: '/side_project.dart', + type: FileType.file, + size: 512, + lastModified: DateTime.now(), + ), + ]; + } else { + // Default for new orgs + _orgFiles[orgId] = []; + } + } + return _orgFiles[orgId]!; + } + + @override + Future> getFiles(String orgId, String path) async { + await Future.delayed(const Duration(seconds: 1)); + final files = _getFilesForOrg(orgId); + if (path == '/') { + return files.where((f) => !f.path.substring(1).contains('/')).toList(); + } else { + return files + .where((f) => f.path.startsWith('$path/') && f.path != path) + .toList(); + } + } + + @override + Future getFile(String orgId, String path) async { + final files = _getFilesForOrg(orgId); + final index = files.indexWhere((f) => f.path == path); + return index != -1 ? files[index] : null; + } + + @override + Future requestEditorSession( + String orgId, + String fileId, + ) async { + await Future.delayed(const Duration(seconds: 1)); + // Mock: determine editability + final isEditable = + fileId.endsWith('.docx') || + fileId.endsWith('.xlsx') || + fileId.endsWith('.pptx'); + final editUrl = Uri.parse( + 'https://office.b0esche.cloud/editor/$orgId/$fileId?editable=$isEditable', + ); + final expiresAt = DateTime.now().add(const Duration(minutes: 30)); + return EditorSession( + editUrl: editUrl, + token: 'mock-editor-token', + readOnly: !isEditable, + expiresAt: expiresAt, + ); + } + + @override + Future deleteFile(String orgId, String path) async { + final files = _getFilesForOrg(orgId); + files.removeWhere((f) => f.path == path); + } + + @override + Future createFolder( + String orgId, + String parentPath, + String folderName, + ) async { + await Future.delayed(const Duration(seconds: 1)); + final normalizedName = folderName.startsWith('/') + ? folderName.substring(1) + : folderName; + final newPath = parentPath == '/' + ? '/$normalizedName' + : '$parentPath/$normalizedName'; + final files = _getFilesForOrg(orgId); + files.add( + FileItem( + id: _uuid.v4(), + name: normalizedName, + path: newPath, + type: FileType.folder, + lastModified: DateTime.now(), + ), + ); + } + + @override + Future moveFile( + String orgId, + String sourcePath, + String targetPath, + ) async { + await Future.delayed(const Duration(seconds: 1)); + final files = _getFilesForOrg(orgId); + final fileIndex = files.indexWhere((f) => f.path == sourcePath); + if (fileIndex != -1) { + final file = files[fileIndex]; + final newName = file.path.split('/').last; + final newPath = targetPath == '/' ? '/$newName' : '$targetPath/$newName'; + files[fileIndex] = FileItem( + id: file.id, + name: file.name, + path: newPath, + type: file.type, + size: file.size, + lastModified: DateTime.now(), + ); + } + } + + @override + Future renameFile(String orgId, String path, String newName) async { + await Future.delayed(const Duration(seconds: 1)); + final files = _getFilesForOrg(orgId); + final fileIndex = files.indexWhere((f) => f.path == path); + if (fileIndex != -1) { + final file = files[fileIndex]; + final parentPath = p.dirname(path); + final newPath = parentPath == '.' ? '/$newName' : '$parentPath/$newName'; + files[fileIndex] = FileItem( + id: file.id, + name: newName, + path: newPath, + type: file.type, + size: file.size, + lastModified: DateTime.now(), + ); + } + } + + @override + Future> searchFiles(String orgId, String query) async { + await Future.delayed(const Duration(seconds: 1)); + final files = _getFilesForOrg(orgId); + return files + .where((f) => f.name.toLowerCase().contains(query.toLowerCase())) + .toList(); + } + + @override + Future uploadFile(String orgId, FileItem file) async { + await Future.delayed(const Duration(seconds: 1)); + final files = _getFilesForOrg(orgId); + files.add( + FileItem( + id: _uuid.v4(), + name: file.name, + path: file.path, + type: file.type, + size: file.size, + lastModified: file.lastModified, + ), + ); + } + + @override + Future requestViewerSession( + String orgId, + String fileId, + ) async { + await Future.delayed(const Duration(seconds: 1)); + if (fileId.contains('forbidden')) { + throw ApiError( + code: 'permission_denied', + message: 'Access denied', + status: 403, + ); + } + if (fileId.contains('notfound')) { + throw ApiError(code: 'not_found', message: 'File not found', status: 404); + } + // Mock: assume fileId is path, determine if PDF + final isPdf = fileId.endsWith('.pdf'); + final caps = DocumentCapabilities( + canEdit: !isPdf && (fileId.endsWith('.docx') || fileId.endsWith('.xlsx')), + canAnnotate: isPdf, + isPdf: isPdf, + ); + // Mock URL + final viewUrl = Uri.parse( + 'https://office.b0esche.cloud/viewer/$orgId/$fileId', + ); + final token = 'mock-viewer-token'; + final expiresAt = DateTime.now().add(const Duration(minutes: 30)); + return ViewerSession( + viewUrl: viewUrl, + capabilities: caps, + token: token, + expiresAt: expiresAt, + ); + } + + @override + Future saveAnnotations( + String orgId, + String fileId, + List annotations, + ) async { + await Future.delayed(const Duration(seconds: 2)); + // Mock: just delay, assume success + } +} diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index d9fa78d..3208ee4 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -496,14 +496,13 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, if host == "" { host = "go.b0esche.cloud" } - downloadPath := fmt.Sprintf("%s://%s/user/files/download?path=%s", scheme, host, url.QueryEscape(file.Path)) + // 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)) // Determine if it's a PDF based on file extension isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf") - // Get JWT token from context - token, _ := middleware.GetToken(r.Context()) - session := struct { ViewUrl string `json:"viewUrl"` Token string `json:"token"` @@ -1441,8 +1440,11 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database // downloadUserFileHandler downloads a file from user's personal workspace func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config) { + // Try to get userID from context (Bearer token), fallback to query parameter userIDStr, ok := middleware.GetUserID(r.Context()) if !ok || userIDStr == "" { + // Token might be in query parameter for PDF viewer compatibility + // This is acceptable since the token is still validated errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) return }