Skip to content

Commit 3ff88da

Browse files
authored
feat(ee): Add REST API to get users and delete a user (#578)
* add get users and delete user endpoints * changelog * changelog typo * update license * add tags to changelog
1 parent 5b1caae commit 3ff88da

File tree

4 files changed

+176
-1
lines changed

4 files changed

+176
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Added support for GitHub Apps for service auth. [#570](https://github.com/sourcebot-dev/sourcebot/pull/570)
1616
- Added prometheus metrics for repo index manager. [#571](https://github.com/sourcebot-dev/sourcebot/pull/571)
1717
- Added experimental environment variable to disable API key creation for non-admin users. [#577](https://github.com/sourcebot-dev/sourcebot/pull/577)
18+
- [Experimental][Sourcebot EE] Added REST API to get users and delete a user. [#578](https://github.com/sourcebot-dev/sourcebot/pull/578)
1819

1920
### Fixed
2021
- Fixed "dubious ownership" errors when cloning / fetching repos. [#553](https://github.com/sourcebot-dev/sourcebot/pull/553)

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Copyright (c) 2025 Taqla Inc.
22

33
Portions of this software are licensed as follows:
44

5-
- All content that resides under the "ee/", "packages/web/src/ee/", "packages/backend/src/ee/", and "packages/shared/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
5+
- All content located within any folder or subfolder named “ee” in this repository is licensed under the terms specified in ee/LICENSE”,
66
- All third party components incorporated into the Sourcebot Software are licensed under the original license provided by the owner of the applicable component.
77
- Content outside of the above mentioned directories or restrictions above is available under the "Functional Source License" as defined below.
88

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use server';
2+
3+
import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2";
4+
import { OrgRole } from "@sourcebot/db";
5+
import { isServiceError } from "@/lib/utils";
6+
import { serviceErrorResponse, missingQueryParam, notFound } from "@/lib/serviceError";
7+
import { createLogger } from "@sourcebot/logger";
8+
import { NextRequest } from "next/server";
9+
import { StatusCodes } from "http-status-codes";
10+
import { ErrorCode } from "@/lib/errorCodes";
11+
import { getAuditService } from "@/ee/features/audit/factory";
12+
13+
const logger = createLogger('ee-user-api');
14+
const auditService = getAuditService();
15+
16+
export const DELETE = async (request: NextRequest) => {
17+
const url = new URL(request.url);
18+
const userId = url.searchParams.get('userId');
19+
20+
if (!userId) {
21+
return serviceErrorResponse(missingQueryParam('userId'));
22+
}
23+
24+
const result = await withAuthV2(async ({ org, role, user: currentUser, prisma }) => {
25+
return withMinimumOrgRole(role, OrgRole.OWNER, async () => {
26+
try {
27+
if (currentUser.id === userId) {
28+
return {
29+
statusCode: StatusCodes.BAD_REQUEST,
30+
errorCode: ErrorCode.INVALID_REQUEST_BODY,
31+
message: 'Cannot delete your own user account',
32+
};
33+
}
34+
35+
const targetUser = await prisma.user.findUnique({
36+
where: {
37+
id: userId,
38+
},
39+
select: {
40+
id: true,
41+
email: true,
42+
name: true,
43+
},
44+
});
45+
46+
if (!targetUser) {
47+
return notFound('User not found');
48+
}
49+
50+
await auditService.createAudit({
51+
action: "user.delete",
52+
actor: {
53+
id: currentUser.id,
54+
type: "user"
55+
},
56+
target: {
57+
id: userId,
58+
type: "user"
59+
},
60+
orgId: org.id,
61+
});
62+
63+
// Delete the user (cascade will handle all related records)
64+
await prisma.user.delete({
65+
where: {
66+
id: userId,
67+
},
68+
});
69+
70+
logger.info('User deleted successfully', {
71+
deletedUserId: userId,
72+
deletedByUserId: currentUser.id,
73+
orgId: org.id
74+
});
75+
76+
return {
77+
success: true,
78+
message: 'User deleted successfully'
79+
};
80+
} catch (error) {
81+
logger.error('Error deleting user', { error, userId });
82+
throw error;
83+
}
84+
});
85+
});
86+
87+
if (isServiceError(result)) {
88+
return serviceErrorResponse(result);
89+
}
90+
91+
return Response.json(result, { status: StatusCodes.OK });
92+
};
93+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use server';
2+
3+
import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2";
4+
import { OrgRole } from "@sourcebot/db";
5+
import { isServiceError } from "@/lib/utils";
6+
import { serviceErrorResponse } from "@/lib/serviceError";
7+
import { createLogger } from "@sourcebot/logger";
8+
import { getAuditService } from "@/ee/features/audit/factory";
9+
10+
const logger = createLogger('ee-users-api');
11+
const auditService = getAuditService();
12+
13+
export const GET = async () => {
14+
const result = await withAuthV2(async ({ prisma, org, role, user }) => {
15+
return withMinimumOrgRole(role, OrgRole.OWNER, async () => {
16+
try {
17+
const memberships = await prisma.userToOrg.findMany({
18+
where: {
19+
orgId: org.id,
20+
},
21+
include: {
22+
user: true,
23+
},
24+
});
25+
26+
const usersWithActivity = await Promise.all(
27+
memberships.map(async (membership) => {
28+
const lastActivity = await prisma.audit.findFirst({
29+
where: {
30+
actorId: membership.user.id,
31+
actorType: 'user',
32+
orgId: org.id,
33+
},
34+
orderBy: {
35+
timestamp: 'desc',
36+
},
37+
select: {
38+
timestamp: true,
39+
},
40+
});
41+
42+
return {
43+
id: membership.user.id,
44+
name: membership.user.name,
45+
email: membership.user.email,
46+
role: membership.role,
47+
createdAt: membership.user.createdAt,
48+
lastActivityAt: lastActivity?.timestamp ?? null,
49+
};
50+
})
51+
);
52+
53+
await auditService.createAudit({
54+
action: "user.list",
55+
actor: {
56+
id: user.id,
57+
type: "user"
58+
},
59+
target: {
60+
id: org.id.toString(),
61+
type: "org"
62+
},
63+
orgId: org.id
64+
});
65+
66+
logger.info('Fetched users list', { count: usersWithActivity.length });
67+
return usersWithActivity;
68+
} catch (error) {
69+
logger.error('Error fetching users', { error });
70+
throw error;
71+
}
72+
});
73+
});
74+
75+
if (isServiceError(result)) {
76+
return serviceErrorResponse(result);
77+
}
78+
79+
return Response.json(result);
80+
};
81+

0 commit comments

Comments
 (0)