Skip to content

Commit 6125d25

Browse files
authored
Merge pull request #670 from contentstack/feature/audit-log
Feature/audit log
2 parents b324659 + bc40d75 commit 6125d25

File tree

22 files changed

+1300
-230
lines changed

22 files changed

+1300
-230
lines changed

.talismanrc

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
fileignoreconfig:
2-
- filename: .github/workflows/secrets-scan.yml
3-
ignore_detectors:
4-
- filecontent
5-
- filename: remove-broken-imports.js
6-
checksum: d9d3ca95b2f4df855c8811c73b5714e80b31e5e84b46affa0cb514dcfcc145bf
1+
ignoreconfig:
2+
- filename: .github/workflows/secrets-scan.yml
3+
ignore_detectors:
4+
- filecontent
5+
6+
- filename: remove-broken-imports.js
7+
checksum: d9d3ca95b2f4df855c8811c73b5714e80b31e5e84b46affa0cb514dcfcc145bf
8+
9+
- filename: ui/package-lock.json
10+
ignore_detectors:
11+
- Base64Detector
12+
713
version: "1.0"

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"p-limit": "^6.2.0",
4949
"path-to-regexp": "^8.2.0",
5050
"router": "^2.0.0",
51-
"shelljs": "^0.8.5",
51+
"shelljs": "^0.9.0",
5252
"socket.io": "^4.7.5",
5353
"uuid": "^9.0.1",
5454
"winston": "^3.11.0"

api/src/constants/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export const LOCALE_MAPPER: any = {
177177
masterLocale: {
178178
"en-us": "en",
179179
},
180-
locales:{fr: "fr-fr",}
180+
locales: { fr: "fr-fr", }
181181
};
182182
export const CHUNK_SIZE = 1048576;
183183

@@ -272,3 +272,13 @@ export const MIGRATION_DATA_CONFIG = {
272272

273273
EXPORT_INFO_FILE: "export-info.json",
274274
};
275+
276+
export const GET_AUDIT_DATA = {
277+
MIGRATION: "migration-v2",
278+
API_DIR: "api",
279+
MIGRATION_DATA_DIR: "migration-data",
280+
LOGS_DIR: "logs",
281+
AUDIT_DIR: "audit",
282+
AUDIT_REPORT: "audit-report",
283+
FILTERALL: "all",
284+
}

api/src/controllers/migration.controller.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,13 @@ const deleteTestStack = async (req: Request, res: Response): Promise<void> => {
4949
const resp = await migrationService.deleteTestStack(req);
5050
res.status(200).json(resp);
5151
};
52-
52+
const getAuditData = async (req: Request, res: Response): Promise<void> => {
53+
const resp = await migrationService.getAuditData(req);
54+
res.status(resp?.status).json(resp);
55+
};
5356
const getLogs = async (req: Request, res: Response): Promise<void> => {
5457
const resp = await migrationService.getLogs(req);
55-
res.status(200).json(resp);
58+
res.status(resp?.status).json(resp);
5659
};
5760

5861
const saveLocales = async (req: Request, res: Response): Promise<void> => {
@@ -72,5 +75,6 @@ export const migrationController = {
7275
startMigration,
7376
getLogs,
7477
saveLocales,
75-
saveMappedLocales
78+
saveMappedLocales,
79+
getAuditData
7680
};

api/src/routes/migration.routes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ router.post(
2020
"/test-stack/:orgId/:projectId",
2121
asyncRouter(migrationController.startTestMigration)
2222
);
23-
23+
router.get(
24+
"/get_audit_data/:orgId/:projectId/:stackId/:moduleName/:skip/:limit/:startIndex/:stopIndex/:searchText/:filter",
25+
asyncRouter(migrationController.getAuditData)
26+
)
2427
/**
2528
* Route for deleting a test stack.
2629
* @route POST /test-stack/:projectId

api/src/services/migration.service.ts

Lines changed: 164 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import https from "../utils/https.utils.js";
77
import { LoginServiceType } from "../models/types.js";
88
import getAuthtoken from "../utils/auth.utils.js";
99
import logger from "../utils/logger.js";
10+
import { GET_AUDIT_DATA } from "../constants/index.js";
1011
import {
1112
HTTP_TEXTS,
1213
HTTP_CODES,
@@ -118,12 +119,10 @@ const createTestStack = async (req: Request): Promise<LoginServiceType> => {
118119
}
119120
return {
120121
data: {
121-
data: res.data,
122-
url: `${
123-
config.CS_URL[token_payload?.region as keyof typeof config.CS_URL]
124-
}/stack/${res.data.stack.api_key}/dashboard`,
122+
data: res?.data,
123+
url: `${config?.CS_URL[token_payload?.region as keyof typeof config.CS_URL]}/stack/${res?.data?.stack?.api_key}/dashboard`,
125124
},
126-
status: res.status,
125+
status: res?.status,
127126
};
128127
} catch (error: any) {
129128
logger.error(
@@ -135,13 +134,158 @@ const createTestStack = async (req: Request): Promise<LoginServiceType> => {
135134
)
136135
);
137136

137+
throw new ExceptionFunction(
138+
error?.message || HTTP_TEXTS?.INTERNAL_ERROR,
139+
error?.statusCode || error?.status || HTTP_CODES.SERVER_ERROR
140+
);
141+
}
142+
};
143+
144+
const getAuditData = async (req: Request): Promise<any> => {
145+
const projectId = path?.basename(req?.params?.projectId);
146+
const stackId = path?.basename(req?.params?.stackId);
147+
const moduleName = path.basename(req?.params?.moduleName);
148+
const limit = parseInt(req?.params?.limit);
149+
const startIndex = parseInt(req?.params?.startIndex);
150+
const stopIndex = startIndex + limit;
151+
const searchText = req?.params?.searchText;
152+
const filter = req?.params?.filter;
153+
const srcFunc = "getAuditData";
154+
155+
if (projectId?.includes('..') || stackId?.includes('..') || moduleName?.includes('..')) {
156+
throw new BadRequestError("Invalid projectId, stackId, or moduleName");
157+
}
158+
159+
try {
160+
const mainPath = process?.cwd()?.split?.(GET_AUDIT_DATA?.MIGRATION)?.[0];
161+
const logsDir = path.join(mainPath, GET_AUDIT_DATA?.MIGRATION, GET_AUDIT_DATA?.API_DIR, GET_AUDIT_DATA?.MIGRATION_DATA_DIR);
162+
163+
const stackFolders = fs.readdirSync(logsDir);
164+
165+
const stackFolder = stackFolders?.find(folder => folder?.startsWith?.(stackId));
166+
if (!stackFolder) {
167+
throw new BadRequestError("Migration data not found for this stack");
168+
}
169+
170+
const auditLogPath = path?.resolve(logsDir, stackFolder, GET_AUDIT_DATA?.LOGS_DIR, GET_AUDIT_DATA?.AUDIT_DIR, GET_AUDIT_DATA?.AUDIT_REPORT);
171+
if (!fs.existsSync(auditLogPath)) {
172+
throw new BadRequestError("Audit log path not found");
173+
}
174+
175+
176+
// Read and parse the JSON file for the module
177+
const filePath = path?.resolve(auditLogPath, `${moduleName}.json`);
178+
let fileData;
179+
if (fs?.existsSync(filePath)) {
180+
const fileContent = await fsPromises.readFile(filePath, 'utf8');
181+
try {
182+
if (typeof fileContent === 'string') {
183+
fileData = JSON?.parse(fileContent);
184+
}
185+
} catch (error) {
186+
logger.error(`Error parsing JSON from file ${filePath}:`, error);
187+
throw new BadRequestError('Invalid JSON format in audit file');
188+
}
189+
}
190+
191+
if (!fileData) {
192+
throw new BadRequestError(`No audit data found for module: ${moduleName}`);
193+
}
194+
let transformedData = transformAndFlattenData(fileData);
195+
if (filter != GET_AUDIT_DATA?.FILTERALL) {
196+
const filters = filter?.split("-");
197+
moduleName === 'Entries_Select_feild' ? transformedData = transformedData?.filter((log) => {
198+
return filters?.some((filter) => {
199+
return (
200+
log?.display_type?.toLowerCase()?.includes(filter?.toLowerCase())
201+
);
202+
});
203+
}) : transformedData = transformedData?.filter((log) => {
204+
return filters?.some((filter) => {
205+
return (
206+
log?.data_type?.toLowerCase()?.includes(filter?.toLowerCase())
207+
);
208+
});
209+
});
210+
211+
}
212+
if (searchText && searchText !== null && searchText !== "null") {
213+
transformedData = transformedData?.filter((item: any) => {
214+
return Object?.values(item)?.some(value =>
215+
value &&
216+
typeof value === 'string' &&
217+
value?.toLowerCase?.()?.includes(searchText?.toLowerCase())
218+
);
219+
});
220+
}
221+
const paginatedData = transformedData?.slice?.(startIndex, stopIndex);
222+
223+
return {
224+
data: paginatedData,
225+
totalCount: transformedData?.length,
226+
status: HTTP_CODES?.OK
227+
};
228+
229+
} catch (error: any) {
230+
logger.error(
231+
getLogMessage(
232+
srcFunc,
233+
`Error getting audit log data for module: ${moduleName}`,
234+
error
235+
)
236+
);
138237
throw new ExceptionFunction(
139238
error?.message || HTTP_TEXTS.INTERNAL_ERROR,
140239
error?.statusCode || error?.status || HTTP_CODES.SERVER_ERROR
141240
);
142241
}
143242
};
243+
/**
244+
* Transforms and flattens nested data structure into an array of items
245+
* with sequential tuid values
246+
*/
247+
const transformAndFlattenData = (data: any): Array<{ [key: string]: any, id: number }> => {
248+
try {
249+
const flattenedItems: Array<{ [key: string]: any }> = [];
144250

251+
// Handle the data based on its structure
252+
if (Array.isArray(data)) {
253+
// If data is already an array, use it directly
254+
data?.forEach((item, index) => {
255+
flattenedItems?.push({
256+
...item ?? {},
257+
uid: item?.uid || `item-${index}`
258+
});
259+
});
260+
} else if (typeof data === 'object' && data !== null) {
261+
Object?.entries?.(data)?.forEach(([key, value]) => {
262+
if (Array.isArray(value)) {
263+
value?.forEach((item, index) => {
264+
flattenedItems?.push({
265+
...item ?? {},
266+
parentKey: key,
267+
uid: item?.uid || `${key}-${index}`
268+
});
269+
});
270+
} else if (typeof value === 'object' && value !== null) {
271+
flattenedItems?.push({
272+
...value,
273+
key,
274+
uid: (value as any)?.uid || key
275+
});
276+
}
277+
});
278+
}
279+
280+
return flattenedItems?.map((item, index) => ({
281+
...item ?? {},
282+
id: index + 1
283+
}));
284+
} catch (error) {
285+
console.error('Error transforming data:', error);
286+
return [];
287+
}
288+
};
145289
/**
146290
* Deletes a test stack.
147291
* @param req - The request object.
@@ -782,7 +926,6 @@ const startMigration = async (req: Request): Promise<any> => {
782926
}
783927
};
784928

785-
786929
const getLogs = async (req: Request): Promise<any> => {
787930
const projectId = req?.params?.projectId ? path?.basename(req.params.projectId) : "";
788931
const stackId = req?.params?.stackId ? path?.basename(req.params.stackId) : "";
@@ -791,9 +934,7 @@ const getLogs = async (req: Request): Promise<any> => {
791934
const stopIndex = startIndex + limit;
792935
const searchText = req?.params?.searchText ?? null;
793936
const filter = req?.params?.filter ?? "all";
794-
795937
const srcFunc = "getLogs";
796-
797938
if (
798939
!projectId ||
799940
!stackId ||
@@ -802,40 +943,44 @@ const getLogs = async (req: Request): Promise<any> => {
802943
) {
803944
throw new BadRequestError("Invalid projectId or stackId");
804945
}
805-
806946
try {
807947
const mainPath = process?.cwd()?.split("migration-v2")?.[0];
808948
if (!mainPath) {
809949
throw new BadRequestError("Invalid application path");
810950
}
811-
812951
const logsDir = path?.join(mainPath, "migration-v2", "api", "logs");
813952
const loggerPath = path?.join(logsDir, projectId, `${stackId}.log`);
814953
const absolutePath = path?.resolve(loggerPath);
815-
816954
if (!absolutePath?.startsWith(logsDir)) {
817955
throw new BadRequestError("Access to this file is not allowed.");
818956
}
819-
820957
if (fs.existsSync(absolutePath)) {
958+
let index = 0;
821959
const logs = await fs.promises.readFile(absolutePath, "utf8");
822960
let logEntries = logs
823961
?.split("\n")
824962
?.map((line) => {
825963
try {
826-
return line ? JSON?.parse(line) : null;
964+
const parsedLine = JSON?.parse(line)
965+
parsedLine['id'] = index;
966+
++index;
967+
return parsedLine ? parsedLine : null;
827968
} catch (error) {
828969
return null;
829970
}
830971
})
831972
?.filter?.((entry) => entry !== null);
832-
833973
if (!logEntries?.length) {
834974
return { logs: [], total: 0 };
835975
}
836-
976+
const filterOptions = Array?.from(new Set(logEntries?.map((log) => log?.level)));
977+
const auditStartIndex = logEntries?.findIndex?.(log => log?.message?.includes("Starting audit process"));
978+
const auditEndIndex = logEntries?.findIndex?.(log => log?.message?.includes("Audit process completed"));
979+
logEntries = [
980+
...logEntries.slice(0, auditStartIndex),
981+
...logEntries.slice(auditEndIndex + 1)
982+
]
837983
logEntries = logEntries?.slice?.(1, logEntries?.length - 2);
838-
839984
if (filter !== "all") {
840985
const filters = filter?.split("-") ?? [];
841986
logEntries = logEntries?.filter((log) => {
@@ -846,17 +991,17 @@ const getLogs = async (req: Request): Promise<any> => {
846991
});
847992
});
848993
}
849-
850994
if (searchText && searchText !== "null") {
851995
logEntries = logEntries?.filter?.((log) =>
852996
matchesSearchText(log, searchText)
853997
);
854998
}
855-
856999
const paginatedLogs = logEntries?.slice?.(startIndex, stopIndex) ?? [];
8571000
return {
8581001
logs: paginatedLogs,
8591002
total: logEntries?.length ?? 0,
1003+
filterOptions: filterOptions,
1004+
status: HTTP_CODES?.OK
8601005
};
8611006
} else {
8621007
logger.error(getLogMessage(srcFunc, HTTP_TEXTS.LOGS_NOT_FOUND));
@@ -971,4 +1116,5 @@ export const migrationService = {
9711116
getLogs,
9721117
createSourceLocales,
9731118
updateLocaleMapper,
1119+
getAuditData
9741120
};

0 commit comments

Comments
 (0)