Skip to content

Commit df2e842

Browse files
committed
Updates to follow current codebase guidelines
1 parent 0fb818b commit df2e842

5 files changed

Lines changed: 265 additions & 279 deletions

File tree

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,62 @@
1-
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
1+
import { DEFAULT_BRANCH_ID } from "@/lib/tenancies";
22
import { globalPrismaClient } from "@/prisma-client";
3-
import { NextRequest, NextResponse } from "next/server";
4-
import { validateSupportAuth } from "../../../support-auth";
3+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
4+
import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
5+
import { supportAuthSchema, validateSupportTeamMembership } from "../../../support-auth";
56

6-
// Internal support endpoint for listing events in a project
7-
// Protected by Stack Auth session - requires @stack-auth.com email
7+
export const GET = createSmartRouteHandler({
8+
metadata: {
9+
hidden: true,
10+
summary: "List events in a project (Support)",
11+
description: "Internal support endpoint for listing events in a project. Requires support team membership.",
12+
tags: ["Internal", "Support"],
13+
},
14+
request: yupObject({
15+
auth: supportAuthSchema,
16+
params: yupObject({
17+
projectId: yupString().defined(),
18+
}).defined(),
19+
query: yupObject({
20+
limit: yupString().optional(),
21+
offset: yupString().optional(),
22+
}),
23+
method: yupString().oneOf(["GET"]).defined(),
24+
}),
25+
response: yupObject({
26+
statusCode: yupNumber().oneOf([200]).defined(),
27+
bodyType: yupString().oneOf(["json"]).defined(),
28+
body: yupObject({
29+
items: yupMixed().defined(),
30+
total: yupNumber().defined(),
31+
}).defined(),
32+
}),
33+
handler: async (req, fullReq) => {
34+
await validateSupportTeamMembership(fullReq.auth!);
835

9-
export async function GET(
10-
request: NextRequest,
11-
{ params }: { params: Promise<{ projectId: string }> }
12-
) {
13-
const auth = await validateSupportAuth(request);
14-
if (!auth.success) {
15-
return auth.response;
16-
}
36+
const { projectId } = req.params;
37+
const limit = Math.min(100, parseInt(req.query.limit ?? "30", 10));
38+
const offset = parseInt(req.query.offset ?? "0", 10);
1739

18-
const { projectId } = await params;
19-
const searchParams = request.nextUrl.searchParams;
20-
const limit = Math.min(100, parseInt(searchParams.get("limit") ?? "30", 10));
21-
const offset = parseInt(searchParams.get("offset") ?? "0", 10);
22-
23-
try {
24-
const tenancy = await getSoleTenancyFromProjectBranch(projectId, DEFAULT_BRANCH_ID);
40+
// Events are stored with projectId in the data field
41+
const whereClause = {
42+
AND: [
43+
{
44+
data: {
45+
path: ["projectId"],
46+
equals: projectId,
47+
},
48+
},
49+
{
50+
data: {
51+
path: ["branchId"],
52+
equals: DEFAULT_BRANCH_ID,
53+
},
54+
},
55+
],
56+
};
2557

26-
// Events are stored in the global prisma client with a data filter by tenancy
27-
// We need to query by the data field's tenancyId
2858
const events = await globalPrismaClient.event.findMany({
29-
where: {
30-
data: {
31-
path: ["tenancyId"],
32-
equals: tenancy.id,
33-
},
34-
},
59+
where: whereClause,
3560
orderBy: { eventStartedAt: "desc" },
3661
take: limit,
3762
skip: offset,
@@ -41,12 +66,7 @@ export async function GET(
4166
});
4267

4368
const total = await globalPrismaClient.event.count({
44-
where: {
45-
data: {
46-
path: ["tenancyId"],
47-
equals: tenancy.id,
48-
},
49-
},
69+
where: whereClause,
5070
});
5171

5272
const items = events.map((event) => ({
@@ -64,12 +84,10 @@ export async function GET(
6484
} : null,
6585
}));
6686

67-
return NextResponse.json({ items, total });
68-
} catch (error) {
69-
console.error("[Support API] Error fetching events:", error);
70-
return NextResponse.json(
71-
{ error: `Internal server error: ${error instanceof Error ? error.message : String(error)}` },
72-
{ status: 500 }
73-
);
74-
}
75-
}
87+
return {
88+
statusCode: 200,
89+
bodyType: "json",
90+
body: { items, total },
91+
};
92+
},
93+
});

apps/backend/src/app/api/latest/internal/support/projects/[projectId]/teams/route.tsx

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,46 @@
11
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
22
import { getPrismaClientForTenancy } from "@/prisma-client";
3-
import { NextRequest, NextResponse } from "next/server";
4-
import { validateSupportAuth } from "../../../support-auth";
3+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
4+
import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
5+
import { supportAuthSchema, validateSupportTeamMembership } from "../../../support-auth";
56

6-
// Internal support endpoint for listing teams in a project
7-
// Protected by Stack Auth session - requires @stack-auth.com email
7+
export const GET = createSmartRouteHandler({
8+
metadata: {
9+
hidden: true,
10+
summary: "List teams in a project (Support)",
11+
description: "Internal support endpoint for listing teams in a project. Requires support team membership.",
12+
tags: ["Internal", "Support"],
13+
},
14+
request: yupObject({
15+
auth: supportAuthSchema,
16+
params: yupObject({
17+
projectId: yupString().defined(),
18+
}).defined(),
19+
query: yupObject({
20+
search: yupString().optional(),
21+
teamId: yupString().optional(),
22+
limit: yupString().optional(),
23+
offset: yupString().optional(),
24+
}),
25+
method: yupString().oneOf(["GET"]).defined(),
26+
}),
27+
response: yupObject({
28+
statusCode: yupNumber().oneOf([200]).defined(),
29+
bodyType: yupString().oneOf(["json"]).defined(),
30+
body: yupObject({
31+
items: yupMixed().defined(),
32+
total: yupNumber().defined(),
33+
}).defined(),
34+
}),
35+
handler: async (req, fullReq) => {
36+
await validateSupportTeamMembership(fullReq.auth!);
837

9-
export async function GET(
10-
request: NextRequest,
11-
{ params }: { params: Promise<{ projectId: string }> }
12-
) {
13-
const auth = await validateSupportAuth(request);
14-
if (!auth.success) {
15-
return auth.response;
16-
}
38+
const { projectId } = req.params;
39+
const search = req.query.search;
40+
const teamId = req.query.teamId;
41+
const limit = Math.min(100, parseInt(req.query.limit ?? "25", 10));
42+
const offset = parseInt(req.query.offset ?? "0", 10);
1743

18-
const { projectId } = await params;
19-
const searchParams = request.nextUrl.searchParams;
20-
const search = searchParams.get("search") ?? undefined;
21-
const teamId = searchParams.get("teamId") ?? undefined; // Exact team ID lookup
22-
const limit = Math.min(100, parseInt(searchParams.get("limit") ?? "25", 10));
23-
const offset = parseInt(searchParams.get("offset") ?? "0", 10);
24-
25-
try {
2644
const tenancy = await getSoleTenancyFromProjectBranch(projectId, DEFAULT_BRANCH_ID);
2745
const prisma = await getPrismaClientForTenancy(tenancy);
2846

@@ -56,7 +74,7 @@ export async function GET(
5674
contactChannels: {
5775
where: {
5876
type: "EMAIL",
59-
isPrimary: "TRUE", // This is a special enum value in Prisma
77+
isPrimary: "TRUE",
6078
},
6179
},
6280
},
@@ -86,12 +104,10 @@ export async function GET(
86104
serverMetadata: team.serverMetadata,
87105
}));
88106

89-
return NextResponse.json({ items, total });
90-
} catch (error) {
91-
console.error("Support API error:", error);
92-
return NextResponse.json(
93-
{ error: "Internal server error" },
94-
{ status: 500 }
95-
);
96-
}
97-
}
107+
return {
108+
statusCode: 200,
109+
bodyType: "json",
110+
body: { items, total },
111+
};
112+
},
113+
});
Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,65 @@
11
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
22
import { getPrismaClientForTenancy } from "@/prisma-client";
3-
import { NextRequest, NextResponse } from "next/server";
4-
import { validateSupportAuth } from "../../../support-auth";
3+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
4+
import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
5+
import { supportAuthSchema, validateSupportTeamMembership } from "../../../support-auth";
56

6-
// Internal support endpoint for listing users in a project
7-
// Protected by Stack Auth session - requires @stack-auth.com email
7+
export const GET = createSmartRouteHandler({
8+
metadata: {
9+
hidden: true,
10+
summary: "List users in a project (Support)",
11+
description: "Internal support endpoint for listing users in a project. Requires support team membership.",
12+
tags: ["Internal", "Support"],
13+
},
14+
request: yupObject({
15+
auth: supportAuthSchema,
16+
params: yupObject({
17+
projectId: yupString().defined(),
18+
}).defined(),
19+
query: yupObject({
20+
search: yupString().optional(),
21+
userId: yupString().optional(),
22+
limit: yupString().optional(),
23+
offset: yupString().optional(),
24+
}),
25+
method: yupString().oneOf(["GET"]).defined(),
26+
}),
27+
response: yupObject({
28+
statusCode: yupNumber().oneOf([200]).defined(),
29+
bodyType: yupString().oneOf(["json"]).defined(),
30+
body: yupObject({
31+
items: yupMixed().defined(),
32+
total: yupNumber().defined(),
33+
}).defined(),
34+
}),
35+
handler: async (req, fullReq) => {
36+
await validateSupportTeamMembership(fullReq.auth!);
837

9-
export async function GET(
10-
request: NextRequest,
11-
{ params }: { params: Promise<{ projectId: string }> }
12-
) {
13-
const auth = await validateSupportAuth(request);
14-
if (!auth.success) {
15-
return auth.response;
16-
}
38+
const { projectId } = req.params;
39+
const search = req.query.search;
40+
const userId = req.query.userId;
41+
const limit = Math.min(100, parseInt(req.query.limit ?? "25", 10));
42+
const offset = parseInt(req.query.offset ?? "0", 10);
1743

18-
const { projectId } = await params;
19-
const searchParams = request.nextUrl.searchParams;
20-
const search = searchParams.get("search") ?? undefined;
21-
const userId = searchParams.get("userId") ?? undefined; // Exact user ID lookup
22-
const limit = Math.min(100, parseInt(searchParams.get("limit") ?? "25", 10));
23-
const offset = parseInt(searchParams.get("offset") ?? "0", 10);
24-
25-
try {
26-
console.log(`[Support API] Fetching users for project: ${projectId}, userId: ${userId ?? 'none'}, search: ${search ?? 'none'}`);
2744
const tenancy = await getSoleTenancyFromProjectBranch(projectId, DEFAULT_BRANCH_ID);
2845
const prisma = await getPrismaClientForTenancy(tenancy);
2946

3047
// Build search filter - exact userId takes priority
31-
const searchFilter = userId
48+
const searchFilter = userId
3249
? { projectUserId: userId }
3350
: search ? {
34-
OR: [
35-
{ displayName: { contains: search, mode: "insensitive" as const } },
36-
{ projectUserId: { contains: search, mode: "insensitive" as const } },
37-
{
38-
contactChannels: {
39-
some: {
40-
value: { contains: search, mode: "insensitive" as const },
41-
},
51+
OR: [
52+
{ displayName: { contains: search, mode: "insensitive" as const } },
53+
{ projectUserId: { contains: search, mode: "insensitive" as const } },
54+
{
55+
contactChannels: {
56+
some: {
57+
value: { contains: search, mode: "insensitive" as const },
4258
},
4359
},
44-
],
45-
} : {};
60+
},
61+
],
62+
} : {};
4663

4764
const [users, total] = await Promise.all([
4865
prisma.projectUser.findMany({
@@ -59,11 +76,18 @@ export async function GET(
5976
team: true,
6077
},
6178
},
62-
authMethods: true,
79+
authMethods: {
80+
include: {
81+
otpAuthMethod: true,
82+
passwordAuthMethod: true,
83+
passkeyAuthMethod: true,
84+
oauthAuthMethod: true,
85+
},
86+
},
6387
contactChannels: {
6488
where: {
6589
type: "EMAIL",
66-
isPrimary: "TRUE", // This is a special enum value in Prisma
90+
isPrimary: "TRUE",
6791
},
6892
},
6993
},
@@ -77,32 +101,35 @@ export async function GET(
77101
]);
78102

79103
const items = users.map((user) => {
80-
const primaryEmailChannel = user.contactChannels[0];
104+
const primaryEmailChannel = user.contactChannels.at(0);
81105
return {
82106
id: user.projectUserId,
83107
displayName: user.displayName,
84108
primaryEmail: primaryEmailChannel?.value ?? null,
85109
primaryEmailVerified: primaryEmailChannel?.isVerified ?? false,
86-
isAnonymous: user.isAnonymous ?? false,
110+
isAnonymous: user.isAnonymous,
87111
createdAt: user.createdAt.toISOString(),
88112
profileImageUrl: user.profileImageUrl,
89113
teams: user.teamMembers.map((tm) => ({
90114
id: tm.team.teamId,
91115
displayName: tm.team.displayName,
92116
})),
93-
authMethods: user.authMethods.map((am) => am.authMethodIdentifier),
117+
authMethods: user.authMethods.map((am) => {
118+
if (am.oauthAuthMethod) return `oauth:${am.oauthAuthMethod.configOAuthProviderId}`;
119+
if (am.passwordAuthMethod) return 'password';
120+
if (am.passkeyAuthMethod) return 'passkey';
121+
if (am.otpAuthMethod) return 'otp';
122+
return 'unknown';
123+
}),
94124
clientMetadata: user.clientMetadata,
95125
serverMetadata: user.serverMetadata,
96126
};
97127
});
98128

99-
console.log(`[Support API] Found ${total} users`);
100-
return NextResponse.json({ items, total });
101-
} catch (error) {
102-
console.error("[Support API] Error fetching users:", error);
103-
return NextResponse.json(
104-
{ error: `Internal server error: ${error instanceof Error ? error.message : String(error)}` },
105-
{ status: 500 }
106-
);
107-
}
108-
}
129+
return {
130+
statusCode: 200,
131+
bodyType: "json",
132+
body: { items, total },
133+
};
134+
},
135+
});

0 commit comments

Comments
 (0)