Compare commits
2 Commits
b60a3bd92f
...
a1f21c38e4
| Author | SHA1 | Date | |
|---|---|---|---|
| a1f21c38e4 | |||
| d6b4fb128f |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"typescript.experimental.useTsgo": false,
|
||||
"typescript.experimental.useTsgo": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "always",
|
||||
"source.fixAll.biome": "always",
|
||||
|
||||
36
apps/server/src/middleware/admin.ts
Normal file
36
apps/server/src/middleware/admin.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createMiddleware } from "hono/factory";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { createHTTPException } from "@/lib/errors";
|
||||
import type { ExtendHonoEnv } from "@/lib/types";
|
||||
|
||||
type AdminEnv = ExtendHonoEnv<{
|
||||
session: Exclude<Awaited<ReturnType<typeof auth.api.getSession>>, null>;
|
||||
user: Exclude<Awaited<ReturnType<typeof auth.api.getUser>>, null>;
|
||||
}>;
|
||||
|
||||
export const adminMiddleware = createMiddleware<AdminEnv>(async (c, next) => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: c.req.raw.headers,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw createHTTPException(401, "You are not authorized");
|
||||
}
|
||||
c.set("session", session);
|
||||
c.set("user", session.user);
|
||||
const hasPermission = await auth.api.userHasPermission({
|
||||
body: {
|
||||
userId: session.user.id,
|
||||
permissions: {
|
||||
dashboard: ["access"],
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!hasPermission) {
|
||||
throw createHTTPException(
|
||||
403,
|
||||
"You do not have permission to access the dashboard",
|
||||
);
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
1
apps/server/src/models/api/dashboard/index.ts
Normal file
1
apps/server/src/models/api/dashboard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./library";
|
||||
95
apps/server/src/models/api/dashboard/library.ts
Normal file
95
apps/server/src/models/api/dashboard/library.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const LIBRARY_TYPES = ["MOVIE", "SERIES"] as const;
|
||||
|
||||
// List all libraries
|
||||
export const LibraryListInput = z.void();
|
||||
export type LibraryListInput = z.infer<typeof LibraryListInput>;
|
||||
|
||||
export const LibraryListOutput = z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
type: z.enum(LIBRARY_TYPES),
|
||||
path: z.string(),
|
||||
primaryLanguage: z.string(),
|
||||
fallbackLanguage: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
}),
|
||||
);
|
||||
export type LibraryListOutput = z.infer<typeof LibraryListOutput>;
|
||||
|
||||
// Get a library by ID
|
||||
export const LibraryGetInput = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
export type LibraryGetInput = z.infer<typeof LibraryGetInput>;
|
||||
|
||||
export const LibraryGetOutput = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
type: z.enum(LIBRARY_TYPES),
|
||||
path: z.string(),
|
||||
primaryLanguage: z.string(),
|
||||
fallbackLanguage: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
export type LibraryGetOutput = z.infer<typeof LibraryGetOutput>;
|
||||
|
||||
// Create a new library
|
||||
export const LibraryCreateInput = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
type: z.enum(LIBRARY_TYPES),
|
||||
path: z.string().min(1).max(255),
|
||||
primaryLanguage: z.string().min(1).max(255),
|
||||
fallbackLanguage: z.string().min(1).max(255),
|
||||
});
|
||||
export type LibraryCreateInput = z.infer<typeof LibraryCreateInput>;
|
||||
|
||||
export const LibraryCreateOutput = z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string(),
|
||||
data: LibraryGetOutput,
|
||||
});
|
||||
export type LibraryCreateOutput = z.infer<typeof LibraryCreateOutput>;
|
||||
|
||||
// Update a library
|
||||
export const LibraryUpdateInputParam = z.object({
|
||||
id: z.number(),
|
||||
});
|
||||
export type LibraryUpdateInputParam = z.infer<typeof LibraryUpdateInputParam>;
|
||||
|
||||
export const LibraryUpdateInput = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
type: z.enum(LIBRARY_TYPES),
|
||||
path: z.string().min(1).max(255),
|
||||
primaryLanguage: z.string().min(1).max(255),
|
||||
fallbackLanguage: z.string().min(1).max(255),
|
||||
});
|
||||
export type LibraryUpdateInput = z.infer<typeof LibraryUpdateInput>;
|
||||
|
||||
export const LibraryUpdateOutput = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
type: z.enum(LIBRARY_TYPES),
|
||||
path: z.string(),
|
||||
primaryLanguage: z.string(),
|
||||
fallbackLanguage: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
export type LibraryUpdateOutput = z.infer<typeof LibraryUpdateOutput>;
|
||||
|
||||
// Delete a library
|
||||
export const LibraryDeleteInput = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
export type LibraryDeleteInput = z.infer<typeof LibraryDeleteInput>;
|
||||
|
||||
export const LibraryDeleteOutput = z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
export type LibraryDeleteOutput = z.infer<typeof LibraryDeleteOutput>;
|
||||
@@ -1,2 +1,6 @@
|
||||
export * from "./dashboard";
|
||||
export * from "./library";
|
||||
export * from "./media";
|
||||
export * from "./onboard";
|
||||
export * from "./playback";
|
||||
export * from "./title";
|
||||
|
||||
4
apps/server/src/routes/dashboard.ts
Normal file
4
apps/server/src/routes/dashboard.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Hono } from "hono";
|
||||
import { LibraryRouter } from "./dashboard/library";
|
||||
|
||||
export const DashboardRouter = new Hono().route("/library ", LibraryRouter);
|
||||
163
apps/server/src/routes/dashboard/library.ts
Normal file
163
apps/server/src/routes/dashboard/library.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Hono } from "hono";
|
||||
import { database } from "@/db";
|
||||
import { libraryFolders } from "@/db/schema";
|
||||
import { createHTTPException } from "@/lib/errors";
|
||||
import {
|
||||
LibraryCreateInput,
|
||||
type LibraryCreateOutput,
|
||||
LibraryDeleteInput,
|
||||
type LibraryDeleteOutput,
|
||||
LibraryGetInput,
|
||||
type LibraryGetOutput,
|
||||
type LibraryListOutput,
|
||||
LibraryUpdateInput,
|
||||
LibraryUpdateInputParam,
|
||||
type LibraryUpdateOutput,
|
||||
} from "@/models/api";
|
||||
export const LibraryRouter = new Hono()
|
||||
.get("/", async (c) => {
|
||||
const libraries = await database.select().from(libraryFolders);
|
||||
return c.json<LibraryListOutput>(libraries);
|
||||
})
|
||||
.get("/:id", zValidator("param", LibraryGetInput), async (c) => {
|
||||
const { id } = c.req.valid("param");
|
||||
const [library] = await database
|
||||
.select()
|
||||
.from(libraryFolders)
|
||||
.where(eq(libraryFolders.id, id))
|
||||
.limit(1);
|
||||
if (!library) {
|
||||
throw createHTTPException(404, `Library with id ${id} not found.`);
|
||||
}
|
||||
return c.json<LibraryGetOutput>(library);
|
||||
})
|
||||
.patch(
|
||||
"/:id",
|
||||
zValidator("param", LibraryUpdateInputParam),
|
||||
zValidator("json", LibraryUpdateInput),
|
||||
async (c) => {
|
||||
const { id } = c.req.valid("param");
|
||||
const input = c.req.valid("json");
|
||||
const [existingLibrary] = await database
|
||||
.select()
|
||||
.from(libraryFolders)
|
||||
.where(eq(libraryFolders.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (!existingLibrary) {
|
||||
throw createHTTPException(404, `Library with id ${id} not found`);
|
||||
}
|
||||
|
||||
if (input.name) {
|
||||
const [existingByName] = await database
|
||||
.select()
|
||||
.from(libraryFolders)
|
||||
.where(eq(libraryFolders.name, input.name))
|
||||
.limit(1);
|
||||
|
||||
if (existingByName && existingByName.id !== id) {
|
||||
throw createHTTPException(
|
||||
409,
|
||||
`A library with the name "${input.name}" already exists.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.path) {
|
||||
const [existingByPath] = await database
|
||||
.select()
|
||||
.from(libraryFolders)
|
||||
.where(eq(libraryFolders.path, input.path))
|
||||
.limit(1);
|
||||
|
||||
if (existingByPath && existingByPath.id !== id) {
|
||||
throw createHTTPException(
|
||||
409,
|
||||
`A library with the path "${input.path}" already exists.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (input.name !== undefined) updateData.name = input.name;
|
||||
if (input.path !== undefined) updateData.path = input.path;
|
||||
if (input.primaryLanguage !== undefined)
|
||||
updateData.primaryLanguage = input.primaryLanguage;
|
||||
if (input.fallbackLanguage !== undefined)
|
||||
updateData.fallbackLanguage = input.fallbackLanguage;
|
||||
|
||||
const [updatedLibrary] = await database
|
||||
.update(libraryFolders)
|
||||
.set(updateData)
|
||||
.where(eq(libraryFolders.id, id))
|
||||
.returning();
|
||||
|
||||
return c.json<LibraryUpdateOutput>(updatedLibrary);
|
||||
},
|
||||
)
|
||||
.delete("/:id", zValidator("param", LibraryDeleteInput), async (c) => {
|
||||
const { id } = c.req.valid("param");
|
||||
const [existingLibrary] = await database
|
||||
.select()
|
||||
.from(libraryFolders)
|
||||
.where(eq(libraryFolders.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (!existingLibrary) {
|
||||
throw createHTTPException(404, `Library with id ${id} not found.`);
|
||||
}
|
||||
|
||||
await database.delete(libraryFolders).where(eq(libraryFolders.id, id));
|
||||
|
||||
return c.json<LibraryDeleteOutput>({
|
||||
success: true,
|
||||
message: `Library with id ${id} deleted successfully.`,
|
||||
});
|
||||
})
|
||||
.post("/create", zValidator("json", LibraryCreateInput), async (c) => {
|
||||
const input = c.req.valid("json");
|
||||
const existingByName = await database
|
||||
.select()
|
||||
.from(libraryFolders)
|
||||
.where(eq(libraryFolders.name, input.name))
|
||||
.limit(1);
|
||||
|
||||
if (existingByName.length > 0) {
|
||||
throw createHTTPException(
|
||||
409,
|
||||
`A library with the name "${input.name}" already exists.`,
|
||||
);
|
||||
}
|
||||
|
||||
const existingByPath = await database
|
||||
.select()
|
||||
.from(libraryFolders)
|
||||
.where(eq(libraryFolders.path, input.path))
|
||||
.limit(1);
|
||||
|
||||
if (existingByPath.length > 0) {
|
||||
throw createHTTPException(
|
||||
409,
|
||||
`A library with the path "${input.path}" already exists.`,
|
||||
);
|
||||
}
|
||||
|
||||
const [newLibrary] = await database
|
||||
.insert(libraryFolders)
|
||||
.values({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
path: input.path,
|
||||
primaryLanguage: input.primaryLanguage,
|
||||
fallbackLanguage: input.fallbackLanguage,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return c.json<LibraryCreateOutput>({
|
||||
success: true,
|
||||
message: `Library with id ${newLibrary.id} created successfully.`,
|
||||
data: newLibrary,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user