Add file management and upload capabilities for IDF Mikrotik projects

Integrate multer and adm-zip for handling file uploads and zip extraction. Add API endpoints for fetching, searching, and uploading files. Update storage interface and schema to support project files.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fadde5c0-d787-4605-8d47-ab3e7884f567
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/c9ITWqD
This commit is contained in:
marco370 2025-11-15 10:54:17 +00:00
parent 0bfe3258b5
commit 76f447fe01
6 changed files with 348 additions and 47 deletions

View File

@ -14,6 +14,10 @@ run = ["npm", "run", "start"]
localPort = 5000
externalPort = 80
[[ports]]
localPort = 41465
externalPort = 3000
[env]
PORT = "5000"

150
package-lock.json generated
View File

@ -40,6 +40,9 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/adm-zip": "^0.5.7",
"@types/multer": "^2.0.0",
"adm-zip": "^0.5.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -54,6 +57,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
"multer": "^2.0.2",
"next-themes": "^0.4.6",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
@ -3303,6 +3307,15 @@
"react": "^18 || ^19"
}
},
"node_modules/@types/adm-zip": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz",
"integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3352,7 +3365,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@ -3363,7 +3375,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@ -3455,7 +3466,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@ -3468,7 +3478,6 @@
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@ -3491,16 +3500,23 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
"integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": {
"version": "20.16.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
@ -3565,14 +3581,12 @@
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
@ -3600,7 +3614,6 @@
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@ -3611,7 +3624,6 @@
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@ -3663,6 +3675,15 @@
"node": ">= 0.6"
}
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@ -3706,6 +3727,12 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -3883,7 +3910,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/bufferutil": {
@ -3900,6 +3926,17 @@
"node": ">=6.14.2"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -4055,6 +4092,21 @@
"node": ">= 6"
}
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/connect-pg-simple": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
@ -5768,6 +5820,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -5783,6 +5844,18 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/modern-screenshot": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz",
@ -5805,6 +5878,24 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/multer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"mkdirp": "^0.5.6",
"object-assign": "^4.1.1",
"type-is": "^1.6.18",
"xtend": "^4.0.2"
},
"engines": {
"node": ">= 10.16.0"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@ -6718,6 +6809,20 @@
"pify": "^2.3.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -7103,6 +7208,23 @@
"node": ">= 0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -7396,6 +7518,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",

View File

@ -42,6 +42,9 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/adm-zip": "^0.5.7",
"@types/multer": "^2.0.0",
"adm-zip": "^0.5.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -56,6 +59,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
"multer": "^2.0.2",
"next-themes": "^0.4.6",
"passport": "^0.7.0",
"passport-local": "^1.0.0",

View File

@ -1,15 +1,151 @@
import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import { insertProjectFileSchema } from "@shared/schema";
import multer from "multer";
import AdmZip from "adm-zip";
import path from "path";
const upload = multer({ storage: multer.memoryStorage() });
export async function registerRoutes(app: Express): Promise<Server> {
// put application routes here
// prefix all routes with /api
// Get all files
app.get("/api/files", async (req, res) => {
try {
const files = await storage.getAllFiles();
res.json(files);
} catch (error) {
res.status(500).json({ error: "Failed to fetch files" });
}
});
// use storage to perform CRUD operations on the storage interface
// e.g. storage.insertUser(user) or storage.getUserByUsername(username)
// Get file by ID
app.get("/api/files/:id", async (req, res) => {
try {
const file = await storage.getFileById(req.params.id);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
res.json(file);
} catch (error) {
res.status(500).json({ error: "Failed to fetch file" });
}
});
// Get files by category
app.get("/api/files/category/:category", async (req, res) => {
try {
const files = await storage.getFilesByCategory(req.params.category);
res.json(files);
} catch (error) {
res.status(500).json({ error: "Failed to fetch files by category" });
}
});
// Search files
app.get("/api/files/search/:query", async (req, res) => {
try {
const files = await storage.searchFiles(req.params.query);
res.json(files);
} catch (error) {
res.status(500).json({ error: "Failed to search files" });
}
});
// Upload ZIP file and extract
app.post("/api/upload-zip", upload.single("file"), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
const zip = new AdmZip(req.file.buffer);
const zipEntries = zip.getEntries();
const uploadedFiles = [];
for (const entry of zipEntries) {
if (entry.isDirectory) continue;
const filename = path.basename(entry.entryName);
const filepath = entry.entryName;
const ext = path.extname(filename).toLowerCase();
let category = "other";
let fileType = "unknown";
let content: string | null = null;
// Categorize files
if (ext === ".py") {
category = "python";
fileType = "python";
content = entry.getData().toString("utf8");
} else if (ext === ".sql") {
category = "database";
fileType = "sql";
content = entry.getData().toString("utf8");
} else if (ext === ".md") {
category = "documentation";
fileType = "markdown";
content = entry.getData().toString("utf8");
} else if (ext === ".sh") {
category = "scripts";
fileType = "shell";
content = entry.getData().toString("utf8");
} else if (ext === ".env") {
category = "config";
fileType = "env";
content = entry.getData().toString("utf8");
} else if (ext === ".json") {
category = "config";
fileType = "json";
content = entry.getData().toString("utf8");
} else if (ext === ".txt") {
category = "text";
fileType = "text";
content = entry.getData().toString("utf8");
} else if ([".joblib", ".pkl", ".h5"].includes(ext)) {
category = "models";
fileType = "model";
} else if (ext === ".log") {
category = "logs";
fileType = "log";
}
const file = await storage.createFile({
filename,
filepath,
fileType,
size: entry.header.size,
content,
category,
});
uploadedFiles.push(file);
}
res.json({
message: `Successfully uploaded ${uploadedFiles.length} files`,
files: uploadedFiles,
});
} catch (error) {
console.error("Upload error:", error);
res.status(500).json({ error: "Failed to upload and extract ZIP file" });
}
});
// Delete file
app.delete("/api/files/:id", async (req, res) => {
try {
const success = await storage.deleteFile(req.params.id);
if (!success) {
return res.status(404).json({ error: "File not found" });
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: "Failed to delete file" });
}
});
const httpServer = createServer(app);
return httpServer;
}

View File

@ -1,37 +1,62 @@
import { type User, type InsertUser } from "@shared/schema";
import { type ProjectFile, type InsertProjectFile } from "@shared/schema";
import { randomUUID } from "crypto";
// modify the interface with any CRUD methods
// you might need
export interface IStorage {
getUser(id: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
getAllFiles(): Promise<ProjectFile[]>;
getFileById(id: string): Promise<ProjectFile | undefined>;
getFilesByCategory(category: string): Promise<ProjectFile[]>;
createFile(file: InsertProjectFile): Promise<ProjectFile>;
deleteFile(id: string): Promise<boolean>;
searchFiles(query: string): Promise<ProjectFile[]>;
}
export class MemStorage implements IStorage {
private users: Map<string, User>;
private files: Map<string, ProjectFile>;
constructor() {
this.users = new Map();
this.files = new Map();
}
async getUser(id: string): Promise<User | undefined> {
return this.users.get(id);
}
async getUserByUsername(username: string): Promise<User | undefined> {
return Array.from(this.users.values()).find(
(user) => user.username === username,
async getAllFiles(): Promise<ProjectFile[]> {
return Array.from(this.files.values()).sort((a, b) =>
b.uploadedAt.getTime() - a.uploadedAt.getTime()
);
}
async createUser(insertUser: InsertUser): Promise<User> {
async getFileById(id: string): Promise<ProjectFile | undefined> {
return this.files.get(id);
}
async getFilesByCategory(category: string): Promise<ProjectFile[]> {
return Array.from(this.files.values())
.filter(file => file.category === category)
.sort((a, b) => b.uploadedAt.getTime() - a.uploadedAt.getTime());
}
async createFile(insertFile: InsertProjectFile): Promise<ProjectFile> {
const id = randomUUID();
const user: User = { ...insertUser, id };
this.users.set(id, user);
return user;
const file: ProjectFile = {
...insertFile,
id,
uploadedAt: new Date(),
};
this.files.set(id, file);
return file;
}
async deleteFile(id: string): Promise<boolean> {
return this.files.delete(id);
}
async searchFiles(query: string): Promise<ProjectFile[]> {
const lowerQuery = query.toLowerCase();
return Array.from(this.files.values())
.filter(file =>
file.filename.toLowerCase().includes(lowerQuery) ||
file.filepath.toLowerCase().includes(lowerQuery) ||
(file.content && file.content.toLowerCase().includes(lowerQuery))
)
.sort((a, b) => b.uploadedAt.getTime() - a.uploadedAt.getTime());
}
}

View File

@ -1,18 +1,22 @@
import { sql } from "drizzle-orm";
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
import { pgTable, text, varchar, integer, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
export const users = pgTable("users", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
username: text("username").notNull().unique(),
password: text("password").notNull(),
export const projectFiles = pgTable("project_files", {
id: varchar("id").primaryKey(),
filename: text("filename").notNull(),
filepath: text("filepath").notNull(),
fileType: text("file_type").notNull(),
size: integer("size").notNull(),
content: text("content"),
category: text("category").notNull(),
uploadedAt: timestamp("uploaded_at").defaultNow().notNull(),
});
export const insertUserSchema = createInsertSchema(users).pick({
username: true,
password: true,
export const insertProjectFileSchema = createInsertSchema(projectFiles).omit({
id: true,
uploadedAt: true,
});
export type InsertUser = z.infer<typeof insertUserSchema>;
export type User = typeof users.$inferSelect;
export type InsertProjectFile = z.infer<typeof insertProjectFileSchema>;
export type ProjectFile = typeof projectFiles.$inferSelect;