feat: Implement multi-tenancy support in middleware and security layers

- Enhanced TenantMiddleware to validate tenant information from JWT tokens.
- Added LicenseValidationMiddleware to check tenant licenses before processing requests.
- Updated security utilities to extract tenant information from tokens and validate company access.
- Introduced CompanyStore to manage active company state and handle company switching in the frontend.
- Modified API routes to include company_id in requests for better resource management.
- Improved logging and error handling throughout the middleware and API layers.
- Updated frontend components to reflect changes in company management and selection.
- Added new API route for fetching user's companies with proper authentication handling.
This commit is contained in:
2025-11-11 14:00:56 -06:00
parent e1eb6bbd01
commit 52b8fcd434
242 changed files with 7067 additions and 3274 deletions

View File

@@ -35,7 +35,7 @@
<Sidebar.Root {collapsible} {...restProps}>
<Sidebar.Header>
<TeamSwitcher teams={data.teams} />
<TeamSwitcher />
</Sidebar.Header>
<Sidebar.Content>
<NavMain items={data.navMain} />

View File

@@ -3,14 +3,18 @@
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { useSidebar } from "$lib/components/ui/sidebar/index.js";
import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
import PlusIcon from "@lucide/svelte/icons/plus";
import BuildingIcon from "@lucide/svelte/icons/building";
import CheckIcon from "@lucide/svelte/icons/check";
import { companyStore } from "$lib/stores/company.svelte";
// This should be `Component` after @lucide/svelte updates types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let { teams }: { teams: { name: string; logo: any; plan: string }[] } = $props();
const sidebar = useSidebar();
let activeTeam = $state(teams[0]);
// Inicializar el store cuando se monta el componente
$effect(() => {
if (companyStore.companies.length === 0) {
companyStore.initialize();
}
});
</script>
<Sidebar.Menu>
@@ -26,15 +30,27 @@
<div
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
>
<activeTeam.logo class="size-4 text-white" />
{#if companyStore.activeCompany?.logo}
<img
src={companyStore.activeCompany.logo}
alt={companyStore.activeCompany.name}
class="size-full rounded-lg object-cover"
/>
{:else}
<BuildingIcon class="size-4 text-white" />
{/if}
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">
{activeTeam.name}
{companyStore.activeCompany?.name || 'Seleccionar compañía'}
</span>
<span class="truncate text-xs">{activeTeam.plan}</span>
{#if companyStore.activeCompany?.rfc}
<span class="truncate text-xs text-muted-foreground">
{companyStore.activeCompany.rfc}
</span>
{/if}
</div>
<ChevronsUpDownIcon class="ml-auto" />
<ChevronsUpDownIcon class="ml-auto size-4" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
@@ -44,25 +60,50 @@
side={sidebar.isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenu.Label class="text-muted-foreground text-xs">Teams</DropdownMenu.Label>
{#each teams as team, index (team.name)}
<DropdownMenu.Item onSelect={() => (activeTeam = team)} class="gap-2 p-2">
<div class="flex size-6 items-center justify-center rounded-md border">
<team.logo class="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenu.Shortcut>{index + 1}</DropdownMenu.Shortcut>
<DropdownMenu.Label class="text-muted-foreground text-xs">
Mis Compañías
</DropdownMenu.Label>
{#if companyStore.loading}
<DropdownMenu.Item disabled class="gap-2 p-2">
<span class="text-muted-foreground">Cargando...</span>
</DropdownMenu.Item>
{/each}
<DropdownMenu.Separator />
<DropdownMenu.Item class="gap-2 p-2">
<div
class="flex size-6 items-center justify-center rounded-md border bg-transparent"
>
<PlusIcon class="size-4" />
</div>
<div class="text-muted-foreground font-medium">Add team</div>
</DropdownMenu.Item>
{:else if companyStore.companies.length === 0}
<DropdownMenu.Item disabled class="gap-2 p-2">
<span class="text-muted-foreground">No hay compañías disponibles</span>
</DropdownMenu.Item>
{:else}
{#each companyStore.companies as company, index (company.id)}
<DropdownMenu.Item
onSelect={() => companyStore.setActiveCompany(company)}
class="gap-2 p-2 cursor-pointer"
>
<div class="flex size-6 items-center justify-center rounded-md border">
{#if company.logo}
<img
src={company.logo}
alt={company.name}
class="size-full rounded object-cover"
/>
{:else}
<BuildingIcon class="size-3.5 shrink-0" />
{/if}
</div>
<div class="flex flex-1 flex-col">
<span class="font-medium">{company.name}</span>
{#if company.rfc}
<span class="text-xs text-muted-foreground">{company.rfc}</span>
{/if}
</div>
{#if companyStore.activeCompany?.id === company.id}
<CheckIcon class="ml-auto size-4 text-primary" />
{/if}
{#if index < 9}
<DropdownMenu.Shortcut>{index + 1}</DropdownMenu.Shortcut>
{/if}
</DropdownMenu.Item>
{/each}
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>

View File

@@ -2,9 +2,10 @@ import { Tooltip as TooltipPrimitive } from "bits-ui";
import Trigger from "./tooltip-trigger.svelte";
import Content from "./tooltip-content.svelte";
const Root = TooltipPrimitive.Root;
const Provider = TooltipPrimitive.Provider;
const Portal = TooltipPrimitive.Portal;
// Handle SSR safely
const Root = TooltipPrimitive?.Root ?? (class {} as any);
const Provider = TooltipPrimitive?.Provider ?? (class {} as any);
const Portal = TooltipPrimitive?.Portal ?? (class {} as any);
export {
Root,