mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
add logic for account link onboarding
This commit is contained in:
parent
6db7aa37dd
commit
b1259c564f
33 changed files with 825 additions and 58 deletions
|
|
@ -66,6 +66,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -137,6 +141,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -482,6 +490,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -553,6 +565,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -3677,6 +3677,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -3748,6 +3752,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -4093,6 +4101,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -4164,6 +4176,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -136,6 +140,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -481,6 +489,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -552,6 +564,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export interface GitHubIdentityProviderConfig {
|
||||||
*/
|
*/
|
||||||
env: string;
|
env: string;
|
||||||
};
|
};
|
||||||
|
required?: boolean;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
export interface GitLabIdentityProviderConfig {
|
export interface GitLabIdentityProviderConfig {
|
||||||
|
|
@ -53,6 +54,7 @@ export interface GitLabIdentityProviderConfig {
|
||||||
*/
|
*/
|
||||||
env: string;
|
env: string;
|
||||||
};
|
};
|
||||||
|
required?: boolean;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
export interface GoogleIdentityProviderConfig {
|
export interface GoogleIdentityProviderConfig {
|
||||||
|
|
|
||||||
|
|
@ -3676,6 +3676,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -3747,6 +3751,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -4092,6 +4100,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -4163,6 +4175,10 @@ const schema = {
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -981,6 +981,7 @@ export interface GitHubIdentityProviderConfig {
|
||||||
*/
|
*/
|
||||||
env: string;
|
env: string;
|
||||||
};
|
};
|
||||||
|
required?: boolean;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
export interface GitLabIdentityProviderConfig {
|
export interface GitLabIdentityProviderConfig {
|
||||||
|
|
@ -1004,6 +1005,7 @@ export interface GitLabIdentityProviderConfig {
|
||||||
*/
|
*/
|
||||||
env: string;
|
env: string;
|
||||||
};
|
};
|
||||||
|
required?: boolean;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
export interface GoogleIdentityProviderConfig {
|
export interface GoogleIdentityProviderConfig {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,11 @@ export const loadJsonFile = async <T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadConfig = async (configPath: string): Promise<SourcebotConfig> => {
|
export const loadConfig = async (configPath?: string): Promise<SourcebotConfig> => {
|
||||||
|
if (!configPath) {
|
||||||
|
throw new Error('CONFIG_PATH is required but not provided');
|
||||||
|
}
|
||||||
|
|
||||||
const configContent = await (async () => {
|
const configContent = await (async () => {
|
||||||
if (isRemotePath(configPath)) {
|
if (isRemotePath(configPath)) {
|
||||||
const response = await fetch(configPath);
|
const response = await fetch(configPath);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import { getAuditService } from "@/ee/features/audit/factory";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
|
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
|
||||||
import { ErrorCode } from "@/lib/errorCodes";
|
import { ErrorCode } from "@/lib/errorCodes";
|
||||||
import { notAuthenticated, notFound, orgNotFound, secretAlreadyExists, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
|
import { notAuthenticated, notFound, orgNotFound, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
|
||||||
import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils";
|
import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils";
|
||||||
import { prisma } from "@/prisma";
|
import { prisma } from "@/prisma";
|
||||||
import { render } from "@react-email/components";
|
import { render } from "@react-email/components";
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import { encrypt, generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto";
|
import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto";
|
||||||
import { ApiKey, ConnectionSyncJobStatus, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db";
|
import { ApiKey, ConnectionSyncJobStatus, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { UpgradeGuard } from "./components/upgradeGuard";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import { getSelectorsByUserAgent } from "react-device-detect";
|
import { getSelectorsByUserAgent } from "react-device-detect";
|
||||||
import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen";
|
import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen";
|
||||||
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants";
|
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME } from "@/lib/constants";
|
||||||
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
|
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
|
||||||
import { SyntaxGuideProvider } from "./components/syntaxGuideProvider";
|
import { SyntaxGuideProvider } from "./components/syntaxGuideProvider";
|
||||||
import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
|
import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
|
||||||
|
|
@ -23,6 +23,8 @@ import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
|
||||||
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
|
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
|
||||||
import { GitHubStarToast } from "./components/githubStarToast";
|
import { GitHubStarToast } from "./components/githubStarToast";
|
||||||
import { UpgradeToast } from "./components/upgradeToast";
|
import { UpgradeToast } from "./components/upgradeToast";
|
||||||
|
import { getUnlinkedIntegrationProviders, userNeedsToLinkIdentityProvider } from "@/ee/features/permissionSyncing/actions";
|
||||||
|
import { LinkAccounts } from "@/ee/features/permissionSyncing/linkAccounts";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
|
|
@ -123,6 +125,49 @@ export default async function Layout(props: LayoutProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasEntitlement("permission-syncing")) {
|
||||||
|
const unlinkedAccounts = await getUnlinkedIntegrationProviders();
|
||||||
|
if (isServiceError(unlinkedAccounts)) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||||
|
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-md p-6 max-w-md w-full text-center">
|
||||||
|
<h2 className="text-lg font-semibold text-red-800 mb-2">An error occurred</h2>
|
||||||
|
<p className="text-red-700 mb-1">
|
||||||
|
{typeof unlinkedAccounts.message === 'string'
|
||||||
|
? unlinkedAccounts.message
|
||||||
|
: "A server error occurred while checking your account status. Please try again or contact support."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unlinkedAccounts.length > 0) {
|
||||||
|
// Separate required and optional providers
|
||||||
|
const requiredProviders = unlinkedAccounts.filter(p => p.required !== false);
|
||||||
|
const hasRequiredProviders = requiredProviders.length > 0;
|
||||||
|
|
||||||
|
// Check if user has skipped optional providers
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const hasSkippedOptional = cookieStore.has(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME);
|
||||||
|
|
||||||
|
// Show LinkAccounts if:
|
||||||
|
// 1. There are required providers, OR
|
||||||
|
// 2. There are only optional providers AND user hasn't skipped yet
|
||||||
|
const shouldShowLinkAccounts = hasRequiredProviders || !hasSkippedOptional;
|
||||||
|
|
||||||
|
if (shouldShowLinkAccounts) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-6">
|
||||||
|
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
|
||||||
|
<LinkAccounts unlinkedAccounts={unlinkedAccounts} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED) {
|
if (IS_BILLING_ENABLED) {
|
||||||
const subscription = await getSubscriptionInfo(domain);
|
const subscription = await getSubscriptionInfo(domain);
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { ServiceErrorException } from "@/lib/serviceError";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { getOrgFromDomain } from "@/data/org";
|
||||||
import { OrgRole } from "@prisma/client";
|
import { OrgRole } from "@prisma/client";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
|
import { hasEntitlement } from "@sourcebot/shared";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -68,6 +69,8 @@ export default async function SettingsLayout(
|
||||||
throw new ServiceErrorException(connectionStats);
|
throw new ServiceErrorException(connectionStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasPermissionSyncingEntitlement = await hasEntitlement("permission-syncing");
|
||||||
|
|
||||||
const sidebarNavItems: SidebarNavItem[] = [
|
const sidebarNavItems: SidebarNavItem[] = [
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "General",
|
||||||
|
|
@ -114,6 +117,12 @@ export default async function SettingsLayout(
|
||||||
title: "Analytics",
|
title: "Analytics",
|
||||||
href: `/${domain}/settings/analytics`,
|
href: `/${domain}/settings/analytics`,
|
||||||
},
|
},
|
||||||
|
...(hasPermissionSyncingEntitlement ? [
|
||||||
|
{
|
||||||
|
title: "Linked Accounts",
|
||||||
|
href: `/${domain}/settings/permission-syncing`,
|
||||||
|
}
|
||||||
|
] : []),
|
||||||
...(env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined ? [
|
...(env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined ? [
|
||||||
{
|
{
|
||||||
title: "License",
|
title: "License",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Link2 } from "lucide-react";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
|
||||||
|
interface LinkButtonProps {
|
||||||
|
provider: string;
|
||||||
|
providerName: string;
|
||||||
|
callbackUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkButton = ({ provider, providerName, callbackUrl }: LinkButtonProps) => {
|
||||||
|
const handleLink = () => {
|
||||||
|
signIn(provider, {
|
||||||
|
redirectTo: callbackUrl
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleLink}
|
||||||
|
>
|
||||||
|
<Link2 className="h-4 w-4 mr-1" />
|
||||||
|
Link
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { hasEntitlement } from "@sourcebot/shared";
|
||||||
|
import { notFound } from "@/lib/serviceError";
|
||||||
|
import { LinkedAccountsSettings } from "@/ee/features/permissionSyncing/linkedAccountsSettings";
|
||||||
|
|
||||||
|
interface PermissionSyncingPageProps {
|
||||||
|
params: Promise<{
|
||||||
|
domain: string;
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PermissionSyncingPage(props: PermissionSyncingPageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
const hasPermissionSyncingEntitlement = await hasEntitlement("permission-syncing");
|
||||||
|
if (!hasPermissionSyncingEntitlement) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LinkedAccountsSettings domain={params.domain} />;
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import { CredentialsForm } from "@/app/login/components/credentialsForm";
|
||||||
import { DividerSet } from "@/app/components/dividerSet";
|
import { DividerSet } from "@/app/components/dividerSet";
|
||||||
import { ProviderButton } from "@/app/components/providerButton";
|
import { ProviderButton } from "@/app/components/providerButton";
|
||||||
import { AuthSecurityNotice } from "@/app/components/authSecurityNotice";
|
import { AuthSecurityNotice } from "@/app/components/authSecurityNotice";
|
||||||
import type { IdentityProviderMetadata } from "@/lib/authProviders";
|
import type { IdentityProviderMetadata } from "@/lib/identityProviders";
|
||||||
|
|
||||||
interface AuthMethodSelectorProps {
|
interface AuthMethodSelectorProps {
|
||||||
providers: IdentityProviderMetadata[];
|
providers: IdentityProviderMetadata[];
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
||||||
import { AuthMethodSelector } from "@/app/components/authMethodSelector";
|
import { AuthMethodSelector } from "@/app/components/authMethodSelector";
|
||||||
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
|
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
|
||||||
import { getIdentityProviderMetadata, IdentityProviderMetadata } from "@/lib/authProviders";
|
import { getIdentityProviderMetadata, IdentityProviderMetadata } from "@/lib/identityProviders";
|
||||||
import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
|
import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
|
||||||
|
|
||||||
interface InvitePageProps {
|
interface InvitePageProps {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
||||||
import { AuthMethodSelector } from "@/app/components/authMethodSelector";
|
import { AuthMethodSelector } from "@/app/components/authMethodSelector";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { IdentityProviderMetadata } from "@/lib/authProviders";
|
import type { IdentityProviderMetadata } from "@/lib/identityProviders";
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
callbackUrl?: string;
|
callbackUrl?: string;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { auth } from "@/auth";
|
||||||
import { LoginForm } from "./components/loginForm";
|
import { LoginForm } from "./components/loginForm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Footer } from "@/app/components/footer";
|
import { Footer } from "@/app/components/footer";
|
||||||
import { getIdentityProviderMetadata } from "@/lib/authProviders";
|
import { getIdentityProviderMetadata } from "@/lib/identityProviders";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { getOrgFromDomain } from "@/data/org";
|
||||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
|
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
|
||||||
import { AuthMethodSelector } from "@/app/components/authMethodSelector"
|
import { AuthMethodSelector } from "@/app/components/authMethodSelector"
|
||||||
import { SourcebotLogo } from "@/app/components/sourcebotLogo"
|
import { SourcebotLogo } from "@/app/components/sourcebotLogo"
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { getIdentityProviderMetadata } from "@/lib/authProviders";
|
import { getIdentityProviderMetadata } from "@/lib/identityProviders";
|
||||||
import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings";
|
import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings";
|
||||||
import { CompleteOnboardingButton } from "./components/completeOnboardingButton";
|
import { CompleteOnboardingButton } from "./components/completeOnboardingButton";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { getOrgFromDomain } from "@/data/org";
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { LoginForm } from "../login/components/loginForm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Footer } from "@/app/components/footer";
|
import { Footer } from "@/app/components/footer";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { getIdentityProviderMetadata } from "@/lib/authProviders";
|
import { getIdentityProviderMetadata } from "@/lib/identityProviders";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { getOrgFromDomain } from "@/data/org";
|
||||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
|
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export const runtime = 'nodejs';
|
||||||
export type IdentityProvider = {
|
export type IdentityProvider = {
|
||||||
provider: Provider;
|
provider: Provider;
|
||||||
purpose: "sso" | "integration";
|
purpose: "sso" | "integration";
|
||||||
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'next-auth' {
|
declare module 'next-auth' {
|
||||||
|
|
|
||||||
119
packages/web/src/ee/features/permissionSyncing/actions.ts
Normal file
119
packages/web/src/ee/features/permissionSyncing/actions.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
'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";
|
||||||
|
|
||||||
|
const logger = createLogger('web-ee-permission-syncing-actions');
|
||||||
|
|
||||||
|
export const userNeedsToLinkIdentityProvider = async () => sew(() =>
|
||||||
|
withAuthV2(async ({ prisma, role, user }) =>
|
||||||
|
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
|
||||||
|
const config = await loadConfig(env.CONFIG_PATH);
|
||||||
|
const identityProviders = config.identityProviders ?? [];
|
||||||
|
|
||||||
|
for (const identityProvider of identityProviders) {
|
||||||
|
if (identityProvider.purpose === "integration") {
|
||||||
|
// Only check required providers (default to true if not specified)
|
||||||
|
const isRequired = 'required' in identityProvider ? identityProvider.required : true;
|
||||||
|
|
||||||
|
if (!isRequired) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedAccount = await prisma.account.findFirst({
|
||||||
|
where: {
|
||||||
|
provider: identityProvider.provider,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!linkedAccount) {
|
||||||
|
logger.info(`Required integration identity provider ${identityProvider.provider} account info not found for user ${user.id}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getUnlinkedIntegrationProviders = async () => sew(() =>
|
||||||
|
withAuthV2(async ({ prisma, role, user }) =>
|
||||||
|
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
|
||||||
|
const config = await loadConfig(env.CONFIG_PATH);
|
||||||
|
const identityProviders = config.identityProviders ?? [];
|
||||||
|
const unlinkedProviders = [];
|
||||||
|
|
||||||
|
for (const identityProvider of identityProviders) {
|
||||||
|
if (identityProvider.purpose === "integration") {
|
||||||
|
const linkedAccount = await prisma.account.findFirst({
|
||||||
|
where: {
|
||||||
|
provider: identityProvider.provider,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!linkedAccount) {
|
||||||
|
const isRequired = 'required' in identityProvider ? identityProvider.required as boolean : true;
|
||||||
|
logger.info(`Integration identity provider ${identityProvider.provider} not linked for user ${user.id}`);
|
||||||
|
unlinkedProviders.push({
|
||||||
|
id: identityProvider.provider,
|
||||||
|
name: identityProvider.provider,
|
||||||
|
purpose: "integration" as const,
|
||||||
|
required: isRequired,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlinkedProviders;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const unlinkIntegrationProvider = async (provider: string) => sew(() =>
|
||||||
|
withAuthV2(async ({ prisma, role, user }) =>
|
||||||
|
withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
|
||||||
|
const config = await loadConfig(env.CONFIG_PATH);
|
||||||
|
const identityProviders = config.identityProviders ?? [];
|
||||||
|
|
||||||
|
// Verify this is an integration provider
|
||||||
|
const isIntegrationProvider = identityProviders.some(
|
||||||
|
idp => idp.provider === provider && idp.purpose === "integration"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isIntegrationProvider) {
|
||||||
|
throw new Error("Provider is not an integration provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the account
|
||||||
|
const result = await prisma.account.deleteMany({
|
||||||
|
where: {
|
||||||
|
provider,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Unlinked integration provider ${provider} for user ${user.id}. Deleted ${result.count} account(s).`);
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
interface ProviderBadgeProps {
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProviderBadge({ required }: ProviderBadgeProps) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant={required ? "default" : "secondary"}
|
||||||
|
className="text-xs font-medium"
|
||||||
|
>
|
||||||
|
{required ? "Required" : "Optional"}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
import { ShieldCheck } from "lucide-react";
|
||||||
|
|
||||||
|
interface ProviderIconProps {
|
||||||
|
icon?: {
|
||||||
|
src: string;
|
||||||
|
className?: string;
|
||||||
|
} | null;
|
||||||
|
displayName: string;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: {
|
||||||
|
container: "h-8 w-8",
|
||||||
|
icon: "h-4 w-4"
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
container: "h-10 w-10",
|
||||||
|
icon: "h-5 w-5"
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
container: "h-12 w-12",
|
||||||
|
icon: "h-6 w-6"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProviderIcon({ icon, displayName, size = "md" }: ProviderIconProps) {
|
||||||
|
const sizes = sizeClasses[size];
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
return (
|
||||||
|
<div className={`${sizes.container} rounded-md border border-border bg-background flex items-center justify-center`}>
|
||||||
|
<Image
|
||||||
|
src={icon.src}
|
||||||
|
alt={displayName}
|
||||||
|
className={`${sizes.icon} ${icon.className || ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${sizes.container} rounded-lg border border-border flex items-center justify-center bg-muted`}>
|
||||||
|
<ShieldCheck className={`${sizes.icon} text-muted-foreground`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { getAuthProviderInfo } from "@/lib/utils";
|
||||||
|
import { ProviderIcon } from "./providerIcon";
|
||||||
|
import { ProviderBadge } from "./providerBadge";
|
||||||
|
|
||||||
|
interface ProviderInfoProps {
|
||||||
|
providerId: string;
|
||||||
|
required: boolean;
|
||||||
|
showBadge?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProviderInfo({ providerId, required, showBadge = true }: ProviderInfoProps) {
|
||||||
|
const providerInfo = getAuthProviderInfo(providerId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-foreground">
|
||||||
|
{providerInfo.displayName}
|
||||||
|
</span>
|
||||||
|
{showBadge && <ProviderBadge required={required} />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
packages/web/src/ee/features/permissionSyncing/linkAccounts.tsx
Normal file
118
packages/web/src/ee/features/permissionSyncing/linkAccounts.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
import { getAuthProviderInfo } from "@/lib/utils";
|
||||||
|
import type { IdentityProviderMetadata } from "@/lib/identityProviders";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { skipOptionalProvidersLink } from "./actions";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ProviderIcon } from "./components/providerIcon";
|
||||||
|
import { ProviderInfo } from "./components/providerInfo";
|
||||||
|
|
||||||
|
interface LinkAccountsProps {
|
||||||
|
unlinkedAccounts: IdentityProviderMetadata[];
|
||||||
|
callbackUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkAccounts = ({ unlinkedAccounts, callbackUrl }: LinkAccountsProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isSkipping, setIsSkipping] = useState(false);
|
||||||
|
|
||||||
|
const handleSignIn = (providerId: string) => {
|
||||||
|
signIn(providerId, {
|
||||||
|
redirectTo: callbackUrl ?? "/"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkip = async () => {
|
||||||
|
setIsSkipping(true);
|
||||||
|
try {
|
||||||
|
await skipOptionalProvidersLink();
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to skip optional providers:", error);
|
||||||
|
setIsSkipping(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Separate required and optional providers
|
||||||
|
const requiredProviders = unlinkedAccounts.filter(p => p.required !== false);
|
||||||
|
const optionalProviders = unlinkedAccounts.filter(p => p.required === false);
|
||||||
|
const hasOnlyOptionalProviders = requiredProviders.length === 0 && optionalProviders.length > 0;
|
||||||
|
|
||||||
|
const renderProviderButton = (provider: IdentityProviderMetadata) => {
|
||||||
|
const providerInfo = getAuthProviderInfo(provider.id);
|
||||||
|
const isRequired = provider.required !== false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={provider.id}
|
||||||
|
onClick={() => handleSignIn(provider.id)}
|
||||||
|
className="group w-full flex items-center gap-4 p-4 rounded-lg border border-border bg-card hover:bg-accent hover:border-accent-foreground/20 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 group-hover:border-primary/20 transition-colors">
|
||||||
|
<ProviderIcon
|
||||||
|
icon={providerInfo.icon}
|
||||||
|
displayName={providerInfo.displayName}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0 text-left">
|
||||||
|
<ProviderInfo
|
||||||
|
providerId={provider.id}
|
||||||
|
required={isRequired}
|
||||||
|
showBadge={true}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Click to connect
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground group-hover:translate-x-0.5 transition-all" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Connect Your Accounts</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{hasOnlyOptionalProviders ? (
|
||||||
|
<>
|
||||||
|
The following optional accounts can be linked to enhance your experience.
|
||||||
|
<br />
|
||||||
|
You can link them now or skip and manage them later in <strong>Settings → Linked Accounts.</strong>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Link the following accounts to enable permission syncing and access all features.
|
||||||
|
<br />
|
||||||
|
You can manage your linked accounts later in <strong>Settings → Linked Accounts.</strong>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{requiredProviders.map(renderProviderButton)}
|
||||||
|
{optionalProviders.map(renderProviderButton)}
|
||||||
|
</div>
|
||||||
|
{hasOnlyOptionalProviders && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleSkip}
|
||||||
|
disabled={isSkipping}
|
||||||
|
>
|
||||||
|
{isSkipping ? "Skipping..." : "Skip for now"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Link2 } from "lucide-react";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
|
||||||
|
interface LinkButtonProps {
|
||||||
|
provider: string;
|
||||||
|
providerName: string;
|
||||||
|
callbackUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkButton = ({ provider, providerName, callbackUrl }: LinkButtonProps) => {
|
||||||
|
const handleLink = () => {
|
||||||
|
signIn(provider, {
|
||||||
|
redirectTo: callbackUrl
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleLink}
|
||||||
|
className="transition-all hover:bg-accent"
|
||||||
|
>
|
||||||
|
<Link2 className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { withAuthV2 } from "@/withAuthV2";
|
||||||
|
import { sew } from "@/actions";
|
||||||
|
import { isServiceError, getAuthProviderInfo } from "@/lib/utils";
|
||||||
|
import { loadConfig } from "@sourcebot/shared";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { Check, X, ShieldCheck } from "lucide-react";
|
||||||
|
import { getUnlinkedIntegrationProviders } from "./actions";
|
||||||
|
import { UnlinkButton } from "./unlinkButton";
|
||||||
|
import { LinkButton } from "./linkButton";
|
||||||
|
import { ServiceErrorException } from "@/lib/serviceError";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { ProviderIcon } from "./components/providerIcon";
|
||||||
|
import { ProviderInfo } from "./components/providerInfo";
|
||||||
|
|
||||||
|
interface LinkedAccountsSettingsProps {
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function LinkedAccountsSettings({ domain }: LinkedAccountsSettingsProps) {
|
||||||
|
const config = await loadConfig(env.CONFIG_PATH);
|
||||||
|
const integrationProviders = (config.identityProviders ?? [])
|
||||||
|
.filter(provider => provider.purpose === "integration");
|
||||||
|
|
||||||
|
// Get user's linked accounts
|
||||||
|
const getLinkedAccounts = async () => sew(() =>
|
||||||
|
withAuthV2(async ({ prisma, user }) => {
|
||||||
|
const accounts = await prisma.account.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
provider: {
|
||||||
|
in: integrationProviders.map(p => p.provider)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
provider: true,
|
||||||
|
providerAccountId: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return accounts;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const linkedAccountsResult = await getLinkedAccounts();
|
||||||
|
if (isServiceError(linkedAccountsResult)) {
|
||||||
|
throw new ServiceErrorException(linkedAccountsResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedAccounts = linkedAccountsResult;
|
||||||
|
|
||||||
|
const unlinkedProvidersResult = await getUnlinkedIntegrationProviders();
|
||||||
|
if (isServiceError(unlinkedProvidersResult)) {
|
||||||
|
throw new ServiceErrorException(unlinkedProvidersResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">Linked Accounts</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Manage your linked integration accounts for permission syncing and code host access.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show linked accounts as separate cards */}
|
||||||
|
{integrationProviders.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<div className="rounded-full bg-muted p-3 mb-4">
|
||||||
|
<ShieldCheck className="h-6 w-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-foreground mb-1">No integration providers configured</p>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-sm">
|
||||||
|
Contact your administrator to configure integration providers for your organization.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{integrationProviders.map((provider) => {
|
||||||
|
const providerInfo = getAuthProviderInfo(provider.provider);
|
||||||
|
const linkedAccount = linkedAccounts.find(
|
||||||
|
account => account.provider === provider.provider
|
||||||
|
);
|
||||||
|
const isLinked = !!linkedAccount;
|
||||||
|
const isRequired = 'required' in provider ? (provider.required as boolean) : true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={provider.provider}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<ProviderIcon
|
||||||
|
icon={providerInfo.icon}
|
||||||
|
displayName={providerInfo.displayName}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5 min-w-0 flex-1">
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
<ProviderInfo
|
||||||
|
providerId={provider.provider}
|
||||||
|
required={isRequired}
|
||||||
|
showBadge={true}
|
||||||
|
/>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs">
|
||||||
|
{isLinked ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Check className="h-3.5 w-3.5 text-green-600 dark:text-green-500" />
|
||||||
|
<span className="text-green-600 dark:text-green-500 font-medium">
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{linkedAccount.providerAccountId && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<span className="text-muted-foreground font-mono truncate">
|
||||||
|
{linkedAccount.providerAccountId}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Not connected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 ml-4">
|
||||||
|
{isLinked ? (
|
||||||
|
<UnlinkButton
|
||||||
|
provider={provider.provider}
|
||||||
|
providerName={providerInfo.displayName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LinkButton
|
||||||
|
provider={provider.provider}
|
||||||
|
providerName={providerInfo.displayName}
|
||||||
|
callbackUrl={`/${domain}/settings/permission-syncing`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Unlink, Loader2 } from "lucide-react";
|
||||||
|
import { unlinkIntegrationProvider } from "./actions";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
|
|
||||||
|
interface UnlinkButtonProps {
|
||||||
|
provider: string;
|
||||||
|
providerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnlinkButton = ({ provider, providerName }: UnlinkButtonProps) => {
|
||||||
|
const [isUnlinking, setIsUnlinking] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const handleUnlink = async () => {
|
||||||
|
if (!confirm(`Are you sure you want to disconnect your ${providerName} account?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUnlinking(true);
|
||||||
|
try {
|
||||||
|
const result = await unlinkIntegrationProvider(provider);
|
||||||
|
|
||||||
|
if (isServiceError(result)) {
|
||||||
|
toast({
|
||||||
|
description: `❌ Failed to disconnect account. ${result.message}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setIsUnlinking(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
description: `✅ ${providerName} account disconnected successfully.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh the page to show updated state
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
description: `❌ Failed to disconnect account. ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setIsUnlinking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleUnlink}
|
||||||
|
disabled={isUnlinking}
|
||||||
|
className="text-destructive hover:text-destructive hover:bg-destructive/10 transition-all"
|
||||||
|
>
|
||||||
|
{isUnlinking ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
||||||
|
Disconnecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Unlink className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Disconnect
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,7 @@ import { prisma } from "@/prisma";
|
||||||
import { OAuth2Client } from "google-auth-library";
|
import { OAuth2Client } from "google-auth-library";
|
||||||
import Credentials from "next-auth/providers/credentials";
|
import Credentials from "next-auth/providers/credentials";
|
||||||
import type { User as AuthJsUser } from "next-auth";
|
import type { User as AuthJsUser } from "next-auth";
|
||||||
|
import type { Provider } from "next-auth/providers";
|
||||||
import { onCreateUser } from "@/lib/authUtils";
|
import { onCreateUser } from "@/lib/authUtils";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { hasEntitlement, loadConfig } from "@sourcebot/shared";
|
import { hasEntitlement, loadConfig } from "@sourcebot/shared";
|
||||||
|
|
@ -21,7 +22,7 @@ const logger = createLogger('web-sso');
|
||||||
export const getEEIdentityProviders = async (): Promise<IdentityProvider[]> => {
|
export const getEEIdentityProviders = async (): Promise<IdentityProvider[]> => {
|
||||||
const providers: IdentityProvider[] = [];
|
const providers: IdentityProvider[] = [];
|
||||||
|
|
||||||
const config = env.CONFIG_PATH ? await loadConfig(env.CONFIG_PATH) : undefined;
|
const config = await loadConfig(env.CONFIG_PATH);
|
||||||
const identityProviders = config?.identityProviders ?? [];
|
const identityProviders = config?.identityProviders ?? [];
|
||||||
|
|
||||||
for (const identityProvider of identityProviders) {
|
for (const identityProvider of identityProviders) {
|
||||||
|
|
@ -30,14 +31,14 @@ export const getEEIdentityProviders = async (): Promise<IdentityProvider[]> => {
|
||||||
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
||||||
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
||||||
const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined;
|
const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined;
|
||||||
providers.push({ provider: createGitHubProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose });
|
providers.push({ provider: createGitHubProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.required ?? true });
|
||||||
}
|
}
|
||||||
if (identityProvider.provider === "gitlab") {
|
if (identityProvider.provider === "gitlab") {
|
||||||
const providerConfig = identityProvider as GitLabIdentityProviderConfig;
|
const providerConfig = identityProvider as GitLabIdentityProviderConfig;
|
||||||
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
||||||
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
||||||
const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined;
|
const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined;
|
||||||
providers.push({ provider: createGitLabProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose });
|
providers.push({ provider: createGitLabProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.required ?? true });
|
||||||
}
|
}
|
||||||
if (identityProvider.provider === "google") {
|
if (identityProvider.provider === "google") {
|
||||||
const providerConfig = identityProvider as GoogleIdentityProviderConfig;
|
const providerConfig = identityProvider as GoogleIdentityProviderConfig;
|
||||||
|
|
@ -57,49 +58,49 @@ export const getEEIdentityProviders = async (): Promise<IdentityProvider[]> => {
|
||||||
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
||||||
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
||||||
const issuer = await getTokenFromConfig(providerConfig.issuer);
|
const issuer = await getTokenFromConfig(providerConfig.issuer);
|
||||||
providers.push({ provider: createKeycloakProvider(clientId, clientSecret, issuer), purpose: "sso"});
|
providers.push({ provider: createKeycloakProvider(clientId, clientSecret, issuer), purpose: "sso" });
|
||||||
}
|
}
|
||||||
if (identityProvider.provider === "microsoft-entra-id") {
|
if (identityProvider.provider === "microsoft-entra-id") {
|
||||||
const providerConfig = identityProvider as MicrosoftEntraIDIdentityProviderConfig;
|
const providerConfig = identityProvider as MicrosoftEntraIDIdentityProviderConfig;
|
||||||
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
||||||
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
||||||
const issuer = await getTokenFromConfig(providerConfig.issuer);
|
const issuer = await getTokenFromConfig(providerConfig.issuer);
|
||||||
providers.push({ provider: createMicrosoftEntraIDProvider(clientId, clientSecret, issuer), purpose: "sso"});
|
providers.push({ provider: createMicrosoftEntraIDProvider(clientId, clientSecret, issuer), purpose: "sso" });
|
||||||
}
|
}
|
||||||
if (identityProvider.provider === "gcp-iap") {
|
if (identityProvider.provider === "gcp-iap") {
|
||||||
const providerConfig = identityProvider as GCPIAPIdentityProviderConfig;
|
const providerConfig = identityProvider as GCPIAPIdentityProviderConfig;
|
||||||
const audience = await getTokenFromConfig(providerConfig.audience);
|
const audience = await getTokenFromConfig(providerConfig.audience);
|
||||||
providers.push({ provider: createGCPIAPProvider(audience), purpose: "sso"});
|
providers.push({ provider: createGCPIAPProvider(audience), purpose: "sso" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @deprecate
|
// @deprecate
|
||||||
if (env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
|
if (env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
|
||||||
providers.push(createGitHubProvider(env.AUTH_EE_GITHUB_CLIENT_ID, env.AUTH_EE_GITHUB_CLIENT_SECRET, env.AUTH_EE_GITHUB_BASE_URL));
|
providers.push({ provider: createGitHubProvider(env.AUTH_EE_GITHUB_CLIENT_ID, env.AUTH_EE_GITHUB_CLIENT_SECRET, env.AUTH_EE_GITHUB_BASE_URL), purpose: "sso" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
|
if (env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
|
||||||
providers.push(createGitLabProvider(env.AUTH_EE_GITLAB_CLIENT_ID, env.AUTH_EE_GITLAB_CLIENT_SECRET, env.AUTH_EE_GITLAB_BASE_URL));
|
providers.push({ provider: createGitLabProvider(env.AUTH_EE_GITLAB_CLIENT_ID, env.AUTH_EE_GITLAB_CLIENT_SECRET, env.AUTH_EE_GITLAB_BASE_URL), purpose: "sso" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_GOOGLE_CLIENT_ID && env.AUTH_EE_GOOGLE_CLIENT_SECRET) {
|
if (env.AUTH_EE_GOOGLE_CLIENT_ID && env.AUTH_EE_GOOGLE_CLIENT_SECRET) {
|
||||||
providers.push(createGoogleProvider(env.AUTH_EE_GOOGLE_CLIENT_ID, env.AUTH_EE_GOOGLE_CLIENT_SECRET));
|
providers.push({ provider: createGoogleProvider(env.AUTH_EE_GOOGLE_CLIENT_ID, env.AUTH_EE_GOOGLE_CLIENT_SECRET), purpose: "sso" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_OKTA_CLIENT_ID && env.AUTH_EE_OKTA_CLIENT_SECRET && env.AUTH_EE_OKTA_ISSUER) {
|
if (env.AUTH_EE_OKTA_CLIENT_ID && env.AUTH_EE_OKTA_CLIENT_SECRET && env.AUTH_EE_OKTA_ISSUER) {
|
||||||
providers.push(createOktaProvider(env.AUTH_EE_OKTA_CLIENT_ID, env.AUTH_EE_OKTA_CLIENT_SECRET, env.AUTH_EE_OKTA_ISSUER));
|
providers.push({ provider: createOktaProvider(env.AUTH_EE_OKTA_CLIENT_ID, env.AUTH_EE_OKTA_CLIENT_SECRET, env.AUTH_EE_OKTA_ISSUER), purpose: "sso" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_KEYCLOAK_CLIENT_ID && env.AUTH_EE_KEYCLOAK_CLIENT_SECRET && env.AUTH_EE_KEYCLOAK_ISSUER) {
|
if (env.AUTH_EE_KEYCLOAK_CLIENT_ID && env.AUTH_EE_KEYCLOAK_CLIENT_SECRET && env.AUTH_EE_KEYCLOAK_ISSUER) {
|
||||||
providers.push(createKeycloakProvider(env.AUTH_EE_KEYCLOAK_CLIENT_ID, env.AUTH_EE_KEYCLOAK_CLIENT_SECRET, env.AUTH_EE_KEYCLOAK_ISSUER));
|
providers.push({ provider: createKeycloakProvider(env.AUTH_EE_KEYCLOAK_CLIENT_ID, env.AUTH_EE_KEYCLOAK_CLIENT_SECRET, env.AUTH_EE_KEYCLOAK_ISSUER), purpose: "sso" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID && env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET && env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER) {
|
if (env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID && env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET && env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER) {
|
||||||
providers.push(createMicrosoftEntraIDProvider(env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID, env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET, env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER));
|
providers.push({ provider: createMicrosoftEntraIDProvider(env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID, env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET, env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER), purpose: "sso" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) {
|
if (env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) {
|
||||||
providers.push(createGCPIAPProvider(env.AUTH_EE_GCP_IAP_AUDIENCE));
|
providers.push({ provider: createGCPIAPProvider(env.AUTH_EE_GCP_IAP_AUDIENCE), purpose: "sso" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return providers;
|
return providers;
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
import { ChatVisibility, OrgRole, Prisma } from "@sourcebot/db";
|
import { ChatVisibility, OrgRole, Prisma } from "@sourcebot/db";
|
||||||
import { LanguageModel } from "@sourcebot/schemas/v3/languageModel.type";
|
import { LanguageModel } from "@sourcebot/schemas/v3/languageModel.type";
|
||||||
import { Token } from "@sourcebot/schemas/v3/shared.type";
|
import { Token } from "@sourcebot/schemas/v3/shared.type";
|
||||||
import { loadConfig } from "@sourcebot/shared";
|
|
||||||
import { generateText, JSONValue, extractReasoningMiddleware, wrapLanguageModel } from "ai";
|
import { generateText, JSONValue, extractReasoningMiddleware, wrapLanguageModel } from "ai";
|
||||||
|
import { loadConfig } from "@sourcebot/shared";
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
@ -360,17 +360,8 @@ export const getConfiguredLanguageModelsInfo = async (): Promise<LanguageModelIn
|
||||||
* or pass the result of calling this function to the client.
|
* or pass the result of calling this function to the client.
|
||||||
*/
|
*/
|
||||||
export const _getConfiguredLanguageModelsFull = async (): Promise<LanguageModel[]> => {
|
export const _getConfiguredLanguageModelsFull = async (): Promise<LanguageModel[]> => {
|
||||||
if (!env.CONFIG_PATH) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = await loadConfig(env.CONFIG_PATH);
|
const config = await loadConfig(env.CONFIG_PATH);
|
||||||
return config.models ?? [];
|
return config.models ?? [];
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load config file ${env.CONFIG_PATH}: ${error}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ const initSingleTenancy = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync anonymous access config from the config file
|
// Sync anonymous access config from the config file
|
||||||
if (env.CONFIG_PATH) {
|
|
||||||
const config = await loadConfig(env.CONFIG_PATH);
|
const config = await loadConfig(env.CONFIG_PATH);
|
||||||
const forceEnableAnonymousAccess = config.settings?.enablePublicAccess ?? env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
|
const forceEnableAnonymousAccess = config.settings?.enablePublicAccess ?? env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
|
||||||
|
|
||||||
|
|
@ -91,7 +90,6 @@ const initSingleTenancy = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initMultiTenancy = async () => {
|
const initMultiTenancy = async () => {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export const TEAM_FEATURES = [
|
||||||
|
|
||||||
export const MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME = 'sb.mobile-unsupported-splash-screen-dismissed';
|
export const MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME = 'sb.mobile-unsupported-splash-screen-dismissed';
|
||||||
export const AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME = 'sb.agentic-search-tutorial-dismissed';
|
export const AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME = 'sb.agentic-search-tutorial-dismissed';
|
||||||
|
export const OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME = 'sb.optional-providers-link-skipped';
|
||||||
|
|
||||||
// NOTE: changing SOURCEBOT_GUEST_USER_ID may break backwards compatibility since this value is used
|
// NOTE: changing SOURCEBOT_GUEST_USER_ID may break backwards compatibility since this value is used
|
||||||
// to detect old guest users in the DB. If you change this value ensure it doesn't break upgrade flows
|
// to detect old guest users in the DB. If you change this value ensure it doesn't break upgrade flows
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export interface IdentityProviderMetadata {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
purpose: "sso" | "integration";
|
purpose: "sso" | "integration";
|
||||||
|
required: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIdentityProviderMetadata = (): IdentityProviderMetadata[] => {
|
export const getIdentityProviderMetadata = (): IdentityProviderMetadata[] => {
|
||||||
|
|
@ -19,6 +19,10 @@
|
||||||
},
|
},
|
||||||
"baseUrl": {
|
"baseUrl": {
|
||||||
"$ref": "./shared.json#/definitions/Token"
|
"$ref": "./shared.json#/definitions/Token"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["provider", "purpose", "clientId", "clientSecret"]
|
"required": ["provider", "purpose", "clientId", "clientSecret"]
|
||||||
|
|
@ -40,6 +44,10 @@
|
||||||
},
|
},
|
||||||
"baseUrl": {
|
"baseUrl": {
|
||||||
"$ref": "./shared.json#/definitions/Token"
|
"$ref": "./shared.json#/definitions/Token"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["provider", "purpose", "clientId", "clientSecret", "baseUrl"]
|
"required": ["provider", "purpose", "clientId", "clientSecret", "baseUrl"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue