完善模型库权限与工艺数据解析

This commit is contained in:
zhangshun
2026-05-25 18:41:25 +08:00
parent 318105443a
commit a31c620420
15 changed files with 1013 additions and 123 deletions

View File

@@ -13,26 +13,32 @@ function fixedLoginRole(): UserRole {
return process.env.FIXED_LOGIN_ROLE === "admin" ? "admin" : "user";
}
function envValue(name: string) {
const value = process.env[name]?.trim();
return value || undefined;
}
export const config = {
host: process.env.HOST ?? "0.0.0.0",
port: Number(process.env.PORT ?? 3001),
jwtSecret: process.env.JWT_SECRET ?? "dev-only-change-me",
host: envValue("HOST") ?? "0.0.0.0",
port: Number(envValue("PORT") ?? 3001),
jwtSecret: envValue("JWT_SECRET") ?? "dev-only-change-me",
authMode: process.env.APP_AUTH_MODE === "fixed" ? "fixed" : "login",
fixedLogin: {
username: process.env.FIXED_LOGIN_USERNAME ?? "viewer",
username: envValue("FIXED_LOGIN_USERNAME") ?? "viewer",
role: fixedLoginRole()
},
allowUserEditSystemLibrary: envValue("ALLOW_USER_EDIT_SYSTEM_LIBRARY") === "true",
dataDir: path.resolve(serverRoot, "data"),
storageDir: path.resolve(serverRoot, "storage"),
databasePath: process.env.DB_PATH ?? path.resolve(serverRoot, "data", "model-library.db"),
storageDriver: (process.env.STORAGE_DRIVER ?? "local").toLowerCase(),
publicBaseUrl: process.env.PUBLIC_BASE_URL ?? "",
cdnBaseUrl: process.env.CDN_BASE_URL ?? "",
databasePath: envValue("DB_PATH") ?? path.resolve(serverRoot, "data", "model-library.db"),
storageDriver: (envValue("STORAGE_DRIVER") ?? "local").toLowerCase(),
publicBaseUrl: envValue("PUBLIC_BASE_URL") ?? "",
cdnBaseUrl: envValue("CDN_BASE_URL") ?? "",
cos: {
secretId: process.env.COS_SECRET_ID ?? "",
secretKey: process.env.COS_SECRET_KEY ?? "",
bucket: process.env.COS_BUCKET ?? "",
region: process.env.COS_REGION ?? "",
prefix: (process.env.COS_PREFIX ?? "").replace(/^\/+|\/+$/g, "")
secretId: envValue("COS_SECRET_ID") ?? "",
secretKey: envValue("COS_SECRET_KEY") ?? "",
bucket: envValue("COS_BUCKET") ?? "",
region: envValue("COS_REGION") ?? "",
prefix: (envValue("COS_PREFIX") ?? "").replace(/^\/+|\/+$/g, "")
}
};

View File

@@ -10,11 +10,6 @@ export const db = new DatabaseSync(config.databasePath);
db.exec("PRAGMA journal_mode = WAL");
db.exec("PRAGMA foreign_keys = ON");
function hasColumn(table: string, column: string) {
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as unknown as { name: string }[];
return rows.some((row) => row.name === column);
}
export function migrate() {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
@@ -33,6 +28,9 @@ export function migrate() {
parent_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
library_type TEXT NOT NULL DEFAULT 'system',
owner_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
is_system INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(parent_id, name)
@@ -60,11 +58,13 @@ export function migrate() {
name TEXT NOT NULL,
original_filename TEXT NOT NULL,
file_path TEXT NOT NULL UNIQUE,
library_type TEXT NOT NULL DEFAULT 'system',
owner_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
storage_provider TEXT NOT NULL DEFAULT 'local',
file_size INTEGER NOT NULL,
thumbnail TEXT,
thumbnail_path TEXT,
thumbnail_provider TEXT,
operation_tree TEXT NOT NULL DEFAULT '{"OperationTree":"[]"}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(folder_id, name)
@@ -83,32 +83,13 @@ export function migrate() {
CREATE INDEX IF NOT EXISTS idx_model_properties_model_id ON model_properties(model_id);
`);
if (!hasColumn("models", "brand_id")) {
db.exec("ALTER TABLE models ADD COLUMN brand_id INTEGER REFERENCES brands(id) ON DELETE SET NULL");
}
if (!hasColumn("models", "type_id")) {
db.exec("ALTER TABLE models ADD COLUMN type_id INTEGER REFERENCES model_types(id) ON DELETE SET NULL");
}
if (!hasColumn("models", "storage_provider")) {
db.exec("ALTER TABLE models ADD COLUMN storage_provider TEXT NOT NULL DEFAULT 'local'");
}
if (!hasColumn("models", "thumbnail_path")) {
db.exec("ALTER TABLE models ADD COLUMN thumbnail_path TEXT");
}
if (!hasColumn("models", "thumbnail_provider")) {
db.exec("ALTER TABLE models ADD COLUMN thumbnail_provider TEXT");
}
db.exec(`
CREATE INDEX IF NOT EXISTS idx_models_brand_id ON models(brand_id);
CREATE INDEX IF NOT EXISTS idx_models_type_id ON models(type_id);
CREATE INDEX IF NOT EXISTS idx_folders_library_owner ON folders(library_type, owner_user_id);
CREATE INDEX IF NOT EXISTS idx_models_library_owner ON models(library_type, owner_user_id);
`);
const rootFolder = db.prepare("SELECT id FROM folders WHERE parent_id IS NULL AND name = ?").get("模型库");
if (!rootFolder) {
db.prepare("INSERT INTO folders (parent_id, name, path) VALUES (NULL, ?, ?)").run("模型库", "模型库");
}
const admin = db.prepare("SELECT id FROM users WHERE username = ?").get("admin");
if (!admin) {
db.prepare(`
@@ -116,4 +97,12 @@ export function migrate() {
VALUES (?, ?, 'admin', 1)
`).run("admin", bcrypt.hashSync("admin123", 10));
}
const systemRoot = db.prepare("SELECT id FROM folders WHERE path = ?").get("system") as { id: number } | undefined;
if (!systemRoot) {
db.prepare(`
INSERT INTO folders (parent_id, name, path, library_type, owner_user_id, is_system)
VALUES (NULL, ?, ?, 'system', NULL, 1)
`).run("系统模型库", "system");
}
}

View File

@@ -3,7 +3,8 @@ import path from "node:path";
import { FastifyInstance } from "fastify";
import { z } from "zod";
import { db } from "./db.js";
import { requireAdmin, requireAuth } from "./auth.js";
import { requireAuth } from "./auth.js";
import { folderPermissions, getFolder, requireFolderWrite } from "./permissions.js";
import { storage } from "./storage.js";
import { FolderRow } from "./types.js";
import { assertValidFolderName, ensureDir, removeIfExists, safeStoragePath, toFolderStorageRelative } from "./utils.js";
@@ -17,65 +18,125 @@ const updateSchema = z.object({
name: z.string()
});
function buildTree(rows: FolderRow[]) {
const nodes = rows.map((row) => ({
function folderPayload(request: { userInfo: { id: number; username: string; role: "admin" | "user" } }, row: FolderRow) {
return {
...row,
permissions: folderPermissions(request.userInfo, row)
};
}
function buildFolderNodes(rows: FolderRow[], userInfo: { id: number; username: string; role: "admin" | "user" }) {
return rows.map((row) => ({
id: String(row.id),
text: `${row.name}(${row.model_count ?? 0})`,
text: `${displayFolderName(row)}(${row.model_count ?? 0})`,
parentId: row.parent_id,
path: row.path,
data: {
permissions: folderPermissions(userInfo, row),
libraryType: row.library_type,
ownerUserId: row.owner_user_id,
isSystem: Boolean(row.is_system)
},
children: [] as unknown[]
}));
}
function buildTree(rows: FolderRow[], userInfo: { id: number; username: string; role: "admin" | "user" }) {
const nodes = buildFolderNodes(rows, userInfo);
const userGroupNode = {
id: "virtual:user-libraries",
text: "用户模型库",
parentId: null as number | null,
path: "users",
data: {
permissions: { read: true, write: false },
virtual: true
},
children: [] as unknown[]
};
const byId = new Map(nodes.map((node) => [Number(node.id), node]));
const roots = [];
for (const node of nodes) {
if (node.parentId && byId.has(node.parentId)) {
byId.get(node.parentId)!.children.push(node);
} else if (node.data.libraryType === "user") {
userGroupNode.children.push(node);
} else {
roots.push(node);
}
}
if (userGroupNode.children.length > 0) {
roots.push(userGroupNode);
}
return roots;
}
function getFolder(id: number) {
return db.prepare("SELECT * FROM folders WHERE id = ?").get(id) as FolderRow | undefined;
}
function childPath(parent: FolderRow | undefined, name: string) {
return parent ? path.posix.join(parent.path, name) : name;
}
function assertCanRenameRoot(folder: FolderRow) {
if (folder.parent_id === null) {
throw new Error("目录不能重命名");
if (folder.is_system) {
throw new Error("系统保留目录不能重命名");
}
}
function displayFolderName(row: FolderRow) {
if (row.library_type === "user" && row.is_system && row.owner_username) {
return row.owner_username;
}
return row.name;
}
function ensureUserLibrary(user: { id: number; username: string }) {
const pathValue = `users/${user.id}`;
const existing = db.prepare("SELECT id FROM folders WHERE path = ?").get(pathValue) as { id: number } | undefined;
if (existing) return existing.id;
const result = db.prepare(`
INSERT INTO folders (parent_id, name, path, library_type, owner_user_id, is_system)
VALUES (NULL, ?, ?, 'user', ?, 1)
`).run("用户模型库", pathValue, user.id);
return Number(result.lastInsertRowid);
}
export async function folderRoutes(app: FastifyInstance) {
app.get("/api/folders", { preHandler: [requireAuth] }, async () => {
app.get("/api/folders", { preHandler: [requireAuth] }, async (request) => {
ensureUserLibrary(request.userInfo);
const rows = db.prepare(`
SELECT
f.*,
u.username AS owner_username,
COUNT(m.id) AS model_count
FROM folders f
LEFT JOIN users u ON u.id = f.owner_user_id
LEFT JOIN models m ON m.folder_id = f.id
GROUP BY f.id
ORDER BY f.parent_id, f.name
ORDER BY
CASE f.library_type WHEN 'system' THEN 0 ELSE 1 END,
f.owner_user_id,
f.parent_id,
f.name
`).all() as unknown as FolderRow[];
return { folders: rows, tree: buildTree(rows) };
return { folders: rows.map((row) => folderPayload(request, row)), tree: buildTree(rows, request.userInfo) };
});
app.post("/api/folders", { preHandler: [requireAdmin] }, async (request, reply) => {
app.post("/api/folders", { preHandler: [requireAuth] }, async (request, reply) => {
const body = createSchema.parse(request.body);
const name = assertValidFolderName(body.name);
const parent = body.parentId ? getFolder(body.parentId) : undefined;
if (body.parentId && !parent) {
return reply.code(404).send({ message: "父目录不存在" });
}
if (!parent) {
return reply.code(400).send({ message: "请选择父目录" });
}
if (!requireFolderWrite(request, reply, parent)) return;
const folderPath = childPath(parent, name);
try {
const result = db.prepare("INSERT INTO folders (parent_id, name, path) VALUES (?, ?, ?)")
.run(body.parentId ?? null, name, folderPath);
const result = db.prepare(`
INSERT INTO folders (parent_id, name, path, library_type, owner_user_id, is_system)
VALUES (?, ?, ?, ?, ?, 0)
`).run(parent.id, name, folderPath, parent.library_type, parent.owner_user_id);
await ensureDir(safeStoragePath(toFolderStorageRelative(folderPath)));
return reply.code(201).send({ id: result.lastInsertRowid, name, path: folderPath });
} catch {
@@ -83,7 +144,7 @@ export async function folderRoutes(app: FastifyInstance) {
}
});
app.put("/api/folders/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
app.put("/api/folders/:id", { preHandler: [requireAuth] }, async (request, reply) => {
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
const body = updateSchema.parse(request.body);
const name = assertValidFolderName(body.name);
@@ -91,6 +152,7 @@ export async function folderRoutes(app: FastifyInstance) {
if (!folder) {
return reply.code(404).send({ message: "目录不存在" });
}
if (!requireFolderWrite(request, reply, folder)) return;
assertCanRenameRoot(folder);
const parent = folder.parent_id ? getFolder(folder.parent_id) : undefined;
const oldPath = folder.path;
@@ -146,14 +208,15 @@ export async function folderRoutes(app: FastifyInstance) {
}
});
app.delete("/api/folders/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
app.delete("/api/folders/:id", { preHandler: [requireAuth] }, async (request, reply) => {
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
const folder = getFolder(params.id);
if (!folder) {
return reply.code(404).send({ message: "目录不存在" });
}
if (folder.parent_id === null) {
return reply.code(400).send({ message: "根目录不能删除" });
if (!requireFolderWrite(request, reply, folder)) return;
if (folder.is_system) {
return reply.code(400).send({ message: "系统保留目录不能删除" });
}
db.prepare("DELETE FROM folders WHERE id = ?").run(folder.id);
await removeIfExists(safeStoragePath(toFolderStorageRelative(folder.path)));

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { nanoid } from "nanoid";
import { db } from "./db.js";
import { requireAdmin, requireAuth } from "./auth.js";
import { getFolder, modelPermissions, requireFolderWrite, requireModelWrite } from "./permissions.js";
import { deleteStoredObject, publicObjectUrl, storage } from "./storage.js";
import { DictionaryRow, FolderRow, ModelRow } from "./types.js";
import {
@@ -16,7 +17,7 @@ const updateSchema = z.object({
name: z.string().min(1),
brandName: z.string().optional(),
typeName: z.string().optional(),
thumbnail: z.string().nullable().optional(),
operationTree: z.string().optional(),
properties: z.record(z.string()).optional()
});
@@ -32,9 +33,7 @@ const dictionaryBodySchema = z.object({
name: z.string().min(1).max(80)
});
function getFolder(id: number) {
return db.prepare("SELECT * FROM folders WHERE id = ?").get(id) as FolderRow | undefined;
}
const defaultOperationTree = JSON.stringify({ OperationTree: "[]" });
function getProperties(modelId: number) {
const rows = db.prepare("SELECT property_key, property_value FROM model_properties WHERE model_id = ?")
@@ -56,13 +55,14 @@ function saveProperties(modelId: number, properties: Record<string, string>) {
}
}
function modelPayload(row: ModelRow) {
function modelPayload(row: ModelRow, userInfo?: { id: number; username: string; role: "admin" | "user" }) {
return {
...row,
file_url: publicObjectUrl(row.file_path, row.storage_provider),
thumbnail_url: row.thumbnail_path && row.thumbnail_provider
? publicObjectUrl(row.thumbnail_path, row.thumbnail_provider)
: row.thumbnail,
: null,
permissions: userInfo ? modelPermissions(userInfo, row) : { read: true, write: false },
properties: getProperties(row.id)
};
}
@@ -78,6 +78,26 @@ function dataImageToBuffer(dataUrl: string) {
};
}
function normalizeOperationTree(value: string | undefined) {
const clean = value?.trim();
if (!clean) return defaultOperationTree;
let parsed: unknown;
try {
parsed = JSON.parse(clean);
} catch {
throw new Error("工艺数据必须是合法 JSON 字符串");
}
if (!parsed || typeof parsed !== "object" || typeof (parsed as { OperationTree?: unknown }).OperationTree !== "string") {
throw new Error("工艺数据格式必须符合 OperationTreeDB");
}
try {
JSON.parse((parsed as { OperationTree: string }).OperationTree);
} catch {
throw new Error("OperationTree 字段必须是合法 JSON 字符串");
}
return JSON.stringify(parsed);
}
function normalizeDictionaryName(name: string | undefined) {
const clean = name?.trim();
return clean || null;
@@ -205,19 +225,19 @@ export async function modelRoutes(app: FastifyInstance) {
total: total.count,
page: query.page,
pageSize: query.pageSize,
items: rows.map(modelPayload)
items: rows.map((row) => modelPayload(row, request.userInfo))
};
});
app.post("/api/models/upload", { preHandler: [requireAdmin] }, async (request, reply) => {
app.post("/api/models/upload", { preHandler: [requireAuth] }, async (request, reply) => {
const parts = request.parts();
let folderId: number | undefined;
let thumbnail: string | null = null;
const properties: Record<string, string> = {};
let uploaded: { filename: string; buffer: Buffer } | undefined;
let modelName: string | undefined;
let brandName: string | undefined;
let typeName: string | undefined;
let operationTree: string | undefined;
for await (const part of parts) {
if (part.type === "file") {
@@ -234,7 +254,7 @@ export async function modelRoutes(app: FastifyInstance) {
if (part.fieldname === "name") modelName = String(part.value ?? "");
if (part.fieldname === "brandName") brandName = String(part.value ?? "");
if (part.fieldname === "typeName") typeName = String(part.value ?? "");
if (part.fieldname === "thumbnail") thumbnail = String(part.value || "") || null;
if (part.fieldname === "operationTree") operationTree = String(part.value ?? "");
if (part.fieldname.startsWith("prop.")) {
properties[part.fieldname.slice(5)] = String(part.value ?? "");
}
@@ -248,6 +268,7 @@ export async function modelRoutes(app: FastifyInstance) {
if (!folder) {
return reply.code(404).send({ message: "目录不存在" });
}
if (!requireFolderWrite(request, reply, folder)) return;
const originalModelName = assertValidFolderName(modelName || normalizeModelName(uploaded.filename));
const storageFilename = `${normalizeModelName(uploaded.filename)}-${nanoid(8)}.glb`;
@@ -262,10 +283,11 @@ export async function modelRoutes(app: FastifyInstance) {
try {
const brandId = findOrCreateDictionary("brands", brandName);
const typeId = findOrCreateDictionary("model_types", typeName);
const normalizedOperationTree = normalizeOperationTree(operationTree);
const result = db.prepare(`
INSERT INTO models (folder_id, brand_id, type_id, name, original_filename, file_path, storage_provider, file_size, thumbnail)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(folderId, brandId, typeId, originalModelName, uploaded.filename, stored.key, stored.provider, uploaded.buffer.length, thumbnail);
INSERT INTO models (folder_id, brand_id, type_id, name, original_filename, file_path, library_type, owner_user_id, storage_provider, file_size, operation_tree)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(folderId, brandId, typeId, originalModelName, uploaded.filename, stored.key, folder.library_type, folder.owner_user_id, stored.provider, uploaded.buffer.length, normalizedOperationTree);
saveProperties(Number(result.lastInsertRowid), properties);
return reply.code(201).send({ id: result.lastInsertRowid });
} catch (error) {
@@ -274,22 +296,24 @@ export async function modelRoutes(app: FastifyInstance) {
}
});
app.put("/api/models/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
app.put("/api/models/:id", { preHandler: [requireAuth] }, async (request, reply) => {
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
const body = updateSchema.parse(request.body);
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
if (!model) {
return reply.code(404).send({ message: "模型不存在" });
}
if (!requireModelWrite(request, reply, model)) return;
const name = assertValidFolderName(body.name);
try {
const brandId = findOrCreateDictionary("brands", body.brandName);
const typeId = findOrCreateDictionary("model_types", body.typeName);
const operationTree = body.operationTree === undefined ? model.operation_tree : normalizeOperationTree(body.operationTree);
db.prepare(`
UPDATE models
SET name = ?, brand_id = ?, type_id = ?, thumbnail = ?, updated_at = CURRENT_TIMESTAMP
SET name = ?, brand_id = ?, type_id = ?, operation_tree = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(name, brandId, typeId, body.thumbnail ?? model.thumbnail, model.id);
`).run(name, brandId, typeId, operationTree, model.id);
if (body.properties) {
saveProperties(model.id, body.properties);
}
@@ -299,19 +323,20 @@ export async function modelRoutes(app: FastifyInstance) {
}
});
app.put("/api/models/:id/thumbnail", { preHandler: [requireAdmin] }, async (request, reply) => {
app.put("/api/models/:id/thumbnail", { preHandler: [requireAuth] }, async (request, reply) => {
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
const body = thumbnailSchema.parse(request.body);
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
if (!model) {
return reply.code(404).send({ message: "模型不存在" });
}
if (!requireModelWrite(request, reply, model)) return;
const image = dataImageToBuffer(body.thumbnail);
const stored = await storage.putObject(`thumbnails/model-${model.id}-${nanoid(8)}.${image.ext}`, image.buffer);
try {
db.prepare(`
UPDATE models
SET thumbnail = NULL, thumbnail_path = ?, thumbnail_provider = ?, updated_at = CURRENT_TIMESTAMP
SET thumbnail_path = ?, thumbnail_provider = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(stored.key, stored.provider, model.id);
if (model.thumbnail_path && model.thumbnail_provider) {
@@ -324,12 +349,13 @@ export async function modelRoutes(app: FastifyInstance) {
}
});
app.delete("/api/models/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
app.delete("/api/models/:id", { preHandler: [requireAuth] }, async (request, reply) => {
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
if (!model) {
return reply.code(404).send({ message: "模型不存在" });
}
if (!requireModelWrite(request, reply, model)) return;
db.prepare("DELETE FROM models WHERE id = ?").run(model.id);
await deleteStoredObject(model.file_path, model.storage_provider);
if (model.thumbnail_path && model.thumbnail_provider) {

55
server/src/permissions.ts Normal file
View File

@@ -0,0 +1,55 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { config } from "./config.js";
import { db } from "./db.js";
import { FolderRow, ModelRow } from "./types.js";
export type PermissionPayload = {
read: boolean;
write: boolean;
};
export function canWriteLibrary(user: FastifyRequest["userInfo"], libraryType: "system" | "user", ownerUserId: number | null) {
if (user.role === "admin") return true;
if (libraryType === "user" && ownerUserId === user.id) return true;
if (libraryType === "system" && config.allowUserEditSystemLibrary) return true;
return false;
}
export function folderPermissions(user: FastifyRequest["userInfo"], folder: Pick<FolderRow, "library_type" | "owner_user_id">): PermissionPayload {
return {
read: true,
write: canWriteLibrary(user, folder.library_type, folder.owner_user_id)
};
}
export function modelPermissions(user: FastifyRequest["userInfo"], model: Pick<ModelRow, "library_type" | "owner_user_id">): PermissionPayload {
return {
read: true,
write: canWriteLibrary(user, model.library_type, model.owner_user_id)
};
}
export function getFolder(id: number) {
return db.prepare(`
SELECT f.*, u.username AS owner_username
FROM folders f
LEFT JOIN users u ON u.id = f.owner_user_id
WHERE f.id = ?
`).get(id) as FolderRow | undefined;
}
export function requireFolderWrite(request: FastifyRequest, reply: FastifyReply, folder: FolderRow) {
if (!folderPermissions(request.userInfo, folder).write) {
reply.code(403).send({ message: "没有该模型库的操作权限" });
return false;
}
return true;
}
export function requireModelWrite(request: FastifyRequest, reply: FastifyReply, model: ModelRow) {
if (!modelPermissions(request.userInfo, model).write) {
reply.code(403).send({ message: "没有该模型的操作权限" });
return false;
}
return true;
}

View File

@@ -22,6 +22,10 @@ export interface FolderRow {
parent_id: number | null;
name: string;
path: string;
library_type: "system" | "user";
owner_user_id: number | null;
is_system: number;
owner_username?: string | null;
model_count?: number;
created_at: string;
updated_at: string;
@@ -35,11 +39,13 @@ export interface ModelRow {
name: string;
original_filename: string;
file_path: string;
library_type: "system" | "user";
owner_user_id: number | null;
storage_provider: "local" | "cos";
file_size: number;
thumbnail: string | null;
thumbnail_path: string | null;
thumbnail_provider: "local" | "cos" | null;
operation_tree: string;
brand_name?: string | null;
type_name?: string | null;
created_at: string;