Fix team review findings: reactive shared-calendar gate + ReorderItem hardening

- Convert hasSharingRef from useRef to useState in useCalendars so
  refetchInterval reacts immediately when sharing is detected (P-01)
- Add extra="forbid" to ReorderItem schema to prevent mass-assignment (S-03)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-13 00:50:02 +08:00
parent a94485b138
commit e270a2f63d
2 changed files with 9 additions and 7 deletions

View File

@ -4,7 +4,7 @@ from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from typing import List, Optional from typing import List, Optional
from datetime import date, timedelta from datetime import date, timedelta
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from app.database import get_db from app.database import get_db
from app.models.project import Project from app.models.project import Project
@ -20,6 +20,7 @@ router = APIRouter()
class ReorderItem(BaseModel): class ReorderItem(BaseModel):
model_config = ConfigDict(extra="forbid")
id: int id: int
sort_order: int sort_order: int

View File

@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react'; import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import api from '@/lib/api'; import api from '@/lib/api';
import type { Calendar, SharedCalendarMembership } from '@/types'; import type { Calendar, SharedCalendarMembership } from '@/types';
@ -18,10 +18,11 @@ export function useCalendars({ pollingEnabled = false }: UseCalendarsOptions = {
// AS-4: Gate shared-calendar polling on whether user participates in sharing. // AS-4: Gate shared-calendar polling on whether user participates in sharing.
// Saves ~12 API calls/min for personal-only users. // Saves ~12 API calls/min for personal-only users.
// Use a ref to latch "has sharing" once discovered, so polling doesn't flicker. // Uses useState (not useRef) so changes trigger a re-render and the
const hasSharingRef = useRef(false); // refetchInterval picks up the new value immediately.
const [hasSharing, setHasSharing] = useState(false);
const ownsShared = (ownedQuery.data ?? []).some((c) => c.is_shared); const ownsShared = (ownedQuery.data ?? []).some((c) => c.is_shared);
if (ownsShared) hasSharingRef.current = true; if (ownsShared && !hasSharing) setHasSharing(true);
const sharedQuery = useQuery({ const sharedQuery = useQuery({
queryKey: ['calendars', 'shared'], queryKey: ['calendars', 'shared'],
@ -29,12 +30,12 @@ export function useCalendars({ pollingEnabled = false }: UseCalendarsOptions = {
const { data } = await api.get<SharedCalendarMembership[]>('/shared-calendars'); const { data } = await api.get<SharedCalendarMembership[]>('/shared-calendars');
return data; return data;
}, },
refetchInterval: pollingEnabled && hasSharingRef.current ? 5_000 : false, refetchInterval: pollingEnabled && hasSharing ? 5_000 : false,
staleTime: 3_000, staleTime: 3_000,
}); });
// Also latch if user is a member of others' shared calendars // Also latch if user is a member of others' shared calendars
if ((sharedQuery.data ?? []).length > 0) hasSharingRef.current = true; if ((sharedQuery.data ?? []).length > 0 && !hasSharing) setHasSharing(true);
const allCalendarIds = useMemo(() => { const allCalendarIds = useMemo(() => {
const owned = (ownedQuery.data ?? []).map((c) => c.id); const owned = (ownedQuery.data ?? []).map((c) => c.id);