v1.6.3: fix project file upload, filter responsible user by permission

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-05-12 20:26:32 +02:00
parent 59b478f262
commit 4cabba395d
7 changed files with 68 additions and 60 deletions

View File

@@ -1,6 +1,5 @@
import fs from "fs";
import path from "path";
import { pipeline } from "stream/promises";
import { config } from "../config/env";
import { localDateStr, localTimeStr } from "../utils/date";
@@ -237,7 +236,7 @@ export class NasFileManager {
if (isLink) {
try {
const target = fs.readlinkSync(fullPath);
item.link_target = target.replace(/\//g, "\\");
item.link_target = target;
} catch {
// ignore
}
@@ -288,14 +287,14 @@ export class NasFileManager {
path: subPath,
items,
breadcrumb,
full_path: realDirPath.replace(/\//g, "\\"),
full_path: realDirPath,
};
}
public async uploadFile(
projectNumber: string,
subPath: string,
fileStream: NodeJS.ReadableStream,
fileBuffer: Buffer,
fileName: string,
): Promise<string | null> {
const dirPath = this.resolveProjectPath(projectNumber, subPath);
@@ -304,14 +303,21 @@ export class NasFileManager {
}
try {
const stat = await fs.promises.stat(dirPath);
if (!stat.isDirectory()) {
return "Cílová složka neexistuje";
}
await fs.promises.mkdir(dirPath, { recursive: true, mode: 0o775 });
} catch {
return "Cílová složka neexistuje";
}
let stat: fs.Stats;
try {
stat = await fs.promises.stat(dirPath);
} catch {
return "Cílová složka neexistuje";
}
if (!stat.isDirectory()) {
return "Cílová složka neexistuje";
}
const originalName = path.basename(fileName);
let safeName = this.sanitizeFilename(originalName);
if (safeName === "") {
@@ -323,46 +329,27 @@ export class NasFileManager {
return "Tento typ souboru není povolen";
}
const tempPath = path.join(
require("os").tmpdir(),
`upload-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
try {
await pipeline(fileStream, fs.createWriteStream(tempPath));
} catch {
await fs.promises.unlink(tempPath).catch(() => {});
return "Nepodařilo se uložit soubor";
}
try {
const typeResult = await FileType.fromFile(tempPath);
if (typeResult) {
if (this.isSuspiciousMime(typeResult.mime)) {
await fs.promises.unlink(tempPath).catch(() => {});
return "Obsah souboru neodpovídá jeho příponě";
}
const expectedMime = ext ? MIME_MAP[ext] : null;
if (expectedMime && typeResult.mime !== expectedMime) {
await fs.promises.unlink(tempPath).catch(() => {});
return "Obsah souboru neodpovídá jeho příponě";
}
// MIME validation
const typeResult = await FileType.fromBuffer(fileBuffer);
if (typeResult) {
if (this.isSuspiciousMime(typeResult.mime)) {
return "Obsah souboru neodpovídá jeho příponě";
}
const expectedMime = ext ? MIME_MAP[ext] : null;
if (expectedMime && typeResult.mime !== expectedMime) {
return "Obsah souboru neodpovídá jeho příponě";
}
} catch {
// If file-type fails, continue without MIME check
}
let destPath = dirPath + "/" + safeName;
// Attempt atomic rename; if destination exists, append counter
let renamed = false;
// If destination exists, append counter
let attempts = 0;
const maxAttempts = 1000;
do {
while (attempts < maxAttempts) {
try {
await fs.promises.rename(tempPath, destPath);
renamed = true;
break;
await fs.promises.writeFile(destPath, fileBuffer, { flag: "wx" });
return null;
} catch (err) {
const e = err as NodeJS.ErrnoException;
if (e.code === "EEXIST") {
@@ -371,17 +358,12 @@ export class NasFileManager {
safeName = base + "_" + attempts + (ext ? "." + ext : "");
destPath = dirPath + "/" + safeName;
} else {
break;
return `Nepodařilo se uložit soubor (${e?.message || String(e)})`;
}
}
} while (!renamed && attempts < maxAttempts);
if (!renamed) {
await fs.promises.unlink(tempPath).catch(() => {});
return "Nepodařilo se uložit soubor";
}
return null;
return "Nepodařilo se uložit soubor";
}
public downloadFile(

View File

@@ -33,6 +33,7 @@ export interface ListUsersParams {
sort: string;
order: "asc" | "desc";
search: string;
permission?: string;
}
export interface CreateUserData {
@@ -56,10 +57,10 @@ export interface UpdateUserData {
}
export async function listUsers(params: ListUsersParams) {
const { page, limit, skip, sort, order, search } = params;
const { page, limit, skip, sort, order, search, permission } = params;
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "id";
const where = search
let where: any = search
? {
OR: [
{ username: { contains: search } },
@@ -70,6 +71,14 @@ export async function listUsers(params: ListUsersParams) {
}
: {};
if (permission) {
where.roles = {
role_permissions: {
some: { permissions: { name: permission } },
},
};
}
const [users, total] = await Promise.all([
prisma.users.findMany({
where,