Skip to content

Commit

Permalink
[server, dashboard] Remove api.maySetTimeout (+ code cleanup in User-…
Browse files Browse the repository at this point in the history
… + EntitlementService) (#18769)

* [dashbaord, server] Remove api.maySetTimeout

* [server] Remove dead code in Entitlement- and UserService

* review comments
  • Loading branch information
geropl authored Sep 22, 2023
1 parent 6c7f47d commit af88bed
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 163 deletions.

This file was deleted.

89 changes: 45 additions & 44 deletions components/dashboard/src/user-settings/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,23 @@ import { getGitpodService } from "../service/service";
import { UserContext } from "../user-context";
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
import { ThemeSelector } from "../components/ThemeSelector";
import Alert from "../components/Alert";
import { Link } from "react-router-dom";
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
import { useUserMaySetTimeout } from "../data/current-user/may-set-timeout-query";
import { Button } from "../components/Button";
import SelectIDE from "./SelectIDE";
import { InputField } from "../components/forms/InputField";
import { TextInput } from "../components/forms/TextInputField";
import { useToast } from "../components/toasts/Toasts";
import { useUpdateCurrentUserDotfileRepoMutation } from "../data/current-user/update-mutation";
import { AdditionalUserData } from "@gitpod/gitpod-protocol";
import { useOrgBillingMode } from "../data/billing-mode/org-billing-mode-query";

export type IDEChangedTrackLocation = "workspace_list" | "workspace_start" | "preferences";

export default function Preferences() {
const { toast } = useToast();
const { user, setUser } = useContext(UserContext);
const maySetTimeout = useUserMaySetTimeout();
const billingMode = useOrgBillingMode();
const updateDotfileRepo = useUpdateCurrentUserDotfileRepoMutation();

const [dotfileRepo, setDotfileRepo] = useState<string>(user?.additionalData?.dotfileRepo || "");
Expand Down Expand Up @@ -57,15 +56,29 @@ export default function Preferences() {
const updatedUser = await getGitpodService().server.getLoggedInUser();
setUser(updatedUser);

toast("Your default workspace timeout was updated.");
let toastMessage = <>Default workspace timeout was updated.</>;
if (billingMode.data?.mode === "usage-based") {
if (!billingMode.data.paid) {
toastMessage = (
<>
{toastMessage} Changes will only affect workspaces in paid organizations. Go to{" "}
<Link to="/billing" className="gp-link">
billing
</Link>{" "}
to upgrade your organization.
</>
);
}
}
toast(toastMessage);
} catch (e) {
// TODO: Convert this to an error style toast
alert("Cannot set custom workspace timeout: " + e.message);
} finally {
setTimeoutUpdating(false);
}
},
[toast, setUser, workspaceTimeout],
[toast, setUser, workspaceTimeout, billingMode],
);

const clearAutostartWorkspaceOptions = useCallback(async () => {
Expand Down Expand Up @@ -136,46 +149,34 @@ export default function Preferences() {
<Subheading>Workspaces will stop after a period of inactivity without any user input.</Subheading>

<div className="mt-4 max-w-xl">
{!maySetTimeout.isLoading && maySetTimeout.data === false && (
<Alert type="message">
Upgrade organization{" "}
<Link to="/billing" className="gp-link">
billing
</Link>{" "}
plan to use a custom inactivity timeout.
</Alert>
)}

{maySetTimeout.data === true && (
<form onSubmit={saveWorkspaceTimeout}>
<InputField
label="Default Workspace Timeout"
hint={
<span>
Use minutes or hours, like <span className="font-semibold">30m</span> or{" "}
<span className="font-semibold">2h</span>
</span>
}
>
<div className="flex space-x-2">
<div className="flex-grow">
<TextInput
value={workspaceTimeout}
placeholder="e.g. 30m"
onChange={setWorkspaceTimeout}
/>
</div>
<Button
htmlType="submit"
loading={timeoutUpdating}
disabled={workspaceTimeout === user?.additionalData?.workspaceTimeout ?? ""}
>
Save
</Button>
<form onSubmit={saveWorkspaceTimeout}>
<InputField
label="Default Workspace Timeout"
hint={
<span>
Use minutes or hours, like <span className="font-semibold">30m</span> or{" "}
<span className="font-semibold">2h</span>
</span>
}
>
<div className="flex space-x-2">
<div className="flex-grow">
<TextInput
value={workspaceTimeout}
placeholder="e.g. 30m"
onChange={setWorkspaceTimeout}
/>
</div>
</InputField>
</form>
)}
<Button
htmlType="submit"
loading={timeoutUpdating}
disabled={workspaceTimeout === user?.additionalData?.workspaceTimeout ?? ""}
>
Save
</Button>
</div>
</InputField>
</form>
</div>
</PageWithSettingsSubMenu>
</div>
Expand Down
5 changes: 2 additions & 3 deletions components/gitpod-protocol/src/billing-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export namespace BillingMode {
}

export function canSetCostCenter(billingMode: BillingMode): boolean {
// if has any Stripe Subscription, either directly or per team
return billingMode.mode === "usage-based";
}
}
Expand All @@ -36,6 +35,6 @@ interface None {
interface UsageBased {
mode: "usage-based";

/** True iff this is a team, and is based on a paid plan. Currently only set for teams! */
paid?: boolean;
/** True if the org has a paid plan. */
paid: boolean;
}
1 change: 0 additions & 1 deletion components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,6 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
reportErrorBoundary(url: string, message: string): Promise<void>;

getSupportedWorkspaceClasses(): Promise<SupportedWorkspaceClass[]>;
maySetTimeout(): Promise<boolean>;
updateWorkspaceTimeoutSetting(setting: Partial<WorkspaceTimeoutSetting>): Promise<void>;

/**
Expand Down
1 change: 0 additions & 1 deletion components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ const defaultFunctions: FunctionsConfig = {
getCostCenter: { group: "default", points: 1 },
setUsageLimit: { group: "default", points: 1 },
getSupportedWorkspaceClasses: { group: "default", points: 1 },
maySetTimeout: { group: "default", points: 1 },
updateWorkspaceTimeoutSetting: { group: "default", points: 1 },
getIDToken: { group: "default", points: 1 },
reportErrorBoundary: { group: "default", points: 1 },
Expand Down
16 changes: 0 additions & 16 deletions components/server/src/billing/billing-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,4 @@ export class BillingModes {
const paid = billingStrategy === CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE;
return { mode: "usage-based", paid };
}

/**
* @deprecated use getBillingMode(userId, organizationId) instead
* @returns
*/
async getBillingModeForUser(): Promise<BillingMode> {
if (!this.config.enablePayment) {
// Payment is not enabled. E.g. Self-Hosted.
return { mode: "none" };
}

// "paid" is not set here, just as before. Also, it's we should remove this whole method once the org-migration is done, and center all capabilities around Organizations
return {
mode: "usage-based",
};
}
}
58 changes: 10 additions & 48 deletions components/server/src/billing/entitlement-service-ubp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* See License.AGPL.txt in the project root for license information.
*/

import { TeamDB } from "@gitpod/gitpod-db/lib";
import {
WorkspaceInstance,
WorkspaceTimeoutDuration,
Expand All @@ -14,7 +13,6 @@ import {
WORKSPACE_LIFETIME_SHORT,
User,
BillingTier,
Team,
} from "@gitpod/gitpod-protocol";
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
import { inject, injectable } from "inversify";
Expand All @@ -31,10 +29,7 @@ const MAX_PARALLEL_WORKSPACES_PAID = 16;
*/
@injectable()
export class EntitlementServiceUBP implements EntitlementService {
constructor(
@inject(UsageService) private readonly usageService: UsageService,
@inject(TeamDB) private readonly teamDB: TeamDB,
) {}
constructor(@inject(UsageService) private readonly usageService: UsageService) {}

async mayStartWorkspace(
user: User,
Expand Down Expand Up @@ -79,7 +74,7 @@ export class EntitlementServiceUBP implements EntitlementService {
}
}

async maySetTimeout(userId: string, organizationId?: string): Promise<boolean> {
async maySetTimeout(userId: string, organizationId: string): Promise<boolean> {
return this.hasPaidSubscription(userId, organizationId);
}

Expand Down Expand Up @@ -109,48 +104,15 @@ export class EntitlementServiceUBP implements EntitlementService {
return true;
}

private async hasPaidSubscription(userId: string, organizationId?: string): Promise<boolean> {
if (organizationId) {
try {
// This is the "stricter", more correct version: We only allow privileges on the Organization that is paying for it
const { billingStrategy } = await this.usageService.getCostCenter(userId, organizationId);
return billingStrategy === CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE;
} catch (err) {
log.warn({ userId, organizationId }, "Error checking if user is subscribed to organization", err);
return false;
}
}

// TODO(gpl) Remove everything below once organizations are fully rolled out
// This is the old behavior, stemming from our transition to PAYF, where our API did-/doesn't pass organizationId, yet
// Member of paid team?
const teams = await this.teamDB.findTeamsByUser(userId);
const isTeamSubscribedPromises = teams.map(async (team: Team) => {
const { billingStrategy } = await this.usageService.getCostCenter(userId, team.id);
private async hasPaidSubscription(userId: string, organizationId: string): Promise<boolean> {
try {
// This is the "stricter", more correct version: We only allow privileges on the Organization that is paying for it
const { billingStrategy } = await this.usageService.getCostCenter(userId, organizationId);
return billingStrategy === CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE;
});
// Return the first truthy promise, or false if all the promises were falsy.
// Source: https://gist.github.com/jbreckmckye/66364021ebaa0785e426deec0410a235
return new Promise((resolve, reject) => {
// If any promise returns true, immediately resolve with true
isTeamSubscribedPromises.forEach(async (isTeamSubscribedPromise: Promise<boolean>) => {
try {
const isTeamSubscribed = await isTeamSubscribedPromise;
if (isTeamSubscribed) resolve(true);
} catch (err) {
log.warn({ userId, organizationId }, "Error checking if user is subscribed to organization", err);
resolve(false);
}
});

// If neither of the above fires, resolve with false
// Check truthiness just in case callbacks fire out-of-band
Promise.all(isTeamSubscribedPromises)
.then((areTeamsSubscribed) => {
resolve(!!areTeamsSubscribed.find((isTeamSubscribed: boolean) => !!isTeamSubscribed));
})
.catch(reject);
});
} catch (err) {
log.warn({ userId, organizationId }, "Error checking if user is subscribed to organization", err);
return false;
}
}

async getBillingTier(userId: string, organizationId: string): Promise<BillingTier> {
Expand Down
7 changes: 3 additions & 4 deletions components/server/src/billing/entitlement-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface EntitlementService {
* @param userId
* @param organizationId
*/
maySetTimeout(userId: string, organizationId?: string): Promise<boolean>;
maySetTimeout(userId: string, organizationId: string): Promise<boolean>;

/**
* Returns the default workspace timeout for the given user at a given point in time
Expand Down Expand Up @@ -127,10 +127,9 @@ export class EntitlementServiceImpl implements EntitlementService {
}
}

async maySetTimeout(userId: string, organizationId?: string): Promise<boolean> {
async maySetTimeout(userId: string, organizationId: string): Promise<boolean> {
try {
// TODO(gpl): We need to replace this with ".getBillingMode(user.id, organizationId);" once all callers forward organizationId
const billingMode = await this.billingModes.getBillingModeForUser();
const billingMode = await this.billingModes.getBillingMode(userId, organizationId);
switch (billingMode.mode) {
case "none":
// when payment is disabled users can do everything
Expand Down
9 changes: 0 additions & 9 deletions components/server/src/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { CreateUserParams } from "./user-authentication";
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
import { TransactionalContext } from "@gitpod/gitpod-db/lib/typeorm/transactional-db-impl";
import { RelationshipUpdater } from "../authorization/relationship-updater";
import { EntitlementService } from "../billing/entitlement-service";

@injectable()
export class UserService {
Expand All @@ -33,7 +32,6 @@ export class UserService {
@inject(Authorizer) private readonly authorizer: Authorizer,
@inject(IAnalyticsWriter) private readonly analytics: IAnalyticsWriter,
@inject(RelationshipUpdater) private readonly relationshipUpdater: RelationshipUpdater,
@inject(EntitlementService) private readonly entitlementService: EntitlementService,
) {}

public async createUser(
Expand Down Expand Up @@ -144,13 +142,6 @@ export class UserService {
}
}

if (!(await this.entitlementService.maySetTimeout(targetUserId))) {
throw new ApplicationError(
ErrorCodes.PERMISSION_DENIED,
"Configure workspace timeout only available for paid user.",
);
}

const user = await this.findUserById(userId, targetUserId);
AdditionalUserData.set(user, setting);
await this.userDb.updateUserPartial(user);
Expand Down
10 changes: 0 additions & 10 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ import {
import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
import { VerificationService } from "../auth/verification-service";
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
import { EntitlementService } from "../billing/entitlement-service";
import { formatPhoneNumber } from "../user/phone-numbers";
import { IDEService } from "../ide-service";
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
Expand Down Expand Up @@ -240,7 +239,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
@inject(IDEService) private readonly ideService: IDEService,

@inject(VerificationService) private readonly verificationService: VerificationService,
@inject(EntitlementService) private readonly entitlementService: EntitlementService,

@inject(Authorizer) private readonly auth: Authorizer,

Expand Down Expand Up @@ -669,14 +667,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
return updatedUser;
}

public async maySetTimeout(ctx: TraceContext): Promise<boolean> {
const user = await this.checkUser("maySetTimeout");
await this.guardAccess({ kind: "user", subject: user }, "get");
await this.auth.checkPermissionOnUser(user.id, "read_info", user.id);

return await this.entitlementService.maySetTimeout(user.id);
}

public async updateWorkspaceTimeoutSetting(
ctx: TraceContext,
setting: Partial<WorkspaceTimeoutSetting>,
Expand Down

0 comments on commit af88bed

Please sign in to comment.