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:
@@ -1,17 +1,17 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Plus, Pencil, Trash2, RefreshCw, Copy, Check } from 'lucide-react'
|
||||
import api from '../../api/client'
|
||||
import Layout from '../../components/Layout'
|
||||
import Modal from '../../components/Modal'
|
||||
import { User, Role } from '../../types'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Pencil, Trash2, RefreshCw, Copy, Check } from 'lucide-react';
|
||||
import api from '../../api/client';
|
||||
import Layout from '../../components/Layout';
|
||||
import Modal from '../../components/Modal';
|
||||
import { User, Role } from '../../types';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
interface UserForm {
|
||||
username: string
|
||||
email: string
|
||||
displayName: string
|
||||
password: string
|
||||
role: Role
|
||||
username: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
password: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
const BLANK_FORM: UserForm = {
|
||||
@@ -20,136 +20,149 @@ const BLANK_FORM: UserForm = {
|
||||
displayName: '',
|
||||
password: '',
|
||||
role: 'AGENT',
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
USER: 'Basic access — view tickets and add comments only',
|
||||
SERVICE: 'Automation account — authenticates via API key, no password login',
|
||||
}
|
||||
};
|
||||
|
||||
export default function AdminUsers() {
|
||||
const { user: authUser } = useAuth()
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [modal, setModal] = useState<'add' | 'edit' | null>(null)
|
||||
const [selected, setSelected] = useState<User | null>(null)
|
||||
const [form, setForm] = useState<UserForm>(BLANK_FORM)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null)
|
||||
const [newApiKey, setNewApiKey] = useState<string | null>(null)
|
||||
const { user: authUser } = useAuth();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [modal, setModal] = useState<'add' | 'edit' | null>(null);
|
||||
const [selected, setSelected] = useState<User | null>(null);
|
||||
const [form, setForm] = useState<UserForm>(BLANK_FORM);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||
const [newApiKey, setNewApiKey] = useState<string | null>(null);
|
||||
|
||||
const fetchUsers = () => {
|
||||
api.get<User[]>('/users').then((r) => setUsers(r.data))
|
||||
}
|
||||
api.get<User[]>('/users').then((r) => setUsers(r.data));
|
||||
};
|
||||
|
||||
useEffect(() => { fetchUsers() }, [])
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const openAdd = () => {
|
||||
setForm(BLANK_FORM)
|
||||
setError('')
|
||||
setNewApiKey(null)
|
||||
setModal('add')
|
||||
}
|
||||
setForm(BLANK_FORM);
|
||||
setError('');
|
||||
setNewApiKey(null);
|
||||
setModal('add');
|
||||
};
|
||||
|
||||
const openEdit = (u: User) => {
|
||||
setSelected(u)
|
||||
setForm({ username: u.username, email: u.email, displayName: u.displayName, password: '', role: u.role })
|
||||
setError('')
|
||||
setNewApiKey(null)
|
||||
setModal('edit')
|
||||
}
|
||||
setSelected(u);
|
||||
setForm({
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
displayName: u.displayName,
|
||||
password: '',
|
||||
role: u.role,
|
||||
});
|
||||
setError('');
|
||||
setNewApiKey(null);
|
||||
setModal('edit');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModal(null)
|
||||
setSelected(null)
|
||||
setNewApiKey(null)
|
||||
fetchUsers()
|
||||
}
|
||||
setModal(null);
|
||||
setSelected(null);
|
||||
setNewApiKey(null);
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSubmitting(true)
|
||||
setError('')
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
const payload: Record<string, string> = {
|
||||
username: form.username,
|
||||
email: form.email,
|
||||
displayName: form.displayName,
|
||||
role: form.role,
|
||||
}
|
||||
if (form.password) payload.password = form.password
|
||||
const res = await api.post<User>('/users', payload)
|
||||
if (res.data.apiKey) setNewApiKey(res.data.apiKey)
|
||||
else closeModal()
|
||||
};
|
||||
if (form.password) payload.password = form.password;
|
||||
const res = await api.post<User>('/users', payload);
|
||||
if (res.data.apiKey) setNewApiKey(res.data.apiKey);
|
||||
else closeModal();
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { data?: { error?: string } } }
|
||||
setError(e.response?.data?.error ?? 'Failed to create user')
|
||||
const e = err as { response?: { data?: { error?: string } } };
|
||||
setError(e.response?.data?.error ?? 'Failed to create user');
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!selected) return
|
||||
setSubmitting(true)
|
||||
setError('')
|
||||
e.preventDefault();
|
||||
if (!selected) return;
|
||||
setSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
const payload: Record<string, string> = {
|
||||
email: form.email,
|
||||
displayName: form.displayName,
|
||||
role: form.role,
|
||||
}
|
||||
if (form.password) payload.password = form.password
|
||||
await api.patch(`/users/${selected.id}`, payload)
|
||||
closeModal()
|
||||
};
|
||||
if (form.password) payload.password = form.password;
|
||||
await api.patch(`/users/${selected.id}`, payload);
|
||||
closeModal();
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { data?: { error?: string } } }
|
||||
setError(e.response?.data?.error ?? 'Failed to update user')
|
||||
const e = err as { response?: { data?: { error?: string } } };
|
||||
setError(e.response?.data?.error ?? 'Failed to update user');
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (u: User) => {
|
||||
if (!confirm(`Delete user "${u.displayName}"?`)) return
|
||||
await api.delete(`/users/${u.id}`)
|
||||
fetchUsers()
|
||||
}
|
||||
if (!confirm(`Delete user "${u.displayName}"?`)) return;
|
||||
await api.delete(`/users/${u.id}`);
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
const handleRegenerateKey = async (u: User) => {
|
||||
if (!confirm(`Regenerate API key for "${u.displayName}"? The old key will stop working immediately.`)) return
|
||||
const res = await api.patch<User>(`/users/${u.id}`, { regenerateApiKey: true })
|
||||
setNewApiKey(res.data.apiKey ?? null)
|
||||
setSelected(u)
|
||||
setModal('edit')
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`Regenerate API key for "${u.displayName}"? The old key will stop working immediately.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
const res = await api.patch<User>(`/users/${u.id}`, { regenerateApiKey: true });
|
||||
setNewApiKey(res.data.apiKey ?? null);
|
||||
setSelected(u);
|
||||
setModal('edit');
|
||||
};
|
||||
|
||||
const copyToClipboard = (key: string) => {
|
||||
navigator.clipboard.writeText(key)
|
||||
setCopiedKey(key)
|
||||
setTimeout(() => setCopiedKey(null), 2000)
|
||||
}
|
||||
navigator.clipboard.writeText(key);
|
||||
setCopiedKey(key);
|
||||
setTimeout(() => setCopiedKey(null), 2000);
|
||||
};
|
||||
|
||||
const inputClass =
|
||||
'w-full bg-gray-800 border border-gray-700 text-gray-100 placeholder-gray-500 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'
|
||||
const labelClass = 'block text-sm font-medium text-gray-300 mb-1'
|
||||
'w-full bg-gray-800 border border-gray-700 text-gray-100 placeholder-gray-500 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent';
|
||||
const labelClass = 'block text-sm font-medium text-gray-300 mb-1';
|
||||
|
||||
return (
|
||||
<Layout
|
||||
@@ -189,7 +202,9 @@ export default function AdminUsers() {
|
||||
<td className="px-5 py-3 font-medium text-gray-100">{u.displayName}</td>
|
||||
<td className="px-5 py-3 text-gray-500 font-mono text-xs">{u.username}</td>
|
||||
<td className="px-5 py-3">
|
||||
<span className={`inline-flex px-2 py-0.5 rounded text-xs font-medium border ${ROLE_BADGE[u.role]}`}>
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 rounded text-xs font-medium border ${ROLE_BADGE[u.role]}`}
|
||||
>
|
||||
{ROLE_LABELS[u.role]}
|
||||
</span>
|
||||
</td>
|
||||
@@ -237,7 +252,7 @@ export default function AdminUsers() {
|
||||
<div className="space-y-4">
|
||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-4">
|
||||
<p className="text-sm font-medium text-amber-400 mb-2">
|
||||
API Key — copy it now, it won't be shown again
|
||||
API Key — copy it now, it won't be shown again
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 text-xs bg-gray-800 border border-gray-700 text-gray-300 rounded px-3 py-2 font-mono break-all">
|
||||
@@ -354,5 +369,5 @@ export default function AdminUsers() {
|
||||
</Modal>
|
||||
)}
|
||||
</Layout>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user