Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Final fixes
  • Loading branch information
N2D4 committed Feb 16, 2026
commit 6c7e41c70aeb1cf974f833f889a1806a53ba273b
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { resetBranchConfigOverrideKeys, resetEnvironmentConfigOverrideKeys } from "@/lib/config";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, adminAuthTypeSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";

const levelSchema = yupString().oneOf(["branch", "environment"]).defined();

const levelConfigs = {
branch: {
reset: (options: { projectId: string, branchId: string, keysToReset: string[] }) =>
resetBranchConfigOverrideKeys(options),
},
environment: {
reset: (options: { projectId: string, branchId: string, keysToReset: string[] }) =>
resetEnvironmentConfigOverrideKeys(options),
},
};

export const POST = createSmartRouteHandler({
metadata: {
hidden: true,
summary: 'Reset config override keys',
description: 'Remove specific keys (and their nested descendants) from the config override at a given level. Uses the same nested key logic as the override algorithm.',
tags: ['Config'],
},
request: yupObject({
auth: yupObject({
type: adminAuthTypeSchema,
tenancy: adaptSchema,
}).defined(),
params: yupObject({
level: levelSchema,
}).defined(),
body: yupObject({
keys: yupArray(yupString().defined()).defined(),
}).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["success"]).defined(),
}),
handler: async (req) => {
const levelConfig = levelConfigs[req.params.level];

await levelConfig.reset({
projectId: req.auth.tenancy.project.id,
branchId: req.auth.tenancy.branchId,
keysToReset: req.body.keys,
});

return {
statusCode: 200 as const,
bodyType: "success" as const,
};
},
});
71 changes: 70 additions & 1 deletion apps/backend/src/lib/config.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Prisma } from "@/generated/prisma/client";
import { Config, getInvalidConfigReason, normalize, override } from "@stackframe/stack-shared/dist/config/format";
import { Config, getInvalidConfigReason, normalize, override, removeKeysFromConfig } from "@stackframe/stack-shared/dist/config/format";
import { BranchConfigOverride, BranchConfigOverrideOverride, BranchIncompleteConfig, BranchRenderedConfig, CompleteConfig, EnvironmentConfigOverride, EnvironmentConfigOverrideOverride, EnvironmentIncompleteConfig, EnvironmentRenderedConfig, OrganizationConfigOverride, OrganizationConfigOverrideOverride, OrganizationIncompleteConfig, ProjectConfigOverride, ProjectConfigOverrideOverride, ProjectIncompleteConfig, ProjectRenderedConfig, applyBranchDefaults, applyEnvironmentDefaults, applyOrganizationDefaults, applyProjectDefaults, assertNoConfigOverrideErrors, branchConfigSchema, environmentConfigSchema, getConfigOverrideErrors, getIncompleteConfigWarnings, migrateConfigOverride, organizationConfigSchema, projectConfigSchema, sanitizeBranchConfig, sanitizeEnvironmentConfig, sanitizeOrganizationConfig, sanitizeProjectConfig } from "@stackframe/stack-shared/dist/config/schema";
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
import { branchConfigSourceSchema, yupBoolean, yupMixed, yupObject, yupRecord, yupString, yupUnion } from "@stackframe/stack-shared/dist/schema-fields";
Expand Down Expand Up @@ -457,6 +457,75 @@ export function overrideOrganizationConfigOverride(options: {
}


// ---------------------------------------------------------------------------------------------------------------------
// reset functions (remove specific keys from config override)
// ---------------------------------------------------------------------------------------------------------------------
// Uses the same nested key logic as the `override` function: resetting key "a.b" also resets "a.b.c".

export async function resetProjectConfigOverrideKeys(options: {
projectId: string,
keysToReset: string[],
}): Promise<void> {
// TODO put this in a serializable transaction (or a single SQL query) to prevent race conditions
const oldConfig = await rawQuery(globalPrismaClient, getProjectConfigOverrideQuery(options));
const newConfig = removeKeysFromConfig(oldConfig, options.keysToReset);

await setProjectConfigOverride({
projectId: options.projectId,
projectConfigOverride: newConfig as ProjectConfigOverride,
});
}

export async function resetBranchConfigOverrideKeys(options: {
projectId: string,
branchId: string,
keysToReset: string[],
}): Promise<void> {
// TODO put this in a serializable transaction (or a single SQL query) to prevent race conditions
const oldConfig = await rawQuery(globalPrismaClient, getBranchConfigOverrideQuery(options));
const newConfig = removeKeysFromConfig(oldConfig, options.keysToReset);

await setBranchConfigOverride({
projectId: options.projectId,
branchId: options.branchId,
branchConfigOverride: newConfig as BranchConfigOverride,
});
}

export async function resetEnvironmentConfigOverrideKeys(options: {
projectId: string,
branchId: string,
keysToReset: string[],
}): Promise<void> {
// TODO put this in a serializable transaction (or a single SQL query) to prevent race conditions
const oldConfig = await rawQuery(globalPrismaClient, getEnvironmentConfigOverrideQuery(options));
const newConfig = removeKeysFromConfig(oldConfig, options.keysToReset);

await setEnvironmentConfigOverride({
projectId: options.projectId,
branchId: options.branchId,
environmentConfigOverride: newConfig as EnvironmentConfigOverride,
});
}

export async function resetOrganizationConfigOverrideKeys(options: {
projectId: string,
branchId: string,
organizationId: string | null,
keysToReset: string[],
}): Promise<void> {
// TODO put this in a serializable transaction (or a single SQL query) to prevent race conditions
const oldConfig = await rawQuery(globalPrismaClient, getOrganizationConfigOverrideQuery(options));
const newConfig = removeKeysFromConfig(oldConfig, options.keysToReset);

await setOrganizationConfigOverride({
projectId: options.projectId,
branchId: options.branchId,
organizationId: options.organizationId,
organizationConfigOverride: newConfig as OrganizationConfigOverride,
});
}

// ---------------------------------------------------------------------------------------------------------------------
// internal functions
// ---------------------------------------------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { allProviders } from "@stackframe/stack-shared/dist/utils/oauth";
import { typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects";
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
import { useMemo, useState } from "react";
import { CardSubtitle } from "../../../../../../../../../packages/stack-ui/dist/components/ui/card";
import { AppEnabledGuard } from "../app-enabled-guard";
import { PageLayout } from "../page-layout";
import { useAdminApp } from "../use-admin-app";
Expand Down Expand Up @@ -434,9 +433,9 @@ export default function PageClient() {
onSave={handleAuthMethodsSave}
onDiscard={handleAuthMethodsDiscard}
/>
<CardSubtitle className="mt-2">
<Typography variant="secondary" className="mt-2">
SSO Providers
</CardSubtitle>
</Typography>

{enabledProviders.map(([, provider]) => provider)
.filter((provider): provider is AdminOAuthProviderConfig => !!provider).map(provider => {
Expand Down
Loading
Loading