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
This commit is contained in:
@@ -194,6 +194,8 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
|||||||
emit(DirectoryInitial());
|
emit(DirectoryInitial());
|
||||||
_currentOrgId = '';
|
_currentOrgId = '';
|
||||||
_currentPath = '/';
|
_currentPath = '/';
|
||||||
|
_currentFiles = [];
|
||||||
|
_filteredFiles = [];
|
||||||
_currentFilter = '';
|
_currentFilter = '';
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
_pageSize = 20;
|
_pageSize = 20;
|
||||||
|
|||||||
@@ -180,10 +180,9 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|||||||
if (state.caps.isPdf) {
|
if (state.caps.isPdf) {
|
||||||
// Log the URL being used for debugging
|
// Log the URL being used for debugging
|
||||||
print('Loading PDF from: ${state.viewUrl}');
|
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(
|
return SfPdfViewer.network(
|
||||||
state.viewUrl.toString(),
|
state.viewUrl.toString(),
|
||||||
headers: {'Authorization': 'Bearer ${state.token}'},
|
|
||||||
onDocumentLoadFailed: (details) {
|
onDocumentLoadFailed: (details) {
|
||||||
print('PDF load failed: ${details.error}');
|
print('PDF load failed: ${details.error}');
|
||||||
print('Description: ${details.description}');
|
print('Description: ${details.description}');
|
||||||
|
|||||||
292
b0esche_cloud/lib/repositories/mock_file_repository.dart
Normal file
292
b0esche_cloud/lib/repositories/mock_file_repository.dart
Normal file
@@ -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<String, List<FileItem>> _orgFiles = {};
|
||||||
|
final _uuid = const Uuid();
|
||||||
|
|
||||||
|
List<FileItem> _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<List<FileItem>> 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<FileItem?> 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<EditorSession> 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<void> deleteFile(String orgId, String path) async {
|
||||||
|
final files = _getFilesForOrg(orgId);
|
||||||
|
files.removeWhere((f) => f.path == path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> 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<void> 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<void> 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<List<FileItem>> 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<void> 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<ViewerSession> 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<void> saveAnnotations(
|
||||||
|
String orgId,
|
||||||
|
String fileId,
|
||||||
|
List<Annotation> annotations,
|
||||||
|
) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
// Mock: just delay, assume success
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -496,14 +496,13 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
host = "go.b0esche.cloud"
|
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
|
// 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")
|
||||||
|
|
||||||
// Get JWT token from context
|
|
||||||
token, _ := middleware.GetToken(r.Context())
|
|
||||||
|
|
||||||
session := struct {
|
session := struct {
|
||||||
ViewUrl string `json:"viewUrl"`
|
ViewUrl string `json:"viewUrl"`
|
||||||
Token string `json:"token"`
|
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
|
// downloadUserFileHandler downloads a file from user's personal workspace
|
||||||
func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config) {
|
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())
|
userIDStr, ok := middleware.GetUserID(r.Context())
|
||||||
if !ok || userIDStr == "" {
|
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)
|
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user