sourcebot/packages/web/src/ee/features/permissionSyncing/actions.ts

108 lines
4.4 KiB
TypeScript
Raw Normal View History

2025-11-03 00:11:48 +00:00
'use server';
import { sew } from "@/actions";
import { createLogger } from "@sourcebot/logger";
import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2";
import { loadConfig } from "@sourcebot/shared";
import { env } from "@/env.mjs";
import { OrgRole } from "@sourcebot/db";
import { cookies } from "next/headers";
import { OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME } from "@/lib/constants";
2025-11-05 03:55:25 +00:00
import { LinkedAccountProviderState } from "@/ee/features/permissionSyncing/types";
2025-11-04 03:39:12 +00:00
import { auth } from "@/auth";
2025-11-03 00:11:48 +00:00
const logger = createLogger('web-ee-permission-syncing-actions');
2025-11-05 03:55:25 +00:00
export const getLinkedAccountProviderStates = async () => sew(() =>
2025-11-03 00:11:48 +00:00
withAuthV2(async ({ prisma, role, user }) =>
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
const config = await loadConfig(env.CONFIG_PATH);
2025-11-05 03:55:25 +00:00
const linkedAccountProviderConfigs = config.identityProviders ?? [];
2025-11-04 01:21:04 +00:00
const linkedAccounts = await prisma.account.findMany({
where: {
userId: user.id,
provider: {
2025-11-05 03:55:25 +00:00
in: linkedAccountProviderConfigs.map(p => p.provider)
2025-11-03 00:11:48 +00:00
}
2025-11-04 01:21:04 +00:00
},
select: {
provider: true,
providerAccountId: true
2025-11-03 00:11:48 +00:00
}
2025-11-04 01:21:04 +00:00
});
2025-11-03 00:11:48 +00:00
2025-11-04 03:39:12 +00:00
// Fetch the session to get token errors
const session = await auth();
2025-11-05 03:55:25 +00:00
const providerErrors = session?.linkedAccountProviderErrors;
2025-11-04 03:39:12 +00:00
2025-11-05 03:55:25 +00:00
const linkedAccountProviderState: LinkedAccountProviderState[] = [];
for (const linkedAccountProviderConfig of linkedAccountProviderConfigs) {
if (linkedAccountProviderConfig.purpose === "account_linking") {
2025-11-04 01:21:04 +00:00
const linkedAccount = linkedAccounts.find(
2025-11-05 03:55:25 +00:00
account => account.provider === linkedAccountProviderConfig.provider
2025-11-04 01:21:04 +00:00
);
const isLinked = !!linkedAccount;
2025-11-05 03:55:25 +00:00
const isRequired = linkedAccountProviderConfig.accountLinkingRequired ?? false;
const providerError = linkedAccount ? providerErrors?.[linkedAccount.providerAccountId] : undefined;
2025-11-04 03:39:12 +00:00
2025-11-05 03:55:25 +00:00
linkedAccountProviderState.push({
id: linkedAccountProviderConfig.provider,
2025-11-04 01:21:04 +00:00
required: isRequired,
isLinked,
2025-11-04 03:39:12 +00:00
linkedAccountId: linkedAccount?.providerAccountId,
error: providerError
2025-11-05 03:55:25 +00:00
} as LinkedAccountProviderState);
2025-11-03 00:11:48 +00:00
}
}
2025-11-05 03:55:25 +00:00
return linkedAccountProviderState;
2025-11-03 00:11:48 +00:00
})
)
);
2025-11-04 01:21:04 +00:00
2025-11-05 03:55:25 +00:00
export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
2025-11-03 00:11:48 +00:00
withAuthV2(async ({ prisma, role, user }) =>
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
const config = await loadConfig(env.CONFIG_PATH);
const identityProviders = config.identityProviders ?? [];
2025-11-04 01:21:04 +00:00
const providerConfig = identityProviders.find(idp => idp.provider === provider)
2025-11-05 03:55:25 +00:00
if (!providerConfig || providerConfig.purpose !== "account_linking") {
throw new Error("Provider is not a linked account provider");
2025-11-03 00:11:48 +00:00
}
// Delete the account
const result = await prisma.account.deleteMany({
where: {
provider,
userId: user.id,
},
});
2025-11-05 03:55:25 +00:00
logger.info(`Unlinked account provider ${provider} for user ${user.id}. Deleted ${result.count} account(s).`);
2025-11-03 00:11:48 +00:00
2025-11-04 01:21:04 +00:00
// If we're unlinking a required identity provider then we want to wipe the optional skip cookie if it exists so that we give the
// user the option of linking optional providers in the same link accounts screen
2025-11-05 03:55:25 +00:00
const isRequired = providerConfig.accountLinkingRequired ?? false;
2025-11-04 01:21:04 +00:00
if (isRequired) {
const cookieStore = await cookies();
cookieStore.delete(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME);
}
2025-11-03 00:11:48 +00:00
return { success: true, count: result.count };
})
)
);
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
httpOnly: false, // Allow client-side access
maxAge: 365 * 24 * 60 * 60, // 1 year in seconds
});
return true;
2025-11-04 02:27:13 +00:00
});