initial design fix

This commit is contained in:
2026-04-22 21:26:59 -05:00
parent 874cbfb6a8
commit 0d44d2cd90
45 changed files with 3509 additions and 84 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "ai-tools-dashboard-server",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"express": "^4.18.3",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.8",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/multer": "^1.4.11",
"@types/node": "^20.11.30",
"@types/uuid": "^9.0.8",
"tsx": "^4.7.1",
"typescript": "^5.4.2"
}
}
+99
View File
@@ -0,0 +1,99 @@
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data');
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
const UPLOAD_DIR = path.join(DATA_DIR, 'uploads');
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
export const UPLOAD_PATH = UPLOAD_DIR;
const db = new Database(path.join(DATA_DIR, 'dashboard.db'));
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'user',
pin_hash TEXT UNIQUE,
password_hash TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_pin_hash ON users (pin_hash)
WHERE pin_hash IS NOT NULL;
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT 'General',
status TEXT NOT NULL DEFAULT 'active',
completion INTEGER NOT NULL DEFAULT 0,
external_url TEXT,
drive_url TEXT,
tags TEXT NOT NULL DEFAULT '[]',
accent_color TEXT NOT NULL DEFAULT '#6366f1',
is_new INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT 'text/markdown',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS tools (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT 'General',
external_url TEXT,
is_new INTEGER NOT NULL DEFAULT 1,
added_at TEXT NOT NULL DEFAULT (datetime('now')),
notes TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT OR IGNORE INTO settings (key, value) VALUES
('app_title', '"CODEDUMP"'),
('logo_url', 'null'),
('accent_color', '"#6366f1"'),
('company_name', '"Your Company"');
`);
function bootstrapAdmin() {
const existing = db.prepare("SELECT id FROM users WHERE role = 'admin' LIMIT 1").get();
if (existing) return;
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
const adminPassword = process.env.ADMIN_PASSWORD || 'codedump2024';
const passwordHash = bcrypt.hashSync(adminPassword, 10);
db.prepare(`
INSERT OR IGNORE INTO users (id, username, role, password_hash)
VALUES (?, ?, 'admin', ?)
`).run(uuidv4(), adminUsername, passwordHash);
console.log(`[CODEDUMP] Admin account created → username: "${adminUsername}"`);
console.log(`[CODEDUMP] Change the default password via Admin → Users.`);
}
bootstrapAdmin();
export default db;
+39
View File
@@ -0,0 +1,39 @@
import express from 'express';
import cors from 'cors';
import path from 'path';
import './db/schema'; // initialize DB + bootstrap admin
import { requireAuth } from './middleware/auth';
import authRouter from './routes/auth';
import projectsRouter from './routes/projects';
import toolsRouter from './routes/tools';
import uploadsRouter from './routes/uploads';
import settingsRouter from './routes/settings';
import usersRouter from './routes/users';
const app = express();
const PORT = Number(process.env.PORT || 3000);
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// Public — auth endpoints (login doesn't require token)
app.use('/api/auth', authRouter);
// Protected — all other API routes require a valid JWT
app.use('/api/projects', requireAuth, projectsRouter);
app.use('/api/tools', requireAuth, toolsRouter);
app.use('/api/uploads', requireAuth, uploadsRouter);
app.use('/api/settings', requireAuth, settingsRouter);
app.use('/api/users', usersRouter); // requireAdmin applied inside router
// Serve built React client in production
const clientDist = path.join(__dirname, '../../client/dist');
app.use(express.static(clientDist));
app.get(/^(?!\/api).*/, (_req, res) => {
res.sendFile(path.join(clientDist, 'index.html'));
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`[CODEDUMP] Running on port ${PORT}`);
console.log(`[CODEDUMP] Data directory: ${process.env.DATA_DIR || 'data/'}`);
});
+9
View File
@@ -0,0 +1,9 @@
import crypto from 'crypto';
// PINs use HMAC-SHA256 keyed on JWT_SECRET so the hash is deterministic
// (needed for direct DB lookup). bcrypt is non-deterministic and would
// require iterating all users, which is too slow at any reasonable work factor.
export function hashPin(pin: string): string {
const secret = process.env.JWT_SECRET || 'codedump-secret-change-in-production';
return crypto.createHmac('sha256', secret).update(String(pin)).digest('hex');
}
+46
View File
@@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export const JWT_SECRET = process.env.JWT_SECRET || 'codedump-secret-change-in-production';
export const JWT_EXPIRY = '12h';
export interface JwtPayload {
id: string;
username: string;
role: 'admin' | 'user';
}
declare global {
namespace Express {
interface Request {
user?: JwtPayload;
}
}
}
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = extractToken(req);
if (!token) return res.status(401).json({ error: 'Authentication required' });
try {
req.user = jwt.verify(token, JWT_SECRET) as JwtPayload;
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
requireAuth(req, res, () => {
if (req.user?.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
});
}
function extractToken(req: Request): string | null {
const auth = req.headers.authorization;
if (auth?.startsWith('Bearer ')) return auth.slice(7);
return null;
}
+54
View File
@@ -0,0 +1,54 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import db from '../db/schema';
import { requireAuth, JWT_SECRET, JWT_EXPIRY, JwtPayload } from '../middleware/auth';
import { hashPin } from '../lib/pinHash';
const router = Router();
// POST /api/auth/pin — identify user by unique PIN, no username needed
router.post('/pin', (req: Request, res: Response) => {
const { pin } = req.body;
if (!pin || !/^\d{4}$/.test(String(pin))) {
return res.status(400).json({ error: 'A 4-digit PIN is required' });
}
const pinHash = hashPin(String(pin));
const user = db.prepare(
"SELECT * FROM users WHERE pin_hash = ? AND role = 'user'"
).get(pinHash) as any;
if (!user) return res.status(401).json({ error: 'Invalid PIN' });
const payload: JwtPayload = { id: user.id, username: user.username, role: 'user' };
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRY });
res.json({ token, user: payload });
});
// POST /api/auth/login — admin username + password login
router.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'username and password required' });
}
const user = db.prepare(
"SELECT * FROM users WHERE username = ? AND role = 'admin'"
).get(username) as any;
if (!user || !bcrypt.compareSync(password, user.password_hash || '')) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const payload: JwtPayload = { id: user.id, username: user.username, role: 'admin' };
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRY });
res.json({ token, user: payload });
});
// GET /api/auth/me — validate token
router.get('/me', requireAuth, (req: Request, res: Response) => {
res.json(req.user);
});
export default router;
+103
View File
@@ -0,0 +1,103 @@
import { Router, Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import db from '../db/schema';
const router = Router();
router.get('/', (_req: Request, res: Response) => {
const projects = db.prepare(`
SELECT p.*,
(SELECT COUNT(*) FROM documents d WHERE d.project_id = p.id) as doc_count
FROM projects p ORDER BY p.updated_at DESC
`).all();
res.json(projects.map(normalizeProject));
});
router.get('/:id', (req: Request, res: Response) => {
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id) as any;
if (!project) return res.status(404).json({ error: 'Not found' });
const docs = db.prepare('SELECT * FROM documents WHERE project_id = ? ORDER BY created_at DESC').all(req.params.id);
res.json({ ...normalizeProject(project), documents: docs });
});
router.post('/', (req: Request, res: Response) => {
const { name, description, category, status, completion, external_url, drive_url, tags, accent_color, is_new } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'name is required' });
const id = uuidv4();
db.prepare(`
INSERT INTO projects (id, name, description, category, status, completion, external_url, drive_url, tags, accent_color, is_new)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
name.trim(),
description || '',
category || 'General',
status || 'active',
completion ?? 0,
external_url || null,
drive_url || null,
JSON.stringify(tags || []),
accent_color || '#6366f1',
is_new ? 1 : 1
);
const created = db.prepare('SELECT * FROM projects WHERE id = ?').get(id) as any;
res.status(201).json(normalizeProject(created));
});
router.put('/:id', (req: Request, res: Response) => {
const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Not found' });
const { name, description, category, status, completion, external_url, drive_url, tags, accent_color, is_new } = req.body;
db.prepare(`
UPDATE projects SET
name = COALESCE(?, name),
description = COALESCE(?, description),
category = COALESCE(?, category),
status = COALESCE(?, status),
completion = COALESCE(?, completion),
external_url = ?,
drive_url = ?,
tags = COALESCE(?, tags),
accent_color = COALESCE(?, accent_color),
is_new = COALESCE(?, is_new),
updated_at = datetime('now')
WHERE id = ?
`).run(
name ?? null,
description ?? null,
category ?? null,
status ?? null,
completion ?? null,
external_url !== undefined ? (external_url || null) : undefined,
drive_url !== undefined ? (drive_url || null) : undefined,
tags !== undefined ? JSON.stringify(tags) : null,
accent_color ?? null,
is_new !== undefined ? (is_new ? 1 : 0) : null,
req.params.id
);
const updated = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id) as any;
res.json(normalizeProject(updated));
});
router.delete('/:id', (req: Request, res: Response) => {
const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Not found' });
db.prepare('DELETE FROM projects WHERE id = ?').run(req.params.id);
res.status(204).end();
});
function normalizeProject(p: any) {
return {
...p,
tags: typeof p.tags === 'string' ? JSON.parse(p.tags) : p.tags,
is_new: Boolean(p.is_new),
completion: Number(p.completion),
};
}
export default router;
+55
View File
@@ -0,0 +1,55 @@
import { Router, Request, Response } from 'express';
import multer from 'multer';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import db, { UPLOAD_PATH } from '../db/schema';
const router = Router();
const logoUpload = multer({
storage: multer.diskStorage({
destination: (_req, _file, cb) => cb(null, UPLOAD_PATH),
filename: (_req, file, cb) => cb(null, `logo-${uuidv4()}${path.extname(file.originalname)}`),
}),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
const allowed = ['.png', '.jpg', '.jpeg', '.svg', '.gif', '.webp'];
cb(null, allowed.includes(path.extname(file.originalname).toLowerCase()));
},
});
router.get('/', (_req: Request, res: Response) => {
const rows = db.prepare('SELECT key, value FROM settings').all() as { key: string; value: string }[];
const settings: Record<string, any> = {};
for (const row of rows) {
try { settings[row.key] = JSON.parse(row.value); } catch { settings[row.key] = row.value; }
}
res.json(settings);
});
router.put('/', (req: Request, res: Response) => {
const allowed = ['app_title', 'logo_url', 'accent_color', 'company_name'];
const upsert = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)');
const update = db.transaction((body: Record<string, any>) => {
for (const key of allowed) {
if (key in body) upsert.run(key, JSON.stringify(body[key]));
}
});
update(req.body);
const rows = db.prepare('SELECT key, value FROM settings').all() as { key: string; value: string }[];
const settings: Record<string, any> = {};
for (const row of rows) {
try { settings[row.key] = JSON.parse(row.value); } catch { settings[row.key] = row.value; }
}
res.json(settings);
});
router.post('/logo', logoUpload.single('logo'), (req: Request, res: Response) => {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
const url = `/api/uploads/${req.file.filename}`;
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run('logo_url', JSON.stringify(url));
res.json({ logo_url: url });
});
export default router;
+79
View File
@@ -0,0 +1,79 @@
import { Router, Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import db from '../db/schema';
const router = Router();
router.get('/', (_req: Request, res: Response) => {
const tools = db.prepare('SELECT * FROM tools ORDER BY added_at DESC').all();
res.json(tools.map(normalizeTool));
});
router.get('/:id', (req: Request, res: Response) => {
const tool = db.prepare('SELECT * FROM tools WHERE id = ?').get(req.params.id) as any;
if (!tool) return res.status(404).json({ error: 'Not found' });
res.json(normalizeTool(tool));
});
router.post('/', (req: Request, res: Response) => {
const { name, description, category, external_url, is_new, notes } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'name is required' });
const id = uuidv4();
db.prepare(`
INSERT INTO tools (id, name, description, category, external_url, is_new, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
id,
name.trim(),
description || '',
category || 'General',
external_url || null,
is_new !== false ? 1 : 0,
notes || ''
);
const created = db.prepare('SELECT * FROM tools WHERE id = ?').get(id) as any;
res.status(201).json(normalizeTool(created));
});
router.put('/:id', (req: Request, res: Response) => {
const existing = db.prepare('SELECT id FROM tools WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Not found' });
const { name, description, category, external_url, is_new, notes } = req.body;
db.prepare(`
UPDATE tools SET
name = COALESCE(?, name),
description = COALESCE(?, description),
category = COALESCE(?, category),
external_url = COALESCE(?, external_url),
is_new = COALESCE(?, is_new),
notes = COALESCE(?, notes)
WHERE id = ?
`).run(
name ?? null,
description ?? null,
category ?? null,
external_url !== undefined ? (external_url || null) : null,
is_new !== undefined ? (is_new ? 1 : 0) : null,
notes ?? null,
req.params.id
);
const updated = db.prepare('SELECT * FROM tools WHERE id = ?').get(req.params.id) as any;
res.json(normalizeTool(updated));
});
router.delete('/:id', (req: Request, res: Response) => {
const existing = db.prepare('SELECT id FROM tools WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Not found' });
db.prepare('DELETE FROM tools WHERE id = ?').run(req.params.id);
res.status(204).end();
});
function normalizeTool(t: any) {
return { ...t, is_new: Boolean(t.is_new) };
}
export default router;
+68
View File
@@ -0,0 +1,68 @@
import { Router, Request, Response } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import db, { UPLOAD_PATH } from '../db/schema';
const router = Router();
const storage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, UPLOAD_PATH),
filename: (_req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${uuidv4()}${ext}`);
},
});
const upload = multer({
storage,
limits: { fileSize: Number(process.env.MAX_UPLOAD_MB || 50) * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
const allowed = ['.md', '.txt', '.pdf', '.png', '.jpg', '.jpeg', '.gif', '.svg'];
const ext = path.extname(file.originalname).toLowerCase();
cb(null, allowed.includes(ext));
},
});
// Upload doc to a project
router.post('/projects/:projectId', upload.single('file'), (req: Request, res: Response) => {
const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(req.params.projectId);
if (!project) return res.status(404).json({ error: 'Project not found' });
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
const id = uuidv4();
db.prepare(`
INSERT INTO documents (id, project_id, filename, original_name, mimetype)
VALUES (?, ?, ?, ?, ?)
`).run(id, req.params.projectId, req.file.filename, req.file.originalname, req.file.mimetype);
res.status(201).json({
id,
project_id: req.params.projectId,
filename: req.file.filename,
original_name: req.file.originalname,
mimetype: req.file.mimetype,
});
});
// Get raw file
router.get('/:filename', (req: Request, res: Response) => {
const filePath = path.join(UPLOAD_PATH, path.basename(req.params.filename));
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'File not found' });
res.sendFile(filePath);
});
// Delete a document
router.delete('/documents/:id', (req: Request, res: Response) => {
const doc = db.prepare('SELECT * FROM documents WHERE id = ?').get(req.params.id) as any;
if (!doc) return res.status(404).json({ error: 'Not found' });
const filePath = path.join(UPLOAD_PATH, doc.filename);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
db.prepare('DELETE FROM documents WHERE id = ?').run(req.params.id);
res.status(204).end();
});
export default router;
+102
View File
@@ -0,0 +1,102 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
import db from '../db/schema';
import { requireAdmin } from '../middleware/auth';
import { hashPin } from '../lib/pinHash';
const router = Router();
router.use(requireAdmin);
router.get('/', (_req: Request, res: Response) => {
const users = db.prepare(
'SELECT id, username, role, created_at FROM users ORDER BY role DESC, username ASC'
).all();
res.json(users);
});
router.post('/', (req: Request, res: Response) => {
const { username, pin, password, role } = req.body;
if (!username?.trim()) return res.status(400).json({ error: 'username is required' });
const userRole = role === 'admin' ? 'admin' : 'user';
if (userRole === 'user') {
if (!pin || !/^\d{4}$/.test(String(pin))) {
return res.status(400).json({ error: 'A 4-digit PIN is required for user accounts' });
}
} else {
if (!password || password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters for admin accounts' });
}
}
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username.trim());
if (existing) return res.status(409).json({ error: 'Username already exists' });
let pinHash: string | null = null;
if (userRole === 'user') {
pinHash = hashPin(String(pin));
const pinTaken = db.prepare('SELECT id FROM users WHERE pin_hash = ?').get(pinHash);
if (pinTaken) return res.status(409).json({ error: 'That PIN is already in use by another user. Choose a different PIN.' });
}
const id = uuidv4();
const passwordHash = userRole === 'admin' ? bcrypt.hashSync(password, 10) : null;
db.prepare(`
INSERT INTO users (id, username, role, pin_hash, password_hash)
VALUES (?, ?, ?, ?, ?)
`).run(id, username.trim(), userRole, pinHash, passwordHash);
res.status(201).json(
db.prepare('SELECT id, username, role, created_at FROM users WHERE id = ?').get(id)
);
});
router.put('/:id', (req: Request, res: Response) => {
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id) as any;
if (!user) return res.status(404).json({ error: 'Not found' });
const { pin, password, username } = req.body;
if (username?.trim() && username.trim() !== user.username) {
const clash = db.prepare('SELECT id FROM users WHERE username = ? AND id != ?').get(username.trim(), req.params.id);
if (clash) return res.status(409).json({ error: 'Username already taken' });
db.prepare('UPDATE users SET username = ? WHERE id = ?').run(username.trim(), req.params.id);
}
if (user.role === 'user' && pin !== undefined && pin !== '') {
if (!/^\d{4}$/.test(String(pin))) {
return res.status(400).json({ error: 'PIN must be exactly 4 digits' });
}
const pinHash = hashPin(String(pin));
const pinTaken = db.prepare('SELECT id FROM users WHERE pin_hash = ? AND id != ?').get(pinHash, req.params.id);
if (pinTaken) return res.status(409).json({ error: 'That PIN is already in use by another user.' });
db.prepare('UPDATE users SET pin_hash = ? WHERE id = ?').run(pinHash, req.params.id);
}
if (user.role === 'admin' && password) {
if (password.length < 6) return res.status(400).json({ error: 'Password must be at least 6 characters' });
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(bcrypt.hashSync(password, 10), req.params.id);
}
res.json(
db.prepare('SELECT id, username, role, created_at FROM users WHERE id = ?').get(req.params.id)
);
});
router.delete('/:id', (req: Request, res: Response) => {
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id) as any;
if (!user) return res.status(404).json({ error: 'Not found' });
if (user.role === 'admin') {
const adminCount = (db.prepare("SELECT COUNT(*) as c FROM users WHERE role = 'admin'").get() as any).c;
if (adminCount <= 1) return res.status(400).json({ error: 'Cannot delete the last admin account' });
}
db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id);
res.status(204).end();
});
export default router;
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}