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 39502c6..bcb844e 100644 --- a/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart +++ b/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart @@ -191,8 +191,13 @@ class FileBrowserBloc extends Bloc { ResetFileBrowser event, Emitter emit, ) { + final oldOrgId = _currentOrgId; + final clearedCount = _currentFiles.length; + print( + '[FILE-BROWSER] Switching workspace, oldOrgId=$oldOrgId, newOrgId=${event.nextOrgId}, clearing=$clearedCount files, reloading state', + ); emit(DirectoryInitial()); - _currentOrgId = ''; + _currentOrgId = event.nextOrgId; _currentPath = '/'; _currentFiles = []; _filteredFiles = []; diff --git a/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart b/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart index 78bcdf6..290dbfe 100644 --- a/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart +++ b/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart @@ -62,7 +62,14 @@ class CreateFolder extends FileBrowserEvent { List get props => [orgId, parentPath, folderName]; } -class ResetFileBrowser extends FileBrowserEvent {} +class ResetFileBrowser extends FileBrowserEvent { + final String nextOrgId; + + const ResetFileBrowser(this.nextOrgId); + + @override + List get props => [nextOrgId]; +} class LoadPage extends FileBrowserEvent { final int page; diff --git a/b0esche_cloud/lib/blocs/organization/organization_bloc.dart b/b0esche_cloud/lib/blocs/organization/organization_bloc.dart index c5b6b6c..e42aa7e 100644 --- a/b0esche_cloud/lib/blocs/organization/organization_bloc.dart +++ b/b0esche_cloud/lib/blocs/organization/organization_bloc.dart @@ -94,7 +94,7 @@ class OrganizationBloc extends Bloc { ); // Reset all dependent blocs permissionBloc.add(PermissionsReset()); - fileBrowserBloc.add(ResetFileBrowser()); + fileBrowserBloc.add(ResetFileBrowser(event.orgId)); uploadBloc.add(ResetUploads()); // Load permissions for the selected org permissionBloc.add(LoadPermissions(event.orgId)); @@ -168,7 +168,7 @@ class OrganizationBloc extends Bloc { emit(OrganizationLoaded(organizations: updatedOrgs, selectedOrg: newOrg)); // Reset blocs and load permissions for new org permissionBloc.add(PermissionsReset()); - fileBrowserBloc.add(ResetFileBrowser()); + fileBrowserBloc.add(ResetFileBrowser(newOrg.id)); uploadBloc.add(ResetUploads()); permissionBloc.add(LoadPermissions(newOrg.id)); } catch (e) { diff --git a/b0esche_cloud/lib/pages/file_explorer.dart b/b0esche_cloud/lib/pages/file_explorer.dart index aaa310d..cfcf33b 100644 --- a/b0esche_cloud/lib/pages/file_explorer.dart +++ b/b0esche_cloud/lib/pages/file_explorer.dart @@ -60,14 +60,17 @@ class _FileExplorerState extends State { ); context.read().add(LoadPermissions(widget.orgId)); } - + @override void didUpdateWidget(covariant FileExplorer oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.orgId != widget.orgId) { // Reset and reload when switching between Personal and Org workspaces final bloc = context.read(); - bloc.add(ResetFileBrowser()); + print( + '[FILE-BROWSER] UI detected workspace switch, oldOrgId=${oldWidget.orgId}, newOrgId=${widget.orgId}, clearing local view', + ); + bloc.add(ResetFileBrowser(widget.orgId)); bloc.add(LoadDirectory(orgId: widget.orgId, path: '/')); } } @@ -824,7 +827,9 @@ class _FileExplorerState extends State { splashColor: Colors.transparent, highlightColor: Colors.transparent, onPressed: () { - final parentPath = _getParentPath(state.currentPath); + final parentPath = _getParentPath( + state.currentPath, + ); context.read().add( LoadDirectory( orgId: widget.orgId, diff --git a/go_cloud/internal/database/db.go b/go_cloud/internal/database/db.go index cd601da..c802741 100644 --- a/go_cloud/internal/database/db.go +++ b/go_cloud/internal/database/db.go @@ -3,6 +3,7 @@ package database import ( "context" "database/sql" + "log" "time" "github.com/google/uuid" @@ -255,7 +256,7 @@ func (db *DB) GetOrgMembers(ctx context.Context, orgID uuid.UUID) ([]Membership, } // GetOrgFiles returns files for a given organization (top-level folder listing) -func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, path string, q string, page, pageSize int) ([]File, error) { +func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, userID uuid.UUID, path string, q string, page, pageSize int) ([]File, error) { if page <= 0 { page = 1 } @@ -264,22 +265,31 @@ func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, path string, q s } offset := (page - 1) * pageSize + orgIDStr := orgID.String() + userIDStr := userID.String() + log.Printf("[DATA-ISOLATION] stage=before, action=list, orgId=%s, userId=%s, fileCount=0, path=%s", orgIDStr, userIDStr, path) + // Basic search and pagination. Returns only direct children of the given path. // For root ("/"), we want files where path doesn't contain "/" after the first character. // For subdirs, we want files where path starts with parent but has no additional "/" after parent. rows, err := db.QueryContext(ctx, ` - SELECT id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at - FROM files - WHERE org_id = $1 - AND path != $2 - AND ( - ($2 = '/' AND path LIKE '/%' AND path NOT LIKE '/%/%') - OR ($2 != '/' AND path LIKE $2 || '/%' AND path NOT LIKE $2 || '/%/%') + SELECT f.id, f.org_id::text, f.user_id::text, f.name, f.path, f.type, f.size, f.last_modified, f.created_at + FROM files f + WHERE f.org_id = $1 + AND EXISTS ( + SELECT 1 + FROM memberships m + WHERE m.org_id = $1 AND m.user_id = $2 ) - AND ($3 = '' OR name ILIKE '%' || $3 || '%') - ORDER BY CASE WHEN type = 'folder' THEN 0 ELSE 1 END, name - LIMIT $4 OFFSET $5 - `, orgID, path, q, pageSize, offset) + AND f.path != $3 + AND ( + ($3 = '/' AND f.path LIKE '/%' AND f.path NOT LIKE '/%/%') + OR ($3 != '/' AND f.path LIKE $3 || '/%' AND f.path NOT LIKE $3 || '/%/%') + ) + AND ($4 = '' OR f.name ILIKE '%' || $4 || '%') + ORDER BY CASE WHEN f.type = 'folder' THEN 0 ELSE 1 END, f.name + LIMIT $5 OFFSET $6 + `, orgID, userID, path, q, pageSize, offset) if err != nil { return nil, err } @@ -303,7 +313,11 @@ func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, path string, q s } files = append(files, f) } - return files, rows.Err() + err = rows.Err() + if err == nil { + log.Printf("[DATA-ISOLATION] stage=after, action=list, orgId=%s, userId=%s, fileCount=%d, path=%s", orgIDStr, userIDStr, len(files), path) + } + return files, err } // GetUserFiles returns files for a user's personal workspace at a given path @@ -317,10 +331,12 @@ func (db *DB) GetUserFiles(ctx context.Context, userID uuid.UUID, path string, q offset := (page - 1) * pageSize // Return only direct children of the given path + log.Printf("[DATA-ISOLATION] stage=before, action=list, orgId=, userId=%s, fileCount=0, path=%s", userID.String(), path) rows, err := db.QueryContext(ctx, ` SELECT id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at FROM files WHERE user_id = $1 + AND org_id IS NULL AND path != $2 AND ( ($2 = '/' AND path LIKE '/%' AND path NOT LIKE '/%/%') @@ -353,7 +369,11 @@ func (db *DB) GetUserFiles(ctx context.Context, userID uuid.UUID, path string, q } files = append(files, f) } - return files, rows.Err() + err = rows.Err() + if err == nil { + log.Printf("[DATA-ISOLATION] stage=after, action=list, orgId=, userId=%s, fileCount=%d, path=%s", userID.String(), len(files), path) + } + return files, err } // CreateFile inserts a file or folder record. orgID or userID may be nil. @@ -361,16 +381,21 @@ func (db *DB) CreateFile(ctx context.Context, orgID *uuid.UUID, userID *uuid.UUI var f File var orgIDVal interface{} var userIDVal interface{} + orgIDStr := "" + userIDStr := "" if orgID != nil { orgIDVal = *orgID + orgIDStr = orgID.String() } else { orgIDVal = nil } if userID != nil { userIDVal = *userID + userIDStr = userID.String() } else { userIDVal = nil } + log.Printf("[DATA-ISOLATION] stage=before, action=create, orgId=%s, userId=%s, fileCount=1, path=%s", orgIDStr, userIDStr, path) err := db.QueryRowContext(ctx, ` INSERT INTO files (org_id, user_id, name, path, type, size) @@ -380,6 +405,7 @@ func (db *DB) CreateFile(ctx context.Context, orgID *uuid.UUID, userID *uuid.UUI if err != nil { return nil, err } + log.Printf("[DATA-ISOLATION] stage=after, action=create, orgId=%s, userId=%s, fileCount=1, path=%s", orgIDStr, userIDStr, f.Path) return &f, nil } diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index 9a5527b..b2af84e 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -357,6 +357,17 @@ func createOrgHandler(w http.ResponseWriter, r *http.Request, db *database.DB, a func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { // Org ID is provided by middleware.Org orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID) + userIDStr, ok := middleware.GetUserID(r.Context()) + if !ok || userIDStr == "" { + errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) + return + } + userID, err := uuid.Parse(userIDStr) + if err != nil { + errors.LogError(r, err, "Invalid user id in context") + errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) + return + } // Query params: path, q (search), page, pageSize path := r.URL.Query().Get("path") if path == "" { @@ -372,7 +383,7 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { fmt.Sscanf(ps, "%d", &pageSize) } - files, err := db.GetOrgFiles(r.Context(), orgID, path, q, page, pageSize) + files, err := db.GetOrgFiles(r.Context(), orgID, userID, path, q, page, pageSize) if err != nil { errors.LogError(r, err, "Failed to get org files") errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)