Merge SERVICE role into AGENT
Build & Push / Test (client) (push) Successful in 31s
Build & Push / Test (server) (push) Successful in 38s
Build & Push / Build Client (push) Successful in 1m17s
Build & Push / Build Server (push) Successful in 1m18s

Every AGENT now gets an auto-generated API key on creation, shown once
in a modal. AGENTs log in with password and authenticate to the API
with X-Api-Key. pre-push.sql defensively migrates any residual SERVICE
rows to AGENT before Prisma rewrites the enum. Goddard is no longer
baked into the seed — create agents via Admin → Users.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 22:44:32 -04:00
parent a9ba74f1af
commit d8785a964d
18 changed files with 73 additions and 130 deletions
+5 -7
View File
@@ -105,13 +105,11 @@ export default function NewTicketModal({ onClose }: NewTicketModalProps) {
<label className={labelClass}>Assignee</label>
<select className={inputClass} {...register('assigneeId')}>
<option value="">Unassigned</option>
{users
.filter((u) => u.role !== 'SERVICE')
.map((u) => (
<option key={u.id} value={u.id}>
{u.displayName}
</option>
))}
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.displayName}
</option>
))}
</select>
</div>
</div>
-25
View File
@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
import { Copy } from 'lucide-react';
import { toast } from 'sonner';
import Layout from '../components/Layout';
import { useAuth } from '../contexts/AuthContext';
@@ -47,12 +46,6 @@ export default function Settings() {
}
};
const copyKey = async () => {
if (!user?.apiKey) return;
await navigator.clipboard.writeText(user.apiKey);
toast.success('API key copied');
};
return (
<Layout title="Settings">
<div className="space-y-6">
@@ -118,24 +111,6 @@ export default function Settings() {
</p>
</section>
{/* API key (service accounts only) */}
{user?.role === 'SERVICE' && user?.apiKey && (
<section className="rounded-md border border-border p-4">
<h2 className="text-sm font-semibold mb-3">API key</h2>
<div className="flex items-center gap-2 bg-muted rounded-md px-3 py-2 font-mono text-xs break-all">
<span className="flex-1">{user.apiKey}</span>
<button
onClick={copyKey}
className="text-muted-foreground hover:text-foreground"
>
<Copy size={14} />
</button>
</div>
<p className="mt-2 text-xs text-muted-foreground">
Pass as <code>x-api-key</code> header on any server-to-server request.
</p>
</section>
)}
</div>
</Layout>
);
+1 -1
View File
@@ -243,7 +243,7 @@ export default function TicketDetail() {
}
const commentCount = ticket.comments?.length ?? 0;
const agentUsers = users.filter((u) => u.role !== 'SERVICE');
const agentUsers = users;
const statusOptions: { value: TicketStatus; label: string }[] = [
{ value: 'OPEN', label: 'Open' },
+1 -1
View File
@@ -214,7 +214,7 @@ export default function Tickets() {
setSearchInput(String(filters.search ?? ''));
};
const agentUsers = users.filter((u) => u.role !== 'SERVICE');
const agentUsers = users;
// Keyboard navigation
useEffect(() => {
+3 -7
View File
@@ -42,21 +42,18 @@ const ROLE_LABELS: Record<Role, string> = {
ADMIN: 'Admin',
AGENT: 'Agent',
USER: 'User',
SERVICE: 'Service',
};
const ROLE_BADGE: Record<Role, string> = {
ADMIN: 'bg-purple-500/20 text-purple-400 border-purple-500/30',
AGENT: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
USER: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
SERVICE: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
};
const ROLE_DESCRIPTIONS: Record<Role, string> = {
ADMIN: 'Full access — manage users, CTI config, close and delete tickets',
AGENT: 'Manage tickets — create, update, assign, comment, change status',
AGENT: 'Manage tickets and automation — logs in with password and can authenticate via API key',
USER: 'Basic access — view tickets and add comments only',
SERVICE: 'Automation account — authenticates via API key, no password login',
};
export default function AdminUsers() {
@@ -227,7 +224,7 @@ export default function AdminUsers() {
<td className="px-5 py-3 text-gray-400">{u.email}</td>
<td className="px-5 py-3">
<div className="flex items-center justify-end gap-2">
{u.role === 'SERVICE' && (
{u.role === 'AGENT' && (
<button
onClick={() => setRotating(u)}
className="text-gray-600 hover:text-gray-300 transition-colors"
@@ -343,7 +340,7 @@ export default function AdminUsers() {
type="password"
value={form.password}
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
required={modal === 'add' && form.role !== 'SERVICE'}
required={modal === 'add'}
className={inputClass}
placeholder={modal === 'edit' ? '••••••••' : ''}
/>
@@ -359,7 +356,6 @@ export default function AdminUsers() {
<option value="AGENT">Agent</option>
<option value="USER">User</option>
<option value="ADMIN">Admin</option>
<option value="SERVICE">Service</option>
</select>
<p className="mt-1.5 text-xs text-gray-500">{ROLE_DESCRIPTIONS[form.role]}</p>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
export type Role = 'ADMIN' | 'AGENT' | 'USER' | 'SERVICE';
export type Role = 'ADMIN' | 'AGENT' | 'USER';
export type TicketStatus = 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED';
export interface User {