mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-30 11:41:05 -05:00
feat: audit logs (#5866)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { ApiAuditLog } from "@/app/lib/api/with-api-logging";
|
||||
import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit";
|
||||
import { formatZodError, handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { ZodRawShape, z } from "zod";
|
||||
@@ -8,10 +9,12 @@ export type HandlerFn<TInput = Record<string, unknown>> = ({
|
||||
authentication,
|
||||
parsedInput,
|
||||
request,
|
||||
auditLog,
|
||||
}: {
|
||||
authentication: TAuthenticationApiKey;
|
||||
parsedInput: TInput;
|
||||
request: Request;
|
||||
auditLog?: ApiAuditLog;
|
||||
}) => Promise<Response>;
|
||||
|
||||
export type ExtendedSchemas = {
|
||||
@@ -41,18 +44,25 @@ export const apiWrapper = async <S extends ExtendedSchemas>({
|
||||
externalParams,
|
||||
rateLimit = true,
|
||||
handler,
|
||||
auditLog,
|
||||
}: {
|
||||
request: Request;
|
||||
schemas?: S;
|
||||
externalParams?: Promise<Record<string, any>>;
|
||||
rateLimit?: boolean;
|
||||
handler: HandlerFn<ParsedSchemas<S>>;
|
||||
auditLog?: ApiAuditLog;
|
||||
}): Promise<Response> => {
|
||||
const authentication = await authenticateRequest(request);
|
||||
if (!authentication.ok) {
|
||||
return handleApiError(request, authentication.error);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.userId = authentication.data.apiKeyId;
|
||||
auditLog.organizationId = authentication.data.organizationId;
|
||||
}
|
||||
|
||||
let parsedInput: ParsedSchemas<S> = {} as ParsedSchemas<S>;
|
||||
|
||||
if (schemas?.body) {
|
||||
@@ -106,5 +116,6 @@ export const apiWrapper = async <S extends ExtendedSchemas>({
|
||||
authentication: authentication.data,
|
||||
parsedInput,
|
||||
request,
|
||||
auditLog,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { buildAuditLogBaseObject } from "@/app/lib/api/with-api-logging";
|
||||
import { handleApiError, logApiRequest } from "@/modules/api/v2/lib/utils";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { TAuditAction, TAuditTarget } from "@/modules/ee/audit-logs/types/audit-log";
|
||||
import { ExtendedSchemas, HandlerFn, ParsedSchemas, apiWrapper } from "./api-wrapper";
|
||||
|
||||
export const authenticatedApiClient = async <S extends ExtendedSchemas>({
|
||||
@@ -8,24 +10,35 @@ export const authenticatedApiClient = async <S extends ExtendedSchemas>({
|
||||
externalParams,
|
||||
rateLimit = true,
|
||||
handler,
|
||||
action,
|
||||
targetType,
|
||||
}: {
|
||||
request: Request;
|
||||
schemas?: S;
|
||||
externalParams?: Promise<Record<string, any>>;
|
||||
rateLimit?: boolean;
|
||||
handler: HandlerFn<ParsedSchemas<S>>;
|
||||
action?: TAuditAction;
|
||||
targetType?: TAuditTarget;
|
||||
}): Promise<Response> => {
|
||||
try {
|
||||
const auditLog =
|
||||
action && targetType ? buildAuditLogBaseObject(action, targetType, request.url) : undefined;
|
||||
|
||||
const response = await apiWrapper({
|
||||
request,
|
||||
schemas,
|
||||
externalParams,
|
||||
rateLimit,
|
||||
handler,
|
||||
auditLog,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logApiRequest(request, response.status);
|
||||
if (auditLog) {
|
||||
auditLog.status = "success";
|
||||
}
|
||||
logApiRequest(request, response.status, auditLog);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -18,6 +18,9 @@ vi.mock("@sentry/nextjs", () => ({
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
SENTRY_DSN: "mocked-sentry-dsn",
|
||||
IS_PRODUCTION: true,
|
||||
AUDIT_LOG_ENABLED: true,
|
||||
ENCRYPTION_KEY: "mocked-encryption-key",
|
||||
REDIS_URL: "mock-url",
|
||||
}));
|
||||
|
||||
describe("utils", () => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Function is this file can be used in edge runtime functions, like api routes.
|
||||
import { IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
export const logApiErrorEdge = (request: Request, error: ApiErrorResponseV2): void => {
|
||||
const correlationId = request.headers.get("x-request-id") ?? "";
|
||||
|
||||
// Send the error to Sentry if the DSN is set and the error type is internal_server_error
|
||||
// This is useful for tracking down issues without overloading Sentry with errors
|
||||
if (SENTRY_DSN && IS_PRODUCTION && error.type === "internal_server_error") {
|
||||
const err = new Error(`API V2 error, id: ${correlationId}`);
|
||||
|
||||
Sentry.captureException(err, {
|
||||
extra: {
|
||||
details: error.details,
|
||||
type: error.type,
|
||||
correlationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger
|
||||
.withContext({
|
||||
correlationId,
|
||||
error,
|
||||
})
|
||||
.error("API Error Details");
|
||||
};
|
||||
@@ -1,14 +1,19 @@
|
||||
// @ts-nocheck // We can remove this when we update the prisma client and the typescript version
|
||||
// if we don't add this we get build errors with prisma due to type-nesting
|
||||
import { IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants";
|
||||
import { AUDIT_LOG_ENABLED } from "@/lib/constants";
|
||||
import { responses } from "@/modules/api/v2/lib/response";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { queueAuditEvent } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { ZodCustomIssue, ZodIssue } from "zod";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { logApiErrorEdge } from "./utils-edge";
|
||||
|
||||
export const handleApiError = (request: Request, err: ApiErrorResponseV2): Response => {
|
||||
logApiError(request, err);
|
||||
export const handleApiError = (
|
||||
request: Request,
|
||||
err: ApiErrorResponseV2,
|
||||
auditLog?: ApiAuditLog
|
||||
): Response => {
|
||||
logApiError(request, err, auditLog);
|
||||
|
||||
switch (err.type) {
|
||||
case "bad_request":
|
||||
@@ -50,7 +55,7 @@ export const formatZodError = (error: { issues: (ZodIssue | ZodCustomIssue)[] })
|
||||
});
|
||||
};
|
||||
|
||||
export const logApiRequest = (request: Request, responseStatus: number): void => {
|
||||
export const logApiRequest = (request: Request, responseStatus: number, auditLog?: ApiAuditLog): void => {
|
||||
const method = request.method;
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname;
|
||||
@@ -73,29 +78,22 @@ export const logApiRequest = (request: Request, responseStatus: number): void =>
|
||||
queryParams: safeQueryParams,
|
||||
})
|
||||
.info("API Request Details");
|
||||
|
||||
logAuditLog(request, auditLog);
|
||||
};
|
||||
|
||||
export const logApiError = (request: Request, error: ApiErrorResponseV2): void => {
|
||||
const correlationId = request.headers.get("x-request-id") ?? "";
|
||||
export const logApiError = (request: Request, error: ApiErrorResponseV2, auditLog?: ApiAuditLog): void => {
|
||||
logApiErrorEdge(request, error);
|
||||
|
||||
// Send the error to Sentry if the DSN is set and the error type is internal_server_error
|
||||
// This is useful for tracking down issues without overloading Sentry with errors
|
||||
if (SENTRY_DSN && IS_PRODUCTION && error.type === "internal_server_error") {
|
||||
const err = new Error(`API V2 error, id: ${correlationId}`);
|
||||
logAuditLog(request, auditLog);
|
||||
};
|
||||
|
||||
Sentry.captureException(err, {
|
||||
extra: {
|
||||
details: error.details,
|
||||
type: error.type,
|
||||
correlationId,
|
||||
},
|
||||
});
|
||||
const logAuditLog = (request: Request, auditLog?: ApiAuditLog): void => {
|
||||
if (AUDIT_LOG_ENABLED && auditLog) {
|
||||
const correlationId = request.headers.get("x-request-id") ?? "";
|
||||
queueAuditEvent({
|
||||
...auditLog,
|
||||
eventId: correlationId,
|
||||
}).catch((err) => logger.error({ err, correlationId }, "Failed to queue audit event from logApiError"));
|
||||
}
|
||||
|
||||
logger
|
||||
.withContext({
|
||||
correlationId,
|
||||
error,
|
||||
})
|
||||
.error("API Error Details");
|
||||
};
|
||||
|
||||
+59
-22
@@ -56,36 +56,55 @@ export const PUT = async (
|
||||
body: ZContactAttributeKeyUpdateSchema,
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { params, body } = parsedInput;
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params.contactAttributeKeyId;
|
||||
}
|
||||
|
||||
const res = await getContactAttributeKey(params.contactAttributeKeyId);
|
||||
|
||||
if (!res.ok) {
|
||||
return handleApiError(request, res.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, res.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "PUT")) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "environment", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "environment", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
if (res.data.isUnique) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "contactAttributeKey", issue: "cannot update unique contact attribute key" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "contactAttributeKey", issue: "cannot update unique contact attribute key" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const updatedContactAttributeKey = await updateContactAttributeKey(params.contactAttributeKeyId, body);
|
||||
|
||||
if (!updatedContactAttributeKey.ok) {
|
||||
return handleApiError(request, updatedContactAttributeKey.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, updatedContactAttributeKey.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = res.data;
|
||||
auditLog.newObject = updatedContactAttributeKey.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(updatedContactAttributeKey);
|
||||
},
|
||||
action: "updated",
|
||||
targetType: "contactAttributeKey",
|
||||
});
|
||||
|
||||
export const DELETE = async (
|
||||
@@ -98,35 +117,53 @@ export const DELETE = async (
|
||||
params: z.object({ contactAttributeKeyId: ZContactAttributeKeyIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { params } = parsedInput;
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params.contactAttributeKeyId;
|
||||
}
|
||||
|
||||
const res = await getContactAttributeKey(params.contactAttributeKeyId);
|
||||
|
||||
if (!res.ok) {
|
||||
return handleApiError(request, res.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, res.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "DELETE")) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "environment", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "environment", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
if (res.data.isUnique) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "contactAttributeKey", issue: "cannot delete unique contact attribute key" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "contactAttributeKey", issue: "cannot delete unique contactAttributeKey" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const deletedContactAttributeKey = await deleteContactAttributeKey(params.contactAttributeKeyId);
|
||||
|
||||
if (!deletedContactAttributeKey.ok) {
|
||||
return handleApiError(request, deletedContactAttributeKey.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, deletedContactAttributeKey.error as ApiErrorResponseV2, auditLog); // NOSONAR // We need to assert or we get a type error
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = res.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(deletedContactAttributeKey);
|
||||
},
|
||||
action: "deleted",
|
||||
targetType: "contactAttributeKey",
|
||||
});
|
||||
|
||||
@@ -51,24 +51,35 @@ export const POST = async (request: NextRequest) =>
|
||||
schemas: {
|
||||
body: ZContactAttributeKeyInput,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { body } = parsedInput;
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, body.environmentId, "POST")) {
|
||||
return handleApiError(request, {
|
||||
type: "forbidden",
|
||||
details: [
|
||||
{ field: "environmentId", issue: "does not have permission to create contact attribute key" },
|
||||
],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "forbidden",
|
||||
details: [
|
||||
{ field: "environmentId", issue: "does not have permission to create contact attribute key" },
|
||||
],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const createContactAttributeKeyResult = await createContactAttributeKey(body);
|
||||
|
||||
if (!createContactAttributeKeyResult.ok) {
|
||||
return handleApiError(request, createContactAttributeKeyResult.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, createContactAttributeKeyResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = createContactAttributeKeyResult.data.id;
|
||||
auditLog.newObject = createContactAttributeKeyResult.data;
|
||||
}
|
||||
|
||||
return responses.createdResponse(createContactAttributeKeyResult);
|
||||
},
|
||||
action: "created",
|
||||
targetType: "contactAttributeKey",
|
||||
});
|
||||
|
||||
@@ -59,35 +59,53 @@ export const DELETE = async (request: Request, props: { params: Promise<{ respon
|
||||
params: z.object({ responseId: ZResponseIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { params } = parsedInput;
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params.responseId;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "params", issue: "missing" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "params", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const environmentIdResult = await getEnvironmentId(params.responseId, true);
|
||||
if (!environmentIdResult.ok) {
|
||||
return handleApiError(request, environmentIdResult.error);
|
||||
return handleApiError(request, environmentIdResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, environmentIdResult.data, "DELETE")) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const response = await deleteResponse(params.responseId);
|
||||
|
||||
if (!response.ok) {
|
||||
return handleApiError(request, response.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, response.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = response.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(response);
|
||||
},
|
||||
action: "deleted",
|
||||
targetType: "response",
|
||||
});
|
||||
|
||||
export const PUT = (request: Request, props: { params: Promise<{ responseId: string }> }) =>
|
||||
@@ -98,44 +116,56 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
|
||||
params: z.object({ responseId: ZResponseIdSchema }),
|
||||
body: ZResponseUpdateSchema,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { body, params } = parsedInput;
|
||||
|
||||
if (!body || !params) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: !body ? "body" : "params", issue: "missing" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: !body ? "body" : "params", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const environmentIdResult = await getEnvironmentId(params.responseId, true);
|
||||
if (!environmentIdResult.ok) {
|
||||
return handleApiError(request, environmentIdResult.error);
|
||||
return handleApiError(request, environmentIdResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, environmentIdResult.data, "PUT")) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const existingResponse = await getResponse(params.responseId);
|
||||
|
||||
if (!existingResponse.ok) {
|
||||
return handleApiError(request, existingResponse.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, existingResponse.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
|
||||
const questionsResponse = await getSurveyQuestions(existingResponse.data.surveyId);
|
||||
|
||||
if (!questionsResponse.ok) {
|
||||
return handleApiError(request, questionsResponse.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, questionsResponse.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
|
||||
if (!validateFileUploads(body.data, questionsResponse.data.questions)) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "response", issue: "Invalid file upload response" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "response", issue: "Invalid file upload response" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
// Validate response data for "other" options exceeding character limit
|
||||
@@ -163,9 +193,16 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
|
||||
const response = await updateResponse(params.responseId, body);
|
||||
|
||||
if (!response.ok) {
|
||||
return handleApiError(request, response.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, response.error as ApiErrorResponseV2, auditLog); // NOSONAR // We need to assert or we get a type error
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = existingResponse.data;
|
||||
auditLog.newObject = response.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(response);
|
||||
},
|
||||
action: "updated",
|
||||
targetType: "response",
|
||||
});
|
||||
|
||||
@@ -51,28 +51,36 @@ export const POST = async (request: Request) =>
|
||||
schemas: {
|
||||
body: ZResponseInput,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { body } = parsedInput;
|
||||
|
||||
if (!body) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "body", issue: "missing" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "body", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const environmentIdResult = await getEnvironmentId(body.surveyId, false);
|
||||
|
||||
if (!environmentIdResult.ok) {
|
||||
return handleApiError(request, environmentIdResult.error);
|
||||
return handleApiError(request, environmentIdResult.error, auditLog);
|
||||
}
|
||||
|
||||
const environmentId = environmentIdResult.data;
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, environmentId, "POST")) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
// if there is a createdAt but no updatedAt, set updatedAt to createdAt
|
||||
@@ -82,14 +90,18 @@ export const POST = async (request: Request) =>
|
||||
|
||||
const surveyQuestions = await getSurveyQuestions(body.surveyId);
|
||||
if (!surveyQuestions.ok) {
|
||||
return handleApiError(request, surveyQuestions.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, surveyQuestions.error as ApiErrorResponseV2, auditLog); // NOSONAR // We need to assert or we get a type error
|
||||
}
|
||||
|
||||
if (!validateFileUploads(body.data, surveyQuestions.data.questions)) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "response", issue: "Invalid file upload response" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "response", issue: "Invalid file upload response" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
// Validate response data for "other" options exceeding character limit
|
||||
@@ -116,9 +128,16 @@ export const POST = async (request: Request) =>
|
||||
|
||||
const createResponseResult = await createResponse(environmentId, body);
|
||||
if (!createResponseResult.ok) {
|
||||
return handleApiError(request, createResponseResult.error);
|
||||
return handleApiError(request, createResponseResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = createResponseResult.data.id;
|
||||
auditLog.newObject = createResponseResult.data;
|
||||
}
|
||||
|
||||
return responses.createdResponse({ data: createResponseResult.data });
|
||||
},
|
||||
action: "created",
|
||||
targetType: "response",
|
||||
});
|
||||
|
||||
@@ -58,55 +58,77 @@ export const PUT = async (request: NextRequest, props: { params: Promise<{ webho
|
||||
body: ZWebhookUpdateSchema,
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { params, body } = parsedInput;
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params?.webhookId;
|
||||
}
|
||||
|
||||
if (!body || !params) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: !body ? "body" : "params", issue: "missing" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: !body ? "body" : "params", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
// get surveys environment
|
||||
const surveysEnvironmentId = await getEnvironmentIdFromSurveyIds(body.surveyIds);
|
||||
|
||||
if (!surveysEnvironmentId.ok) {
|
||||
return handleApiError(request, surveysEnvironmentId.error);
|
||||
return handleApiError(request, surveysEnvironmentId.error, auditLog);
|
||||
}
|
||||
|
||||
// get webhook environment
|
||||
const webhook = await getWebhook(params.webhookId);
|
||||
|
||||
if (!webhook.ok) {
|
||||
return handleApiError(request, webhook.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, webhook.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "PUT")) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "webhook", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "webhook", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
// check if webhook environment matches the surveys environment
|
||||
if (webhook.data.environmentId !== surveysEnvironmentId.data) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [
|
||||
{ field: "surveys id", issue: "webhook environment does not match the surveys environment" },
|
||||
],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [
|
||||
{ field: "surveys id", issue: "webhook environment does not match the surveys environment" },
|
||||
],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const updatedWebhook = await updateWebhook(params.webhookId, body);
|
||||
|
||||
if (!updatedWebhook.ok) {
|
||||
return handleApiError(request, updatedWebhook.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, updatedWebhook.error as ApiErrorResponseV2, auditLog); // NOSONAR // We need to assert or we get a type error
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = webhook.data;
|
||||
auditLog.newObject = updatedWebhook.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(updatedWebhook);
|
||||
},
|
||||
action: "updated",
|
||||
targetType: "webhook",
|
||||
});
|
||||
|
||||
export const DELETE = async (request: NextRequest, props: { params: Promise<{ webhookId: string }> }) =>
|
||||
@@ -116,35 +138,52 @@ export const DELETE = async (request: NextRequest, props: { params: Promise<{ we
|
||||
params: z.object({ webhookId: ZWebhookIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { params } = parsedInput;
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params?.webhookId;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "params", issue: "missing" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "params", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const webhook = await getWebhook(params.webhookId);
|
||||
|
||||
if (!webhook.ok) {
|
||||
return handleApiError(request, webhook.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, webhook.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "DELETE")) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "webhook", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "webhook", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const deletedWebhook = await deleteWebhook(params.webhookId);
|
||||
|
||||
if (!deletedWebhook.ok) {
|
||||
return handleApiError(request, deletedWebhook.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, deletedWebhook.error as ApiErrorResponseV2, auditLog); // NOSONAR // We need to assert or we get a type error
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = webhook.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(deletedWebhook);
|
||||
},
|
||||
action: "deleted",
|
||||
targetType: "webhook",
|
||||
});
|
||||
|
||||
@@ -43,35 +43,50 @@ export const POST = async (request: NextRequest) =>
|
||||
schemas: {
|
||||
body: ZWebhookInput,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { body } = parsedInput;
|
||||
|
||||
if (!body) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "body", issue: "missing" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "body", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const environmentIdResult = await getEnvironmentIdFromSurveyIds(body.surveyIds);
|
||||
|
||||
if (!environmentIdResult.ok) {
|
||||
return handleApiError(request, environmentIdResult.error);
|
||||
return handleApiError(request, environmentIdResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, body.environmentId, "POST")) {
|
||||
return handleApiError(request, {
|
||||
type: "forbidden",
|
||||
details: [{ field: "environmentId", issue: "does not have permission to create webhook" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "forbidden",
|
||||
details: [{ field: "environmentId", issue: "does not have permission to create webhook" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const createWebhookResult = await createWebhook(body);
|
||||
|
||||
if (!createWebhookResult.ok) {
|
||||
return handleApiError(request, createWebhookResult.error);
|
||||
return handleApiError(request, createWebhookResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = createWebhookResult.data.id;
|
||||
auditLog.newObject = createWebhookResult.data;
|
||||
}
|
||||
|
||||
return responses.createdResponse(createWebhookResult);
|
||||
},
|
||||
action: "created",
|
||||
targetType: "webhook",
|
||||
});
|
||||
|
||||
@@ -4,7 +4,9 @@ import { handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { hasOrganizationIdAndAccess } from "@/modules/api/v2/organizations/[organizationId]/lib/utils";
|
||||
import { checkAuthenticationAndAccess } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils";
|
||||
import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations";
|
||||
import { UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
||||
import { z } from "zod";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { OrganizationAccessType } from "@formbricks/types/api-key";
|
||||
import {
|
||||
createProjectTeam,
|
||||
@@ -53,20 +55,28 @@ export async function POST(request: Request, props: { params: Promise<{ organiza
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ parsedInput: { body, params }, authentication }) => {
|
||||
handler: async ({ parsedInput: { body, params }, authentication, auditLog }) => {
|
||||
const { teamId, projectId } = body!;
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = `${projectId}-${teamId}`;
|
||||
}
|
||||
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const hasAccess = await checkAuthenticationAndAccess(teamId, projectId, authentication);
|
||||
|
||||
if (!hasAccess.ok) {
|
||||
return handleApiError(request, hasAccess.error);
|
||||
return handleApiError(request, hasAccess.error, auditLog);
|
||||
}
|
||||
|
||||
// check if project team already exists
|
||||
@@ -80,22 +90,32 @@ export async function POST(request: Request, props: { params: Promise<{ organiza
|
||||
});
|
||||
|
||||
if (!existingProjectTeam.ok) {
|
||||
return handleApiError(request, existingProjectTeam.error);
|
||||
return handleApiError(request, existingProjectTeam.error, auditLog);
|
||||
}
|
||||
|
||||
if (existingProjectTeam.data.data.length > 0) {
|
||||
return handleApiError(request, {
|
||||
type: "conflict",
|
||||
details: [{ field: "projectTeam", issue: "Project team already exists" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "conflict",
|
||||
details: [{ field: "projectTeam", issue: "Project team already exists" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
const result = await createProjectTeam(body!);
|
||||
if (!result.ok) {
|
||||
return handleApiError(request, result.error);
|
||||
return handleApiError(request, result.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.newObject = result.data;
|
||||
}
|
||||
|
||||
return responses.successResponse({ data: result.data });
|
||||
},
|
||||
action: "created",
|
||||
targetType: "projectTeam",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,29 +127,65 @@ export async function PUT(request: Request, props: { params: Promise<{ organizat
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ parsedInput: { body, params }, authentication }) => {
|
||||
handler: async ({ parsedInput: { body, params }, authentication, auditLog }) => {
|
||||
const { teamId, projectId } = body!;
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = `${projectId}-${teamId}`;
|
||||
}
|
||||
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const hasAccess = await checkAuthenticationAndAccess(teamId, projectId, authentication);
|
||||
|
||||
if (!hasAccess.ok) {
|
||||
return handleApiError(request, hasAccess.error);
|
||||
return handleApiError(request, hasAccess.error, auditLog);
|
||||
}
|
||||
|
||||
// Fetch old object for audit log
|
||||
let oldProjectTeamData: any = UNKNOWN_DATA;
|
||||
try {
|
||||
const oldProjectTeamResult = await getProjectTeams(authentication.organizationId, {
|
||||
teamId,
|
||||
projectId,
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
sortBy: "createdAt",
|
||||
order: "desc",
|
||||
});
|
||||
|
||||
if (oldProjectTeamResult.ok && oldProjectTeamResult.data.data.length > 0) {
|
||||
oldProjectTeamData = oldProjectTeamResult.data.data[0];
|
||||
} else {
|
||||
logger.error(`Failed to fetch old project team data for audit log`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to fetch old project team data for audit log`);
|
||||
}
|
||||
|
||||
const result = await updateProjectTeam(teamId, projectId, body!);
|
||||
if (!result.ok) {
|
||||
return handleApiError(request, result.error);
|
||||
return handleApiError(request, result.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = oldProjectTeamData;
|
||||
auditLog.newObject = result.data;
|
||||
}
|
||||
|
||||
return responses.successResponse({ data: result.data });
|
||||
},
|
||||
action: "updated",
|
||||
targetType: "projectTeam",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,28 +197,63 @@ export async function DELETE(request: Request, props: { params: Promise<{ organi
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ parsedInput: { query, params }, authentication }) => {
|
||||
handler: async ({ parsedInput: { query, params }, authentication, auditLog }) => {
|
||||
const { teamId, projectId } = query!;
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = `${projectId}-${teamId}`;
|
||||
}
|
||||
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const hasAccess = await checkAuthenticationAndAccess(teamId, projectId, authentication);
|
||||
|
||||
if (!hasAccess.ok) {
|
||||
return handleApiError(request, hasAccess.error);
|
||||
return handleApiError(request, hasAccess.error, auditLog);
|
||||
}
|
||||
|
||||
// Fetch old object for audit log
|
||||
let oldProjectTeamData: any = UNKNOWN_DATA;
|
||||
try {
|
||||
const oldProjectTeamResult = await getProjectTeams(authentication.organizationId, {
|
||||
teamId,
|
||||
projectId,
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
sortBy: "createdAt",
|
||||
order: "desc",
|
||||
});
|
||||
|
||||
if (oldProjectTeamResult.ok && oldProjectTeamResult.data.data.length > 0) {
|
||||
oldProjectTeamData = oldProjectTeamResult.data.data[0];
|
||||
} else {
|
||||
logger.error(`Failed to fetch old project team data for audit log`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to fetch old project team data for audit log`);
|
||||
}
|
||||
|
||||
const result = await deleteProjectTeam(teamId, projectId);
|
||||
if (!result.ok) {
|
||||
return handleApiError(request, result.error);
|
||||
return handleApiError(request, result.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = oldProjectTeamData;
|
||||
}
|
||||
|
||||
return responses.successResponse({ data: result.data });
|
||||
},
|
||||
action: "deleted",
|
||||
targetType: "projectTeam",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
} from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams";
|
||||
import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
||||
import { z } from "zod";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { OrganizationAccessType } from "@formbricks/types/api-key";
|
||||
|
||||
export const GET = async (
|
||||
@@ -53,22 +55,46 @@ export const DELETE = async (
|
||||
params: z.object({ teamId: ZTeamIdSchema, organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput: { params } }) => {
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
handler: async ({ authentication, parsedInput: { params }, auditLog }) => {
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params.teamId;
|
||||
}
|
||||
|
||||
const team = await deleteTeam(params!.organizationId, params!.teamId);
|
||||
if (!hasOrganizationIdAndAccess(params.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
let oldTeamData: any = UNKNOWN_DATA;
|
||||
try {
|
||||
const oldTeamResult = await getTeam(params.organizationId, params.teamId);
|
||||
if (oldTeamResult.ok) {
|
||||
oldTeamData = oldTeamResult.data;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch old team data for audit log: ${JSON.stringify(error)}`);
|
||||
}
|
||||
|
||||
const team = await deleteTeam(params.organizationId, params.teamId);
|
||||
|
||||
if (!team.ok) {
|
||||
return handleApiError(request, team.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, team.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = oldTeamData;
|
||||
}
|
||||
|
||||
return responses.successResponse(team);
|
||||
},
|
||||
action: "deleted",
|
||||
targetType: "team",
|
||||
});
|
||||
|
||||
export const PUT = (
|
||||
@@ -82,20 +108,45 @@ export const PUT = (
|
||||
params: z.object({ teamId: ZTeamIdSchema, organizationId: ZOrganizationIdSchema }),
|
||||
body: ZTeamUpdateSchema,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput: { body, params } }) => {
|
||||
handler: async ({ authentication, parsedInput: { body, params }, auditLog }) => {
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params.teamId;
|
||||
}
|
||||
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
let oldTeamData: any = UNKNOWN_DATA;
|
||||
try {
|
||||
const oldTeamResult = await getTeam(params.organizationId, params.teamId);
|
||||
if (oldTeamResult.ok) {
|
||||
oldTeamData = oldTeamResult.data;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch old team data for audit log: ${JSON.stringify(error)}`);
|
||||
}
|
||||
|
||||
const team = await updateTeam(params!.organizationId, params!.teamId, body!);
|
||||
|
||||
if (!team.ok) {
|
||||
return handleApiError(request, team.error as ApiErrorResponseV2);
|
||||
return handleApiError(request, team.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = oldTeamData;
|
||||
auditLog.newObject = team.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(team);
|
||||
},
|
||||
action: "updated",
|
||||
targetType: "team",
|
||||
});
|
||||
|
||||
@@ -46,19 +46,30 @@ export const POST = async (request: Request, props: { params: Promise<{ organiza
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput: { body, params } }) => {
|
||||
handler: async ({ authentication, parsedInput: { body, params }, auditLog }) => {
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const createTeamResult = await createTeam(body!, authentication.organizationId);
|
||||
if (!createTeamResult.ok) {
|
||||
return handleApiError(request, createTeamResult.error);
|
||||
return handleApiError(request, createTeamResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = createTeamResult.data.id;
|
||||
auditLog.newObject = createTeamResult.data;
|
||||
}
|
||||
|
||||
return responses.createdResponse({ data: createTeamResult.data });
|
||||
},
|
||||
action: "created",
|
||||
targetType: "team",
|
||||
});
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
ZUserInput,
|
||||
ZUserInputPatch,
|
||||
} from "@/modules/api/v2/organizations/[organizationId]/users/types/users";
|
||||
import { UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { OrganizationAccessType } from "@formbricks/types/api-key";
|
||||
|
||||
export const GET = async (request: NextRequest, props: { params: Promise<{ organizationId: string }> }) =>
|
||||
@@ -59,28 +61,45 @@ export const POST = async (request: Request, props: { params: Promise<{ organiza
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput: { body, params } }) => {
|
||||
handler: async ({ authentication, parsedInput: { body, params }, auditLog }) => {
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "organizationId", issue: "This endpoint is not supported on Formbricks Cloud" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [
|
||||
{ field: "organizationId", issue: "This endpoint is not supported on Formbricks Cloud" },
|
||||
],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const createUserResult = await createUser(body!, authentication.organizationId);
|
||||
if (!createUserResult.ok) {
|
||||
return handleApiError(request, createUserResult.error);
|
||||
return handleApiError(request, createUserResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = createUserResult.data.id;
|
||||
auditLog.newObject = createUserResult.data;
|
||||
}
|
||||
|
||||
return responses.createdResponse({ data: createUserResult.data });
|
||||
},
|
||||
action: "created",
|
||||
targetType: "user",
|
||||
});
|
||||
|
||||
export const PATCH = async (request: Request, props: { params: Promise<{ organizationId: string }> }) =>
|
||||
@@ -91,33 +110,75 @@ export const PATCH = async (request: Request, props: { params: Promise<{ organiz
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput: { body, params } }) => {
|
||||
handler: async ({ authentication, parsedInput: { body, params }, auditLog }) => {
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "organizationId", issue: "This endpoint is not supported on Formbricks Cloud" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [
|
||||
{ field: "organizationId", issue: "This endpoint is not supported on Formbricks Cloud" },
|
||||
],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) {
|
||||
return handleApiError(request, {
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
});
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "unauthorized",
|
||||
details: [{ field: "organizationId", issue: "unauthorized" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
if (!body?.email) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "email", issue: "Email is required" }],
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "email", issue: "Email is required" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
let oldUserData: any = UNKNOWN_DATA;
|
||||
try {
|
||||
const oldUserResult = await getUsers(authentication.organizationId, {
|
||||
email: body.email,
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
sortBy: "createdAt",
|
||||
order: "desc",
|
||||
});
|
||||
if (oldUserResult.ok) {
|
||||
oldUserData = oldUserResult.data.data[0];
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch old user data for audit log: ${JSON.stringify(error)}`);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = oldUserData !== UNKNOWN_DATA ? oldUserData?.id : UNKNOWN_DATA;
|
||||
}
|
||||
|
||||
const updateUserResult = await updateUser(body, authentication.organizationId);
|
||||
if (!updateUserResult.ok) {
|
||||
return handleApiError(request, updateUserResult.error);
|
||||
return handleApiError(request, updateUserResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = auditLog.targetId === UNKNOWN_DATA ? updateUserResult.data.id : auditLog.targetId;
|
||||
auditLog.oldObject = oldUserData;
|
||||
auditLog.newObject = updateUserResult.data;
|
||||
}
|
||||
|
||||
return responses.successResponse({ data: updateUserResult.data });
|
||||
},
|
||||
action: "updated",
|
||||
targetType: "user",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user