Skip to content

Commit 559633c

Browse files
committed
wip api to fetch audits
1 parent b00a429 commit 559633c

File tree

12 files changed

+117
-18
lines changed

12 files changed

+117
-18
lines changed

packages/db/prisma/migrations/20250613223529_add_audit_table/migration.sql renamed to packages/db/prisma/migrations/20250617031335_add_audit_table/migration.sql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ CREATE TABLE "Audit" (
99
"targetType" TEXT NOT NULL,
1010
"sourcebotVersion" TEXT NOT NULL,
1111
"metadata" JSONB,
12+
"orgId" INTEGER NOT NULL,
1213

1314
CONSTRAINT "Audit_pkey" PRIMARY KEY ("id")
1415
);
1516

1617
-- CreateIndex
17-
CREATE INDEX "Audit_actorId_actorType_targetId_targetType_idx" ON "Audit"("actorId", "actorType", "targetId", "targetType");
18+
CREATE INDEX "Audit_actorId_actorType_targetId_targetType_orgId_idx" ON "Audit"("actorId", "actorType", "targetId", "targetType", "orgId");
19+
20+
-- AddForeignKey
21+
ALTER TABLE "Audit" ADD CONSTRAINT "Audit_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;

packages/db/prisma/schema.prisma

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ model Org {
172172
/// List of pending invites to this organization
173173
invites Invite[]
174174
175+
audits Audit[]
176+
175177
accountRequests AccountRequest[]
176178
177179
searchContexts SearchContext[]
@@ -239,7 +241,10 @@ model Audit {
239241
sourcebotVersion String
240242
metadata Json?
241243
242-
@@index([actorId, actorType, targetId, targetType])
244+
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
245+
orgId Int
246+
247+
@@index([actorId, actorType, targetId, targetType, orgId])
243248
}
244249

245250
// @see : https://authjs.dev/concepts/database-models#user

packages/web/src/actions.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ export const createApiKey = async (name: string, domain: string): Promise<{ key:
473473
id: org.id.toString(),
474474
type: "org"
475475
},
476+
orgId: org.id,
476477
metadata: {
477478
message: `API key ${name} already exists`,
478479
api_key: name
@@ -505,6 +506,7 @@ export const createApiKey = async (name: string, domain: string): Promise<{ key:
505506
id: apiKey.hash,
506507
type: "api_key"
507508
},
509+
orgId: org.id,
508510
metadata: {
509511
api_key: name
510512
}
@@ -517,7 +519,7 @@ export const createApiKey = async (name: string, domain: string): Promise<{ key:
517519

518520
export const deleteApiKey = async (name: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
519521
withAuth((userId) =>
520-
withOrgMembership(userId, domain, async () => {
522+
withOrgMembership(userId, domain, async ({ org }) => {
521523
const apiKey = await prisma.apiKey.findFirst({
522524
where: {
523525
name,
@@ -536,6 +538,7 @@ export const deleteApiKey = async (name: string, domain: string): Promise<{ succ
536538
id: domain,
537539
type: "org"
538540
},
541+
orgId: org.id,
539542
metadata: {
540543
message: `API key ${name} not found for user ${userId}`,
541544
api_key: name
@@ -564,6 +567,7 @@ export const deleteApiKey = async (name: string, domain: string): Promise<{ succ
564567
id: apiKey.hash,
565568
type: "api_key"
566569
},
570+
orgId: org.id,
567571
metadata: {
568572
api_key: name
569573
}
@@ -978,6 +982,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
978982
id: org.id.toString(),
979983
type: "org"
980984
},
985+
orgId: org.id,
981986
metadata: {
982987
message: error,
983988
emails: emails.join(", ")
@@ -1001,6 +1006,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
10011006
id: org.id.toString(),
10021007
type: "org"
10031008
},
1009+
orgId: org.id,
10041010
metadata: {
10051011
message: "Organization has reached maximum number of seats",
10061012
emails: emails.join(", ")
@@ -1130,6 +1136,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
11301136
id: org.id.toString(),
11311137
type: "org"
11321138
},
1139+
orgId: org.id,
11331140
metadata: {
11341141
emails: emails.join(", ")
11351142
}
@@ -1206,6 +1213,19 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
12061213
return user;
12071214
}
12081215

1216+
const invite = await prisma.invite.findUnique({
1217+
where: {
1218+
id: inviteId,
1219+
},
1220+
include: {
1221+
org: true,
1222+
}
1223+
});
1224+
1225+
if (!invite) {
1226+
return notFound();
1227+
}
1228+
12091229
const failAuditCallback = async (error: string) => {
12101230
await auditService.createAudit({
12111231
action: "user.invite_accept_failed",
@@ -1217,23 +1237,13 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
12171237
id: inviteId,
12181238
type: "invite"
12191239
},
1240+
orgId: invite.org.id,
12201241
metadata: {
12211242
message: error
12221243
}
12231244
});
12241245
}
1225-
const invite = await prisma.invite.findUnique({
1226-
where: {
1227-
id: inviteId,
1228-
},
1229-
include: {
1230-
org: true,
1231-
}
1232-
});
12331246

1234-
if (!invite) {
1235-
return notFound();
1236-
}
12371247

12381248
const hasAvailability = await orgHasAvailability(invite.org.domain);
12391249
if (!hasAvailability) {
@@ -1293,6 +1303,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
12931303
id: user.id,
12941304
type: "user"
12951305
},
1306+
orgId: invite.org.id,
12961307
target: {
12971308
id: accountRequest.id,
12981309
type: "account_join_request"
@@ -1325,6 +1336,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
13251336
id: user.id,
13261337
type: "user"
13271338
},
1339+
orgId: invite.org.id,
13281340
target: {
13291341
id: inviteId,
13301342
type: "invite"
@@ -1394,6 +1406,7 @@ export const transferOwnership = async (newOwnerId: string, domain: string): Pro
13941406
id: org.id.toString(),
13951407
type: "org"
13961408
},
1409+
orgId: org.id,
13971410
metadata: {
13981411
message: error
13991412
}
@@ -1461,6 +1474,7 @@ export const transferOwnership = async (newOwnerId: string, domain: string): Pro
14611474
id: org.id.toString(),
14621475
type: "org"
14631476
},
1477+
orgId: org.id,
14641478
metadata: {
14651479
message: `Ownership transferred from ${currentUserId} to ${newOwnerId}`
14661480
}
@@ -1780,6 +1794,7 @@ export const approveAccountRequest = async (requestId: string, domain: string) =
17801794
id: requestId,
17811795
type: "account_join_request"
17821796
},
1797+
orgId: org.id,
17831798
metadata: {
17841799
message: error,
17851800
}
@@ -1894,6 +1909,7 @@ export const approveAccountRequest = async (requestId: string, domain: string) =
18941909
id: userId,
18951910
type: "user"
18961911
},
1912+
orgId: org.id,
18971913
target: {
18981914
id: requestId,
18991915
type: "account_join_request"
@@ -1930,6 +1946,7 @@ export const rejectAccountRequest = async (requestId: string, domain: string) =>
19301946
id: userId,
19311947
type: "user"
19321948
},
1949+
orgId: org.id,
19331950
target: {
19341951
id: requestId,
19351952
type: "account_join_request"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use server';
2+
3+
import { NextRequest } from "next/server";
4+
import { fetchAuditRecords } from "@/ee/features/audit/utils";
5+
import { isServiceError } from "@/lib/utils";
6+
import { serviceErrorResponse } from "@/lib/serviceError";
7+
import { StatusCodes } from "http-status-codes";
8+
import { ErrorCode } from "@/lib/errorCodes";
9+
10+
export const GET = async (request: NextRequest) => {
11+
const domain = request.headers.get("X-Org-Domain");
12+
const apiKey = request.headers.get("X-Sourcebot-Api-Key") ?? undefined;
13+
14+
if (!domain) {
15+
return serviceErrorResponse({
16+
statusCode: StatusCodes.BAD_REQUEST,
17+
errorCode: ErrorCode.MISSING_ORG_DOMAIN_HEADER,
18+
message: "Missing X-Org-Domain header",
19+
});
20+
}
21+
22+
const result = await fetchAuditRecords(domain, apiKey);
23+
if (isServiceError(result)) {
24+
return serviceErrorResponse(result);
25+
}
26+
return Response.json(result);
27+
};

packages/web/src/auth.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getSSOProviders } from '@/ee/features/sso/sso';
1717
import { hasEntitlement } from '@/features/entitlements/server';
1818
import { onCreateUser } from '@/lib/authUtils';
1919
import { getAuditService } from '@/ee/features/audit/factory';
20+
import { SINGLE_TENANT_ORG_ID } from './lib/constants';
2021

2122
const auditService = getAuditService();
2223

@@ -148,6 +149,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
148149
id: user.id,
149150
type: "user"
150151
},
152+
orgId: SINGLE_TENANT_ORG_ID, // TODO(mt)
151153
target: {
152154
id: user.id,
153155
type: "user"
@@ -164,6 +166,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
164166
id: token.token.userId,
165167
type: "user"
166168
},
169+
orgId: SINGLE_TENANT_ORG_ID, // TODO(mt)
167170
target: {
168171
id: token.token.userId,
169172
type: "user"

packages/web/src/ee/features/audit/auditService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class AuditService implements IAuditService {
1414
targetType: event.target.type,
1515
sourcebotVersion,
1616
metadata: event.metadata,
17+
orgId: event.orgId,
1718
},
1819
});
1920

packages/web/src/ee/features/audit/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const auditEventSchema = z.object({
2626
actor: auditActorSchema,
2727
target: auditTargetSchema,
2828
sourcebotVersion: z.string(),
29+
orgId: z.number(),
2930
metadata: auditMetadataSchema.optional()
3031
})
3132
export type AuditEvent = z.infer<typeof auditEventSchema>;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { prisma } from "@/prisma";
2+
import { serviceErrorResponse } from "@/lib/serviceError";
3+
import { ErrorCode } from "@/lib/errorCodes";
4+
import { StatusCodes } from "http-status-codes";
5+
import { sew, withAuth, withOrgMembership } from "@/actions";
6+
import { OrgRole } from "@sourcebot/db";
7+
8+
export const fetchAuditRecords = async (domain: string, apiKey: string | undefined = undefined) => sew(() =>
9+
withAuth((userId) =>
10+
withOrgMembership(userId, domain, async ({ org }) => {
11+
try {
12+
const auditRecords = await prisma.audit.findMany({
13+
where: {
14+
orgId: org.id,
15+
},
16+
orderBy: {
17+
timestamp: 'desc'
18+
}
19+
});
20+
21+
return auditRecords;
22+
} catch (error) {
23+
console.error('Error fetching audit logs:', error);
24+
return serviceErrorResponse({
25+
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
26+
errorCode: ErrorCode.UNEXPECTED_ERROR,
27+
message: "Failed to fetch audit logs",
28+
});
29+
}
30+
}, /* minRequiredRole = */ OrgRole.OWNER), /* allowSingleTenantUnauthedAccess = */ true, apiKey ? { apiKey, domain } : undefined)
31+
);

packages/web/src/features/search/fileSourceApi.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const auditService = getAuditService();
1616

1717
export const getFileSource = async ({ fileName, repository, branch }: FileSourceRequest, domain: string, apiKey: string | undefined = undefined): Promise<FileSourceResponse | ServiceError> => sew(() =>
1818
withAuth((userId) =>
19-
withOrgMembership(userId, domain, async () => {
19+
withOrgMembership(userId, domain, async ({ org }) => {
2020
const escapedFileName = escapeStringRegexp(fileName);
2121
const escapedRepository = escapeStringRegexp(repository);
2222

@@ -51,6 +51,7 @@ export const getFileSource = async ({ fileName, repository, branch }: FileSource
5151
id: apiKey ? apiKey : userId,
5252
type: apiKey ? "api_key" : "user"
5353
},
54+
orgId: org.id,
5455
target: {
5556
id: `${escapedRepository}/${escapedFileName}${branch ? `:${branch}` : ''}`,
5657
type: "file"

packages/web/src/features/search/listReposApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const listRepositories = async (domain: string, apiKey: string | undefine
5757
id: org.id.toString(),
5858
type: "org"
5959
},
60+
orgId: org.id,
6061
metadata: {
6162
message: result.repos.map((repo) => repo.name).join(", ")
6263
}

0 commit comments

Comments
 (0)