Phase 1a: shared schemas, service layer, server tooling
- shared/schemas/: move Zod schemas out of routes so client + server share them - shared/types.ts: inferred types and enums for cross-package use - server tsconfig rootDir raised to ".." so shared/ compiles in-tree - server/src/services/: ticket, comment, cti, user, auth, notification (stub), search (stub) - Routes thinned to validate-delegate-return; business logic now testable in isolation - server/src/lib/httpError.ts: typed HttpError replaces ad-hoc throw shapes - server/src/lib/logger.ts: pino structured logging replaces console.log - autoClose job delegates to ticketService.closeStale() - express-rate-limit on /api/auth/login (10 / 15min / IP) - vitest + vitest-mock-extended; 20 service-level tests cover auth, ticket, comment, user flows - CI: lint + test jobs before docker builds Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import prisma from '../lib/prisma';
|
||||
import { HttpError } from '../lib/httpError';
|
||||
import type { CreateUserInput, UpdateUserInput } from '../../../shared/schemas/user';
|
||||
|
||||
const userSelect = {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
apiKey: true,
|
||||
createdAt: true,
|
||||
} as const;
|
||||
|
||||
const userListSelect = {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
} as const;
|
||||
|
||||
export function listUsers() {
|
||||
return prisma.user.findMany({
|
||||
select: userListSelect,
|
||||
orderBy: { displayName: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCurrentUser(id: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: userListSelect,
|
||||
});
|
||||
if (!user) throw new HttpError(404, 'User not found');
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createUser(data: CreateUserInput) {
|
||||
const passwordHash = data.password
|
||||
? await bcrypt.hash(data.password, 12)
|
||||
: await bcrypt.hash(crypto.randomBytes(32).toString('hex'), 12);
|
||||
|
||||
const apiKey =
|
||||
data.role === 'SERVICE' ? `sk_${crypto.randomBytes(32).toString('hex')}` : undefined;
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
displayName: data.displayName,
|
||||
passwordHash,
|
||||
role: data.role,
|
||||
apiKey,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUser(id: string, data: UpdateUserInput) {
|
||||
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;
|
||||
if (data.role === 'SERVICE' && !update.apiKey) {
|
||||
update.apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`;
|
||||
}
|
||||
}
|
||||
if (data.regenerateApiKey) {
|
||||
update.apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`;
|
||||
}
|
||||
|
||||
return prisma.user.update({ where: { id }, data: update, select: userSelect });
|
||||
}
|
||||
|
||||
export async function deleteUser(id: string, actorId: string) {
|
||||
if (id === actorId) {
|
||||
throw new HttpError(400, 'Cannot delete your own account');
|
||||
}
|
||||
await prisma.user.delete({ where: { id } });
|
||||
}
|
||||
Reference in New Issue
Block a user