Skip to content

Commit b1e21cf

Browse files
authored
feat(api): add admin endpoint for updating org feature flags (#2891)
1 parent c3f2d07 commit b1e21cf

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { prisma } from "~/db.server";
4+
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
5+
import { validatePartialFeatureFlags } from "~/v3/featureFlags.server";
6+
7+
const ParamsSchema = z.object({
8+
organizationId: z.string(),
9+
});
10+
11+
async function authenticateAdmin(request: Request) {
12+
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
13+
14+
if (!authenticationResult) {
15+
return { error: json({ error: "Invalid or Missing API key" }, { status: 401 }) };
16+
}
17+
18+
const user = await prisma.user.findUnique({
19+
where: {
20+
id: authenticationResult.userId,
21+
},
22+
});
23+
24+
if (!user) {
25+
return { error: json({ error: "Invalid or Missing API key" }, { status: 401 }) };
26+
}
27+
28+
if (!user.admin) {
29+
return { error: json({ error: "You must be an admin to perform this action" }, { status: 403 }) };
30+
}
31+
32+
return { user };
33+
}
34+
35+
export async function loader({ request, params }: LoaderFunctionArgs) {
36+
const authResult = await authenticateAdmin(request);
37+
38+
if ("error" in authResult) {
39+
return authResult.error;
40+
}
41+
42+
const { organizationId } = ParamsSchema.parse(params);
43+
44+
const organization = await prisma.organization.findUnique({
45+
where: {
46+
id: organizationId,
47+
},
48+
select: {
49+
id: true,
50+
slug: true,
51+
featureFlags: true,
52+
},
53+
});
54+
55+
if (!organization) {
56+
return json({ error: "Organization not found" }, { status: 404 });
57+
}
58+
59+
const flagsResult = organization.featureFlags
60+
? validatePartialFeatureFlags(organization.featureFlags as Record<string, unknown>)
61+
: { success: false as const };
62+
63+
const featureFlags = flagsResult.success ? flagsResult.data : {};
64+
65+
return json({
66+
organizationId: organization.id,
67+
organizationSlug: organization.slug,
68+
featureFlags,
69+
});
70+
}
71+
72+
export async function action({ request, params }: ActionFunctionArgs) {
73+
const authResult = await authenticateAdmin(request);
74+
75+
if ("error" in authResult) {
76+
return authResult.error;
77+
}
78+
79+
const { organizationId } = ParamsSchema.parse(params);
80+
81+
const organization = await prisma.organization.findUnique({
82+
where: {
83+
id: organizationId,
84+
},
85+
select: {
86+
id: true,
87+
featureFlags: true,
88+
},
89+
});
90+
91+
if (!organization) {
92+
return json({ error: "Organization not found" }, { status: 404 });
93+
}
94+
95+
try {
96+
const body = await request.json();
97+
98+
// Validate the input using the partial schema
99+
const validationResult = validatePartialFeatureFlags(body as Record<string, unknown>);
100+
if (!validationResult.success) {
101+
return json(
102+
{
103+
error: "Invalid feature flags data",
104+
details: validationResult.error.issues,
105+
},
106+
{ status: 400 }
107+
);
108+
}
109+
110+
// Merge new flags with existing flags
111+
const existingFlags = organization.featureFlags
112+
? validatePartialFeatureFlags(organization.featureFlags as Record<string, unknown>)
113+
: { success: false as const };
114+
115+
const mergedFlags = {
116+
...(existingFlags.success ? existingFlags.data : {}),
117+
...validationResult.data,
118+
};
119+
120+
// Update the organization's feature flags
121+
const updatedOrganization = await prisma.organization.update({
122+
where: {
123+
id: organizationId,
124+
},
125+
data: {
126+
featureFlags: mergedFlags,
127+
},
128+
select: {
129+
id: true,
130+
slug: true,
131+
featureFlags: true,
132+
},
133+
});
134+
135+
const updatedFlagsResult = updatedOrganization.featureFlags
136+
? validatePartialFeatureFlags(updatedOrganization.featureFlags as Record<string, unknown>)
137+
: { success: false as const };
138+
139+
return json({
140+
success: true,
141+
organizationId: updatedOrganization.id,
142+
organizationSlug: updatedOrganization.slug,
143+
featureFlags: updatedFlagsResult.success ? updatedFlagsResult.data : {},
144+
});
145+
} catch (error) {
146+
return json(
147+
{
148+
error: error instanceof Error ? error.message : String(error),
149+
},
150+
{ status: 400 }
151+
);
152+
}
153+
}

0 commit comments

Comments
 (0)