Skip to content

Commit 7da5d55

Browse files
refactor(web): rename withAuthV2 to withAuth (#1073)
* refactor(web): rename withAuthV2 to withAuth and move to middleware/ Renames withAuthV2/withOptionalAuthV2 to withAuth/withOptionalAuth and relocates them from src/withAuthV2.ts to src/middleware/withAuth.ts. Extracts withMinimumOrgRole and sew into their own files under middleware/. Fixes 'use server' build error by removing logger export from actions.ts and fixing mock path in withAuth.test.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): update mock paths in listCommitsApi.test.ts Update vi.mock paths from @/withAuthV2 to @/middleware/withAuth and from @/actions to @/middleware/sew to match the renamed modules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): use auth context prisma instead of global import in audit and userManagement actions Replace direct @/prisma imports with the prisma instance from the withAuth callback to ensure userScopedPrismaClientExtension is applied. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feedback --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 85cf4be commit 7da5d55

File tree

43 files changed

+273
-255
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+273
-255
lines changed

CLAUDE.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ Server actions should be used for mutations (POST/PUT/DELETE operations), not fo
155155

156156
## Authentication
157157

158-
Use `withAuthV2` or `withOptionalAuthV2` from `@/withAuthV2` to protect server actions and API routes.
158+
Use `withAuth` or `withOptionalAuth` from `@/middleware/withAuth` to protect server actions and API routes.
159159

160-
- **`withAuthV2`** - Requires authentication. Returns `notAuthenticated()` if user is not logged in.
161-
- **`withOptionalAuthV2`** - Allows anonymous access if the org has anonymous access enabled. `user` may be `undefined`.
162-
- **`withMinimumOrgRole`** - Wrap inside auth context to require a minimum role (e.g., `OrgRole.OWNER`).
160+
- **`withAuth`** - Requires authentication. Returns `notAuthenticated()` if user is not logged in.
161+
- **`withOptionalAuth`** - Allows anonymous access if the org has anonymous access enabled. `user` may be `undefined`.
162+
- **`withMinimumOrgRole`** - Wrap inside auth context to require a minimum role (e.g., `OrgRole.OWNER`). Import from `@/middleware/withMinimumOrgRole`.
163163

164164
**Important:** Always use the `prisma` instance provided by the auth context. This instance has `userScopedPrismaClientExtension` applied, which enforces repository visibility rules (e.g., filtering repos based on user permissions). Do NOT import `prisma` directly from `@/prisma` in actions or routes that return data to the client.
165165

@@ -168,19 +168,19 @@ Use `withAuthV2` or `withOptionalAuthV2` from `@/withAuthV2` to protect server a
168168
```ts
169169
'use server';
170170

171-
import { sew } from "@/actions";
172-
import { withAuthV2 } from "@/withAuthV2";
171+
import { sew } from "@/middleware/sew";
172+
import { withAuth } from "@/middleware/withAuth";
173173

174174
export const myProtectedAction = async ({ id }: { id: string }) => sew(() =>
175-
withAuthV2(async ({ org, user, prisma }) => {
175+
withAuth(async ({ org, user, prisma }) => {
176176
// user is guaranteed to be defined
177177
// prisma is scoped to the user
178178
return { success: true };
179179
})
180180
);
181181

182182
export const myPublicAction = async ({ id }: { id: string }) => sew(() =>
183-
withOptionalAuthV2(async ({ org, user, prisma }) => {
183+
withOptionalAuth(async ({ org, user, prisma }) => {
184184
// user may be undefined for anonymous access
185185
return { success: true };
186186
})
@@ -192,10 +192,10 @@ export const myPublicAction = async ({ id }: { id: string }) => sew(() =>
192192
```ts
193193
import { serviceErrorResponse } from "@/lib/serviceError";
194194
import { isServiceError } from "@/lib/utils";
195-
import { withAuthV2 } from "@/withAuthV2";
195+
import { withAuth } from "@/middleware/withAuth";
196196

197197
export const GET = apiHandler(async (request: NextRequest) => {
198-
const result = await withAuthV2(async ({ org, user, prisma }) => {
198+
const result = await withAuth(async ({ org, user, prisma }) => {
199199
// ... your logic
200200
return data;
201201
});

packages/web/src/actions.ts

Lines changed: 29 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import { getAuditService } from "@/ee/features/audit/factory";
44
import { env, getSMTPConnectionURL } from "@sourcebot/shared";
55
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
66
import { ErrorCode } from "@/lib/errorCodes";
7-
import { notFound, orgNotFound, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
7+
import { notFound, orgNotFound, ServiceError } from "@/lib/serviceError";
88
import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils";
99
import { prisma } from "@/prisma";
1010
import { render } from "@react-email/components";
11-
import * as Sentry from '@sentry/nextjs';
1211
import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/shared";
1312
import { ApiKey, ConnectionSyncJobStatus, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db";
1413
import { createLogger } from "@sourcebot/shared";
@@ -26,36 +25,17 @@ import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail";
2625
import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail";
2726
import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants";
2827
import { ApiKeyPayload, RepositoryQuery } from "./lib/types";
29-
import { withAuthV2, withOptionalAuthV2, withMinimumOrgRole, withAuthV2_skipOrgMembershipCheck } from "./withAuthV2";
28+
import { withAuth, withOptionalAuth, withAuth_skipOrgMembershipCheck } from "./middleware/withAuth";
29+
import { withMinimumOrgRole } from "./middleware/withMinimumOrgRole";
3030
import { getBrowsePath } from "./app/[domain]/browse/hooks/utils";
31+
import { sew } from "@/middleware/sew";
3132

3233
const logger = createLogger('web-actions');
3334
const auditService = getAuditService();
3435

35-
/**
36-
* "Service Error Wrapper".
37-
*
38-
* Captures any thrown exceptions, logs them to the console and Sentry,
39-
* and returns a generic unexpected service error.
40-
*/
41-
export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => {
42-
try {
43-
return await fn();
44-
} catch (e) {
45-
Sentry.captureException(e);
46-
logger.error(e);
47-
48-
if (e instanceof ServiceErrorException) {
49-
return e.serviceError;
50-
}
51-
52-
return unexpectedError(`An unexpected error occurred. Please try again later.`);
53-
}
54-
}
55-
5636
////// Actions ///////
5737
export const completeOnboarding = async (): Promise<{ success: boolean } | ServiceError> => sew(() =>
58-
withAuthV2(async ({ org, prisma }) => {
38+
withAuth(async ({ org, prisma }) => {
5939
await prisma.org.update({
6040
where: { id: org.id },
6141
data: {
@@ -122,7 +102,7 @@ export const verifyApiKey = async (apiKeyPayload: ApiKeyPayload): Promise<{ apiK
122102

123103

124104
export const createApiKey = async (name: string): Promise<{ key: string } | ServiceError> => sew(() =>
125-
withAuthV2(async ({ org, user, role, prisma }) => {
105+
withAuth(async ({ org, user, role, prisma }) => {
126106
if ((env.DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS === 'true' || env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true') && role !== OrgRole.OWNER) {
127107
logger.error(`API key creation is disabled for non-admin users. User ${user.id} is not an owner.`);
128108
return {
@@ -192,7 +172,7 @@ export const createApiKey = async (name: string): Promise<{ key: string } | Serv
192172
}));
193173

194174
export const deleteApiKey = async (name: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
195-
withAuthV2(async ({ org, user, prisma }) => {
175+
withAuth(async ({ org, user, prisma }) => {
196176
const apiKey = await prisma.apiKey.findFirst({
197177
where: {
198178
name,
@@ -252,7 +232,7 @@ export const deleteApiKey = async (name: string): Promise<{ success: boolean } |
252232
}));
253233

254234
export const getUserApiKeys = async (): Promise<{ name: string; createdAt: Date; lastUsedAt: Date | null }[] | ServiceError> => sew(() =>
255-
withAuthV2(async ({ org, user, prisma }) => {
235+
withAuth(async ({ org, user, prisma }) => {
256236
const apiKeys = await prisma.apiKey.findMany({
257237
where: {
258238
orgId: org.id,
@@ -277,7 +257,7 @@ export const getRepos = async ({
277257
where?: Prisma.RepoWhereInput,
278258
take?: number
279259
} = {}) => sew(() =>
280-
withOptionalAuthV2(async ({ org, prisma }) => {
260+
withOptionalAuth(async ({ org, prisma }) => {
281261
const repos = await prisma.repo.findMany({
282262
where: {
283263
orgId: org.id,
@@ -313,7 +293,7 @@ export const getRepos = async ({
313293
* Returns a set of aggregated stats about the repos in the org
314294
*/
315295
export const getReposStats = async () => sew(() =>
316-
withOptionalAuthV2(async ({ org, prisma }) => {
296+
withOptionalAuth(async ({ org, prisma }) => {
317297
const [
318298
// Total number of repos.
319299
numberOfRepos,
@@ -364,7 +344,7 @@ export const getReposStats = async () => sew(() =>
364344
)
365345

366346
export const getConnectionStats = async () => sew(() =>
367-
withAuthV2(async ({ org, prisma }) => {
347+
withAuth(async ({ org, prisma }) => {
368348
const [
369349
numberOfConnections,
370350
numberOfConnectionsWithFirstTimeSyncJobsInProgress,
@@ -400,7 +380,7 @@ export const getConnectionStats = async () => sew(() =>
400380
);
401381

402382
export const getRepoInfoByName = async (repoName: string) => sew(() =>
403-
withOptionalAuthV2(async ({ org, prisma }) => {
383+
withOptionalAuth(async ({ org, prisma }) => {
404384
// @note: repo names are represented by their remote url
405385
// on the code host. E.g.,:
406386
// - github.com/sourcebot-dev/sourcebot
@@ -459,7 +439,7 @@ export const getRepoInfoByName = async (repoName: string) => sew(() =>
459439
}));
460440

461441
export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: string): Promise<{ connectionId: number } | ServiceError> => sew(() =>
462-
withOptionalAuthV2(async ({ org, prisma }) => {
442+
withOptionalAuth(async ({ org, prisma }) => {
463443
if (env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED !== 'true') {
464444
return {
465445
statusCode: StatusCodes.BAD_REQUEST,
@@ -595,12 +575,12 @@ export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: strin
595575
}));
596576

597577
export const getCurrentUserRole = async (): Promise<OrgRole | ServiceError> => sew(() =>
598-
withOptionalAuthV2(async ({ role }) => {
578+
withOptionalAuth(async ({ role }) => {
599579
return role;
600580
}));
601581

602582
export const createInvites = async (emails: string[]): Promise<{ success: boolean } | ServiceError> => sew(() =>
603-
withAuthV2(async ({ org, user, role, prisma }) =>
583+
withAuth(async ({ org, user, role, prisma }) =>
604584
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
605585
const failAuditCallback = async (error: string) => {
606586
await auditService.createAudit({
@@ -776,7 +756,7 @@ export const createInvites = async (emails: string[]): Promise<{ success: boolea
776756
));
777757

778758
export const cancelInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
779-
withAuthV2(async ({ org, role, prisma }) =>
759+
withAuth(async ({ org, role, prisma }) =>
780760
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
781761
const invite = await prisma.invite.findUnique({
782762
where: {
@@ -802,7 +782,7 @@ export const cancelInvite = async (inviteId: string): Promise<{ success: boolean
802782
));
803783

804784
export const getMe = async () => sew(() =>
805-
withAuthV2(async ({ user, prisma }) => {
785+
withAuth(async ({ user, prisma }) => {
806786
const userWithOrgs = await prisma.user.findUnique({
807787
where: {
808788
id: user.id,
@@ -835,7 +815,7 @@ export const getMe = async () => sew(() =>
835815
}));
836816

837817
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
838-
withAuthV2_skipOrgMembershipCheck(async ({ user, prisma }) => {
818+
withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => {
839819
const invite = await prisma.invite.findUnique({
840820
where: {
841821
id: inviteId,
@@ -909,7 +889,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
909889
}));
910890

911891
export const getInviteInfo = async (inviteId: string) => sew(() =>
912-
withAuthV2_skipOrgMembershipCheck(async ({ user, prisma }) => {
892+
withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => {
913893
const invite = await prisma.invite.findUnique({
914894
where: {
915895
id: inviteId,
@@ -946,7 +926,7 @@ export const getInviteInfo = async (inviteId: string) => sew(() =>
946926
}));
947927

948928
export const getOrgMembers = async () => sew(() =>
949-
withAuthV2(async ({ org, prisma }) => {
929+
withAuth(async ({ org, prisma }) => {
950930
const members = await prisma.userToOrg.findMany({
951931
where: {
952932
orgId: org.id,
@@ -970,7 +950,7 @@ export const getOrgMembers = async () => sew(() =>
970950
}));
971951

972952
export const getOrgInvites = async () => sew(() =>
973-
withAuthV2(async ({ org, prisma }) => {
953+
withAuth(async ({ org, prisma }) => {
974954
const invites = await prisma.invite.findMany({
975955
where: {
976956
orgId: org.id,
@@ -985,7 +965,7 @@ export const getOrgInvites = async () => sew(() =>
985965
}));
986966

987967
export const getOrgAccountRequests = async () => sew(() =>
988-
withAuthV2(async ({ org, prisma }) => {
968+
withAuth(async ({ org, prisma }) => {
989969
const requests = await prisma.accountRequest.findMany({
990970
where: {
991971
orgId: org.id,
@@ -1122,7 +1102,7 @@ export const getMemberApprovalRequired = async (domain: string): Promise<boolean
11221102
});
11231103

11241104
export const setMemberApprovalRequired = async (required: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () =>
1125-
withAuthV2(async ({ org, role, prisma }) =>
1105+
withAuth(async ({ org, role, prisma }) =>
11261106
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
11271107
await prisma.org.update({
11281108
where: { id: org.id },
@@ -1137,7 +1117,7 @@ export const setMemberApprovalRequired = async (required: boolean): Promise<{ su
11371117
);
11381118

11391119
export const setInviteLinkEnabled = async (enabled: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () =>
1140-
withAuthV2(async ({ org, role, prisma }) =>
1120+
withAuth(async ({ org, role, prisma }) =>
11411121
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
11421122
await prisma.org.update({
11431123
where: { id: org.id },
@@ -1152,7 +1132,7 @@ export const setInviteLinkEnabled = async (enabled: boolean): Promise<{ success:
11521132
);
11531133

11541134
export const approveAccountRequest = async (requestId: string) => sew(async () =>
1155-
withAuthV2(async ({ org, user, role, prisma }) =>
1135+
withAuth(async ({ org, user, role, prisma }) =>
11561136
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
11571137
const failAuditCallback = async (error: string) => {
11581138
await auditService.createAudit({
@@ -1242,7 +1222,7 @@ export const approveAccountRequest = async (requestId: string) => sew(async () =
12421222
));
12431223

12441224
export const rejectAccountRequest = async (requestId: string) => sew(() =>
1245-
withAuthV2(async ({ org, role, prisma }) =>
1225+
withAuth(async ({ org, role, prisma }) =>
12461226
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
12471227
const request = await prisma.accountRequest.findUnique({
12481228
where: {
@@ -1268,7 +1248,7 @@ export const rejectAccountRequest = async (requestId: string) => sew(() =>
12681248

12691249

12701250
export const getSearchContexts = async () => sew(() =>
1271-
withOptionalAuthV2(async ({ org, prisma }) => {
1251+
withOptionalAuth(async ({ org, prisma }) => {
12721252
const searchContexts = await prisma.searchContext.findMany({
12731253
where: {
12741254
orgId: org.id,
@@ -1287,7 +1267,7 @@ export const getSearchContexts = async () => sew(() =>
12871267
}));
12881268

12891269
export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | ServiceError> => sew(async () => {
1290-
return await withOptionalAuthV2(async ({ org, prisma }) => {
1270+
return await withOptionalAuth(async ({ org, prisma }) => {
12911271
const repo = await prisma.repo.findUnique({
12921272
where: {
12931273
id: repoId,
@@ -1383,7 +1363,7 @@ export const getAnonymousAccessStatus = async (domain: string): Promise<boolean
13831363
});
13841364

13851365
export const setAnonymousAccessStatus = async (enabled: boolean): Promise<ServiceError | boolean> => sew(async () => {
1386-
return await withAuthV2(async ({ org, role, prisma }) => {
1366+
return await withAuth(async ({ org, role, prisma }) => {
13871367
return await withMinimumOrgRole(role, OrgRole.OWNER, async () => {
13881368
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");
13891369
if (!hasAnonymousAccessEntitlement) {

packages/web/src/app/[domain]/askgh/[owner]/[repo]/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import 'server-only';
22

3-
import { sew } from '@/actions';
3+
import { sew } from "@/middleware/sew";
44
import { notFound, ServiceError } from '@/lib/serviceError';
5-
import { withOptionalAuthV2 } from '@/withAuthV2';
5+
import { withOptionalAuth } from '@/middleware/withAuth';
66
import { RepoInfo } from './types';
77

88
export const getRepoInfo = async (repoId: number): Promise<RepoInfo | ServiceError> => sew(() =>
9-
withOptionalAuthV2(async ({ prisma }) => {
9+
withOptionalAuth(async ({ prisma }) => {
1010
const repo = await prisma.repo.findUnique({
1111
where: { id: repoId },
1212
include: {

packages/web/src/app/[domain]/repos/[id]/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { getCurrentUserRole, sew } from "@/actions"
1+
import { getCurrentUserRole } from "@/actions"
2+
import { sew } from "@/middleware/sew"
23
import { Badge } from "@/components/ui/badge"
34
import { Button } from "@/components/ui/button"
45
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
@@ -8,7 +9,7 @@ import { env } from "@sourcebot/shared"
89
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
910
import { ServiceErrorException } from "@/lib/serviceError"
1011
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils"
11-
import { withOptionalAuthV2 } from "@/withAuthV2"
12+
import { withOptionalAuth } from "@/middleware/withAuth"
1213
import { getConfigSettings, repoMetadataSchema } from "@sourcebot/shared"
1314
import { ExternalLink, Info } from "lucide-react"
1415
import Image from "next/image"
@@ -190,7 +191,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
190191
}
191192

192193
const getRepoWithJobs = async (repoId: number) => sew(() =>
193-
withOptionalAuthV2(async ({ prisma, org }) => {
194+
withOptionalAuth(async ({ prisma, org }) => {
194195

195196
const repo = await prisma.repo.findUnique({
196197
where: {

packages/web/src/app/[domain]/repos/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { sew } from "@/actions";
1+
import { sew } from "@/middleware/sew";
22
import { ServiceErrorException } from "@/lib/serviceError";
33
import { isServiceError } from "@/lib/utils";
4-
import { withOptionalAuthV2 } from "@/withAuthV2";
4+
import { withOptionalAuth } from "@/middleware/withAuth";
55
import { ReposTable } from "./components/reposTable";
66
import { RepoIndexingJobStatus, Prisma } from "@sourcebot/db";
77
import z from "zod";
@@ -96,7 +96,7 @@ interface GetReposParams {
9696
}
9797

9898
const getRepos = async ({ skip, take, search, status, sortBy, sortOrder }: GetReposParams) => sew(() =>
99-
withOptionalAuthV2(async ({ prisma }) => {
99+
withOptionalAuth(async ({ prisma }) => {
100100
const whereClause: Prisma.RepoWhereInput = {
101101
...(search ? {
102102
displayName: { contains: search, mode: 'insensitive' },

0 commit comments

Comments
 (0)