feat: Plan-based feature permissions and quota enforcement
Backend: - Add HasQuota() permission factory for quota limits (resources, users, services, appointments, email templates, automated tasks) - Add HasFeaturePermission() factory for feature-based permissions (SMS, masked calling, custom domains, white label, plugins, webhooks, calendar sync, analytics) - Add has_feature() method to Tenant model for flexible permission checking - Add new tenant permission fields: can_create_plugins, can_use_webhooks, can_use_calendar_sync, can_export_data - Create Data Export API with CSV/JSON support for appointments, customers, resources, services - Create Analytics API with dashboard, appointments, revenue endpoints - Add calendar sync views and URL configuration Frontend: - Add usePlanFeatures hook for checking feature availability - Add UpgradePrompt components (inline, banner, overlay variants) - Add LockedSection wrapper and LockedButton for feature gating - Update settings pages with permission checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ import { useOutletContext } from 'react-router-dom';
|
||||
import { Key } from 'lucide-react';
|
||||
import { Business, User } from '../../types';
|
||||
import ApiTokensSection from '../../components/ApiTokensSection';
|
||||
import { usePlanFeatures } from '../../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../../components/UpgradePrompt';
|
||||
|
||||
const ApiSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,6 +21,7 @@ const ApiSettings: React.FC = () => {
|
||||
}>();
|
||||
|
||||
const isOwner = user.role === 'owner';
|
||||
const { canUse } = usePlanFeatures();
|
||||
|
||||
if (!isOwner) {
|
||||
return (
|
||||
@@ -44,7 +47,9 @@ const ApiSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* API Tokens Section */}
|
||||
<ApiTokensSection />
|
||||
<LockedSection feature="api_access" isLocked={!canUse('api_access')}>
|
||||
<ApiTokensSection />
|
||||
</LockedSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Lock, Users, Key, Save, Check, AlertCircle, Eye, EyeOff } from 'lucide-
|
||||
import { Business, User } from '../../types';
|
||||
import { useBusinessOAuthSettings, useUpdateBusinessOAuthSettings } from '../../hooks/useBusinessOAuth';
|
||||
import { useBusinessOAuthCredentials, useUpdateBusinessOAuthCredentials } from '../../hooks/useBusinessOAuthCredentials';
|
||||
import { usePlanFeatures } from '../../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../../components/UpgradePrompt';
|
||||
|
||||
// Provider display names and icons
|
||||
const providerInfo: Record<string, { name: string; icon: string }> = {
|
||||
@@ -57,6 +59,7 @@ const AuthenticationSettings: React.FC = () => {
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const isOwner = user.role === 'owner';
|
||||
const { canUse } = usePlanFeatures();
|
||||
|
||||
// Update OAuth settings when data loads
|
||||
useEffect(() => {
|
||||
@@ -167,10 +170,11 @@ const AuthenticationSettings: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* OAuth & Social Login */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<LockedSection feature="custom_oauth" isLocked={!canUse('custom_oauth')}>
|
||||
{/* OAuth & Social Login */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Users size={20} className="text-indigo-500" /> Social Login
|
||||
</h3>
|
||||
@@ -420,6 +424,7 @@ const AuthenticationSettings: React.FC = () => {
|
||||
Changes saved successfully
|
||||
</div>
|
||||
)}
|
||||
</LockedSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
useUpdateCreditsSettings,
|
||||
} from '../../hooks/useCommunicationCredits';
|
||||
import { CreditPaymentModal } from '../../components/CreditPaymentForm';
|
||||
import { usePlanFeatures } from '../../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../../components/UpgradePrompt';
|
||||
|
||||
const CommunicationSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -59,6 +61,7 @@ const CommunicationSettings: React.FC = () => {
|
||||
const [topUpAmount, setTopUpAmount] = useState(2500);
|
||||
|
||||
const isOwner = user.role === 'owner';
|
||||
const { canUse } = usePlanFeatures();
|
||||
|
||||
// Update settings form when credits data loads
|
||||
useEffect(() => {
|
||||
@@ -178,6 +181,8 @@ const CommunicationSettings: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LockedSection feature="sms_reminders" isLocked={!canUse('sms_reminders')}>
|
||||
|
||||
{/* Setup Wizard or Main Content */}
|
||||
{needsSetup || showWizard ? (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
@@ -720,6 +725,7 @@ const CommunicationSettings: React.FC = () => {
|
||||
defaultAmount={topUpAmount}
|
||||
onSuccess={handlePaymentSuccess}
|
||||
/>
|
||||
</LockedSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
useSetPrimaryDomain
|
||||
} from '../../hooks/useCustomDomains';
|
||||
import DomainPurchase from '../../components/DomainPurchase';
|
||||
import { usePlanFeatures } from '../../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../../components/UpgradePrompt';
|
||||
|
||||
const DomainsSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -42,6 +44,7 @@ const DomainsSettings: React.FC = () => {
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const isOwner = user.role === 'owner';
|
||||
const { canUse } = usePlanFeatures();
|
||||
|
||||
const handleAddDomain = () => {
|
||||
if (!newDomain.trim()) return;
|
||||
@@ -125,9 +128,10 @@ const DomainsSettings: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Domain Setup - Booking URL */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<LockedSection feature="custom_domain" isLocked={!canUse('custom_domain')}>
|
||||
{/* Quick Domain Setup - Booking URL */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Link2 size={20} className="text-brand-500" /> Your Booking URL
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
@@ -326,6 +330,7 @@ const DomainsSettings: React.FC = () => {
|
||||
Changes saved successfully
|
||||
</div>
|
||||
)}
|
||||
</LockedSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user