Skip to content
File

Blob: src/worker/api/launcher.ts

typescript38 lines
1import { asc, eq } from "drizzle-orm";
2 
3import { isLauncherIconName, isLauncherTintName } from "@/shared/launcher";
4import { launcherApps } from "@/worker/db/schema";
5import type { AppContext } from "@/worker/hono";
6import { isSafeHttpUrl } from "@/worker/services/url";
7 
8// Sorts by name for stable ordering across page loads. The `isSafeHttpUrl`
9// re-filter on read is defense-in-depth: admin writes already validate,
10// but a manually-edited DB row cannot crash render or smuggle a
11// `javascript:` href onto the page. Unknown icon/tint names coerce to
12// null and the tile renders with default fallback.
13export const handleListLauncherApps = async (c: AppContext): Promise<Response> => {
14 const rows = await c.var.db
15 .select({
16 id: launcherApps.id,
17 name: launcherApps.name,
18 url: launcherApps.url,
19 icon: launcherApps.icon,
20 tint: launcherApps.tint,
21 })
22 .from(launcherApps)
23 .where(eq(launcherApps.enabled, true))
24 .orderBy(asc(launcherApps.name));
25 
26 const apps = rows
27 .filter((row) => isSafeHttpUrl(row.url))
28 .map((row) => ({
29 id: row.id,
30 name: row.name,
31 url: row.url,
32 icon: isLauncherIconName(row.icon) ? row.icon : null,
33 tint: isLauncherTintName(row.tint) ? row.tint : null,
34 }));
35 
36 return c.json(apps);
37};