Skip to content

Commit 6cc9d0b

Browse files
committed
refactor ui
1 parent 9676088 commit 6cc9d0b

File tree

17 files changed

+214
-342
lines changed

17 files changed

+214
-342
lines changed

docs/snippets/schemas/v3/identityProvider.schema.mdx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,7 @@
229229
"provider",
230230
"purpose",
231231
"clientId",
232-
"clientSecret",
233-
"baseUrl"
232+
"clientSecret"
234233
]
235234
},
236235
"GoogleIdentityProviderConfig": {
@@ -887,8 +886,7 @@
887886
"provider",
888887
"purpose",
889888
"clientId",
890-
"clientSecret",
891-
"baseUrl"
889+
"clientSecret"
892890
]
893891
},
894892
{

docs/snippets/schemas/v3/index.schema.mdx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4633,8 +4633,7 @@
46334633
"provider",
46344634
"purpose",
46354635
"clientId",
4636-
"clientSecret",
4637-
"baseUrl"
4636+
"clientSecret"
46384637
]
46394638
},
46404639
"GoogleIdentityProviderConfig": {
@@ -5291,8 +5290,7 @@
52915290
"provider",
52925291
"purpose",
52935292
"clientId",
5294-
"clientSecret",
5295-
"baseUrl"
5293+
"clientSecret"
52965294
]
52975295
},
52985296
{

packages/schemas/src/v3/identityProvider.schema.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,7 @@ const schema = {
228228
"provider",
229229
"purpose",
230230
"clientId",
231-
"clientSecret",
232-
"baseUrl"
231+
"clientSecret"
233232
]
234233
},
235234
"GoogleIdentityProviderConfig": {
@@ -886,8 +885,7 @@ const schema = {
886885
"provider",
887886
"purpose",
888887
"clientId",
889-
"clientSecret",
890-
"baseUrl"
888+
"clientSecret"
891889
]
892890
},
893891
{

packages/schemas/src/v3/identityProvider.type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export interface GitLabIdentityProviderConfig {
8383
*/
8484
googleCloudSecret: string;
8585
};
86-
baseUrl:
86+
baseUrl?:
8787
| {
8888
/**
8989
* The name of the environment variable that contains the token.

packages/schemas/src/v3/index.schema.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4632,8 +4632,7 @@ const schema = {
46324632
"provider",
46334633
"purpose",
46344634
"clientId",
4635-
"clientSecret",
4636-
"baseUrl"
4635+
"clientSecret"
46374636
]
46384637
},
46394638
"GoogleIdentityProviderConfig": {
@@ -5290,8 +5289,7 @@ const schema = {
52905289
"provider",
52915290
"purpose",
52925291
"clientId",
5293-
"clientSecret",
5294-
"baseUrl"
5292+
"clientSecret"
52955293
]
52965294
},
52975295
{

packages/schemas/src/v3/index.type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,7 @@ export interface GitLabIdentityProviderConfig {
11901190
*/
11911191
googleCloudSecret: string;
11921192
};
1193-
baseUrl:
1193+
baseUrl?:
11941194
| {
11951195
/**
11961196
* The name of the environment variable that contains the token.

packages/web/src/app/[domain]/layout.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
2323
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
2424
import { GitHubStarToast } from "./components/githubStarToast";
2525
import { UpgradeToast } from "./components/upgradeToast";
26-
import { getUnlinkedIntegrationProviders, userNeedsToLinkIdentityProvider } from "@/ee/features/permissionSyncing/actions";
26+
import { getIntegrationProviderStates } from "@/ee/features/permissionSyncing/actions";
2727
import { LinkAccounts } from "@/ee/features/permissionSyncing/linkAccounts";
2828

2929
interface LayoutProps {
@@ -126,42 +126,35 @@ export default async function Layout(props: LayoutProps) {
126126
}
127127

128128
if (hasEntitlement("permission-syncing")) {
129-
const unlinkedAccounts = await getUnlinkedIntegrationProviders();
130-
if (isServiceError(unlinkedAccounts)) {
129+
const integrationProviderStates = await getIntegrationProviderStates();
130+
if (isServiceError(integrationProviderStates)) {
131131
return (
132132
<div className="min-h-screen flex flex-col items-center justify-center p-6">
133133
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
134134
<div className="bg-red-50 border border-red-200 rounded-md p-6 max-w-md w-full text-center">
135135
<h2 className="text-lg font-semibold text-red-800 mb-2">An error occurred</h2>
136136
<p className="text-red-700 mb-1">
137-
{typeof unlinkedAccounts.message === 'string'
138-
? unlinkedAccounts.message
137+
{typeof integrationProviderStates.message === 'string'
138+
? integrationProviderStates.message
139139
: "A server error occurred while checking your account status. Please try again or contact support."}
140140
</p>
141141
</div>
142142
</div>
143143
)
144144
}
145145

146-
if (unlinkedAccounts.length > 0) {
147-
// Separate required and optional providers
148-
const requiredProviders = unlinkedAccounts.filter(p => p.required !== false);
149-
const hasRequiredProviders = requiredProviders.length > 0;
150-
151-
// Check if user has skipped optional providers
146+
const hasUnlinkedProviders = integrationProviderStates.some(state => state.isLinked === false);
147+
if (hasUnlinkedProviders) {
152148
const cookieStore = await cookies();
153149
const hasSkippedOptional = cookieStore.has(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME);
154150

155-
// Show LinkAccounts if:
156-
// 1. There are required providers, OR
157-
// 2. There are only optional providers AND user hasn't skipped yet
158-
const shouldShowLinkAccounts = hasRequiredProviders || !hasSkippedOptional;
159-
151+
const hasUnlinkedRequiredProviders = integrationProviderStates.some(state => state.required && !state.isLinked)
152+
const shouldShowLinkAccounts = hasUnlinkedRequiredProviders || !hasSkippedOptional;
160153
if (shouldShowLinkAccounts) {
161154
return (
162155
<div className="min-h-screen flex items-center justify-center p-6">
163156
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
164-
<LinkAccounts unlinkedAccounts={unlinkedAccounts} />
157+
<LinkAccounts integrationProviderStates={integrationProviderStates} callbackUrl={`/${domain}`}/>
165158
</div>
166159
)
167160
}

packages/web/src/app/[domain]/settings/permission-syncing/linkButton.tsx

Lines changed: 0 additions & 30 deletions
This file was deleted.

packages/web/src/app/[domain]/settings/permission-syncing/page.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,11 @@ import { hasEntitlement } from "@sourcebot/shared";
22
import { notFound } from "@/lib/serviceError";
33
import { LinkedAccountsSettings } from "@/ee/features/permissionSyncing/linkedAccountsSettings";
44

5-
interface PermissionSyncingPageProps {
6-
params: Promise<{
7-
domain: string;
8-
}>
9-
}
10-
11-
export default async function PermissionSyncingPage(props: PermissionSyncingPageProps) {
12-
const params = await props.params;
13-
5+
export default async function PermissionSyncingPage() {
146
const hasPermissionSyncingEntitlement = await hasEntitlement("permission-syncing");
157
if (!hasPermissionSyncingEntitlement) {
168
notFound();
179
}
1810

19-
return <LinkedAccountsSettings domain={params.domain} />;
11+
return <LinkedAccountsSettings />;
2012
}

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

Lines changed: 40 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,89 +8,60 @@ import { env } from "@/env.mjs";
88
import { OrgRole } from "@sourcebot/db";
99
import { cookies } from "next/headers";
1010
import { OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME } from "@/lib/constants";
11+
import { IntegrationIdentityProviderState } from "@/ee/features/permissionSyncing/types";
1112

1213
const logger = createLogger('web-ee-permission-syncing-actions');
1314

14-
export const userNeedsToLinkIdentityProvider = async () => sew(() =>
15+
export const getIntegrationProviderStates = async () => sew(() =>
1516
withAuthV2(async ({ prisma, role, user }) =>
1617
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
1718
const config = await loadConfig(env.CONFIG_PATH);
18-
const identityProviders = config.identityProviders ?? [];
19-
20-
for (const identityProvider of identityProviders) {
21-
if (identityProvider.purpose === "integration") {
22-
// Only check required providers (default to true if not specified)
23-
const isRequired = 'required' in identityProvider ? identityProvider.required : true;
24-
25-
if (!isRequired) {
26-
continue;
27-
}
28-
29-
const linkedAccount = await prisma.account.findFirst({
30-
where: {
31-
provider: identityProvider.provider,
32-
userId: user.id,
33-
},
34-
});
35-
36-
if (!linkedAccount) {
37-
logger.info(`Required integration identity provider ${identityProvider.provider} account info not found for user ${user.id}`);
38-
return true;
19+
const integrationProviderConfigs = config.identityProviders ?? [];
20+
const linkedAccounts = await prisma.account.findMany({
21+
where: {
22+
userId: user.id,
23+
provider: {
24+
in: integrationProviderConfigs.map(p => p.provider)
3925
}
26+
},
27+
select: {
28+
provider: true,
29+
providerAccountId: true
4030
}
41-
}
42-
43-
return false;
44-
})
45-
)
46-
);
47-
48-
export const getUnlinkedIntegrationProviders = async () => sew(() =>
49-
withAuthV2(async ({ prisma, role, user }) =>
50-
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
51-
const config = await loadConfig(env.CONFIG_PATH);
52-
const identityProviders = config.identityProviders ?? [];
53-
const unlinkedProviders = [];
54-
55-
for (const identityProvider of identityProviders) {
56-
if (identityProvider.purpose === "integration") {
57-
const linkedAccount = await prisma.account.findFirst({
58-
where: {
59-
provider: identityProvider.provider,
60-
userId: user.id,
61-
},
62-
});
31+
});
6332

64-
if (!linkedAccount) {
65-
const isRequired = 'required' in identityProvider ? identityProvider.required as boolean : true;
66-
logger.info(`Integration identity provider ${identityProvider.provider} not linked for user ${user.id}`);
67-
unlinkedProviders.push({
68-
id: identityProvider.provider,
69-
name: identityProvider.provider,
70-
purpose: "integration" as const,
71-
required: isRequired,
72-
});
73-
}
33+
const integrationProviderState: IntegrationIdentityProviderState[] = [];
34+
for (const integrationProviderConfig of integrationProviderConfigs) {
35+
if (integrationProviderConfig.purpose === "integration") {
36+
const linkedAccount = linkedAccounts.find(
37+
account => account.provider === integrationProviderConfig.provider
38+
);
39+
40+
const isLinked = !!linkedAccount;
41+
const isRequired = integrationProviderConfig.required ?? true;
42+
integrationProviderState.push({
43+
id: integrationProviderConfig.provider,
44+
required: isRequired,
45+
isLinked,
46+
linkedAccountId: linkedAccount?.providerAccountId
47+
} as IntegrationIdentityProviderState);
7448
}
7549
}
7650

77-
return unlinkedProviders;
51+
return integrationProviderState;
7852
})
7953
)
8054
);
8155

56+
8257
export const unlinkIntegrationProvider = async (provider: string) => sew(() =>
8358
withAuthV2(async ({ prisma, role, user }) =>
8459
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
8560
const config = await loadConfig(env.CONFIG_PATH);
8661
const identityProviders = config.identityProviders ?? [];
8762

88-
// Verify this is an integration provider
89-
const isIntegrationProvider = identityProviders.some(
90-
idp => idp.provider === provider && idp.purpose === "integration"
91-
);
92-
93-
if (!isIntegrationProvider) {
63+
const providerConfig = identityProviders.find(idp => idp.provider === provider)
64+
if (!providerConfig || !('purpose' in providerConfig) || providerConfig.purpose !== "integration") {
9465
throw new Error("Provider is not an integration provider");
9566
}
9667

@@ -104,6 +75,14 @@ export const unlinkIntegrationProvider = async (provider: string) => sew(() =>
10475

10576
logger.info(`Unlinked integration provider ${provider} for user ${user.id}. Deleted ${result.count} account(s).`);
10677

78+
// 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
79+
// user the option of linking optional providers in the same link accounts screen
80+
const isRequired = providerConfig.required ?? true;
81+
if (isRequired) {
82+
const cookieStore = await cookies();
83+
cookieStore.delete(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME);
84+
}
85+
10786
return { success: true, count: result.count };
10887
})
10988
)

0 commit comments

Comments
 (0)