diff --git a/app/api/bath-log/route.ts b/app/api/bath-log/route.ts index 0d149b6..8f3e539 100644 --- a/app/api/bath-log/route.ts +++ b/app/api/bath-log/route.ts @@ -3,8 +3,15 @@ import prisma from '../db'; import { ApiResponse, BathLogCreate, BathLogResponse } from '../types'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId, caretakerId } = authContext; if (!userFamilyId) { @@ -60,6 +67,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { } async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { @@ -217,6 +230,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { } async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { diff --git a/app/api/diaper-log/route.ts b/app/api/diaper-log/route.ts index 47d9905..68f29cc 100644 --- a/app/api/diaper-log/route.ts +++ b/app/api/diaper-log/route.ts @@ -3,8 +3,15 @@ import prisma from '../db'; import { ApiResponse, DiaperLogCreate, DiaperLogResponse } from '../types'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId, caretakerId } = authContext; if (!userFamilyId) { @@ -58,6 +65,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { } async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { @@ -212,6 +225,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { } async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { diff --git a/app/api/feed-log/route.ts b/app/api/feed-log/route.ts index e73a1dd..21f80b8 100644 --- a/app/api/feed-log/route.ts +++ b/app/api/feed-log/route.ts @@ -4,8 +4,15 @@ import { ApiResponse, FeedLogCreate, FeedLogResponse } from '../types'; import { FeedType } from '@prisma/client'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const body: FeedLogCreate = await req.json(); const { familyId, caretakerId } = authContext; @@ -72,6 +79,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { } async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { searchParams } = new URL(req.url); const id = searchParams.get('id'); @@ -243,6 +256,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { } async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { searchParams } = new URL(req.url); const id = searchParams.get('id'); diff --git a/app/api/measurement-log/route.ts b/app/api/measurement-log/route.ts index 30c5fc7..e7a53e8 100644 --- a/app/api/measurement-log/route.ts +++ b/app/api/measurement-log/route.ts @@ -3,8 +3,15 @@ import prisma from '../db'; import { ApiResponse, MeasurementCreate, MeasurementResponse } from '../types'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId, caretakerId } = authContext; if (!userFamilyId) { @@ -59,6 +66,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { } async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { @@ -247,6 +260,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { } async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { diff --git a/app/api/medicine-log/route.ts b/app/api/medicine-log/route.ts index 1723fec..a566e04 100644 --- a/app/api/medicine-log/route.ts +++ b/app/api/medicine-log/route.ts @@ -3,11 +3,18 @@ import prisma from '../db'; import { ApiResponse, MedicineLogCreate, MedicineLogResponse } from '../types'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; /** * Handle POST request to create a new medicine log entry */ async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId, caretakerId } = authContext; if (!userFamilyId) { @@ -69,6 +76,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { * Handle PUT request to update a medicine log entry */ async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { @@ -271,6 +284,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { * Handle DELETE request to hard delete a medicine log */ async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { diff --git a/app/api/milestone-log/route.ts b/app/api/milestone-log/route.ts index 3c15671..d0c197d 100644 --- a/app/api/milestone-log/route.ts +++ b/app/api/milestone-log/route.ts @@ -3,8 +3,15 @@ import prisma from '../db'; import { ApiResponse, MilestoneCreate, MilestoneResponse } from '../types'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId, caretakerId } = authContext; if (!userFamilyId) { @@ -59,6 +66,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { } async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { @@ -248,6 +261,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { } async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { diff --git a/app/api/pump-log/route.ts b/app/api/pump-log/route.ts index 6fbc842..d775d70 100644 --- a/app/api/pump-log/route.ts +++ b/app/api/pump-log/route.ts @@ -3,8 +3,15 @@ import prisma from '../db'; import { ApiResponse, PumpLogCreate, PumpLogResponse } from '../types'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse, calculateDurationMinutes } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId, caretakerId } = authContext; if (!userFamilyId) { @@ -80,6 +87,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { } async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { @@ -280,6 +293,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { } async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { diff --git a/app/api/sleep-log/route.ts b/app/api/sleep-log/route.ts index be64ce7..7d18947 100644 --- a/app/api/sleep-log/route.ts +++ b/app/api/sleep-log/route.ts @@ -3,8 +3,15 @@ import prisma from '../db'; import { ApiResponse, SleepLogCreate, SleepLogResponse } from '../types'; import { withAuthContext, AuthResult } from '../utils/auth'; import { toUTC, formatForResponse, calculateDurationMinutes } from '../utils/timezone'; +import { checkWritePermission } from '../utils/writeProtection'; async function handlePost(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId, caretakerId } = authContext; if (!userFamilyId) { @@ -66,6 +73,12 @@ async function handlePost(req: NextRequest, authContext: AuthResult) { } async function handlePut(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { @@ -237,6 +250,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) { } async function handleDelete(req: NextRequest, authContext: AuthResult) { + // Check write permissions for expired accounts + const writeCheck = checkWritePermission(authContext); + if (!writeCheck.allowed) { + return writeCheck.response!; + } + try { const { familyId: userFamilyId } = authContext; if (!userFamilyId) { diff --git a/app/api/utils/writeProtection.ts b/app/api/utils/writeProtection.ts new file mode 100644 index 0000000..04b72a8 --- /dev/null +++ b/app/api/utils/writeProtection.ts @@ -0,0 +1,97 @@ +import { NextResponse } from 'next/server'; +import { AuthResult } from './auth'; +import type { ApiResponse } from '../types'; + +export type WriteProtectionResponse = { + allowed: boolean; + response?: NextResponse>; +}; + +/** + * Check if a write operation should be allowed based on account expiration status + * Use this with an existing authContext (from withAuthContext wrapper) + * + * IMPORTANT: This only enforces write protection in SaaS mode. + * In self-hosted mode, all write operations are allowed (maintains backward compatibility). + * + * @param authContext - The authentication context (from withAuthContext) + * @returns WriteProtectionResponse with allowed flag and response (if blocked) + * + * @example + * ```typescript + * async function handlePost(req: NextRequest, authContext: AuthResult) { + * const writeCheck = checkWritePermission(authContext); + * if (!writeCheck.allowed) { + * return writeCheck.response; // Returns 403 with expiration info + * } + * // ... rest of endpoint + * } + * ``` + */ +export function checkWritePermission( + authContext: AuthResult +): WriteProtectionResponse { + + if (!authContext.authenticated) { + return { + allowed: false, + response: NextResponse.json>( + { + success: false, + error: authContext.error || 'Authentication required', + }, + { status: 401 } + ) + }; + } + + // Check if account is expired and write operations are blocked + // This will only happen in SaaS mode, as auth.ts only sets isExpired in SaaS mode + if (authContext.isExpired) { + const { trialEnds, planExpires } = authContext; + + // Determine expiration type for user-friendly messaging + let expirationType: 'TRIAL_EXPIRED' | 'PLAN_EXPIRED' | 'NO_PLAN' = 'NO_PLAN'; + let expirationDate: string | undefined; + + if (trialEnds) { + expirationType = 'TRIAL_EXPIRED'; + expirationDate = trialEnds; + } else if (planExpires) { + expirationType = 'PLAN_EXPIRED'; + expirationDate = planExpires; + } + + // Generate user-friendly error message + let errorMessage = 'Your account has expired. Please upgrade to continue.'; + if (expirationType === 'TRIAL_EXPIRED') { + errorMessage = 'Your free trial has ended. Upgrade to continue tracking.'; + } else if (expirationType === 'PLAN_EXPIRED') { + errorMessage = 'Your subscription has expired. Please renew to continue.'; + } else if (expirationType === 'NO_PLAN') { + errorMessage = 'No active subscription found. Please subscribe to continue.'; + } + + return { + allowed: false, + response: NextResponse.json>( + { + success: false, + error: errorMessage, + data: { + expirationInfo: { + type: expirationType, + date: expirationDate, + familySlug: authContext.familySlug + } + } + }, + { status: 403 } + ) + }; + } + + return { + allowed: true + }; +}