authentication authorization

This commit is contained in:
2025-12-26 15:32:22 +01:00
parent 68ae9da22a
commit 64377de548
54 changed files with 2833 additions and 95 deletions

View File

@@ -7,6 +7,7 @@ import Link from 'next/link';
import React, { useEffect, useMemo, useState } from 'react';
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
import { CapabilityGate } from '@/components/shared/CapabilityGate';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/DriverViewModel';
@@ -190,40 +191,54 @@ export default function UserPill() {
</div>
{/* Menu Items */}
<div className="py-2 text-sm text-gray-200">
<Link
href="/sponsor"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<BarChart3 className="h-4 w-4 text-performance-green" />
<span>Dashboard</span>
</Link>
<Link
href="/sponsor/campaigns"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Megaphone className="h-4 w-4 text-primary-blue" />
<span>My Sponsorships</span>
</Link>
<Link
href="/sponsor/billing"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<CreditCard className="h-4 w-4 text-warning-amber" />
<span>Billing</span>
</Link>
<Link
href="/sponsor/settings"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Settings className="h-4 w-4 text-gray-400" />
<span>Settings</span>
</Link>
</div>
<CapabilityGate
capabilityKey="sponsors.portal"
fallback={
<div className="py-2 px-4 text-xs text-gray-500">
Sponsor portal is currently unavailable.
</div>
}
comingSoon={
<div className="py-2 px-4 text-xs text-gray-500">
Sponsor portal is coming soon.
</div>
}
>
<div className="py-2 text-sm text-gray-200">
<Link
href="/sponsor"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<BarChart3 className="h-4 w-4 text-performance-green" />
<span>Dashboard</span>
</Link>
<Link
href="/sponsor/campaigns"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Megaphone className="h-4 w-4 text-primary-blue" />
<span>My Sponsorships</span>
</Link>
<Link
href="/sponsor/billing"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<CreditCard className="h-4 w-4 text-warning-amber" />
<span>Billing</span>
</Link>
<Link
href="/sponsor/settings"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Settings className="h-4 w-4 text-gray-400" />
<span>Settings</span>
</Link>
</div>
</CapabilityGate>
{/* Footer */}
<div className="border-t border-charcoal-outline">

View File

@@ -0,0 +1,44 @@
'use client';
import { ReactNode } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useServices } from '@/lib/services/ServiceProvider';
type CapabilityGateProps = {
capabilityKey: string;
children: ReactNode;
fallback?: ReactNode;
comingSoon?: ReactNode;
};
export function CapabilityGate({
capabilityKey,
children,
fallback = null,
comingSoon = null,
}: CapabilityGateProps) {
const { policyService } = useServices();
const { data, isLoading, isError } = useQuery({
queryKey: ['policySnapshot'],
queryFn: () => policyService.getSnapshot(),
staleTime: 60_000,
gcTime: 5 * 60_000,
});
if (isLoading || isError || !data) {
return <>{fallback}</>;
}
const state = policyService.getCapabilityState(data, capabilityKey);
if (state === 'enabled') {
return <>{children}</>;
}
if (state === 'coming_soon') {
return <>{comingSoon ?? fallback}</>;
}
return <>{fallback}</>;
}