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,12 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
import * as Page from '@cio/ui/base/page';
</script>
<Page.Root class="w-full pb-10 md:max-w-3xl lg:mx-auto">
{@render children?.()}
</Page.Root>
@@ -0,0 +1,42 @@
<script lang="ts">
import { ProfilePage } from '$features/settings/pages';
import { VisitOrgSiteButton } from '$features/ui';
import { t } from '$lib/utils/functions/translations';
import { Button } from '@cio/ui/base/button';
import * as Page from '@cio/ui/base/page';
let profileComponent: ProfilePage | null = $state(null);
let isLoading = $state(false);
async function handleUpdate() {
isLoading = true;
try {
await profileComponent?.handleUpdate();
} finally {
isLoading = false;
}
}
</script>
<svelte:head>
<title>Settings - ClassroomIO</title>
</svelte:head>
<Page.Header isSticky>
<Page.HeaderContent>
<Page.Title>{$t('settings.profile.heading')}</Page.Title>
<Page.Subtitle>{$t('settings.profile.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
<Page.Action>
<Button variant="secondary" loading={isLoading} onclick={handleUpdate}>
{$t('settings.profile.update_profile')}
</Button>
<VisitOrgSiteButton />
</Page.Action>
</Page.Header>
<Page.Body>
{#snippet child()}
<ProfilePage bind:this={profileComponent} />
{/snippet}
</Page.Body>
@@ -0,0 +1,21 @@
<script>
import { AuthPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
</script>
<svelte:head>
<title>{t.get('settings.auth.title')} - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{t.get('settings.auth.title')}</Page.Title>
<Page.Subtitle>{t.get('settings.auth.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<AuthPage />
{/snippet}
</Page.Body>
@@ -0,0 +1,21 @@
<script>
import { AuthPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
</script>
<svelte:head>
<title>{t.get('settings.auth.tabs.sso')} - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{t.get('settings.auth.title')}</Page.Title>
<Page.Subtitle>{t.get('settings.auth.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<AuthPage />
{/snippet}
</Page.Body>
@@ -0,0 +1,21 @@
<script>
import { AuthPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
</script>
<svelte:head>
<title>{t.get('settings.auth.tabs.token_auth')} - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{t.get('settings.auth.title')}</Page.Title>
<Page.Subtitle>{t.get('settings.auth.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<AuthPage />
{/snippet}
</Page.Body>
@@ -0,0 +1,21 @@
<script>
import { BillingPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
</script>
<svelte:head>
<title>Billing - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{$t('settings.billing.sub_title')}</Page.Title>
<Page.Subtitle>{$t('settings.billing.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<BillingPage />
{/snippet}
</Page.Body>
@@ -0,0 +1,41 @@
<script lang="ts">
import { CustomizeLmsPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import { Button } from '@cio/ui/base/button';
import * as Page from '@cio/ui/base/page';
let customizeLmsComponent: CustomizeLmsPage | null = $state(null);
let isSaving = $state(false);
async function handleSave() {
isSaving = true;
try {
await customizeLmsComponent?.handleSave();
} finally {
isSaving = false;
}
}
</script>
<svelte:head>
<title>Customize LMS - ClassroomIO</title>
</svelte:head>
<Page.Root class="w-full md:max-w-4xl lg:mx-auto">
<Page.Header>
<Page.HeaderContent>
<Page.Title>{$t('components.settings.customize_lms.title')}</Page.Title>
<Page.Subtitle>{$t('components.settings.customize_lms.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
<Page.Action>
<Button variant="default" loading={isSaving} disabled={isSaving} onclick={handleSave}>
{$t('components.settings.customize_lms.save')}
</Button>
</Page.Action>
</Page.Header>
<Page.Body>
{#snippet child()}
<CustomizeLmsPage bind:this={customizeLmsComponent} />
{/snippet}
</Page.Body>
</Page.Root>
@@ -0,0 +1,21 @@
<script>
import { DomainsPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
</script>
<svelte:head>
<title>Custom domain - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{$t('components.settings.domains.title')}</Page.Title>
<Page.Subtitle>{$t('components.settings.domains.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<DomainsPage />
{/snippet}
</Page.Body>
@@ -0,0 +1,21 @@
<script>
import { IntegrationsPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
</script>
<svelte:head>
<title>Integrations - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{$t('settings.integrations.heading')}</Page.Title>
<Page.Subtitle>{$t('settings.integrations.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<IntegrationsPage />
{/snippet}
</Page.Body>
@@ -0,0 +1,43 @@
<script lang="ts">
import { LandingpagePage } from '$features/settings/pages';
import { VisitOrgSiteButton } from '$features/ui';
import { t } from '$lib/utils/functions/translations';
import { Button } from '@cio/ui/base/button';
import * as Page from '@cio/ui/base/page';
let landingPageComponent: LandingpagePage | null = $state(null);
let hasUnsavedChanges = $state(false);
let isSaving = $state(false);
async function handleSave() {
isSaving = true;
try {
await landingPageComponent?.handleSave();
} finally {
isSaving = false;
}
}
</script>
<svelte:head>
<title>Landing Page Settings - ClassroomIO</title>
</svelte:head>
<Page.Header isSticky>
<Page.HeaderContent>
<Page.Title>{$t('settings.landing_page.heading')}</Page.Title>
<Page.Subtitle>{$t('settings.landing_page.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
<Page.Action>
<VisitOrgSiteButton labelKey="markdown_editor.preview" variant="secondary" />
<Button variant="default" loading={isSaving} disabled={isSaving || !hasUnsavedChanges} onclick={handleSave}>
{$t('settings.landing_page.save_changes')}
</Button>
</Page.Action>
</Page.Header>
<Page.Body>
{#snippet child()}
<LandingpagePage bind:this={landingPageComponent} bind:hasUnsavedChanges />
{/snippet}
</Page.Body>
@@ -0,0 +1,125 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { onMount } from 'svelte';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import type { AccountOrg } from '$features/app/types';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { orgApi } from '$features/org/api/org.svelte';
import { landingPageSettings } from '$features/settings/utils/store';
import { buildOrgLandingPageProps, normalizeLandingPageSettings } from '$features/org/utils/landing-page';
import { landingPageThemeComponents } from '$features/org/utils/landing-page-components';
import LandingpageEditor from '$features/settings/pages/landingpage-editor.svelte';
import * as Sidebar from '@cio/ui/base/sidebar';
import { basePath } from '$lib/utils/store/app';
import { t } from '$lib/utils/functions/translations';
import { user } from '$lib/utils/store/user';
let sidebarOpen = $state(true);
let hasInitialized = $state(false);
function isLandingPageTheme(value: string | null): value is 'minimal' | 'bold' | 'classic' {
return value === 'minimal' || value === 'bold' || value === 'classic';
}
const savedLandingPageSettings = $derived(normalizeLandingPageSettings($currentOrg.landingpage));
const previewTheme = $derived.by(() => {
const queryTheme = page.url.searchParams.get('theme');
if (isLandingPageTheme(queryTheme)) {
return queryTheme;
}
return savedLandingPageSettings.theme;
});
const previewLandingPageSettings = $derived({
...$landingPageSettings,
theme: previewTheme
});
const authAction = $derived(
$user.isLoggedIn
? {
label: t.get($basePath === '/lms' || $basePath === '#' ? 'navigation.goto_lms' : 'navigation.goto_dashboard'),
href: resolve($basePath !== '#' ? $basePath : '/lms', {})
}
: {
label: t.get('navigation.login'),
href: '/login'
}
);
const previewProps = $derived(
buildOrgLandingPageProps(
$currentOrg,
previewLandingPageSettings,
orgApi.publicCourses,
orgApi.hasMorePublicCourses,
authAction
)
);
const ThemeComponent = $derived(landingPageThemeComponents[previewTheme] ?? landingPageThemeComponents.minimal);
onMount(() => {
if ($currentOrg.siteName) {
orgApi.publicCourses = [];
orgApi.hasMorePublicCourses = false;
void orgApi.getPublicCoursesBySiteName($currentOrg.siteName);
}
});
$effect(() => {
if (hasInitialized || !$currentOrg.id) {
return;
}
$landingPageSettings = normalizeLandingPageSettings($currentOrg.landingpage);
hasInitialized = true;
});
function handleClose() {
const queryTheme = page.url.searchParams.get('theme');
const querySuffix = isLandingPageTheme(queryTheme) ? `?theme=${encodeURIComponent(queryTheme)}` : '';
goto(resolve(`${$currentOrgPath}/settings/landingpage${querySuffix}`, {}));
}
</script>
<svelte:head>
<title>Landing Page Editor - ClassroomIO</title>
</svelte:head>
<div
class="bg-background fixed inset-0 z-250 h-screen w-screen"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<Sidebar.Provider bind:open={sidebarOpen} style="--sidebar-width: 280px">
<Sidebar.Root side="left" collapsible="icon" class="h-full">
<LandingpageEditor
bind:settings={$landingPageSettings}
onSave={async () => {
const updatedLandingPage = normalizeLandingPageSettings({
...$landingPageSettings,
theme: page.url.searchParams.get('theme') ? savedLandingPageSettings.theme : $landingPageSettings.theme
});
await orgApi.update($currentOrg.id, {
landingpage: updatedLandingPage as AccountOrg['landingpage']
});
}}
onClose={handleClose}
/>
</Sidebar.Root>
<Sidebar.Inset
class="relative h-screen! w-[calc(100vw-var(--sidebar-width))] overflow-y-auto group-data-[collapsible=icon]:w-[calc(100vw-var(--sidebar-width-icon))]"
>
<div class="absolute top-2 left-2 z-60">
<Sidebar.Trigger variant="secondary" />
</div>
<ThemeComponent {...previewProps} disableCourseLinks={true} />
</Sidebar.Inset>
</Sidebar.Provider>
</div>
@@ -0,0 +1,39 @@
<script lang="ts">
import { OrgPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import { Button } from '@cio/ui/base/button';
import * as Page from '@cio/ui/base/page';
let orgComponent: OrgPage | null = $state(null);
let loading = $state(false);
async function handleUpdate() {
loading = true;
try {
await orgComponent?.handleUpdate();
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Organization Settings - ClassroomIO</title>
</svelte:head>
<Page.Header isSticky>
<Page.HeaderContent>
<Page.Title>{$t('settings.organization.organization_profile.heading')}</Page.Title>
<Page.Subtitle>{$t('settings.organization.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
<Page.Action>
<Button variant="default" {loading} disabled={loading} onclick={handleUpdate}>
{$t('settings.organization.organization_profile.update_organization')}
</Button>
</Page.Action>
</Page.Header>
<Page.Body>
{#snippet child()}
<OrgPage bind:this={orgComponent} />
{/snippet}
</Page.Body>
@@ -0,0 +1,21 @@
<script>
import { TeamsPage } from '$features/settings/pages';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
</script>
<svelte:head>
<title>Teams - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{$t('upgrade.team_members')}</Page.Title>
<Page.Subtitle>{$t('settings.teams.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<TeamsPage />
{/snippet}
</Page.Body>