fix(boards): add permissions to board summary schema to fix manage page crash (#5917)
* fix(boards): add permissions to board summary schema to fix manage page crash The getAllBoards query fetches userPermissions and groupPermissions from the DB, but boardSummarySchema (the output validator) did not include them. tRPC stripped the fields, causing a TypeError when getBoardPermissionsAsync called .some() on undefined. * fix(backup): cast Buffer to Uint8Array for NextResponse body Node 24 narrowed the BodyInit type, making Buffer incompatible. Wrapping in Uint8Array satisfies the type constraint. * fix(lint): remove forbidden non-null assertions across backup and board code Replace non-null assertions with safe guards, optional chaining, and for-of iteration so oxlint passes in CI. Also remove unused Collapse import.
This commit is contained in:
1 parent
f26007a29a
commit
a42af0e678
8 files changed
+34
-19
No files matched your search
@@ -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";
|
||||
|
||||
@@ -129,14 +129,20 @@ export async function POST(req: Request) {
|
||||
let tempDb: InstanceType<typeof Database> | 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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -17,12 +17,12 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { MigrationFile } from "./types";
|
||||
|
||||
const BASE_PHASE_DURATIONS: Record<string, number> = {
|
||||
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?.();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<boolean>({ 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<boolean>({ 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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user