mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
Enable Dedicated Onboarding Flow – WEB-193 (#17303)
* uncommitted yarn.lock * [protocol] adding ONBOARDING_IN_PROGRESS * [gitpod-db] add `someOrgWithSSO` to team db * [server] add ONBOARDING_IN_PROGRESS hook * [dashboard] catch ONBOARDING_IN_PROGRESS error * [configcat] adding `custom.gitpod_host` attribute to select preview envs
This commit is contained in:
parent
dafcf19ded
commit
ee82d153e3
54
components/dashboard/src/DedicatedOnboarding.tsx
Normal file
54
components/dashboard/src/DedicatedOnboarding.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
|
||||||
|
* Licensed under the GNU Affero General Public License (AGPL).
|
||||||
|
* See License.AGPL.txt in the project root for license information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Button } from "./components/Button";
|
||||||
|
import Modal, { ModalBody, ModalFooter, ModalHeader } from "./components/Modal";
|
||||||
|
import { OIDCClientConfigModal } from "./teams/sso/OIDCClientConfigModal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onComplete?: () => void;
|
||||||
|
};
|
||||||
|
export default function DedicatedOnboarding({ onComplete }: Props) {
|
||||||
|
const [showModal] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const acceptAndContinue = useCallback(() => {
|
||||||
|
// // setShowModal(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// const onAuthorize = useCallback(
|
||||||
|
// (payload?: string) => {
|
||||||
|
// onComplete && onComplete();
|
||||||
|
|
||||||
|
// // run without await, so the integrated closing of new tab isn't blocked
|
||||||
|
// (async () => {
|
||||||
|
// window.location.href = gitpodHostUrl.asDashboard().toString();
|
||||||
|
// })();
|
||||||
|
// },
|
||||||
|
// [onComplete],
|
||||||
|
// );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!showModal && (
|
||||||
|
<Modal visible={true} onClose={() => {}} closeable={false}>
|
||||||
|
<ModalHeader>Welcome to Gitpod Dedicated 🚀</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<p className="pb-4 text-gray-500 text-base">
|
||||||
|
Before this instance can be used, you need to set up Single Sign-on.
|
||||||
|
</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button className={"ml-2"} onClick={acceptAndContinue}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{showModal && <OIDCClientConfigModal onClose={() => {}} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import { isGitpodIo } from "../../utils";
|
|||||||
import { CaughtError } from "./ReloadPageErrorBoundary";
|
import { CaughtError } from "./ReloadPageErrorBoundary";
|
||||||
|
|
||||||
const Setup = lazy(() => import(/* webpackPrefetch: true */ "../../Setup"));
|
const Setup = lazy(() => import(/* webpackPrefetch: true */ "../../Setup"));
|
||||||
|
const DedicatedOnboarding = lazy(() => import(/* webpackPrefetch: true */ "../../DedicatedOnboarding"));
|
||||||
|
|
||||||
// Error boundary intended to catch and handle expected errors from api calls
|
// Error boundary intended to catch and handle expected errors from api calls
|
||||||
export const QueryErrorBoundary: FC = ({ children }) => {
|
export const QueryErrorBoundary: FC = ({ children }) => {
|
||||||
@ -50,6 +51,15 @@ const ExpectedQueryErrorsFallback: FC<FallbackProps> = ({ error, resetErrorBound
|
|||||||
return <Login onLoggedIn={resetErrorBoundary} />;
|
return <Login onLoggedIn={resetErrorBoundary} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup needed before proceeding
|
||||||
|
if (caughtError.code === ErrorCodes.ONBOARDING_IN_PROGRESS) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<AppLoading />}>
|
||||||
|
<DedicatedOnboarding onComplete={resetErrorBoundary} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Setup needed before proceeding
|
// Setup needed before proceeding
|
||||||
if (caughtError.code === ErrorCodes.SETUP_REQUIRED) {
|
if (caughtError.code === ErrorCodes.SETUP_REQUIRED) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
import * as chai from "chai";
|
import * as chai from "chai";
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
import { suite, test, timeout } from "mocha-typescript";
|
import { suite, test, timeout } from "mocha-typescript";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import { testContainer } from "./test-container";
|
import { testContainer } from "./test-container";
|
||||||
import { TeamDBImpl } from "./typeorm/team-db-impl";
|
import { TeamDBImpl } from "./typeorm/team-db-impl";
|
||||||
@ -16,6 +17,7 @@ import { DBTeam } from "./typeorm/entity/db-team";
|
|||||||
import { DBTeamMembership } from "./typeorm/entity/db-team-membership";
|
import { DBTeamMembership } from "./typeorm/entity/db-team-membership";
|
||||||
import { DBUser } from "./typeorm/entity/db-user";
|
import { DBUser } from "./typeorm/entity/db-user";
|
||||||
import { DBIdentity } from "./typeorm/entity/db-identity";
|
import { DBIdentity } from "./typeorm/entity/db-identity";
|
||||||
|
import { Connection } from "typeorm";
|
||||||
|
|
||||||
@suite
|
@suite
|
||||||
class TeamDBSpec {
|
class TeamDBSpec {
|
||||||
@ -37,6 +39,7 @@ class TeamDBSpec {
|
|||||||
await manager.getRepository(DBTeamMembership).delete({});
|
await manager.getRepository(DBTeamMembership).delete({});
|
||||||
await manager.getRepository(DBUser).delete({});
|
await manager.getRepository(DBUser).delete({});
|
||||||
await manager.getRepository(DBIdentity).delete({});
|
await manager.getRepository(DBIdentity).delete({});
|
||||||
|
await manager.query("delete from d_b_oidc_client_config;");
|
||||||
}
|
}
|
||||||
|
|
||||||
@test(timeout(10000))
|
@test(timeout(10000))
|
||||||
@ -151,6 +154,31 @@ class TeamDBSpec {
|
|||||||
const result = await this.db.findTeams(0, 10, "creationTime", "DESC", searchTerm);
|
const result = await this.db.findTeams(0, 10, "creationTime", "DESC", searchTerm);
|
||||||
expect(result.rows.length).to.eq(1);
|
expect(result.rows.length).to.eq(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test(timeout(10000))
|
||||||
|
public async test_someOrgWithSSOExists() {
|
||||||
|
expect(await this.db.someOrgWithSSOExists(), "case 1: empty db").to.be.false;
|
||||||
|
|
||||||
|
const user = await this.userDb.newUser();
|
||||||
|
const org = await this.db.createTeam(user.id, "Some Org");
|
||||||
|
expect(await this.db.someOrgWithSSOExists(), "case 2: org without sso").to.be.false;
|
||||||
|
|
||||||
|
await this.exec(async (c) => {
|
||||||
|
await c.query("INSERT INTO d_b_oidc_client_config (id, issuer, organizationId, data) VALUES (?,?,?,?)", [
|
||||||
|
uuidv4(),
|
||||||
|
"https://issuer.local",
|
||||||
|
org.id,
|
||||||
|
"{}",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
expect(await this.db.someOrgWithSSOExists(), "case 3: org with sso").to.be.true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async exec(queryFn: (connection: Connection) => Promise<void>) {
|
||||||
|
const typeorm = testContainer.get<TypeORM>(TypeORM);
|
||||||
|
const connection = await typeorm.getConnection();
|
||||||
|
await queryFn(connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new TeamDBSpec();
|
module.exports = new TeamDBSpec();
|
||||||
|
|||||||
@ -40,4 +40,6 @@ export interface TeamDB {
|
|||||||
|
|
||||||
findOrgSettings(teamId: string): Promise<OrganizationSettings | undefined>;
|
findOrgSettings(teamId: string): Promise<OrganizationSettings | undefined>;
|
||||||
setOrgSettings(teamId: string, settings: Partial<OrganizationSettings>): Promise<void>;
|
setOrgSettings(teamId: string, settings: Partial<OrganizationSettings>): Promise<void>;
|
||||||
|
|
||||||
|
someOrgWithSSOExists(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -365,4 +365,12 @@ export class TeamDBImpl implements TeamDB {
|
|||||||
repo.save(team);
|
repo.save(team);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async someOrgWithSSOExists(): Promise<boolean> {
|
||||||
|
const repo = await this.getOrgSettingsRepo();
|
||||||
|
const result = await repo.query(
|
||||||
|
`select org.id from d_b_team as org inner join d_b_oidc_client_config as oidc on org.id = oidc.organizationId limit 1;`,
|
||||||
|
);
|
||||||
|
return result.length === 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,9 @@ const (
|
|||||||
// 411 No User
|
// 411 No User
|
||||||
NEEDS_VERIFICATION = 411
|
NEEDS_VERIFICATION = 411
|
||||||
|
|
||||||
|
// 412 Dedicated Cell is in "onboarding" mode
|
||||||
|
ONBOARDING_IN_PROGRESS = 412
|
||||||
|
|
||||||
// 429 Too Many Requests
|
// 429 Too Many Requests
|
||||||
TOO_MANY_REQUESTS = 429
|
TOO_MANY_REQUESTS = 429
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const PROJECT_ID_ATTRIBUTE = "project_id";
|
|||||||
export const TEAM_ID_ATTRIBUTE = "team_id";
|
export const TEAM_ID_ATTRIBUTE = "team_id";
|
||||||
export const TEAM_NAME_ATTRIBUTE = "team_name";
|
export const TEAM_NAME_ATTRIBUTE = "team_name";
|
||||||
export const BILLING_TIER_ATTRIBUTE = "billing_tier";
|
export const BILLING_TIER_ATTRIBUTE = "billing_tier";
|
||||||
|
export const GITPOD_HOST = "gitpod_host";
|
||||||
|
|
||||||
export class ConfigCatClient implements Client {
|
export class ConfigCatClient implements Client {
|
||||||
private client: IConfigCatClient;
|
private client: IConfigCatClient;
|
||||||
@ -51,6 +52,9 @@ export function attributesToUser(attributes: Attributes): ConfigCatUser {
|
|||||||
if (attributes.billingTier) {
|
if (attributes.billingTier) {
|
||||||
custom[BILLING_TIER_ATTRIBUTE] = attributes.billingTier;
|
custom[BILLING_TIER_ATTRIBUTE] = attributes.billingTier;
|
||||||
}
|
}
|
||||||
|
if (attributes.gitpodHost) {
|
||||||
|
custom[GITPOD_HOST] = attributes.gitpodHost;
|
||||||
|
}
|
||||||
|
|
||||||
return new ConfigCatUser(userId, email, "", custom);
|
return new ConfigCatUser(userId, email, "", custom);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,12 @@ export interface Attributes {
|
|||||||
|
|
||||||
// Currently selected Gitpod Team ID (mapped to "custom.team_id")
|
// Currently selected Gitpod Team ID (mapped to "custom.team_id")
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
|
|
||||||
// Currently selected Gitpod Team Name (mapped to "custom.team_name")
|
// Currently selected Gitpod Team Name (mapped to "custom.team_name")
|
||||||
teamName?: string;
|
teamName?: string;
|
||||||
|
|
||||||
|
// Host name of the Gitpod installation (mapped to "custom.gitpod_host")
|
||||||
|
gitpodHost?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Client {
|
export interface Client {
|
||||||
|
|||||||
@ -26,6 +26,9 @@ export namespace ErrorCodes {
|
|||||||
// 411 No User
|
// 411 No User
|
||||||
export const NEEDS_VERIFICATION = 411;
|
export const NEEDS_VERIFICATION = 411;
|
||||||
|
|
||||||
|
// 412 Dedicated Cell is in "onboarding" mode
|
||||||
|
export const ONBOARDING_IN_PROGRESS = 412;
|
||||||
|
|
||||||
// 429 Too Many Requests
|
// 429 Too Many Requests
|
||||||
export const TOO_MANY_REQUESTS = 429;
|
export const TOO_MANY_REQUESTS = 429;
|
||||||
|
|
||||||
|
|||||||
@ -357,9 +357,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected checkUser(methodName?: string, logPayload?: {}, ctx?: LogContext): User {
|
protected checkUser(methodName?: string, logPayload?: {}, ctx?: LogContext): User {
|
||||||
if (this.showSetupCondition?.value) {
|
// Old self-hosted mode did not require a session, therefore it's checked first.
|
||||||
|
// But we need to make sure, it's not triggered if the new Dedicated Onboarding is active.
|
||||||
|
if (this.showSetupCondition?.value && !this.showOnboardingFlowCondition?.value) {
|
||||||
throw new ResponseError(ErrorCodes.SETUP_REQUIRED, "Setup required.");
|
throw new ResponseError(ErrorCodes.SETUP_REQUIRED, "Setup required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generally, a user session is required.
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "User is not authenticated. Please login.");
|
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "User is not authenticated. Please login.");
|
||||||
}
|
}
|
||||||
@ -377,6 +381,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
|
|||||||
}
|
}
|
||||||
log.debug(userContext, methodName, payload);
|
log.debug(userContext, methodName, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A session is required to continue with an activated onboarding flow.
|
||||||
|
if (this.showOnboardingFlowCondition?.value) {
|
||||||
|
throw new ResponseError(ErrorCodes.ONBOARDING_IN_PROGRESS, "Dedicated Onboarding Flow in progress.");
|
||||||
|
}
|
||||||
|
|
||||||
return this.user;
|
return this.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,7 +413,26 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected showSetupCondition: { value: boolean } | undefined = undefined;
|
protected showSetupCondition: { value: boolean } | undefined = undefined;
|
||||||
|
protected showOnboardingFlowCondition: { value: boolean } | undefined = undefined;
|
||||||
protected async doUpdateUser(): Promise<void> {
|
protected async doUpdateUser(): Promise<void> {
|
||||||
|
// Conditionally enable Dedicated Onboarding Flow
|
||||||
|
const enableDedicatedOnboardingFlow = await this.configCatClientFactory().getValueAsync(
|
||||||
|
"enableDedicatedOnboardingFlow",
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
gitpodHost: new URL(this.config.hostUrl.toString()).host,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (enableDedicatedOnboardingFlow) {
|
||||||
|
const someOrgWithSSOExists = await this.teamDB.someOrgWithSSOExists();
|
||||||
|
const shouldShowOnboardingFlow = !someOrgWithSSOExists;
|
||||||
|
this.showOnboardingFlowCondition = { value: shouldShowOnboardingFlow };
|
||||||
|
if (shouldShowOnboardingFlow) {
|
||||||
|
console.log(`Dedicated Onboarding flow is active.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showOnboardingFlowCondition = { value: false };
|
||||||
|
}
|
||||||
// execute the check for the setup to be shown until the setup is not required.
|
// execute the check for the setup to be shown until the setup is not required.
|
||||||
// cf. evaluation of the condition in `checkUser`
|
// cf. evaluation of the condition in `checkUser`
|
||||||
if (!this.config.showSetupModal) {
|
if (!this.config.showSetupModal) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user