feat: implement automation key management with middleware and routes for organization API keys, invite improvements

This commit is contained in:
Rotimi Best
2026-03-13 18:40:07 +01:00
parent 77a1188573
commit 492f8dc5ed
71 changed files with 3655 additions and 456 deletions
@@ -10,6 +10,7 @@ import {
import type { AccountOrg } from '$features/app/types';
import type { Component } from 'svelte';
import BracesIcon from '@lucide/svelte/icons/braces';
import TagIcon from '@lucide/svelte/icons/tag';
import { isActive } from '$lib/utils/functions/app';
@@ -22,6 +23,7 @@ export interface NavItem {
isExpanded?: boolean;
items?: NavItem[]; // for nested items like settings
isPaid?: boolean; // Show upgrade indicator for free plan users
disabled?: boolean;
// Metadata for breadcrumb generation
useHashUrl?: boolean; // Use '#' as URL (for collapsible items like settings)
nestedRoutes?: NestedRouteConfig[]; // Static nested routes (like community/ask, settings/customize-lms)
@@ -33,6 +35,7 @@ export interface NavItemConfig {
path: string;
icon?: Component;
requiresAdmin?: boolean;
disableWhenNotAdmin?: boolean;
items?: NavItemConfig[];
useHashUrl?: boolean; // Use '#' as URL (for collapsible items like settings)
nestedRoutes?: NestedRouteConfig[]; // Static nested routes
@@ -99,6 +102,14 @@ const baseNavConfig: NavItemConfig[] = [
requiresAdmin: true,
matchPattern: '^/org/[^/]+/setup(/.*)?$' // Matches nested routes
},
{
titleKey: 'org_navigation.automation',
path: '/automation',
icon: BracesIcon,
requiresAdmin: true,
disableWhenNotAdmin: true,
matchPattern: '^/org/[^/]+/automation(/.*)?$'
},
{
titleKey: 'org_navigation.settings',
path: '/settings',
@@ -172,7 +183,7 @@ export function getOrgNavigationItems(
for (const config of baseNavConfig) {
// Skip admin-only items if user is not admin
if (config.requiresAdmin && !isOrgAdmin) {
if (config.requiresAdmin && !isOrgAdmin && !config.disableWhenNotAdmin) {
continue;
}
@@ -190,6 +201,7 @@ export function getOrgNavigationItems(
icon: config.icon,
isActive: isActive(pagePathname, fullPath, matchPattern),
isExpanded: config.items ? isActive(pagePathname, fullPath, matchPattern) : undefined,
disabled: Boolean(config.disableWhenNotAdmin && !isOrgAdmin),
useHashUrl: config.useHashUrl,
nestedRoutes: config.nestedRoutes,
supportsDynamicSegment: config.supportsDynamicSegment,
@@ -27,21 +27,37 @@
{#snippet child({ props })}
<HoverableItem>
{#snippet children(isHovered)}
<a href={item.url} {...props}>
{#if item.icon}
{@const Icon = item.icon}
<Icon {isHovered} size={16} class="custom" />
{#if item.disabled}
<span
{...props}
aria-disabled="true"
class="flex cursor-not-allowed items-center gap-2 opacity-50"
>
{#if item.icon}
{@const Icon = item.icon}
<Icon {isHovered} size={16} class="custom" />
<span>{item.title}</span>
{:else}
<span>{item.title}</span>
{/if}
</span>
{:else}
<a href={item.url} {...props}>
{#if item.icon}
{@const Icon = item.icon}
<Icon {isHovered} size={16} class="custom" />
<span>{item.title}</span>
{:else}
<span>{item.title}</span>
{/if}
{#if item.items}
<ChevronRightIcon
class="custom ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
/>
{/if}
</a>
<span>{item.title}</span>
{:else}
<span>{item.title}</span>
{/if}
{#if item.items}
<ChevronRightIcon
class="custom ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
/>
{/if}
</a>
{/if}
{/snippet}
</HoverableItem>
{/snippet}