feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.

This commit is contained in:
Rotimi Best
2026-04-12 08:58:41 +01:00
parent 5a8f50f67b
commit d6e812d08b
573 changed files with 31964 additions and 8708 deletions
@@ -0,0 +1 @@
export { default as ProgramSidebar } from './program-sidebar.svelte';
@@ -0,0 +1,105 @@
<script lang="ts">
import { page } from '$app/state';
import { resolve } from '$app/paths';
import * as Sidebar from '@cio/ui/base/sidebar';
import { BackButton } from '@cio/ui';
import { HoverableItem, NewsFeedIcon, CourseIcon, PeopleIcon, SettingsIcon } from '@cio/ui/custom/moving-icons';
import { t } from '$lib/utils/functions/translations';
import { currentOrgPath, isOrgAdmin } from '$lib/utils/store/org';
import { isStudentExperience } from '$lib/utils/store/app';
import { profile } from '$lib/utils/store/user';
import { programApi } from '$features/program/api';
import { ROLE } from '@cio/utils/constants';
interface Props {
path: string;
id: string;
}
let { path, id }: Props = $props();
const currentPath = $derived(path || page.url.pathname);
const isLmsProgramContext = $derived(page.url.searchParams.get('source') === 'lms' || $isStudentExperience);
const programsListPath = $derived(isLmsProgramContext ? '/lms/programs' : `${$currentOrgPath}/programs`);
const currentMemberRole = $derived(programApi.members.find((m) => m.profileId === $profile.id)?.roleId ?? null);
// Admin + Tutor can manage members; org admins always can
const canManageMembers = $derived(
$isOrgAdmin === true || (currentMemberRole !== null && currentMemberRole <= ROLE.TUTOR)
);
// Only Admins and org admins can access settings
const canManageSettings = $derived($isOrgAdmin === true || currentMemberRole === ROLE.ADMIN);
const navItems = $derived(
[
{
id: 'newsfeed',
title: $t('programs.sidebar.newsfeed') || 'Newsfeed',
url: `/programs/${id}/newsfeed`,
isActive: currentPath.endsWith('/newsfeed'),
icon: NewsFeedIcon,
show: () => true
},
{
id: 'courses',
title: $t('programs.sidebar.courses') || 'Courses',
url: `/programs/${id}/courses`,
isActive: currentPath.endsWith('/courses'),
icon: CourseIcon,
show: () => true
},
{
id: 'people',
title: $t('programs.sidebar.people') || 'People',
url: `/programs/${id}/people`,
isActive: currentPath.endsWith('/people'),
icon: PeopleIcon,
show: () => canManageMembers
},
{
id: 'settings',
title: $t('programs.sidebar.settings') || 'Settings',
url: `/programs/${id}/settings`,
isActive: currentPath.endsWith('/settings'),
icon: SettingsIcon,
show: () => canManageSettings
}
].filter((item) => item.show())
);
</script>
<Sidebar.Group class="pt-0!">
<BackButton
href={resolve(programsListPath, {})}
label={$t(isLmsProgramContext ? 'lms_navigation.programs' : 'org_navigation.programs') || 'Programs'}
class="px-2! py-2!"
/>
<Sidebar.Menu>
{#each navItems as item (item.id)}
<Sidebar.MenuItem>
<Sidebar.MenuButton tooltipContent={item.title} isActive={item.isActive}>
{#snippet child({ props })}
<HoverableItem>
{#snippet children(isHovered)}
{@const Icon = item.icon}
<a href={resolve(item.url, {})} {...props}>
<Icon {isHovered} size={16} />
<span>{item.title}</span>
</a>
{/snippet}
</HoverableItem>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.Group>
<style>
a {
text-decoration: none;
}
</style>
@@ -0,0 +1,71 @@
<script lang="ts">
import { page } from '$app/state';
import * as Sidebar from '@cio/ui/base/sidebar';
import { Skeleton } from '@cio/ui/base/skeleton';
import { profile } from '$lib/utils/store/user';
import { orgs, isFreePlan } from '$lib/utils/store/org';
import OrgLogo from '$features/ui/sidebar/org-sidebar/org-logo.svelte';
import Navigation from './program-sidebar-navigation.svelte';
import SidebarSkeleton from '$features/ui/sidebar/sidebar-skeleton.svelte';
import UpgradePoweredBy from '$features/ui/upgrade-powered-by.svelte';
import { useSidebar } from '@cio/ui/base/sidebar';
const SIDEBAR_ITEM_SKELETON_COUNT = 4;
interface Props {
path: string;
id: string;
isProgramReady?: boolean;
}
let { path, id, isProgramReady = true }: Props = $props();
const isOrgLoaded = $derived($orgs.length > 0 && $profile.id);
const sidebar = useSidebar();
const currentPath = $derived(path || page.url.pathname);
</script>
{#if !isOrgLoaded}
<SidebarSkeleton />
{:else}
<Sidebar.Root collapsible="icon">
<Sidebar.Header>
<OrgLogo />
</Sidebar.Header>
<Sidebar.Content>
{#if isProgramReady}
<Navigation path={currentPath} {id} />
{:else}
<Sidebar.Group class="pt-0!">
<div class="flex h-8 items-center gap-2 rounded-md px-2">
<Skeleton class="ui:size-4 ui:rounded-md" />
<Skeleton class="ui:h-4 ui:w-24" />
</div>
<Sidebar.Menu>
{#each Array(SIDEBAR_ITEM_SKELETON_COUNT) as _, i (i)}
<Sidebar.MenuItem>
<div class="ui:flex ui:h-8 ui:items-center ui:gap-2 ui:rounded-md ui:px-2" data-sidebar="menu-skeleton">
<Skeleton class="ui:size-4 ui:rounded-md" />
<Skeleton class="ui:h-4 ui:max-w-32 ui:flex-1" />
</div>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.Group>
{/if}
</Sidebar.Content>
<Sidebar.Rail />
{#if $isFreePlan}
<Sidebar.Footer>
<UpgradePoweredBy
class="static [&_span]:font-normal [&_span]:shadow-none"
showOnlyLogo={!sidebar.open || sidebar.isMobile}
/>
</Sidebar.Footer>
{/if}
</Sidebar.Root>
{/if}