fix: enforce workspace isolation logging
This commit is contained in:
@@ -191,8 +191,13 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
ResetFileBrowser event,
|
||||
Emitter<FileBrowserState> 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 = [];
|
||||
|
||||
@@ -62,7 +62,14 @@ class CreateFolder extends FileBrowserEvent {
|
||||
List<Object> get props => [orgId, parentPath, folderName];
|
||||
}
|
||||
|
||||
class ResetFileBrowser extends FileBrowserEvent {}
|
||||
class ResetFileBrowser extends FileBrowserEvent {
|
||||
final String nextOrgId;
|
||||
|
||||
const ResetFileBrowser(this.nextOrgId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [nextOrgId];
|
||||
}
|
||||
|
||||
class LoadPage extends FileBrowserEvent {
|
||||
final int page;
|
||||
|
||||
@@ -94,7 +94,7 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
|
||||
);
|
||||
// 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<OrganizationEvent, OrganizationState> {
|
||||
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) {
|
||||
|
||||
@@ -60,14 +60,17 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
);
|
||||
context.read<PermissionBloc>().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<FileBrowserBloc>();
|
||||
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<FileExplorer> {
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
final parentPath = _getParentPath(state.currentPath);
|
||||
final parentPath = _getParentPath(
|
||||
state.currentPath,
|
||||
);
|
||||
context.read<FileBrowserBloc>().add(
|
||||
LoadDirectory(
|
||||
orgId: widget.orgId,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user