Files
nontara/apps/server/src/routes/onboard.ts
kenzuya 2ba4afa252 feat(server): add library API endpoints and schemas
- Add Zod schemas for library titles and all libraries in models/api/library.ts
- Create new library router with /all and /titles endpoints
- Update onboard router to use namespaced API imports
- Export API models as namespace in models/index.ts
2025-10-30 13:39:58 +00:00

150 lines
4.1 KiB
TypeScript

import { exists, readdir, stat } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { createHTTPException } from "@/lib/errors";
import type { ExtendHonoEnv } from "@/lib/types";
import type { Directory } from "@/models";
import { API, Path } from "@/models";
import { OnboardService } from "@/services/onboard";
type OnboardRouterEnv = {
needsOnboarding: boolean;
};
const OnboardRouter = new Hono<ExtendHonoEnv<OnboardRouterEnv>>()
.use((c, next) => {
const service = c.get("service");
const onboardService = service.get(OnboardService);
c.set("needsOnboarding", onboardService.getNeedsOnboarding());
return next();
})
.get("/status", (c) => {
return c.json<API.OnboardStatusOutput>({
needsOnboarding: c.get("needsOnboarding"),
});
})
.use((c, next) => {
if (!c.get("needsOnboarding")) {
throw createHTTPException(403, "You cannot access this resource");
}
return next();
})
.post("/folder", zValidator("json", Path), async (c) => {
const body = c.req.valid("json");
try {
if (body.path.length === 0) {
if (os.platform() === "linux") {
try {
const drives = await readdir("/", { withFileTypes: true });
return c.json<Directory[]>(
drives
.filter((path) => path.isDirectory())
.map((value) => ({
name: `/${value.name}`,
type: "FOLDER",
})),
);
} catch (_) {
return c.json<Directory[]>([]);
}
}
}
// On Linux, ensure path starts with / for absolute paths
const inputPath =
os.platform() === "linux"
? path.join("/", ...body.path)
: path.join(...body.path);
if (!(await exists(inputPath))) {
throw createHTTPException(404, `Path does not exit: ${body.path}`);
}
const stats = await stat(inputPath);
if (!stats.isDirectory()) {
throw createHTTPException(400, `Path is not directory: ${inputPath}`);
}
const directories = await readdir(inputPath, { withFileTypes: true });
const filtered = directories
.filter((v) => v.isDirectory())
.map((value): Directory => ({ name: value.name, type: "FOLDER" }));
return c.json<Directory[]>(filtered);
} catch (error) {
if (error instanceof Error) {
const nodeError = error as NodeJS.ErrnoException;
// Access denied / Permission denied
if (nodeError.code === "EACCES" || nodeError.code === "EPERM") {
throw createHTTPException(
500,
`Access denied: You don't have permission to access this directory`,
);
}
// Path not found
if (nodeError.code === "ENOENT") {
throw createHTTPException(
500,
"Directory not found: The specified path does not exist",
);
}
// Path too long
if (nodeError.code === "ENAMETOOLONG") {
throw createHTTPException(500, "Path is too long");
}
// Not a directory
if (nodeError.code === "ENOTDIR") {
throw createHTTPException(
500,
"Not a directory: The path is not a valid directory",
);
}
// Too many symbolic links
if (nodeError.code === "ELOOP") {
throw createHTTPException(
500,
"Too many symbolic links: Unable to resolve the path",
);
}
// Input/output error
if (nodeError.code === "EIO") {
throw createHTTPException(
500,
"I/O error: Unable to read from the filesystem",
);
}
// Re-throw the original error message if it's already formatted
throw createHTTPException(500, "Failed processing your request");
}
// Unknown error
throw createHTTPException(
500,
`Failed to read directory: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
})
.post(
"/complete",
zValidator("json", API.OnboardCompletedInput),
async (c) => {
const body = c.req.valid("json");
const service = c.get("service");
const onboardService = service.get(OnboardService);
await onboardService.completeOnboarding(body);
return c.json<API.OnboardCompletedOutput>({
success: true,
completed: true,
});
},
);
export { OnboardRouter };