Add ESLint + Prettier + EditorConfig tooling at repo root
v1.0 Phase 1.1 — repo-wide lint/format baseline. - eslint.config.mjs (flat config) lints server, client, shared - .prettierrc.json, .prettierignore, .editorconfig, .nvmrc - Root package.json holds shared devDeps; per-package scripts keep their typecheck + test runners - Fix 7 lint issues surfaced by the baseline run: - TicketDetail.tsx: replace ternary-with-side-effects with if/else - admin/Users.tsx: escape apostrophe in JSX - errorHandler.ts: typed err as unknown with ErrorLike refinement - users.ts: Prisma.UserUpdateInput instead of Record<string, any> - seed.ts: drop unused goddard binding - Run prettier across tracked sources for a clean formatting baseline
This commit is contained in:
Generated
+3667
-1
File diff suppressed because it is too large
Load Diff
+12
-2
@@ -9,7 +9,10 @@
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate",
|
||||
"db:seed": "tsx prisma/seed.ts"
|
||||
"db:seed": "tsx prisma/seed.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
@@ -18,8 +21,12 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"express-async-errors": "^3.1.0",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"pino": "^9.5.0",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -29,8 +36,11 @@
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"prisma": "^5.22.0",
|
||||
"supertest": "^7.0.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
+27
-24
@@ -1,11 +1,11 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import crypto from 'crypto'
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding database...')
|
||||
console.log('Seeding database...');
|
||||
|
||||
// Admin user
|
||||
await prisma.user.upsert({
|
||||
@@ -18,11 +18,11 @@ async function main() {
|
||||
passwordHash: await bcrypt.hash('admin123', 12),
|
||||
role: 'ADMIN',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Goddard — n8n service account
|
||||
const apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`
|
||||
const goddard = await prisma.user.upsert({
|
||||
const apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`;
|
||||
await prisma.user.upsert({
|
||||
where: { username: 'goddard' },
|
||||
update: {},
|
||||
create: {
|
||||
@@ -33,70 +33,73 @@ async function main() {
|
||||
role: 'SERVICE',
|
||||
apiKey,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const existingGoddard = await prisma.user.findUnique({ where: { username: 'goddard' } })
|
||||
console.log(`\nGoddard API key: ${existingGoddard?.apiKey ?? apiKey}`)
|
||||
console.log('(This key is only displayed once on first seed — copy it now)\n')
|
||||
const existingGoddard = await prisma.user.findUnique({ where: { username: 'goddard' } });
|
||||
console.log(`\nGoddard API key: ${existingGoddard?.apiKey ?? apiKey}`);
|
||||
console.log('(This key is only displayed once on first seed — copy it now)\n');
|
||||
|
||||
// Sample CTI structure
|
||||
const theWrightServer = await prisma.category.upsert({
|
||||
where: { name: 'TheWrightServer' },
|
||||
update: {},
|
||||
create: { name: 'TheWrightServer' },
|
||||
})
|
||||
});
|
||||
|
||||
const homelab = await prisma.category.upsert({
|
||||
where: { name: 'Homelab' },
|
||||
update: {},
|
||||
create: { name: 'Homelab' },
|
||||
})
|
||||
});
|
||||
|
||||
const automation = await prisma.type.upsert({
|
||||
where: { categoryId_name: { categoryId: theWrightServer.id, name: 'Automation' } },
|
||||
update: {},
|
||||
create: { name: 'Automation', categoryId: theWrightServer.id },
|
||||
})
|
||||
});
|
||||
|
||||
const media = await prisma.type.upsert({
|
||||
where: { categoryId_name: { categoryId: theWrightServer.id, name: 'Media' } },
|
||||
update: {},
|
||||
create: { name: 'Media', categoryId: theWrightServer.id },
|
||||
})
|
||||
});
|
||||
|
||||
const infrastructure = await prisma.type.upsert({
|
||||
where: { categoryId_name: { categoryId: homelab.id, name: 'Infrastructure' } },
|
||||
update: {},
|
||||
create: { name: 'Infrastructure', categoryId: homelab.id },
|
||||
})
|
||||
});
|
||||
|
||||
await prisma.item.upsert({
|
||||
where: { typeId_name: { typeId: automation.id, name: 'Backup' } },
|
||||
update: {},
|
||||
create: { name: 'Backup', typeId: automation.id },
|
||||
})
|
||||
});
|
||||
|
||||
await prisma.item.upsert({
|
||||
where: { typeId_name: { typeId: automation.id, name: 'Sync' } },
|
||||
update: {},
|
||||
create: { name: 'Sync', typeId: automation.id },
|
||||
})
|
||||
});
|
||||
|
||||
await prisma.item.upsert({
|
||||
where: { typeId_name: { typeId: media.id, name: 'Plex' } },
|
||||
update: {},
|
||||
create: { name: 'Plex', typeId: media.id },
|
||||
})
|
||||
});
|
||||
|
||||
await prisma.item.upsert({
|
||||
where: { typeId_name: { typeId: infrastructure.id, name: 'Proxmox' } },
|
||||
update: {},
|
||||
create: { name: 'Proxmox', typeId: infrastructure.id },
|
||||
})
|
||||
});
|
||||
|
||||
console.log('Seed complete.')
|
||||
console.log('Seed complete.');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1) })
|
||||
.finally(() => prisma.$disconnect())
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
||||
+26
-26
@@ -1,41 +1,41 @@
|
||||
import 'express-async-errors'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import dotenv from 'dotenv'
|
||||
import 'express-async-errors';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import authRoutes from './routes/auth'
|
||||
import ticketRoutes from './routes/tickets'
|
||||
import ctiRoutes from './routes/cti'
|
||||
import userRoutes from './routes/users'
|
||||
import { authenticate } from './middleware/auth'
|
||||
import { errorHandler } from './middleware/errorHandler'
|
||||
import { startAutoCloseJob } from './jobs/autoClose'
|
||||
import authRoutes from './routes/auth';
|
||||
import ticketRoutes from './routes/tickets';
|
||||
import ctiRoutes from './routes/cti';
|
||||
import userRoutes from './routes/users';
|
||||
import { authenticate } from './middleware/auth';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
import { startAutoCloseJob } from './jobs/autoClose';
|
||||
|
||||
dotenv.config()
|
||||
dotenv.config();
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
console.error('FATAL: JWT_SECRET is not set')
|
||||
process.exit(1)
|
||||
console.error('FATAL: JWT_SECRET is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express()
|
||||
const app = express();
|
||||
|
||||
app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:5173' }))
|
||||
app.use(express.json())
|
||||
app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:5173' }));
|
||||
app.use(express.json());
|
||||
|
||||
// Public
|
||||
app.use('/api/auth', authRoutes)
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
// Protected
|
||||
app.use('/api/tickets', authenticate, ticketRoutes)
|
||||
app.use('/api/cti', authenticate, ctiRoutes)
|
||||
app.use('/api/users', authenticate, userRoutes)
|
||||
app.use('/api/tickets', authenticate, ticketRoutes);
|
||||
app.use('/api/cti', authenticate, ctiRoutes);
|
||||
app.use('/api/users', authenticate, userRoutes);
|
||||
|
||||
app.use(errorHandler)
|
||||
app.use(errorHandler);
|
||||
|
||||
startAutoCloseJob()
|
||||
startAutoCloseJob();
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3000
|
||||
const PORT = Number(process.env.PORT) || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`)
|
||||
})
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import cron from 'node-cron'
|
||||
import prisma from '../lib/prisma'
|
||||
import cron from 'node-cron';
|
||||
import prisma from '../lib/prisma';
|
||||
|
||||
export function startAutoCloseJob() {
|
||||
// Run every hour — closes RESOLVED tickets that have been resolved for 14+ days
|
||||
cron.schedule('0 * * * *', async () => {
|
||||
const twoWeeksAgo = new Date()
|
||||
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14)
|
||||
const twoWeeksAgo = new Date();
|
||||
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
|
||||
|
||||
const result = await prisma.ticket.updateMany({
|
||||
where: {
|
||||
@@ -13,12 +13,12 @@ export function startAutoCloseJob() {
|
||||
resolvedAt: { lte: twoWeeksAgo },
|
||||
},
|
||||
data: { status: 'CLOSED' },
|
||||
})
|
||||
});
|
||||
|
||||
if (result.count > 0) {
|
||||
console.log(`[AutoClose] Closed ${result.count} ticket(s) after 2-week resolution period`)
|
||||
console.log(`[AutoClose] Closed ${result.count} ticket(s) after 2-week resolution period`);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
console.log('[AutoClose] Job scheduled — runs every hour')
|
||||
console.log('[AutoClose] Job scheduled — runs every hour');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default prisma
|
||||
export default prisma;
|
||||
|
||||
@@ -1,69 +1,57 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import prisma from '../lib/prisma'
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import prisma from '../lib/prisma';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: {
|
||||
id: string
|
||||
role: string
|
||||
username: string
|
||||
}
|
||||
id: string;
|
||||
role: string;
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const authenticate = async (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const apiKey = req.headers['x-api-key'] as string | undefined
|
||||
export const authenticate = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
const apiKey = req.headers['x-api-key'] as string | undefined;
|
||||
|
||||
if (apiKey) {
|
||||
const user = await prisma.user.findUnique({ where: { apiKey } })
|
||||
const user = await prisma.user.findUnique({ where: { apiKey } });
|
||||
if (!user || user.role !== 'SERVICE') {
|
||||
return res.status(401).json({ error: 'Invalid API key' })
|
||||
return res.status(401).json({ error: 'Invalid API key' });
|
||||
}
|
||||
req.user = { id: user.id, role: user.role, username: user.username }
|
||||
return next()
|
||||
req.user = { id: user.id, role: user.role, username: user.username };
|
||||
return next();
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Unauthorized' })
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1]
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as {
|
||||
id: string
|
||||
role: string
|
||||
username: string
|
||||
}
|
||||
req.user = payload
|
||||
next()
|
||||
id: string;
|
||||
role: string;
|
||||
username: string;
|
||||
};
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' })
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const requireAdmin = (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
export const requireAdmin = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
if (req.user?.role !== 'ADMIN') {
|
||||
return res.status(403).json({ error: 'Admin access required' })
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
next()
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Blocks USER role — allows ADMIN, AGENT, SERVICE
|
||||
export const requireAgent = (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
export const requireAgent = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
if (req.user?.role === 'USER') {
|
||||
return res.status(403).json({ error: 'Insufficient permissions' })
|
||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||
}
|
||||
next()
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import { ZodError } from 'zod'
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
export function errorHandler(
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
console.error(err)
|
||||
type ErrorLike = {
|
||||
code?: string;
|
||||
status?: number;
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) {
|
||||
console.error(err);
|
||||
|
||||
if (err instanceof ZodError) {
|
||||
return res.status(400).json({ error: 'Validation error', details: err.flatten() })
|
||||
return res.status(400).json({ error: 'Validation error', details: err.flatten() });
|
||||
}
|
||||
|
||||
// Prisma unique constraint violation
|
||||
if (err.code === 'P2002') {
|
||||
return res.status(409).json({ error: 'A record with that value already exists' })
|
||||
const e = (err ?? {}) as ErrorLike;
|
||||
|
||||
if (e.code === 'P2002') {
|
||||
return res.status(409).json({ error: 'A record with that value already exists' });
|
||||
}
|
||||
|
||||
// Prisma record not found
|
||||
if (err.code === 'P2025') {
|
||||
return res.status(404).json({ error: 'Record not found' })
|
||||
if (e.code === 'P2025') {
|
||||
return res.status(404).json({ error: 'Record not found' });
|
||||
}
|
||||
|
||||
const status = err.status || err.statusCode || 500
|
||||
const message = err.message || 'Internal server error'
|
||||
res.status(status).json({ error: message })
|
||||
const status = e.status || e.statusCode || 500;
|
||||
const message = e.message || 'Internal server error';
|
||||
res.status(status).json({ error: message });
|
||||
}
|
||||
|
||||
+21
-21
@@ -1,34 +1,34 @@
|
||||
import { Router } from 'express'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authenticate, AuthRequest } from '../middleware/auth'
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { z } from 'zod';
|
||||
import prisma from '../lib/prisma';
|
||||
import { authenticate, AuthRequest } from '../middleware/auth';
|
||||
|
||||
const router = Router()
|
||||
const router = Router();
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
})
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const { username, password } = loginSchema.parse(req.body)
|
||||
const { username, password } = loginSchema.parse(req.body);
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { username } })
|
||||
const user = await prisma.user.findUnique({ where: { username } });
|
||||
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' })
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
if (user.role === 'SERVICE') {
|
||||
return res.status(401).json({ error: 'Service accounts must authenticate via API key' })
|
||||
return res.status(401).json({ error: 'Service accounts must authenticate via API key' });
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, role: user.role, username: user.username },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: '24h' }
|
||||
)
|
||||
{ expiresIn: '24h' },
|
||||
);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
@@ -39,16 +39,16 @@ router.post('/login', async (req, res) => {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
},
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/me', authenticate, async (req: AuthRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id },
|
||||
select: { id: true, username: true, displayName: true, email: true, role: true },
|
||||
})
|
||||
if (!user) return res.status(404).json({ error: 'User not found' })
|
||||
res.json(user)
|
||||
})
|
||||
});
|
||||
if (!user) return res.status(404).json({ error: 'User not found' });
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Router } from 'express'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../lib/prisma'
|
||||
import { AuthRequest } from '../middleware/auth'
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import prisma from '../lib/prisma';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
|
||||
const router = Router({ mergeParams: true })
|
||||
const router = Router({ mergeParams: true });
|
||||
|
||||
const commentSchema = z.object({
|
||||
body: z.string().min(1),
|
||||
})
|
||||
});
|
||||
|
||||
router.post('/', async (req: AuthRequest, res) => {
|
||||
const { body } = commentSchema.parse(req.body)
|
||||
const ticketId = (req.params as Record<string, string>).ticketId
|
||||
const { body } = commentSchema.parse(req.body);
|
||||
const ticketId = (req.params as Record<string, string>).ticketId;
|
||||
|
||||
const ticket = await prisma.ticket.findFirst({
|
||||
where: { OR: [{ id: ticketId }, { displayId: ticketId }] },
|
||||
})
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' })
|
||||
});
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' });
|
||||
|
||||
const [comment] = await prisma.$transaction([
|
||||
prisma.comment.create({
|
||||
@@ -26,19 +26,19 @@ router.post('/', async (req: AuthRequest, res) => {
|
||||
prisma.auditLog.create({
|
||||
data: { ticketId: ticket.id, userId: req.user!.id, action: 'COMMENT_ADDED', detail: body },
|
||||
}),
|
||||
])
|
||||
]);
|
||||
|
||||
res.status(201).json(comment)
|
||||
})
|
||||
res.status(201).json(comment);
|
||||
});
|
||||
|
||||
router.delete('/:commentId', async (req: AuthRequest, res) => {
|
||||
const comment = await prisma.comment.findUnique({
|
||||
where: { id: req.params.commentId },
|
||||
})
|
||||
if (!comment) return res.status(404).json({ error: 'Comment not found' })
|
||||
});
|
||||
if (!comment) return res.status(404).json({ error: 'Comment not found' });
|
||||
|
||||
if (comment.authorId !== req.user!.id && req.user!.role !== 'ADMIN') {
|
||||
return res.status(403).json({ error: 'Not allowed' })
|
||||
return res.status(403).json({ error: 'Not allowed' });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
@@ -51,9 +51,9 @@ router.delete('/:commentId', async (req: AuthRequest, res) => {
|
||||
detail: comment.body,
|
||||
},
|
||||
}),
|
||||
])
|
||||
]);
|
||||
|
||||
res.status(204).send()
|
||||
})
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
+51
-51
@@ -1,113 +1,113 @@
|
||||
import { Router } from 'express'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../lib/prisma'
|
||||
import { requireAdmin } from '../middleware/auth'
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import prisma from '../lib/prisma';
|
||||
import { requireAdmin } from '../middleware/auth';
|
||||
|
||||
const router = Router()
|
||||
const router = Router();
|
||||
|
||||
const nameSchema = z.object({ name: z.string().min(1).max(100) })
|
||||
const nameSchema = z.object({ name: z.string().min(1).max(100) });
|
||||
|
||||
// ── Categories ────────────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/categories', async (_req, res) => {
|
||||
const categories = await prisma.category.findMany({ orderBy: { name: 'asc' } })
|
||||
res.json(categories)
|
||||
})
|
||||
const categories = await prisma.category.findMany({ orderBy: { name: 'asc' } });
|
||||
res.json(categories);
|
||||
});
|
||||
|
||||
router.post('/categories', requireAdmin, async (req, res) => {
|
||||
const { name } = nameSchema.parse(req.body)
|
||||
const category = await prisma.category.create({ data: { name } })
|
||||
res.status(201).json(category)
|
||||
})
|
||||
const { name } = nameSchema.parse(req.body);
|
||||
const category = await prisma.category.create({ data: { name } });
|
||||
res.status(201).json(category);
|
||||
});
|
||||
|
||||
router.put('/categories/:id', requireAdmin, async (req, res) => {
|
||||
const { name } = nameSchema.parse(req.body)
|
||||
const { name } = nameSchema.parse(req.body);
|
||||
const category = await prisma.category.update({
|
||||
where: { id: req.params.id },
|
||||
data: { name },
|
||||
})
|
||||
res.json(category)
|
||||
})
|
||||
});
|
||||
res.json(category);
|
||||
});
|
||||
|
||||
router.delete('/categories/:id', requireAdmin, async (req, res) => {
|
||||
await prisma.category.delete({ where: { id: req.params.id } })
|
||||
res.status(204).send()
|
||||
})
|
||||
await prisma.category.delete({ where: { id: req.params.id } });
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/types', async (req, res) => {
|
||||
const { categoryId } = req.query
|
||||
const { categoryId } = req.query;
|
||||
const types = await prisma.type.findMany({
|
||||
where: categoryId ? { categoryId: categoryId as string } : undefined,
|
||||
include: { category: true },
|
||||
orderBy: { name: 'asc' },
|
||||
})
|
||||
res.json(types)
|
||||
})
|
||||
});
|
||||
res.json(types);
|
||||
});
|
||||
|
||||
router.post('/types', requireAdmin, async (req, res) => {
|
||||
const { name, categoryId } = z
|
||||
.object({ name: z.string().min(1).max(100), categoryId: z.string().min(1) })
|
||||
.parse(req.body)
|
||||
.parse(req.body);
|
||||
const type = await prisma.type.create({
|
||||
data: { name, categoryId },
|
||||
include: { category: true },
|
||||
})
|
||||
res.status(201).json(type)
|
||||
})
|
||||
});
|
||||
res.status(201).json(type);
|
||||
});
|
||||
|
||||
router.put('/types/:id', requireAdmin, async (req, res) => {
|
||||
const { name } = nameSchema.parse(req.body)
|
||||
const { name } = nameSchema.parse(req.body);
|
||||
const type = await prisma.type.update({
|
||||
where: { id: req.params.id },
|
||||
data: { name },
|
||||
include: { category: true },
|
||||
})
|
||||
res.json(type)
|
||||
})
|
||||
});
|
||||
res.json(type);
|
||||
});
|
||||
|
||||
router.delete('/types/:id', requireAdmin, async (req, res) => {
|
||||
await prisma.type.delete({ where: { id: req.params.id } })
|
||||
res.status(204).send()
|
||||
})
|
||||
await prisma.type.delete({ where: { id: req.params.id } });
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
// ── Items ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/items', async (req, res) => {
|
||||
const { typeId } = req.query
|
||||
const { typeId } = req.query;
|
||||
const items = await prisma.item.findMany({
|
||||
where: typeId ? { typeId: typeId as string } : undefined,
|
||||
include: { type: { include: { category: true } } },
|
||||
orderBy: { name: 'asc' },
|
||||
})
|
||||
res.json(items)
|
||||
})
|
||||
});
|
||||
res.json(items);
|
||||
});
|
||||
|
||||
router.post('/items', requireAdmin, async (req, res) => {
|
||||
const { name, typeId } = z
|
||||
.object({ name: z.string().min(1).max(100), typeId: z.string().min(1) })
|
||||
.parse(req.body)
|
||||
.parse(req.body);
|
||||
const item = await prisma.item.create({
|
||||
data: { name, typeId },
|
||||
include: { type: { include: { category: true } } },
|
||||
})
|
||||
res.status(201).json(item)
|
||||
})
|
||||
});
|
||||
res.status(201).json(item);
|
||||
});
|
||||
|
||||
router.put('/items/:id', requireAdmin, async (req, res) => {
|
||||
const { name } = nameSchema.parse(req.body)
|
||||
const { name } = nameSchema.parse(req.body);
|
||||
const item = await prisma.item.update({
|
||||
where: { id: req.params.id },
|
||||
data: { name },
|
||||
include: { type: { include: { category: true } } },
|
||||
})
|
||||
res.json(item)
|
||||
})
|
||||
});
|
||||
res.json(item);
|
||||
});
|
||||
|
||||
router.delete('/items/:id', requireAdmin, async (req, res) => {
|
||||
await prisma.item.delete({ where: { id: req.params.id } })
|
||||
res.status(204).send()
|
||||
})
|
||||
await prisma.item.delete({ where: { id: req.params.id } });
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Router } from 'express'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../lib/prisma'
|
||||
import { requireAdmin, requireAgent, AuthRequest } from '../middleware/auth'
|
||||
import commentRouter from './comments'
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import prisma from '../lib/prisma';
|
||||
import { requireAdmin, requireAgent, AuthRequest } from '../middleware/auth';
|
||||
import commentRouter from './comments';
|
||||
|
||||
const router = Router()
|
||||
const router = Router();
|
||||
|
||||
const ticketInclude = {
|
||||
category: true,
|
||||
@@ -16,21 +16,21 @@ const ticketInclude = {
|
||||
include: { author: { select: { id: true, username: true, displayName: true } } },
|
||||
orderBy: { createdAt: 'asc' as const },
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
OPEN: 'Open',
|
||||
IN_PROGRESS: 'In Progress',
|
||||
RESOLVED: 'Resolved',
|
||||
CLOSED: 'Closed',
|
||||
}
|
||||
};
|
||||
|
||||
async function generateDisplayId(): Promise<string> {
|
||||
while (true) {
|
||||
const num = Math.floor(Math.random() * 900_000_000) + 100_000_000
|
||||
const displayId = `V${num}`
|
||||
const exists = await prisma.ticket.findUnique({ where: { displayId } })
|
||||
if (!exists) return displayId
|
||||
const num = Math.floor(Math.random() * 900_000_000) + 100_000_000;
|
||||
const displayId = `V${num}`;
|
||||
const exists = await prisma.ticket.findUnique({ where: { displayId } });
|
||||
if (!exists) return displayId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ async function generateDisplayId(): Promise<string> {
|
||||
function findByIdOrDisplay(idOrDisplay: string) {
|
||||
return prisma.ticket.findFirst({
|
||||
where: { OR: [{ id: idOrDisplay }, { displayId: idOrDisplay }] },
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const createSchema = z.object({
|
||||
@@ -49,7 +49,7 @@ const createSchema = z.object({
|
||||
typeId: z.string().min(1),
|
||||
itemId: z.string().min(1),
|
||||
assigneeId: z.string().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
const updateSchema = z.object({
|
||||
title: z.string().min(1).max(255).optional(),
|
||||
@@ -60,28 +60,28 @@ const updateSchema = z.object({
|
||||
typeId: z.string().min(1).optional(),
|
||||
itemId: z.string().min(1).optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
// Mount comment sub-router
|
||||
router.use('/:ticketId/comments', commentRouter)
|
||||
router.use('/:ticketId/comments', commentRouter);
|
||||
|
||||
// GET /api/tickets
|
||||
router.get('/', async (req: AuthRequest, res) => {
|
||||
const { status, severity, assigneeId, categoryId, typeId, itemId, search } = req.query
|
||||
const { status, severity, assigneeId, categoryId, typeId, itemId, search } = req.query;
|
||||
|
||||
const where: Record<string, unknown> = {}
|
||||
if (status) where.status = status
|
||||
if (severity) where.severity = Number(severity)
|
||||
if (assigneeId) where.assigneeId = assigneeId
|
||||
if (itemId) where.itemId = itemId
|
||||
else if (typeId) where.typeId = typeId
|
||||
else if (categoryId) where.categoryId = categoryId
|
||||
const where: Record<string, unknown> = {};
|
||||
if (status) where.status = status;
|
||||
if (severity) where.severity = Number(severity);
|
||||
if (assigneeId) where.assigneeId = assigneeId;
|
||||
if (itemId) where.itemId = itemId;
|
||||
else if (typeId) where.typeId = typeId;
|
||||
else if (categoryId) where.categoryId = categoryId;
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search as string, mode: 'insensitive' } },
|
||||
{ overview: { contains: search as string, mode: 'insensitive' } },
|
||||
{ displayId: { contains: search as string, mode: 'insensitive' } },
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
const tickets = await prisma.ticket.findMany({
|
||||
@@ -95,60 +95,60 @@ router.get('/', async (req: AuthRequest, res) => {
|
||||
_count: { select: { comments: true } },
|
||||
},
|
||||
orderBy: [{ severity: 'asc' }, { createdAt: 'desc' }],
|
||||
})
|
||||
});
|
||||
|
||||
res.json(tickets)
|
||||
})
|
||||
res.json(tickets);
|
||||
});
|
||||
|
||||
// GET /api/tickets/:id
|
||||
router.get('/:id', async (req, res) => {
|
||||
const ticket = await prisma.ticket.findFirst({
|
||||
where: { OR: [{ id: req.params.id }, { displayId: req.params.id }] },
|
||||
include: ticketInclude,
|
||||
})
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' })
|
||||
res.json(ticket)
|
||||
})
|
||||
});
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' });
|
||||
res.json(ticket);
|
||||
});
|
||||
|
||||
// GET /api/tickets/:id/audit
|
||||
router.get('/:id/audit', async (req, res) => {
|
||||
const ticket = await findByIdOrDisplay(req.params.id)
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' })
|
||||
const ticket = await findByIdOrDisplay(req.params.id);
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' });
|
||||
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
where: { ticketId: ticket.id },
|
||||
include: { user: { select: { id: true, username: true, displayName: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
});
|
||||
|
||||
res.json(logs)
|
||||
})
|
||||
res.json(logs);
|
||||
});
|
||||
|
||||
// POST /api/tickets
|
||||
router.post('/', requireAgent, async (req: AuthRequest, res) => {
|
||||
const data = createSchema.parse(req.body)
|
||||
const displayId = await generateDisplayId()
|
||||
const data = createSchema.parse(req.body);
|
||||
const displayId = await generateDisplayId();
|
||||
|
||||
const ticket = await prisma.$transaction(async (tx) => {
|
||||
const created = await tx.ticket.create({
|
||||
data: { displayId, ...data, createdById: req.user!.id },
|
||||
})
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: { ticketId: created.id, userId: req.user!.id, action: 'CREATED' },
|
||||
})
|
||||
return tx.ticket.findUnique({ where: { id: created.id }, include: ticketInclude })
|
||||
})
|
||||
});
|
||||
return tx.ticket.findUnique({ where: { id: created.id }, include: ticketInclude });
|
||||
});
|
||||
|
||||
res.status(201).json(ticket)
|
||||
})
|
||||
res.status(201).json(ticket);
|
||||
});
|
||||
|
||||
// PATCH /api/tickets/:id
|
||||
router.patch('/:id', requireAgent, async (req: AuthRequest, res) => {
|
||||
const data = updateSchema.parse(req.body)
|
||||
const data = updateSchema.parse(req.body);
|
||||
|
||||
// Only admins can set status to CLOSED
|
||||
if (data.status === 'CLOSED' && req.user?.role !== 'ADMIN') {
|
||||
return res.status(403).json({ error: 'Only admins can close tickets' })
|
||||
return res.status(403).json({ error: 'Only admins can close tickets' });
|
||||
}
|
||||
|
||||
const existing = await prisma.ticket.findFirst({
|
||||
@@ -159,17 +159,17 @@ router.patch('/:id', requireAgent, async (req: AuthRequest, res) => {
|
||||
item: true,
|
||||
assignee: { select: { displayName: true } },
|
||||
},
|
||||
})
|
||||
if (!existing) return res.status(404).json({ error: 'Ticket not found' })
|
||||
});
|
||||
if (!existing) return res.status(404).json({ error: 'Ticket not found' });
|
||||
|
||||
// Build audit entries
|
||||
const auditEntries: { action: string; detail?: string }[] = []
|
||||
const auditEntries: { action: string; detail?: string }[] = [];
|
||||
|
||||
if (data.status && data.status !== existing.status) {
|
||||
auditEntries.push({
|
||||
action: 'STATUS_CHANGED',
|
||||
detail: `${STATUS_LABELS[existing.status]} → ${STATUS_LABELS[data.status]}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if ('assigneeId' in data && data.assigneeId !== existing.assigneeId) {
|
||||
@@ -178,25 +178,25 @@ router.patch('/:id', requireAgent, async (req: AuthRequest, res) => {
|
||||
where: { id: data.assigneeId },
|
||||
select: { displayName: true },
|
||||
})
|
||||
: null
|
||||
: null;
|
||||
auditEntries.push({
|
||||
action: 'ASSIGNEE_CHANGED',
|
||||
detail: `${existing.assignee?.displayName ?? 'Unassigned'} → ${newAssignee?.displayName ?? 'Unassigned'}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (data.severity && data.severity !== existing.severity) {
|
||||
auditEntries.push({
|
||||
action: 'SEVERITY_CHANGED',
|
||||
detail: `SEV ${existing.severity} → SEV ${data.severity}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CTI rerouting — only log if any CTI field actually changed
|
||||
const ctiChanged =
|
||||
(data.categoryId && data.categoryId !== existing.categoryId) ||
|
||||
(data.typeId && data.typeId !== existing.typeId) ||
|
||||
(data.itemId && data.itemId !== existing.itemId)
|
||||
(data.itemId && data.itemId !== existing.itemId);
|
||||
|
||||
if (ctiChanged) {
|
||||
const [newCat, newType, newItem] = await Promise.all([
|
||||
@@ -209,34 +209,34 @@ router.patch('/:id', requireAgent, async (req: AuthRequest, res) => {
|
||||
data.itemId && data.itemId !== existing.itemId
|
||||
? prisma.item.findUnique({ where: { id: data.itemId } })
|
||||
: Promise.resolve(existing.item),
|
||||
])
|
||||
]);
|
||||
auditEntries.push({
|
||||
action: 'REROUTED',
|
||||
detail: `${existing.category.name} › ${existing.type.name} › ${existing.item.name} → ${newCat?.name} › ${newType?.name} › ${newItem?.name}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (data.title && data.title !== existing.title) {
|
||||
auditEntries.push({ action: 'TITLE_CHANGED', detail: data.title })
|
||||
auditEntries.push({ action: 'TITLE_CHANGED', detail: data.title });
|
||||
}
|
||||
|
||||
if (data.overview && data.overview !== existing.overview) {
|
||||
auditEntries.push({ action: 'OVERVIEW_CHANGED' })
|
||||
auditEntries.push({ action: 'OVERVIEW_CHANGED' });
|
||||
}
|
||||
|
||||
// Handle resolvedAt tracking
|
||||
const update: Record<string, unknown> = { ...data }
|
||||
const update: Record<string, unknown> = { ...data };
|
||||
if (data.status === 'RESOLVED' && existing.status !== 'RESOLVED') {
|
||||
update.resolvedAt = new Date()
|
||||
update.resolvedAt = new Date();
|
||||
} else if (data.status && data.status !== 'RESOLVED' && existing.status === 'RESOLVED') {
|
||||
update.resolvedAt = null
|
||||
update.resolvedAt = null;
|
||||
}
|
||||
|
||||
const ticket = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.ticket.update({
|
||||
where: { id: existing.id },
|
||||
data: update,
|
||||
})
|
||||
});
|
||||
if (auditEntries.length > 0) {
|
||||
await tx.auditLog.createMany({
|
||||
data: auditEntries.map((e) => ({
|
||||
@@ -245,20 +245,20 @@ router.patch('/:id', requireAgent, async (req: AuthRequest, res) => {
|
||||
action: e.action,
|
||||
detail: e.detail ?? null,
|
||||
})),
|
||||
})
|
||||
});
|
||||
}
|
||||
return tx.ticket.findUnique({ where: { id: updated.id }, include: ticketInclude })
|
||||
})
|
||||
return tx.ticket.findUnique({ where: { id: updated.id }, include: ticketInclude });
|
||||
});
|
||||
|
||||
res.json(ticket)
|
||||
})
|
||||
res.json(ticket);
|
||||
});
|
||||
|
||||
// DELETE /api/tickets/:id — admin only
|
||||
router.delete('/:id', requireAdmin, async (req, res) => {
|
||||
const ticket = await findByIdOrDisplay(req.params.id)
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' })
|
||||
await prisma.ticket.delete({ where: { id: ticket.id } })
|
||||
res.status(204).send()
|
||||
})
|
||||
const ticket = await findByIdOrDisplay(req.params.id);
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' });
|
||||
await prisma.ticket.delete({ where: { id: ticket.id } });
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
+42
-34
@@ -1,11 +1,12 @@
|
||||
import { Router } from 'express'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import crypto from 'crypto'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../lib/prisma'
|
||||
import { requireAdmin, AuthRequest } from '../middleware/auth'
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
import { z } from 'zod';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import prisma from '../lib/prisma';
|
||||
import { requireAdmin, AuthRequest } from '../middleware/auth';
|
||||
|
||||
const router = Router()
|
||||
const router = Router();
|
||||
|
||||
const userSelect = {
|
||||
id: true,
|
||||
@@ -15,15 +16,22 @@ const userSelect = {
|
||||
role: true,
|
||||
apiKey: true,
|
||||
createdAt: true,
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
const users = await prisma.user.findMany({
|
||||
select: { id: true, username: true, displayName: true, email: true, role: true, createdAt: true },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { displayName: 'asc' },
|
||||
})
|
||||
res.json(users)
|
||||
})
|
||||
});
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
router.post('/', requireAdmin, async (req, res) => {
|
||||
const data = z
|
||||
@@ -34,14 +42,14 @@ router.post('/', requireAdmin, async (req, res) => {
|
||||
password: z.string().min(8).optional(),
|
||||
role: z.enum(['ADMIN', 'AGENT', 'USER', 'SERVICE']).default('AGENT'),
|
||||
})
|
||||
.parse(req.body)
|
||||
.parse(req.body);
|
||||
|
||||
const passwordHash = data.password
|
||||
? await bcrypt.hash(data.password, 12)
|
||||
: await bcrypt.hash(crypto.randomBytes(32).toString('hex'), 12)
|
||||
: await bcrypt.hash(crypto.randomBytes(32).toString('hex'), 12);
|
||||
|
||||
const apiKey =
|
||||
data.role === 'SERVICE' ? `sk_${crypto.randomBytes(32).toString('hex')}` : undefined
|
||||
data.role === 'SERVICE' ? `sk_${crypto.randomBytes(32).toString('hex')}` : undefined;
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
@@ -53,10 +61,10 @@ router.post('/', requireAdmin, async (req, res) => {
|
||||
apiKey,
|
||||
},
|
||||
select: userSelect,
|
||||
})
|
||||
});
|
||||
|
||||
res.status(201).json(user)
|
||||
})
|
||||
res.status(201).json(user);
|
||||
});
|
||||
|
||||
router.patch('/:id', requireAdmin, async (req, res) => {
|
||||
const data = z
|
||||
@@ -67,37 +75,37 @@ router.patch('/:id', requireAdmin, async (req, res) => {
|
||||
role: z.enum(['ADMIN', 'AGENT', 'USER', 'SERVICE']).optional(),
|
||||
regenerateApiKey: z.boolean().optional(),
|
||||
})
|
||||
.parse(req.body)
|
||||
.parse(req.body);
|
||||
|
||||
const update: Record<string, any> = {}
|
||||
if (data.displayName) update.displayName = data.displayName
|
||||
if (data.email) update.email = data.email
|
||||
if (data.password) update.passwordHash = await bcrypt.hash(data.password, 12)
|
||||
const update: Prisma.UserUpdateInput = {};
|
||||
if (data.displayName) update.displayName = data.displayName;
|
||||
if (data.email) update.email = data.email;
|
||||
if (data.password) update.passwordHash = await bcrypt.hash(data.password, 12);
|
||||
if (data.role) {
|
||||
update.role = data.role
|
||||
update.role = data.role;
|
||||
if (data.role === 'SERVICE' && !update.apiKey) {
|
||||
update.apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`
|
||||
update.apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`;
|
||||
}
|
||||
}
|
||||
if (data.regenerateApiKey) {
|
||||
update.apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`
|
||||
update.apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`;
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id: req.params.id },
|
||||
data: update,
|
||||
select: userSelect,
|
||||
})
|
||||
});
|
||||
|
||||
res.json(user)
|
||||
})
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
router.delete('/:id', requireAdmin, async (req: AuthRequest, res) => {
|
||||
if (req.params.id === req.user!.id) {
|
||||
return res.status(400).json({ error: 'Cannot delete your own account' })
|
||||
return res.status(400).json({ error: 'Cannot delete your own account' });
|
||||
}
|
||||
await prisma.user.delete({ where: { id: req.params.id } })
|
||||
res.status(204).send()
|
||||
})
|
||||
await prisma.user.delete({ where: { id: req.params.id } });
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user