Some checks failed
Basil CI/CD Pipeline / Code Linting (push) Successful in 3m18s
Basil CI/CD Pipeline / Web Tests (push) Successful in 3m31s
Basil CI/CD Pipeline / Security Scanning (push) Has been cancelled
Basil CI/CD Pipeline / API Tests (push) Failing after 3m56s
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 3m11s
Basil CI/CD Pipeline / Trigger Deployment (push) Has been cancelled
Basil CI/CD Pipeline / Build All Packages (push) Has been cancelled
Basil CI/CD Pipeline / E2E Tests (push) Has been cancelled
Basil CI/CD Pipeline / Build & Push Docker Images (push) Has been cancelled
Introduces Family as the tenant boundary so recipes and cookbooks can be scoped per household instead of every user seeing everything. Adds a centralized access filter, an invite/membership UI, a first-login prompt to create a family, and locks down the previously unauthenticated backup routes to admin only. - Family and FamilyMember models with OWNER/MEMBER roles; familyId on Recipe and Cookbook (ON DELETE SET NULL so deleting a family orphans content rather than destroying it). - access.service.ts composes a single WhereInput covering owner, family, PUBLIC visibility, and direct share; admins short-circuit to full access. - recipes/cookbooks routes now require auth, strip client-supplied userId/familyId on create, and gate mutations with canMutate checks. Auto-filter helpers scoped to the same family to prevent cross-tenant leakage via shared tag names. - families.routes.ts exposes list/create/get/rename/delete plus add/remove member, with last-owner protection on removal. - FamilyGate component blocks the authenticated UI with a modal if the user has zero memberships, prompting them to create their first family; Family page provides ongoing management. - backup.routes.ts now requires admin; it had no auth at all before. - Bumps version to 2026.04.008 and documents the monotonic PPP counter in CLAUDE.md. Migration SQL is generated locally but not tracked (per existing .gitignore); apply 20260416010000_add_family_tenant to prod during deploy. Run backfill-family-tenant.ts once post-migration to assign existing content to a default owner's family. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
import { useEffect, useState, FormEvent, ReactNode } from 'react';
|
|
import { familiesApi } from '../services/api';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import '../styles/FamilyGate.css';
|
|
|
|
interface FamilyGateProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
type CheckState = 'idle' | 'checking' | 'needs-family' | 'ready';
|
|
|
|
export default function FamilyGate({ children }: FamilyGateProps) {
|
|
const { isAuthenticated, loading: authLoading, logout } = useAuth();
|
|
const [state, setState] = useState<CheckState>('idle');
|
|
const [name, setName] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (authLoading) return;
|
|
if (!isAuthenticated) {
|
|
setState('idle');
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
(async () => {
|
|
setState('checking');
|
|
try {
|
|
const res = await familiesApi.list();
|
|
if (cancelled) return;
|
|
const count = res.data?.length ?? 0;
|
|
setState(count === 0 ? 'needs-family' : 'ready');
|
|
} catch {
|
|
if (!cancelled) setState('ready');
|
|
}
|
|
})();
|
|
return () => { cancelled = true; };
|
|
}, [isAuthenticated, authLoading]);
|
|
|
|
async function handleCreate(e: FormEvent) {
|
|
e.preventDefault();
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return;
|
|
setSubmitting(true);
|
|
setError(null);
|
|
try {
|
|
await familiesApi.create(trimmed);
|
|
setState('ready');
|
|
} catch (e: any) {
|
|
setError(e?.response?.data?.error || 'Failed to create family');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
const showModal = isAuthenticated && state === 'needs-family';
|
|
|
|
return (
|
|
<>
|
|
{children}
|
|
{showModal && (
|
|
<div className="family-gate-overlay" role="dialog" aria-modal="true">
|
|
<div className="family-gate-modal">
|
|
<h2>Create your family</h2>
|
|
<p>
|
|
To keep recipes organized and shareable, every account belongs to
|
|
a family. Name yours to get started — you can invite others later.
|
|
</p>
|
|
<form onSubmit={handleCreate}>
|
|
<label htmlFor="family-gate-name">Family name</label>
|
|
<input
|
|
id="family-gate-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g. Smith Family"
|
|
autoFocus
|
|
disabled={submitting}
|
|
required
|
|
/>
|
|
{error && <div className="family-gate-error">{error}</div>}
|
|
<div className="family-gate-actions">
|
|
<button
|
|
type="button"
|
|
className="family-gate-secondary"
|
|
onClick={logout}
|
|
disabled={submitting}
|
|
>
|
|
Sign out
|
|
</button>
|
|
<button type="submit" disabled={submitting || !name.trim()}>
|
|
{submitting ? 'Creating…' : 'Create family'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|