Skip to content

Commit b1259c5

Browse files
committed
add logic for account link onboarding
1 parent 6db7aa3 commit b1259c5

File tree

33 files changed

+825
-58
lines changed

33 files changed

+825
-58
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
"additionalProperties": false
6767
}
6868
]
69+
},
70+
"required": {
71+
"type": "boolean",
72+
"default": true
6973
}
7074
},
7175
"required": [
@@ -137,6 +141,10 @@
137141
"additionalProperties": false
138142
}
139143
]
144+
},
145+
"required": {
146+
"type": "boolean",
147+
"default": true
140148
}
141149
},
142150
"required": [
@@ -482,6 +490,10 @@
482490
"additionalProperties": false
483491
}
484492
]
493+
},
494+
"required": {
495+
"type": "boolean",
496+
"default": true
485497
}
486498
},
487499
"required": [
@@ -553,6 +565,10 @@
553565
"additionalProperties": false
554566
}
555567
]
568+
},
569+
"required": {
570+
"type": "boolean",
571+
"default": true
556572
}
557573
},
558574
"required": [

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3677,6 +3677,10 @@
36773677
"additionalProperties": false
36783678
}
36793679
]
3680+
},
3681+
"required": {
3682+
"type": "boolean",
3683+
"default": true
36803684
}
36813685
},
36823686
"required": [
@@ -3748,6 +3752,10 @@
37483752
"additionalProperties": false
37493753
}
37503754
]
3755+
},
3756+
"required": {
3757+
"type": "boolean",
3758+
"default": true
37513759
}
37523760
},
37533761
"required": [
@@ -4093,6 +4101,10 @@
40934101
"additionalProperties": false
40944102
}
40954103
]
4104+
},
4105+
"required": {
4106+
"type": "boolean",
4107+
"default": true
40964108
}
40974109
},
40984110
"required": [
@@ -4164,6 +4176,10 @@
41644176
"additionalProperties": false
41654177
}
41664178
]
4179+
},
4180+
"required": {
4181+
"type": "boolean",
4182+
"default": true
41674183
}
41684184
},
41694185
"required": [

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ const schema = {
6565
"additionalProperties": false
6666
}
6767
]
68+
},
69+
"required": {
70+
"type": "boolean",
71+
"default": true
6872
}
6973
},
7074
"required": [
@@ -136,6 +140,10 @@ const schema = {
136140
"additionalProperties": false
137141
}
138142
]
143+
},
144+
"required": {
145+
"type": "boolean",
146+
"default": true
139147
}
140148
},
141149
"required": [
@@ -481,6 +489,10 @@ const schema = {
481489
"additionalProperties": false
482490
}
483491
]
492+
},
493+
"required": {
494+
"type": "boolean",
495+
"default": true
484496
}
485497
},
486498
"required": [
@@ -552,6 +564,10 @@ const schema = {
552564
"additionalProperties": false
553565
}
554566
]
567+
},
568+
"required": {
569+
"type": "boolean",
570+
"default": true
555571
}
556572
},
557573
"required": [

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface GitHubIdentityProviderConfig {
3030
*/
3131
env: string;
3232
};
33+
required?: boolean;
3334
[k: string]: unknown;
3435
}
3536
export interface GitLabIdentityProviderConfig {
@@ -53,6 +54,7 @@ export interface GitLabIdentityProviderConfig {
5354
*/
5455
env: string;
5556
};
57+
required?: boolean;
5658
[k: string]: unknown;
5759
}
5860
export interface GoogleIdentityProviderConfig {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3676,6 +3676,10 @@ const schema = {
36763676
"additionalProperties": false
36773677
}
36783678
]
3679+
},
3680+
"required": {
3681+
"type": "boolean",
3682+
"default": true
36793683
}
36803684
},
36813685
"required": [
@@ -3747,6 +3751,10 @@ const schema = {
37473751
"additionalProperties": false
37483752
}
37493753
]
3754+
},
3755+
"required": {
3756+
"type": "boolean",
3757+
"default": true
37503758
}
37513759
},
37523760
"required": [
@@ -4092,6 +4100,10 @@ const schema = {
40924100
"additionalProperties": false
40934101
}
40944102
]
4103+
},
4104+
"required": {
4105+
"type": "boolean",
4106+
"default": true
40954107
}
40964108
},
40974109
"required": [
@@ -4163,6 +4175,10 @@ const schema = {
41634175
"additionalProperties": false
41644176
}
41654177
]
4178+
},
4179+
"required": {
4180+
"type": "boolean",
4181+
"default": true
41664182
}
41674183
},
41684184
"required": [

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,7 @@ export interface GitHubIdentityProviderConfig {
981981
*/
982982
env: string;
983983
};
984+
required?: boolean;
984985
[k: string]: unknown;
985986
}
986987
export interface GitLabIdentityProviderConfig {
@@ -1004,6 +1005,7 @@ export interface GitLabIdentityProviderConfig {
10041005
*/
10051006
env: string;
10061007
};
1008+
required?: boolean;
10071009
[k: string]: unknown;
10081010
}
10091011
export interface GoogleIdentityProviderConfig {

packages/shared/src/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ export const loadJsonFile = async <T>(
8181
}
8282
}
8383

84-
export const loadConfig = async (configPath: string): Promise<SourcebotConfig> => {
84+
export const loadConfig = async (configPath?: string): Promise<SourcebotConfig> => {
85+
if (!configPath) {
86+
throw new Error('CONFIG_PATH is required but not provided');
87+
}
88+
8589
const configContent = await (async () => {
8690
if (isRemotePath(configPath)) {
8791
const response = await fetch(configPath);

packages/web/src/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { getAuditService } from "@/ee/features/audit/factory";
44
import { env } from "@/env.mjs";
55
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
66
import { ErrorCode } from "@/lib/errorCodes";
7-
import { notAuthenticated, notFound, orgNotFound, secretAlreadyExists, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
7+
import { notAuthenticated, notFound, orgNotFound, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
88
import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils";
99
import { prisma } from "@/prisma";
1010
import { render } from "@react-email/components";
1111
import * as Sentry from '@sentry/nextjs';
12-
import { encrypt, generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto";
12+
import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto";
1313
import { ApiKey, ConnectionSyncJobStatus, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db";
1414
import { createLogger } from "@sourcebot/logger";
1515
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { UpgradeGuard } from "./components/upgradeGuard";
77
import { cookies, headers } from "next/headers";
88
import { getSelectorsByUserAgent } from "react-device-detect";
99
import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen";
10-
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants";
10+
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME } from "@/lib/constants";
1111
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
1212
import { SyntaxGuideProvider } from "./components/syntaxGuideProvider";
1313
import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
@@ -23,6 +23,8 @@ 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";
27+
import { LinkAccounts } from "@/ee/features/permissionSyncing/linkAccounts";
2628

2729
interface LayoutProps {
2830
children: React.ReactNode,
@@ -123,6 +125,49 @@ export default async function Layout(props: LayoutProps) {
123125
)
124126
}
125127

128+
if (hasEntitlement("permission-syncing")) {
129+
const unlinkedAccounts = await getUnlinkedIntegrationProviders();
130+
if (isServiceError(unlinkedAccounts)) {
131+
return (
132+
<div className="min-h-screen flex flex-col items-center justify-center p-6">
133+
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
134+
<div className="bg-red-50 border border-red-200 rounded-md p-6 max-w-md w-full text-center">
135+
<h2 className="text-lg font-semibold text-red-800 mb-2">An error occurred</h2>
136+
<p className="text-red-700 mb-1">
137+
{typeof unlinkedAccounts.message === 'string'
138+
? unlinkedAccounts.message
139+
: "A server error occurred while checking your account status. Please try again or contact support."}
140+
</p>
141+
</div>
142+
</div>
143+
)
144+
}
145+
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
152+
const cookieStore = await cookies();
153+
const hasSkippedOptional = cookieStore.has(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME);
154+
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+
160+
if (shouldShowLinkAccounts) {
161+
return (
162+
<div className="min-h-screen flex items-center justify-center p-6">
163+
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
164+
<LinkAccounts unlinkedAccounts={unlinkedAccounts} />
165+
</div>
166+
)
167+
}
168+
}
169+
}
170+
126171
if (IS_BILLING_ENABLED) {
127172
const subscription = await getSubscriptionInfo(domain);
128173
if (

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ServiceErrorException } from "@/lib/serviceError";
1111
import { getOrgFromDomain } from "@/data/org";
1212
import { OrgRole } from "@prisma/client";
1313
import { env } from "@/env.mjs";
14+
import { hasEntitlement } from "@sourcebot/shared";
1415

1516
interface LayoutProps {
1617
children: React.ReactNode;
@@ -68,6 +69,8 @@ export default async function SettingsLayout(
6869
throw new ServiceErrorException(connectionStats);
6970
}
7071

72+
const hasPermissionSyncingEntitlement = await hasEntitlement("permission-syncing");
73+
7174
const sidebarNavItems: SidebarNavItem[] = [
7275
{
7376
title: "General",
@@ -114,6 +117,12 @@ export default async function SettingsLayout(
114117
title: "Analytics",
115118
href: `/${domain}/settings/analytics`,
116119
},
120+
...(hasPermissionSyncingEntitlement ? [
121+
{
122+
title: "Linked Accounts",
123+
href: `/${domain}/settings/permission-syncing`,
124+
}
125+
] : []),
117126
...(env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined ? [
118127
{
119128
title: "License",

0 commit comments

Comments
 (0)