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 typing import List, Optional
from datetime import date, timedelta
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from app.database import get_db
from app.models.project import Project
@ -20,6 +20,7 @@ router = APIRouter()
class ReorderItem(BaseModel):
model_config = ConfigDict(extra="forbid")
id: 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 api from '@/lib/api';
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.
// Saves ~12 API calls/min for personal-only users.
// Use a ref to latch "has sharing" once discovered, so polling doesn't flicker.
const hasSharingRef = useRef(false);
// Uses useState (not useRef) so changes trigger a re-render and the
// refetchInterval picks up the new value immediately.
const [hasSharing, setHasSharing] = useState(false);
const ownsShared = (ownedQuery.data ?? []).some((c) => c.is_shared);
if (ownsShared) hasSharingRef.current = true;
if (ownsShared && !hasSharing) setHasSharing(true);
const sharedQuery = useQuery({
queryKey: ['calendars', 'shared'],
@ -29,12 +30,12 @@ export function useCalendars({ pollingEnabled = false }: UseCalendarsOptions = {
const { data } = await api.get<SharedCalendarMembership[]>('/shared-calendars');
return data;
},
refetchInterval: pollingEnabled && hasSharingRef.current ? 5_000 : false,
refetchInterval: pollingEnabled && hasSharing ? 5_000 : false,
staleTime: 3_000,
});
// 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 owned = (ownedQuery.data ?? []).map((c) => c.id);