Merge SERVICE role into AGENT
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:
@@ -51,24 +51,4 @@ describe('authService.login', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects SERVICE role from password login', async () => {
|
||||
const password = 'svc-pw';
|
||||
prismaMock.user.findUnique.mockResolvedValue({
|
||||
id: 'svc',
|
||||
username: 'goddard',
|
||||
email: 'g@x.io',
|
||||
displayName: 'Goddard',
|
||||
passwordHash: await bcrypt.hash(password, 4),
|
||||
role: 'SERVICE',
|
||||
apiKey: 'sk_xyz',
|
||||
notificationPrefs: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await expect(login({ username: 'goddard', password })).rejects.toMatchObject({
|
||||
status: 401,
|
||||
message: expect.stringMatching(/API key/i),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,6 @@ export async function login({ username, password }: LoginInput) {
|
||||
throw new HttpError(401, 'Invalid credentials');
|
||||
}
|
||||
|
||||
if (user.role === 'SERVICE') {
|
||||
throw new HttpError(401, 'Service accounts must authenticate via API key');
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, role: user.role, username: user.username },
|
||||
process.env.JWT_SECRET!,
|
||||
|
||||
@@ -17,15 +17,15 @@ const stubUser = {
|
||||
};
|
||||
|
||||
describe('userService.createUser', () => {
|
||||
it('hashes the password and omits apiKey for non-SERVICE roles', async () => {
|
||||
prismaMock.user.create.mockResolvedValue(stubUser);
|
||||
it('hashes the password and omits apiKey for ADMIN and USER roles', async () => {
|
||||
prismaMock.user.create.mockResolvedValue({ ...stubUser, role: 'USER' });
|
||||
|
||||
await createUser({
|
||||
username: 'bob',
|
||||
email: 'b@x.io',
|
||||
displayName: 'Bob',
|
||||
password: 'hunter2!',
|
||||
role: 'AGENT',
|
||||
role: 'USER',
|
||||
});
|
||||
|
||||
const call = prismaMock.user.create.mock.calls[0][0] as { data: Record<string, unknown> };
|
||||
@@ -34,14 +34,15 @@ describe('userService.createUser', () => {
|
||||
expect(call.data.apiKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('assigns an apiKey for SERVICE role', async () => {
|
||||
prismaMock.user.create.mockResolvedValue({ ...stubUser, role: 'SERVICE' });
|
||||
it('assigns an apiKey for AGENT role', async () => {
|
||||
prismaMock.user.create.mockResolvedValue(stubUser);
|
||||
|
||||
await createUser({
|
||||
username: 'svc',
|
||||
email: 's@x.io',
|
||||
displayName: 'Svc',
|
||||
role: 'SERVICE',
|
||||
username: 'agent',
|
||||
email: 'a@x.io',
|
||||
displayName: 'Agent',
|
||||
password: 'hunter2!',
|
||||
role: 'AGENT',
|
||||
});
|
||||
|
||||
const call = prismaMock.user.create.mock.calls[0][0] as { data: Record<string, unknown> };
|
||||
|
||||
@@ -41,12 +41,10 @@ export async function getCurrentUser(id: string) {
|
||||
}
|
||||
|
||||
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 passwordHash = await bcrypt.hash(data.password, 12);
|
||||
|
||||
const apiKey =
|
||||
data.role === 'SERVICE' ? `sk_${crypto.randomBytes(32).toString('hex')}` : undefined;
|
||||
data.role === 'AGENT' ? `sk_${crypto.randomBytes(32).toString('hex')}` : undefined;
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
@@ -68,8 +66,13 @@ export async function updateUser(id: string, data: UpdateUserInput) {
|
||||
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.role === 'AGENT') {
|
||||
const existing = await prisma.user.findUnique({ where: { id }, select: { apiKey: true } });
|
||||
if (!existing?.apiKey) {
|
||||
update.apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`;
|
||||
}
|
||||
} else {
|
||||
update.apiKey = null;
|
||||
}
|
||||
}
|
||||
if (data.regenerateApiKey) {
|
||||
|
||||
Reference in New Issue
Block a user