feat: plan/limit system + role-based page access

Plan & limit enforcement:
- src/lib/plans.ts: PLAN_LIMITS (free: 15 props, 25 customers, 5 presentations, 1 member, 5 imgs/prop)
- checkLimit() + limitErrorMessage() — role-aware error messages (owner: upgrade CTA, others: contact admin)
- Create actions (property, customer, presentation, team invite): hard limit check before create
- PropertyImageUploader: maxImages prop + isOwner-aware toast
- UsageBadge component: usage counter shown only to owner (green→amber→red)

Role-based access:
- TenantContext + ActiveContext: add memberCount + role fields
- Dashboard layout: non-owner on free plan with >1 member → /plan-limit
- /plan-limit: blocked-access page with owner contact info + sign-out
- AppSidebar: minRole filtering — Plan & Faturalama (owner only), Çalışma Alanı/Yatırımcılar (admin+)
- settings/billing: owner-only hard redirect
- settings/workspace + settings/members: member redirect, admin read-only
- settings/investors: member redirect
- workspace-actions + logo-actions: restricted to owner only
- Workspace form: canEdit = owner only (admin sees read-only view)
This commit is contained in:
egecankomur
2026-05-12 18:46:02 +03:00
parent 7c23a2b4ae
commit 933cb17107
27 changed files with 475 additions and 44 deletions
+6
View File
@@ -7,6 +7,7 @@ import { customerSchema } from "@/lib/validation/customers";
import { DATABASE_ID, TABLES, type CustomerStage } from "./schema";
import { createAdminClient } from "./server";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { checkLimit, limitErrorMessage } from "@/lib/plans";
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
@@ -21,6 +22,11 @@ export async function createCustomerAction(
return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
}
const limitCheck = await checkLimit(ctx.tenantId, ctx.settings?.plan, "customers");
if (!limitCheck.allowed) {
return { ok: false, error: limitErrorMessage("customers", limitCheck.limit, ctx.role) };
}
const { tablesDB } = createAdminClient();
const data = parsed.data;