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
+5
View File
@@ -0,0 +1,5 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@source "../../../packages/ui/src/**/*.{ts,tsx}";
+15
View File
@@ -0,0 +1,15 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
...components
} satisfies MDXComponents;
}
export const useMDXComponents = getMDXComponents;
declare global {
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
}
+28
View File
@@ -0,0 +1,28 @@
import { Link } from 'react-router';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
export function NotFound() {
return (
<HomeLayout
nav={{
title: 'ClassroomIO'
}}
className="justify-center py-32 text-center"
>
<div className="flex flex-col items-center gap-4">
<h1 className="text-fd-muted-foreground text-6xl font-bold">404</h1>
<h2 className="text-2xl font-semibold">Page Not Found</h2>
<p className="text-fd-muted-foreground max-w-md">
The page you are looking for might have been removed, had its name changed, or is
temporarily unavailable.
</p>
<Link
to="/home"
className="bg-fd-primary text-fd-primary-foreground mt-4 rounded-lg px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90"
>
Back to Home
</Link>
</div>
</HomeLayout>
);
}
+59
View File
@@ -0,0 +1,59 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { SquareArrowOutUpRight } from 'lucide-react';
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: (
<div className="flex items-center gap-2">
<img src="/docs/logo-192.png" className="size-8" alt="ClassroomIO logo" />
<span className="font-medium">ClassroomIO</span>
</div>
)
},
links: [
{
text: (
<div className="flex items-center gap-2">
Contact <SquareArrowOutUpRight />
</div>
),
url: 'https://twitter.com/classroomio',
external: true
},
{
text: (
<div className="flex items-center gap-2">
Demo <SquareArrowOutUpRight />
</div>
),
url: 'https://dub.sh/ciodemo',
external: true
},
{
text: (
<div className="flex items-center gap-2">
Dashboard <SquareArrowOutUpRight />
</div>
),
url: 'https://app.classroomio.com/',
external: true
},
{
type: 'icon',
label: 'Discord',
icon: (
<img
src="/docs/discord-blue.png"
alt="discord icon"
className="w-7 transition-all duration-500 hover:scale-90"
/>
),
text: 'Discord',
url: 'https://dub.sh/ciodiscord',
external: true
}
],
githubUrl: 'https://github.com/classroomio/classroomio'
};
}
+35
View File
@@ -0,0 +1,35 @@
import { loader, multiple } from 'fumadocs-core/source';
import * as icons from 'lucide-static';
import { docs } from 'fumadocs-mdx:collections/server';
export const source = loader({
source: multiple({
docs: docs.toFumadocsSource()
}),
baseUrl: '/',
icon(icon) {
if (!icon) {
return;
}
if (icon in icons) {
return icons[icon as keyof typeof icons];
}
}
});
export async function getDocsRouteData(slugs: string[]) {
const page = source.getPage(slugs);
if (!page) {
return null;
}
if (page.data.type === 'docs') {
return {
tree: source.pageTree as object,
slugs,
pageType: 'docs' as const,
path: page.path
};
}
}
+134
View File
@@ -0,0 +1,134 @@
import * as React from 'react';
import type { LinksFunction, MetaFunction } from 'react-router';
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useRouteError
} from 'react-router';
import { RootProvider } from 'fumadocs-ui/provider/react-router';
import { NotFound } from '@/components/not-found';
import './app.css';
export const links: LinksFunction = () => [
{ rel: 'icon', href: '/docs/favicon.ico' }
];
export const meta: MetaFunction = () => [
{
title: 'ClassroomIO | The Open Source Learning Management System for Companies'
},
{
name: 'description',
content:
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations'
},
{
property: 'og:url',
content: 'https://classroomio.com/docs'
},
{
property: 'og:type',
content: 'website'
},
{
property: 'og:title',
content: 'ClassroomIO | The Open Source Learning Management System for Companies'
},
{
property: 'og:description',
content:
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations'
},
{
property: 'og:image:type',
content: 'image/png'
},
{
property: 'og:image:width',
content: '1920'
},
{
property: 'og:image:height',
content: '1080'
},
{
property: 'og:image:secure_url',
content: 'https://brand.cdn.clsrio.com/og/classroomio-og.png'
},
{
name: 'twitter:card',
content: 'summary_large_image'
},
{
property: 'twitter:domain',
content: 'classroomio.com'
},
{
property: 'twitter:url',
content: 'https://classroomio.com/docs'
},
{
name: 'twitter:title',
content: 'ClassroomIO | The Open Source Learning Management System for Companies'
},
{
name: 'twitter:description',
content:
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations'
},
{
name: 'twitter:creator',
content: '@classroomio'
},
{
name: 'twitter:image',
content: 'https://brand.cdn.clsrio.com/og/classroomio-og.png'
}
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="flex min-h-screen flex-col">
<RootProvider>{children}</RootProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return <NotFound />;
}
return (
<Layout>
<main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-4 px-6 py-24">
<h1 className="text-3xl font-semibold">Docs Error</h1>
<p className="text-fd-muted-foreground">
The docs app hit an unexpected error while rendering this page.
</p>
</main>
</Layout>
);
}
+8
View File
@@ -0,0 +1,8 @@
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('api/reference', 'routes/api-reference.ts'),
route('api/search', 'routes/search.ts'),
route('*', 'routes/docs.tsx')
] satisfies RouteConfig;
+18
View File
@@ -0,0 +1,18 @@
import { getHtmlDocument } from '@scalar/core/libs/html-rendering';
const publicApiSpecUrl = 'https://api.cdn.clsrio.com/openapi/public-api/openapi-latest.json';
export function loader() {
return new Response(
getHtmlDocument({
url: publicApiSpecUrl,
pageTitle: 'ClassroomIO Public API Reference',
theme: 'none'
}),
{
headers: {
'content-type': 'text/html; charset=utf-8'
}
}
);
}
+122
View File
@@ -0,0 +1,122 @@
import { useMemo } from 'react';
import type * as PageTree from 'fumadocs-core/page-tree';
import browserCollections from 'fumadocs-mdx:collections/browser';
import { isRouteErrorResponse, useLoaderData, useRouteError } from 'react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { NotFound } from '@/components/not-found';
import { baseOptions } from '@/lib/layout.shared';
const clientLoader = browserCollections.docs.createClientLoader({
component({ toc, frontmatter, default: MDX }) {
return (
<DocsPage toc={toc}>
<DocsTitle>{frontmatter.title}</DocsTitle>
<DocsDescription>{frontmatter.description}</DocsDescription>
<DocsBody>
<MDX />
</DocsBody>
</DocsPage>
);
}
});
export async function loader({ params }: { params: Record<string, string | undefined> }) {
const slug = params['*'];
const slugs = slug ? slug.split('/') : ['home'];
const { getDocsRouteData } = await import('@/lib/source.server');
const routeData = await getDocsRouteData(slugs);
if (!routeData) {
throw new Response('Not Found', { status: 404 });
}
if (routeData.pageType === 'docs') {
await clientLoader.preload(routeData.path);
}
return routeData;
}
export default function DocsRoute() {
const data = useLoaderData<typeof loader>();
const tree = useMemo(() => transformPageTree(data.tree as PageTree.Root), [data.tree]);
return (
<HomeLayout {...baseOptions()}>
<DocsLayout
sidebar={{
collapsible: false
}}
themeSwitch={{
enabled: false
}}
searchToggle={{
enabled: false
}}
tree={tree}
>
<MdxContent path={data.path} />
</DocsLayout>
</HomeLayout>
);
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return <NotFound />;
}
return (
<HomeLayout {...baseOptions()} className="justify-center py-32 text-center">
<div className="flex flex-col items-center gap-4">
<h1 className="text-2xl font-semibold">Docs Error</h1>
<p className="text-fd-muted-foreground max-w-md">
The docs page could not be rendered.
</p>
</div>
</HomeLayout>
);
}
function MdxContent({ path }: { path: string }) {
const Content = clientLoader.getComponent(path);
return <Content />;
}
function transformPageTree(root: PageTree.Root): PageTree.Root {
function mapNode<T extends PageTree.Node>(item: T): T {
if (typeof item.icon === 'string') {
item = {
...item,
icon: (
<span
dangerouslySetInnerHTML={{
__html: item.icon
}}
/>
)
};
}
if (item.type === 'folder') {
return {
...item,
index: item.index ? mapNode(item.index) : undefined,
children: item.children.map(mapNode)
};
}
return item;
}
return {
...root,
children: root.children.map(mapNode),
fallback: root.fallback ? transformPageTree(root.fallback) : undefined
};
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'react-router';
export function loader() {
throw redirect('/home');
}
+10
View File
@@ -0,0 +1,10 @@
import { createFromSource } from 'fumadocs-core/search/server';
export async function loader({ request }: { request: Request }) {
const { source } = await import('@/lib/source.server');
const server = createFromSource(source, {
language: 'english'
});
return server.GET(request);
}