Security & resource hardening: eliminate CPU/disk attack surface
Addresses production CPU spike incident. Key changes: - Guard OTel exporter behind OTEL_EXPORTER_OTLP_ENDPOINT env var; filter tracing to /api paths only — unconditional export was primary suspect - Remove /healthz endpoint entirely (unauthenticated, hit DB on every call) - Replace KnownUserMiddleware with POST /api/users/me called once on login from TokenSync — eliminates unconditional DB write on every request - Add DB indexes: (BudgetId, IsDeleted) on Incomes/Outgos, OwnerUserId on Budgets, SharedWithUserId and (IsPending, SharedWithEmail) on BudgetShares - Move UseRateLimiter() before UseStaticFiles() so all requests are throttled - Replace full-array reorder with move-by-position (id + newIndex) — bounded input, fewer DB writes, better API design - Lock ForwardedHeaders to 172.20.0.0/16 subnet; fixes KnownNetworks deprecation warning (0 warnings in build now) - Add AsNoTracking() to all read-only queries in Summary/Incomes/OutgosController - FrequencyCalculator returns 0 for unknown enum values instead of throwing - Thread.Sleep → await Task.Delay in OIDC startup loop - AllowedHosts locked to budget.stwaddle.com Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>('GET', path),
|
||||
post: <T>(path: string, body: unknown) => request<T>('POST', path, body),
|
||||
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
|
||||
put: <T>(path: string, body: unknown) => request<T>('PUT', path, body),
|
||||
delete: (path: string) => request<void>('DELETE', path),
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { IncomeDto, Frequency } from '../types/index';
|
||||
|
||||
interface CreateIncomeRequest { name: string; frequency: Frequency; amount: number; }
|
||||
interface UpdateIncomeRequest { name: string; frequency: Frequency; amount: number; }
|
||||
interface ReorderRequest { orderedIds: string[]; }
|
||||
interface MoveRequest { id: string; newIndex: number; }
|
||||
|
||||
export function useIncomes(budgetId: string) {
|
||||
return useQuery({
|
||||
@@ -44,7 +44,7 @@ export function useDeleteIncome(budgetId: string) {
|
||||
export function useReorderIncomes(budgetId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (req: ReorderRequest) => api.put<void>(`/api/budgets/${budgetId}/incomes/order`, req),
|
||||
mutationFn: (req: MoveRequest) => api.put<void>(`/api/budgets/${budgetId}/incomes/order`, req),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['budgets', budgetId, 'incomes'] }),
|
||||
meta: { errorMessage: 'Failed to save order' },
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ interface CreateOutgoRequest {
|
||||
frequency: Frequency; amount: number; paymentSource: string | null; notes: string | null;
|
||||
}
|
||||
interface UpdateOutgoRequest extends CreateOutgoRequest { id: string; }
|
||||
interface ReorderRequest { orderedIds: string[]; }
|
||||
interface MoveRequest { id: string; newIndex: number; }
|
||||
|
||||
export function useOutgos(budgetId: string) {
|
||||
return useQuery({
|
||||
@@ -61,7 +61,7 @@ export function useDeleteOutgo(budgetId: string) {
|
||||
export function useReorderOutgos(budgetId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (req: ReorderRequest) => api.put<void>(`/api/budgets/${budgetId}/outgos/order`, req),
|
||||
mutationFn: (req: MoveRequest) => api.put<void>(`/api/budgets/${budgetId}/outgos/order`, req),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['budgets', budgetId, 'outgos'] }),
|
||||
meta: { errorMessage: 'Failed to save order' },
|
||||
});
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { setTokenProvider } from '../api/client';
|
||||
import { setTokenProvider, api } from '../api/client';
|
||||
|
||||
export function TokenSync() {
|
||||
const auth = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
setTokenProvider(() => auth.user?.access_token ?? null);
|
||||
if (auth.user?.access_token) {
|
||||
api.post<void>('/api/users/me', undefined).catch(() => {});
|
||||
}
|
||||
}, [auth.user]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -238,9 +238,8 @@ export function IncomePage() {
|
||||
if (!over || active.id === over.id) return;
|
||||
const oldIdx = displayItems.findIndex(i => i.id === active.id);
|
||||
const newIdx = displayItems.findIndex(i => i.id === over.id);
|
||||
const reordered = arrayMove(displayItems, oldIdx, newIdx);
|
||||
setDisplayItems(reordered);
|
||||
reorderIncomes.mutate({ orderedIds: reordered.map(i => i.id) });
|
||||
setDisplayItems(items => arrayMove(items, oldIdx, newIdx));
|
||||
reorderIncomes.mutate({ id: active.id as string, newIndex: newIdx });
|
||||
};
|
||||
|
||||
if (isLoading) return <><BudgetNav /><LoadingSkeleton rows={5} cols={6} /></>;
|
||||
|
||||
@@ -384,9 +384,8 @@ export function OutgoPage() {
|
||||
if (!over || active.id === over.id) return;
|
||||
const oldIdx = displayItems.findIndex(o => o.id === active.id);
|
||||
const newIdx = displayItems.findIndex(o => o.id === over.id);
|
||||
const reordered = arrayMove(displayItems, oldIdx, newIdx);
|
||||
setDisplayItems(reordered);
|
||||
reorderOutgos.mutate({ orderedIds: reordered.map(o => o.id) });
|
||||
setDisplayItems(items => arrayMove(items, oldIdx, newIdx));
|
||||
reorderOutgos.mutate({ id: active.id as string, newIndex: newIdx });
|
||||
};
|
||||
|
||||
if (isLoading) return <><BudgetNav /><LoadingSkeleton rows={5} cols={9} /></>;
|
||||
|
||||
Reference in New Issue
Block a user