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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for GitHub Apps for service auth. [#570](https://github.com/sourcebot-dev/sourcebot/pull/570)
- Added prometheus metrics for repo index manager. [#571](https://github.com/sourcebot-dev/sourcebot/pull/571)
- Added experimental environment variable to disable API key creation for non-admin users. [#577](https://github.com/sourcebot-dev/sourcebot/pull/577)
- [Experimental][Sourcebot EE] Added REST API to get users and delete a user. [#578](https://github.com/sourcebot-dev/sourcebot/pull/578)

### Fixed
- Fixed "dubious ownership" errors when cloning / fetching repos. [#553](https://github.com/sourcebot-dev/sourcebot/pull/553)
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Copyright (c) 2025 Taqla Inc.

Portions of this software are licensed as follows:

- 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".
- All content located within any folder or subfolder named “ee” in this repository is licensed under the terms specified in ee/LICENSE”,
- All third party components incorporated into the Sourcebot Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "Functional Source License" as defined below.

Expand Down
93 changes: 93 additions & 0 deletions packages/web/src/app/api/(server)/ee/user/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use server';

import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2";
import { OrgRole } from "@sourcebot/db";
import { isServiceError } from "@/lib/utils";
import { serviceErrorResponse, missingQueryParam, notFound } from "@/lib/serviceError";
import { createLogger } from "@sourcebot/logger";
import { NextRequest } from "next/server";
import { StatusCodes } from "http-status-codes";
import { ErrorCode } from "@/lib/errorCodes";
import { getAuditService } from "@/ee/features/audit/factory";

const logger = createLogger('ee-user-api');
const auditService = getAuditService();

export const DELETE = async (request: NextRequest) => {
const url = new URL(request.url);
const userId = url.searchParams.get('userId');

if (!userId) {
return serviceErrorResponse(missingQueryParam('userId'));
}

const result = await withAuthV2(async ({ org, role, user: currentUser, prisma }) => {
return withMinimumOrgRole(role, OrgRole.OWNER, async () => {
try {
if (currentUser.id === userId) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: 'Cannot delete your own user account',
};
}

const targetUser = await prisma.user.findUnique({
where: {
id: userId,
},
select: {
id: true,
email: true,
name: true,
},
});

if (!targetUser) {
return notFound('User not found');
}

await auditService.createAudit({
action: "user.delete",
actor: {
id: currentUser.id,
type: "user"
},
target: {
id: userId,
type: "user"
},
orgId: org.id,
});

// Delete the user (cascade will handle all related records)
await prisma.user.delete({
where: {
id: userId,
},
});

logger.info('User deleted successfully', {
deletedUserId: userId,
deletedByUserId: currentUser.id,
orgId: org.id
});

return {
success: true,
message: 'User deleted successfully'
};
} catch (error) {
logger.error('Error deleting user', { error, userId });
throw error;
}
});
});

if (isServiceError(result)) {
return serviceErrorResponse(result);
}

return Response.json(result, { status: StatusCodes.OK });
};

81 changes: 81 additions & 0 deletions packages/web/src/app/api/(server)/ee/users/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use server';

import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2";
import { OrgRole } from "@sourcebot/db";
import { isServiceError } from "@/lib/utils";
import { serviceErrorResponse } from "@/lib/serviceError";
import { createLogger } from "@sourcebot/logger";
import { getAuditService } from "@/ee/features/audit/factory";

const logger = createLogger('ee-users-api');
const auditService = getAuditService();

export const GET = async () => {
const result = await withAuthV2(async ({ prisma, org, role, user }) => {
return withMinimumOrgRole(role, OrgRole.OWNER, async () => {
try {
const memberships = await prisma.userToOrg.findMany({
where: {
orgId: org.id,
},
include: {
user: true,
},
});

const usersWithActivity = await Promise.all(
memberships.map(async (membership) => {
const lastActivity = await prisma.audit.findFirst({
where: {
actorId: membership.user.id,
actorType: 'user',
orgId: org.id,
},
orderBy: {
timestamp: 'desc',
},
select: {
timestamp: true,
},
});

return {
id: membership.user.id,
name: membership.user.name,
email: membership.user.email,
role: membership.role,
createdAt: membership.user.createdAt,
lastActivityAt: lastActivity?.timestamp ?? null,
};
})
);

await auditService.createAudit({
action: "user.list",
actor: {
id: user.id,
type: "user"
},
target: {
id: org.id.toString(),
type: "org"
},
orgId: org.id
});

logger.info('Fetched users list', { count: usersWithActivity.length });
return usersWithActivity;
} catch (error) {
logger.error('Error fetching users', { error });
throw error;
}
});
});

if (isServiceError(result)) {
return serviceErrorResponse(result);
}

return Response.json(result);
};

Loading