fix: hardening pass — permissions, socket reconnect, canvas setup, arrangements
- Fix 403 on save for public collection viewers (return role in GET board response) - Add read-only status indicator (StatusBar + StatusIndicator) - Fix beforeunload save to use fetch+keepalive with auth header - Socket reconnect now rejoins board room automatically - Canvas setup uses polling instead of brittle 200ms timer - Fix double user:left on disconnect (use disconnecting event, snapshot rooms) - Thread + comment creation wrapped in db.transaction - Prevent owner downgrade via addCollectionMember (check existing member) - Bound redirect depth in downloadImage to 5 - Arrangement operations anchor to bounding box top-left (no drift) - Distribute H/V also anchor to top-left - Fix annotations fetch to use axios api instance (401 interceptor) - Replace require() with static import in shortcut-definitions
This commit is contained in:
@@ -13,6 +13,7 @@ import { AnnotationStore } from '../stores/annotationStore';
|
||||
import { PinOverlay } from '../canvas/PinOverlay';
|
||||
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
|
||||
import { connectSocket, disconnectSocket } from '../socket';
|
||||
import api from '../api';
|
||||
|
||||
interface OnlineUser {
|
||||
userId: string;
|
||||
@@ -73,10 +74,20 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
||||
useEffect(() => {
|
||||
if (!boardData || !resolvedBoardId) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 100; // 5 seconds max (100 × 50ms)
|
||||
const poll = setInterval(() => {
|
||||
attempts++;
|
||||
const scene = canvasRef.current?.getScene();
|
||||
const viewport = canvasRef.current?.getViewport();
|
||||
if (!scene || !viewport) return;
|
||||
if (!scene || !viewport) {
|
||||
if (attempts >= maxAttempts) {
|
||||
clearInterval(poll);
|
||||
console.error('[canvas-setup] Canvas not ready after 5s, giving up');
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearInterval(poll);
|
||||
|
||||
// Create SelectionManager
|
||||
const selection = new SelectionManager(viewport, scene);
|
||||
@@ -309,17 +320,11 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
||||
});
|
||||
|
||||
// ── Annotations: load threads, wire socket events ──
|
||||
const token = localStorage.getItem('refboard_token');
|
||||
if (token) {
|
||||
fetch(`/api/boards/${resolvedBoardId}/threads`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
api.get(`/api/boards/${resolvedBoardId}/threads`)
|
||||
.then((res) => {
|
||||
if (res.data.threads) annotationStoreRef.current?.loadThreads(res.data.threads);
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (data.threads) annotationStoreRef.current?.loadThreads(data.threads);
|
||||
})
|
||||
.catch((err) => console.error('[annotations] load threads error:', err));
|
||||
}
|
||||
.catch((err) => console.error('[annotations] load threads error:', err));
|
||||
|
||||
socket.on('thread:add', (data: any) => {
|
||||
annotationStoreRef.current?.onThreadAdd(data.thread, data.comment);
|
||||
@@ -372,10 +377,10 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
||||
}
|
||||
pasteCleanupRef.current = setupPaste(viewport, scene, resolvedBoardId, onCanvasChange, selection, uploadManager);
|
||||
}
|
||||
}, 200);
|
||||
}, 50);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
clearInterval(poll);
|
||||
selectionRef.current?.destroy();
|
||||
selectionRef.current = null;
|
||||
if (inboxZoneRef.current) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { SaveStatus } from '../components/StatusBar';
|
||||
interface SaveManagerOptions {
|
||||
resolvedBoardId: string | undefined;
|
||||
isPublicView?: boolean;
|
||||
readOnly?: boolean;
|
||||
canvasRef: React.RefObject<PixiCanvasHandle | null>;
|
||||
setSaveStatus: (s: SaveStatus) => void;
|
||||
}
|
||||
@@ -13,11 +14,11 @@ interface SaveManagerOptions {
|
||||
/**
|
||||
* Debounced save with thumbnail generation from PixiJS renderer.
|
||||
*/
|
||||
export function useSaveManager({ resolvedBoardId, isPublicView, canvasRef, setSaveStatus }: SaveManagerOptions) {
|
||||
export function useSaveManager({ resolvedBoardId, isPublicView, readOnly, canvasRef, setSaveStatus }: SaveManagerOptions) {
|
||||
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const scheduleSave = useCallback(() => {
|
||||
if (!resolvedBoardId || isPublicView) return;
|
||||
if (!resolvedBoardId || isPublicView || readOnly) return;
|
||||
setSaveStatus('unsaved');
|
||||
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
||||
saveTimerRef.current = setTimeout(async () => {
|
||||
@@ -59,11 +60,15 @@ export function useSaveManager({ resolvedBoardId, isPublicView, canvasRef, setSa
|
||||
}
|
||||
await saveCanvas(resolvedBoardId, state, thumbnail);
|
||||
setSaveStatus('saved');
|
||||
} catch {
|
||||
setSaveStatus('unsaved');
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 403) {
|
||||
setSaveStatus('readonly');
|
||||
} else {
|
||||
setSaveStatus('unsaved');
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}, [resolvedBoardId, isPublicView, canvasRef, setSaveStatus]);
|
||||
}, [resolvedBoardId, isPublicView, readOnly, canvasRef, setSaveStatus]);
|
||||
|
||||
return { scheduleSave, saveTimerRef };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user