Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 73 additions & 59 deletions src/modules/admin/admin.controllers.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,95 @@
import { AsyncController } from '../../types/auth.types';
import { sendSuccess, sendValidationError, sendNotFound } from '../../utils/api-response.utils';
import {
sendSuccess,
sendValidationError,
sendNotFound,
sendForbidden,
} from '../../utils/api-response.utils';
import { prisma } from '../../utils/prisma.utils';
import { emitAuditEvent } from '../../utils/audit.utils';
import { z } from 'zod';

const UpdateCreatorMetadataSchema = z.object({
isVerified: z.boolean().optional(),
isVerified: z.boolean().optional(),
});

type UpdateCreatorMetadataInput = z.infer<typeof UpdateCreatorMetadataSchema>;

export const httpUpdateCreatorMetadata: AsyncController = async (req, res, next) => {
try {
const { id } = req.params as { id: string };
const adminIdHeader = req.headers['x-admin-id'];
const actorId =
typeof adminIdHeader === 'string'
? adminIdHeader
: Array.isArray(adminIdHeader)
? adminIdHeader[0]
: undefined;
export const httpUpdateCreatorMetadata: AsyncController = async (
req,
res,
next
) => {
try {
const { id } = req.params as { id: string };
const adminIdHeader = req.headers['x-admin-id'];
const actorId =
typeof adminIdHeader === 'string'
? adminIdHeader
: Array.isArray(adminIdHeader)
? adminIdHeader[0]
: undefined;

if (!id || !actorId) {
return sendValidationError(res, 'Missing required parameters', [
{ field: 'id', message: 'Creator ID is required' },
{ field: 'x-admin-id', message: 'Admin ID header is required' },
]);
}
if (!actorId) {
return sendForbidden(res, 'Admin access required', [
{ field: 'x-admin-id', message: 'Admin ID header is required' },
]);
}

const parsed = UpdateCreatorMetadataSchema.safeParse(req.body);
if (!parsed.success) {
return sendValidationError(res, 'Invalid request body', [
{ field: 'body', message: 'Invalid metadata update' },
]);
}
if (!id) {
return sendValidationError(res, 'Missing required parameters', [
{ field: 'id', message: 'Creator ID is required' },
]);
}

const updates = parsed.data as UpdateCreatorMetadataInput;
const parsed = UpdateCreatorMetadataSchema.safeParse(req.body);
if (!parsed.success) {
return sendValidationError(res, 'Invalid request body', [
{ field: 'body', message: 'Invalid metadata update' },
]);
}

const creator = await prisma.creatorProfile.findUnique({
where: { id },
});
const updates = parsed.data as UpdateCreatorMetadataInput;

if (!creator) {
return sendNotFound(res, 'Creator');
}
const creator = await prisma.creatorProfile.findUnique({
where: { id },
});

const previousValues = {
isVerified: creator.isVerified,
};
if (!creator) {
return sendNotFound(res, 'Creator');
}

const updated = await prisma.creatorProfile.update({
where: { id },
data: updates,
});
const previousValues = {
isVerified: creator.isVerified,
};

const changes: Record<string, unknown> = {};
Object.entries(updates).forEach(([key, value]) => {
if (value !== previousValues[key as keyof typeof previousValues]) {
changes[key] = {
before: previousValues[key as keyof typeof previousValues],
after: value,
};
}
});
const updated = await prisma.creatorProfile.update({
where: { id },
data: updates,
});

if (Object.keys(changes).length > 0) {
await emitAuditEvent({
actor: actorId,
action: 'update_creator_metadata',
target: 'CreatorProfile',
targetId: id,
metadata: changes,
const changes: Record<string, unknown> = {};
Object.entries(updates).forEach(([key, value]) => {
if (value !== previousValues[key as keyof typeof previousValues]) {
changes[key] = {
before: previousValues[key as keyof typeof previousValues],
after: value,
};
}
});
}

sendSuccess(res, updated);
} catch (error) {
next(error);
}
if (Object.keys(changes).length > 0) {
await emitAuditEvent({
actor: actorId,
action: 'update_creator_metadata',
target: 'CreatorProfile',
targetId: id,
metadata: changes,
});
}

sendSuccess(res, updated);
} catch (error) {
next(error);
}
};
10 changes: 6 additions & 4 deletions src/utils/api-response.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,18 @@ export function sendNotFound(res: Response, resource: string): void {

export function sendUnauthorized(
res: Response,
message = 'Unauthorized access'
message = 'Unauthorized access',
details?: Array<{ field?: string; message: string }>
): void {
sendError(res, 401, ErrorCode.UNAUTHORIZED, message);
sendError(res, 401, ErrorCode.UNAUTHORIZED, message, details);
}

export function sendForbidden(
res: Response,
message = 'Access forbidden'
message = 'Access forbidden',
details?: Array<{ field?: string; message: string }>
): void {
sendError(res, 403, ErrorCode.FORBIDDEN, message);
sendError(res, 403, ErrorCode.FORBIDDEN, message, details);
}

export function sendConflict(res: Response, message: string): void {
Expand Down
65 changes: 65 additions & 0 deletions src/utils/test/api-response.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Response } from 'express';
import {
sendForbidden,
sendUnauthorized,
ErrorCode,
} from '../api-response.utils';

describe('api-response.utils', () => {
let mockResponse: Partial<Response>;
let jsonMock: jest.Mock;
let statusMock: jest.Mock;

beforeEach(() => {
jsonMock = jest.fn();
statusMock = jest.fn().mockReturnValue({ json: jsonMock });
mockResponse = {
status: statusMock,
};
});

describe('sendForbidden', () => {
it('should send a 403 response with default message', () => {
sendForbidden(mockResponse as Response);

expect(statusMock).toHaveBeenCalledWith(403);
expect(jsonMock).toHaveBeenCalledWith({
success: false,
error: {
code: ErrorCode.FORBIDDEN,
message: 'Access forbidden',
},
});
});

it('should send a 403 response with custom message and details', () => {
const details = [{ field: 'role', message: 'Required admin role' }];
sendForbidden(mockResponse as Response, 'Custom forbidden', details);

expect(statusMock).toHaveBeenCalledWith(403);
expect(jsonMock).toHaveBeenCalledWith({
success: false,
error: {
code: ErrorCode.FORBIDDEN,
message: 'Custom forbidden',
details,
},
});
});
});

describe('sendUnauthorized', () => {
it('should send a 401 response with default message', () => {
sendUnauthorized(mockResponse as Response);

expect(statusMock).toHaveBeenCalledWith(401);
expect(jsonMock).toHaveBeenCalledWith({
success: false,
error: {
code: ErrorCode.UNAUTHORIZED,
message: 'Unauthorized access',
},
});
});
});
});
Loading