diff --git a/apps/nextjs/src/app/[locale]/init/_steps/start/sqlite-restore.tsx b/apps/nextjs/src/app/[locale]/init/_steps/start/sqlite-restore.tsx index 75bc39f5f..ac5e433df 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/start/sqlite-restore.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/start/sqlite-restore.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Button, Collapse, Stack, Text } from "@mantine/core"; +import { Button, Stack, Text } from "@mantine/core"; import { IconDatabaseImport } from "@tabler/icons-react"; import { getMantineColor } from "@homarr/common"; diff --git a/apps/nextjs/src/app/api/backup/import/route.ts b/apps/nextjs/src/app/api/backup/import/route.ts index 32002014a..995cce828 100644 --- a/apps/nextjs/src/app/api/backup/import/route.ts +++ b/apps/nextjs/src/app/api/backup/import/route.ts @@ -129,14 +129,20 @@ export async function POST(req: Request) { let tempDb: InstanceType | null = null; try { - const dbEntry = zip.getEntry("db.sqlite")!; + const dbEntry = zip.getEntry("db.sqlite"); + if (!dbEntry) { + return NextResponse.json({ error: "Invalid backup: missing db.sqlite" }, { status: 400 }); + } fs.writeFileSync(tempPath, dbEntry.getData()); tempDb = new Database(tempPath); const drizzleDb = drizzle(tempDb, { casing: DB_CASING }); migrate(drizzleDb, { migrationsFolder }); - const metadataEntry = zip.getEntry("metadata.json")!; + const metadataEntry = zip.getEntry("metadata.json"); + if (!metadataEntry) { + return NextResponse.json({ error: "Invalid backup: missing metadata.json" }, { status: 400 }); + } const metadata = JSON.parse(metadataEntry.getData().toString()); const rawKey = metadata.encryptionKey ?? zip.getEntry("encryption-key.txt")?.getData().toString().trim(); diff --git a/apps/nextjs/src/app/api/backup/sql-wasm/route.ts b/apps/nextjs/src/app/api/backup/sql-wasm/route.ts index de5917ecb..14cd4a8cd 100644 --- a/apps/nextjs/src/app/api/backup/sql-wasm/route.ts +++ b/apps/nextjs/src/app/api/backup/sql-wasm/route.ts @@ -21,7 +21,7 @@ export async function GET() { cachedWasm = fs.readFileSync(found); } - return new NextResponse(cachedWasm, { + return new NextResponse(new Uint8Array(cachedWasm), { headers: { "Content-Type": "application/wasm", "Cache-Control": "public, max-age=31536000, immutable", diff --git a/apps/nextjs/src/app/api/backup/test/backup.spec.ts b/apps/nextjs/src/app/api/backup/test/backup.spec.ts index 8e4e6039d..4584b415f 100644 --- a/apps/nextjs/src/app/api/backup/test/backup.spec.ts +++ b/apps/nextjs/src/app/api/backup/test/backup.spec.ts @@ -85,7 +85,7 @@ describe("SQLite backup", () => { const dbEntry = readBack.getEntry("db.sqlite"); expect(dbEntry).toBeDefined(); - expect(dbEntry!.header.size).toBeGreaterThan(0); + expect(dbEntry?.header.size).toBeGreaterThan(0); }); it("should produce a valid SQLite database in the ZIP", () => { @@ -256,7 +256,7 @@ describe("SQLite backup", () => { const app = backupDb.prepare('SELECT "name" FROM "app" WHERE "id" = ?').get("app-1") as { name: string } | undefined; expect(app).toBeDefined(); - expect(app!.name).toBe("TestApp"); + expect(app?.name).toBe("TestApp"); backupDb.close(); sqlite.close(); @@ -283,7 +283,7 @@ describe("SQLite backup", () => { const board = importDb.prepare('SELECT "name" FROM "board" WHERE "id" = ?').get("board-1") as { name: string } | undefined; expect(board).toBeDefined(); - expect(board!.name).toBe("TestBoard"); + expect(board?.name).toBe("TestBoard"); importDb.close(); }); diff --git a/apps/nextjs/src/components/backup/restore-progress-panel.tsx b/apps/nextjs/src/components/backup/restore-progress-panel.tsx index 4faa397be..ebbbf9330 100644 --- a/apps/nextjs/src/components/backup/restore-progress-panel.tsx +++ b/apps/nextjs/src/components/backup/restore-progress-panel.tsx @@ -17,12 +17,12 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { MigrationFile } from "./types"; -const BASE_PHASE_DURATIONS: Record = { +const BASE_PHASE_DURATIONS = { extracting: 1200, encrypting: 1500, swapping: 800, restarting: 10000, -}; +} as const; const MIGRATION_STEP_DURATION = 100; @@ -45,7 +45,7 @@ export const RestoreProgressPanel = ({ active, migrations, onComplete }: Restore const steps = useMemo((): TimelineStep[] => { const result: TimelineStep[] = [ - { key: "extracting", label: t("extracting"), icon: IconFileZip, duration: BASE_PHASE_DURATIONS.extracting! }, + { key: "extracting", label: t("extracting"), icon: IconFileZip, duration: BASE_PHASE_DURATIONS.extracting }, ]; if (migrations.length > 0) { @@ -67,9 +67,9 @@ export const RestoreProgressPanel = ({ active, migrations, onComplete }: Restore } result.push( - { key: "encrypting", label: t("encrypting"), icon: IconKey, duration: BASE_PHASE_DURATIONS.encrypting! }, - { key: "swapping", label: t("swapping"), icon: IconTransform, duration: BASE_PHASE_DURATIONS.swapping! }, - { key: "restarting", label: t("restarting"), icon: IconRefresh, duration: BASE_PHASE_DURATIONS.restarting! }, + { key: "encrypting", label: t("encrypting"), icon: IconKey, duration: BASE_PHASE_DURATIONS.encrypting }, + { key: "swapping", label: t("swapping"), icon: IconTransform, duration: BASE_PHASE_DURATIONS.swapping }, + { key: "restarting", label: t("restarting"), icon: IconRefresh, duration: BASE_PHASE_DURATIONS.restarting }, ); return result; @@ -80,10 +80,10 @@ export const RestoreProgressPanel = ({ active, migrations, onComplete }: Restore let cancelled = false; const run = async () => { - for (let i = 0; i < steps.length; i++) { + for (const [i, step] of steps.entries()) { if (cancelled) return; setCurrentStepIdx(i); - await new Promise((r) => setTimeout(r, steps[i]!.duration)); + await new Promise((r) => setTimeout(r, step.duration)); } if (!cancelled) { onComplete?.(); diff --git a/apps/nextjs/src/components/backup/use-backup-analysis.ts b/apps/nextjs/src/components/backup/use-backup-analysis.ts index a563e24cd..64c3dd23d 100644 --- a/apps/nextjs/src/components/backup/use-backup-analysis.ts +++ b/apps/nextjs/src/components/backup/use-backup-analysis.ts @@ -109,8 +109,7 @@ export const useBackupAnalysis = () => { }; if (pendingMigrations.length > 0) { - for (let i = 0; i < pendingMigrations.length; i++) { - const migration = pendingMigrations[i]!; + for (const [i, migration] of pendingMigrations.entries()) { setMigrationProgress({ current: i + 1, total: pendingMigrations.length, diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index 3d4939559..fe8c0dcb9 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -1430,8 +1430,10 @@ export const boardRouter = createTRPCRouter({ for (const il of existingInSection) { for (let y = il.yOffset; y < il.yOffset + il.height; y++) { while (occupied.length <= y) occupied.push(Array.from({ length: layout.columnCount }).fill(false)); + const occupiedRow = occupied[y]; + if (!occupiedRow) continue; for (let x = il.xOffset; x < il.xOffset + il.width; x++) { - occupied[y]![x] = true; + occupiedRow[x] = true; } } } @@ -1439,8 +1441,10 @@ export const boardRouter = createTRPCRouter({ let placed = false; for (let y = 0; y < 9999 && !placed; y++) { if (!occupied[y]) occupied.push(Array.from({ length: layout.columnCount }).fill(false)); + const occupiedRow = occupied[y]; + if (!occupiedRow) continue; for (let x = 0; x < layout.columnCount && !placed; x++) { - if (!occupied[y]![x]) { + if (!occupiedRow[x]) { layoutRows.push({ itemId, sectionId: emptySection.id, diff --git a/packages/validation/src/board.ts b/packages/validation/src/board.ts index 24a0f3608..6c9e1a650 100644 --- a/packages/validation/src/board.ts +++ b/packages/validation/src/board.ts @@ -96,6 +96,10 @@ export const boardCreateSchema = z.object({ export const boardSavePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions)); +const boardPermissionEntrySchema = z.object({ + permission: z.enum(boardPermissions), +}); + export const boardSummarySchema = z.object({ id: z.string(), name: z.string(), @@ -111,6 +115,8 @@ export const boardSummarySchema = z.object({ .nullable(), isHome: z.boolean(), isMobileHome: z.boolean(), + userPermissions: z.array(boardPermissionEntrySchema), + groupPermissions: z.array(boardPermissionEntrySchema), }); export const addItemToBoardSchema = z.object({