Initial commit: TicketingSystem
Some checks failed
Build & Push / Build Client (push) Failing after 9s
Build & Push / Build Server (push) Failing after 28s

Internal ticketing app with CTI routing, severity levels, and n8n integration.

Stack: Express + TypeScript + Prisma + PostgreSQL / React + Vite + Tailwind
- JWT auth for users, API key auth for service accounts (Goddard/n8n)
- CTI hierarchy (Category > Type > Item) for ticket routing
- Severity 1-5, auto-close resolved tickets after 14 days
- Gitea Actions CI/CD building separate server/client images
- Production docker-compose.yml with Traefik integration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 19:38:32 -04:00
commit 21894fad7a
50 changed files with 3293 additions and 0 deletions

100
server/prisma/schema.prisma Normal file
View File

@@ -0,0 +1,100 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
ADMIN
AGENT
SERVICE
}
enum TicketStatus {
OPEN
IN_PROGRESS
RESOLVED
CLOSED
}
model User {
id String @id @default(cuid())
username String @unique
email String @unique
passwordHash String
displayName String
role Role @default(AGENT)
apiKey String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignedTickets Ticket[] @relation("AssignedTickets")
createdTickets Ticket[] @relation("CreatedTickets")
comments Comment[]
}
model Category {
id String @id @default(cuid())
name String @unique
types Type[]
tickets Ticket[]
}
model Type {
id String @id @default(cuid())
name String
categoryId String
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
items Item[]
tickets Ticket[]
@@unique([categoryId, name])
}
model Item {
id String @id @default(cuid())
name String
typeId String
type Type @relation(fields: [typeId], references: [id], onDelete: Cascade)
tickets Ticket[]
@@unique([typeId, name])
}
model Ticket {
id String @id @default(cuid())
title String
overview String
severity Int
status TicketStatus @default(OPEN)
categoryId String
typeId String
itemId String
assigneeId String?
createdById String
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
category Category @relation(fields: [categoryId], references: [id])
type Type @relation(fields: [typeId], references: [id])
item Item @relation(fields: [itemId], references: [id])
assignee User? @relation("AssignedTickets", fields: [assigneeId], references: [id])
createdBy User @relation("CreatedTickets", fields: [createdById], references: [id])
comments Comment[]
}
model Comment {
id String @id @default(cuid())
body String
ticketId String
authorId String
createdAt DateTime @default(now())
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id])
}

102
server/prisma/seed.ts Normal file
View File

@@ -0,0 +1,102 @@
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcryptjs'
import crypto from 'crypto'
const prisma = new PrismaClient()
async function main() {
console.log('Seeding database...')
// Admin user
await prisma.user.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
email: 'admin@internal',
displayName: 'Admin',
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({
where: { username: 'goddard' },
update: {},
create: {
username: 'goddard',
email: 'goddard@internal',
displayName: 'Goddard',
passwordHash: await bcrypt.hash(crypto.randomBytes(32).toString('hex'), 12),
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')
// 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.')
}
main()
.catch((e) => { console.error(e); process.exit(1) })
.finally(() => prisma.$disconnect())