Skip to content

GetAuthenticatedUser in Dashboard #19142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"countries-list": "^2.6.1",
"crypto-browserify": "3.12.0",
"dayjs": "^1.11.5",
"deepmerge": "^4.2.2",
"file-saver": "^2.0.5",
"idb-keyval": "^6.2.0",
"js-cookie": "^3.0.1",
Expand Down
78 changes: 40 additions & 38 deletions components/dashboard/src/AppNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
*/

import dayjs from "dayjs";
import deepMerge from "deepmerge";
import { useCallback, useEffect, useState } from "react";
import Alert, { AlertType } from "./components/Alert";
import { useUserLoader } from "./hooks/use-user-loader";
import { getGitpodService } from "./service/service";
import { isGitpodIo } from "./utils";
import { trackEvent } from "./Analytics";
import { useUpdateCurrentUserMutation } from "./data/current-user/update-mutation";
import { User as UserProtocol } from "@gitpod/gitpod-protocol";
import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb";

const KEY_APP_DISMISSED_NOTIFICATIONS = "gitpod-app-notifications-dismissed";
const PRIVACY_POLICY_LAST_UPDATED = "2023-10-17";
Expand All @@ -24,59 +25,60 @@ interface Notification {
onClose?: () => void;
}

const UPDATED_PRIVACY_POLICY: Notification = {
id: "privacy-policy-update",
type: "info",
preventDismiss: true,
onClose: async () => {
let dismissSuccess = false;
try {
const userUpdates = { additionalData: { profile: { acceptedPrivacyPolicyDate: dayjs().toISOString() } } };
const previousUser = await getGitpodService().server.getLoggedInUser();
const updatedUser = await getGitpodService().server.updateLoggedInUser(
deepMerge(previousUser, userUpdates),
);
dismissSuccess = !!updatedUser;
} catch (err) {
console.error("Failed to update user's privacy policy acceptance date", err);
dismissSuccess = false;
} finally {
trackEvent("privacy_policy_update_accepted", {
path: window.location.pathname,
success: dismissSuccess,
});
}
},
message: (
<span className="text-md">
We've updated our Privacy Policy. You can review it{" "}
<a className="gp-link" href="https://www.gitpod.io/privacy" target="_blank" rel="noreferrer">
here
</a>
.
</span>
),
const UPDATED_PRIVACY_POLICY = (updateUser: (user: Partial<UserProtocol>) => Promise<User>) => {
return {
id: "privacy-policy-update",
type: "info",
preventDismiss: true,
onClose: async () => {
let dismissSuccess = false;
try {
const updatedUser = await updateUser({
additionalData: { profile: { acceptedPrivacyPolicyDate: dayjs().toISOString() } },
});
dismissSuccess = !!updatedUser;
} catch (err) {
console.error("Failed to update user's privacy policy acceptance date", err);
dismissSuccess = false;
} finally {
trackEvent("privacy_policy_update_accepted", {
path: window.location.pathname,
success: dismissSuccess,
});
}
},
message: (
<span className="text-md">
We've updated our Privacy Policy. You can review it{" "}
<a className="gp-link" href="https://www.gitpod.io/privacy" target="_blank" rel="noreferrer">
here
</a>
.
</span>
),
} as Notification;
};

export function AppNotifications() {
const [topNotification, setTopNotification] = useState<Notification | undefined>(undefined);
const { user, loading } = useUserLoader();
const updateUser = useUpdateCurrentUserMutation();

useEffect(() => {
const notifications = [];
if (!loading && isGitpodIo()) {
if (
!user?.additionalData?.profile?.acceptedPrivacyPolicyDate ||
new Date(PRIVACY_POLICY_LAST_UPDATED) > new Date(user.additionalData.profile.acceptedPrivacyPolicyDate)
!user?.profile?.acceptedPrivacyPolicyDate ||
new Date(PRIVACY_POLICY_LAST_UPDATED) > new Date(user.profile.acceptedPrivacyPolicyDate)
) {
notifications.push(UPDATED_PRIVACY_POLICY);
notifications.push(UPDATED_PRIVACY_POLICY((u: Partial<UserProtocol>) => updateUser.mutateAsync(u)));
}
}

const dismissedNotifications = getDismissedNotifications();
const topNotification = notifications.find((n) => !dismissedNotifications.includes(n.id));
setTopNotification(topNotification);
}, [loading, setTopNotification, user]);
}, [loading, updateUser, setTopNotification, user]);

const dismissNotification = useCallback(() => {
if (!topNotification) {
Expand Down
9 changes: 6 additions & 3 deletions components/dashboard/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useNeedsSetup } from "./dedicated-setup/use-needs-setup";
import { AuthProviderDescription } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { Button, ButtonProps } from "@podkit/buttons/Button";
import { cn } from "@podkit/lib/cn";
import { userClient } from "./service/public-api";

export function markLoggedIn() {
document.cookie = GitpodCookie.generateCookie(window.location.hostname);
Expand Down Expand Up @@ -67,9 +68,11 @@ export const Login: FC<LoginProps> = ({ onLoggedIn }) => {

const updateUser = useCallback(async () => {
await getGitpodService().reconnect();
const user = await getGitpodService().server.getLoggedInUser();
setUser(user);
markLoggedIn();
const { user } = await userClient.getAuthenticatedUser({});
if (user) {
setUser(user);
markLoggedIn();
}
}, [setUser]);

const authorizeSuccessful = useCallback(async () => {
Expand Down
3 changes: 2 additions & 1 deletion components/dashboard/src/admin/UserSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AdminPageHeader } from "./AdminPageHeader";
import UserDetail from "./UserDetail";
import searchIcon from "../icons/search.svg";
import Tooltip from "../components/Tooltip";
import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";

export default function UserSearch() {
const location = useLocation();
Expand Down Expand Up @@ -129,7 +130,7 @@ function UserEntry(p: { user: User }) {
if (!p) {
return <></>;
}
const email = User.getPrimaryEmail(p.user) || "---";
const email = getPrimaryEmail(p.user) || "---";
return (
<Link key={p.user.id} to={"/admin/users/" + p.user.id} data-analytics='{"button_type":"sidebar_menu"}'>
<div className="rounded-xl whitespace-nowrap flex space-x-2 py-6 px-6 w-full justify-between hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-kumquat-light group">
Expand Down
3 changes: 2 additions & 1 deletion components/dashboard/src/app/AdminRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { useContext } from "react";
import { Redirect, Route } from "react-router";
import { UserContext } from "../user-context";
import { User_RoleOrPermission } from "@gitpod/public-api/lib/gitpod/v1/user_pb";

// A wrapper for <Route> that redirects to the workspaces screen if the user isn't a admin.
// This wrapper only accepts the component property
Expand All @@ -15,7 +16,7 @@ export function AdminRoute({ component }: any) {
return (
<Route
render={({ location }: any) =>
user?.rolesOrPermissions?.includes("admin") ? (
user?.rolesOrPermissions?.includes(User_RoleOrPermission.ADMIN) ? (
<Route component={component}></Route>
) : (
<Redirect
Expand Down
7 changes: 0 additions & 7 deletions components/dashboard/src/app/AppBlockingFlows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import { FC, lazy } from "react";
import { useShowDedicatedSetup } from "../dedicated-setup/use-show-dedicated-setup";
import { useCurrentUser } from "../user-context";
import { MigrationPage, useShouldSeeMigrationPage } from "../whatsnew/MigrationPage";
import { useShowUserOnboarding } from "../onboarding/use-show-user-onboarding";
import { useHistory } from "react-router";
import { useCurrentOrg } from "../data/organizations/orgs-query";
Expand All @@ -22,7 +21,6 @@ export const AppBlockingFlows: FC = ({ children }) => {
const history = useHistory();
const user = useCurrentUser();
const org = useCurrentOrg();
const shouldSeeMigrationPage = useShouldSeeMigrationPage();
const showDedicatedSetup = useShowDedicatedSetup();
const showUserOnboarding = useShowUserOnboarding();

Expand All @@ -31,11 +29,6 @@ export const AppBlockingFlows: FC = ({ children }) => {
return <></>;
}

// If orgOnlyAttribution is enabled and the user hasn't been migrated, yet, we need to show the migration page
if (shouldSeeMigrationPage) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on this change? I'm not familiar. I want to understand why it is ok to remove now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Org migration path dried out by now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't make sense to update code unused.

Copy link
Member

@akosyakov akosyakov Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I'm not the best person if you want some confirmation :)

return <MigrationPage />;
}

// Handle dedicated setup if necessary
if (showDedicatedSetup.showSetup) {
return (
Expand Down
10 changes: 1 addition & 9 deletions components/dashboard/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* See License.AGPL.txt in the project root for license information.
*/

import React, { useState } from "react";
import React from "react";
import { Redirect, Route, Switch, useLocation } from "react-router";
import OAuthClientApproval from "../OauthClientApproval";
import Menu from "../menu/Menu";
Expand All @@ -25,15 +25,13 @@ import {
usagePathMain,
} from "../user-settings/settings.routes";
import { getURLHash, isGitpodIo } from "../utils";
import { WhatsNew, shouldSeeWhatsNew } from "../whatsnew/WhatsNew";
import { workspacesPathMain } from "../workspaces/workspaces.routes";
import { AdminRoute } from "./AdminRoute";
import { Blocked } from "./Blocked";

// TODO: Can we bundle-split/lazy load these like other pages?
import { BlockedRepositories } from "../admin/BlockedRepositories";
import { Heading1, Subheading } from "../components/typography/headings";
import { useCurrentUser } from "../user-context";
import PersonalAccessTokenCreateView from "../user-settings/PersonalAccessTokensCreateView";
import { CreateWorkspacePage } from "../workspaces/CreateWorkspacePage";
import { WebsocketClients } from "./WebsocketClients";
Expand Down Expand Up @@ -84,8 +82,6 @@ const ConfigurationDetailPage = React.lazy(

export const AppRoutes = () => {
const hash = getURLHash();
const user = useCurrentUser();
const [isWhatsNewShown, setWhatsNewShown] = useState(user && shouldSeeWhatsNew(user));
const location = useLocation();
const repoConfigListAndDetail = useFeatureFlag("repoConfigListAndDetail");

Expand All @@ -99,10 +95,6 @@ export const AppRoutes = () => {
return <OAuthClientApproval />;
}

if (isWhatsNewShown) {
return <WhatsNew onClose={() => setWhatsNewShown(false)} />;
}

// TODO: Try and encapsulate this in a route for "/" (check for hash in route component, render or redirect accordingly)
const isCreation = location.pathname === "/" && hash !== "";
if (isCreation) {
Expand Down
15 changes: 9 additions & 6 deletions components/dashboard/src/components/AuthorizeGit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FC, useCallback, useContext } from "react";
import { Link } from "react-router-dom";
import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query";
import { openAuthorizeWindow } from "../provider-utils";
import { getGitpodService } from "../service/service";
import { userClient } from "../service/public-api";
import { UserContext, useCurrentUser } from "../user-context";
import { Button } from "./Button";
import { Heading2, Heading3, Subheading } from "./typography/headings";
Expand All @@ -18,20 +18,23 @@ import { useIsOwner } from "../data/organizations/members-query";
import { AuthProviderDescription } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";

export function useNeedsGitAuthorization() {
const authProviders = useAuthProviderDescriptions();
const { data: authProviders } = useAuthProviderDescriptions();
const user = useCurrentUser();
if (!user || !authProviders.data) {
if (!user || !authProviders) {
return false;
}
return !authProviders.data.some((ap) => user.identities.some((i) => ap.id === i.authProviderId));
return !authProviders.some((ap) => user.identities.some((i) => ap.id === i.authProviderId));
}

export const AuthorizeGit: FC<{ className?: string }> = ({ className }) => {
const { setUser } = useContext(UserContext);
const owner = useIsOwner();
const { data: authProviders } = useAuthProviderDescriptions();
const updateUser = useCallback(() => {
getGitpodService().server.getLoggedInUser().then(setUser);
const updateUser = useCallback(async () => {
const response = await userClient.getAuthenticatedUser({});
if (response.user) {
setUser(response.user);
}
}, [setUser]);

const connect = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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 { useQuery } from "@tanstack/react-query";
import { userClient } from "../../service/public-api";
import { GetAuthenticatedUserRequest, User } from "@gitpod/public-api/lib/gitpod/v1/user_pb";

export const useAuthenticatedUser = () => {
const query = useQuery<User>({
queryKey: getAuthenticatedUserQueryKey(),
queryFn: async () => {
const params = new GetAuthenticatedUserRequest();
const response = await userClient.getAuthenticatedUser(params);
return response.user!;
},
});
return query;
};

export const getAuthenticatedUserQueryKey = () => ["authenticated-user", {}];
30 changes: 23 additions & 7 deletions components/dashboard/src/data/current-user/update-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,35 @@
* See License.AGPL.txt in the project root for license information.
*/

import { User } from "@gitpod/gitpod-protocol";
import { AdditionalUserData, User as UserProtocol } from "@gitpod/gitpod-protocol";
import { useMutation } from "@tanstack/react-query";
import { trackEvent } from "../../Analytics";
import { getGitpodService } from "../../service/service";
import { useCurrentUser } from "../../user-context";
import { converter } from "../../service/public-api";
import deepmerge from "deepmerge";

type UpdateCurrentUserArgs = Partial<User>;
type UpdateCurrentUserArgs = Partial<UserProtocol>;

export const useUpdateCurrentUserMutation = () => {
return useMutation({
mutationFn: async (partialUser: UpdateCurrentUserArgs) => {
return await getGitpodService().server.updateLoggedInUser(partialUser);
const current = await getGitpodService().server.getLoggedInUser();
const currentAdditionalData = { ...current.additionalData };
// workspaceAutostartOptions needs to be overriden
if (partialUser.additionalData?.workspaceAutostartOptions) {
currentAdditionalData.workspaceAutostartOptions = [];
}
const update: UpdateCurrentUserArgs = {
id: current.id,
fullName: partialUser.fullName || current.fullName,
additionalData: deepmerge<AdditionalUserData>(
currentAdditionalData || {},
partialUser.additionalData || {},
),
};
const user = await getGitpodService().server.updateLoggedInUser(update);
return converter.toUser(user);
},
});
};
Expand All @@ -31,7 +48,6 @@ export const useUpdateCurrentUserDotfileRepoMutation = () => {
}

const additionalData = {
...(user.additionalData || {}),
dotfileRepo,
};
const updatedUser = await updateUser.mutateAsync({ additionalData });
Expand All @@ -40,14 +56,14 @@ export const useUpdateCurrentUserDotfileRepoMutation = () => {
},
onMutate: async () => {
return {
previousDotfileRepo: user?.additionalData?.dotfileRepo || "",
previousDotfileRepo: user?.dotfileRepo || "",
};
},
onSuccess: (updatedUser, _, context) => {
if (updatedUser?.additionalData?.dotfileRepo !== context?.previousDotfileRepo) {
if (updatedUser?.dotfileRepo !== context?.previousDotfileRepo) {
trackEvent("dotfile_repo_changed", {
previous: context?.previousDotfileRepo ?? "",
current: updatedUser?.additionalData?.dotfileRepo ?? "",
current: updatedUser?.dotfileRepo ?? "",
});
}
},
Expand Down
6 changes: 5 additions & 1 deletion components/dashboard/src/data/featureflag-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* See License.AGPL.txt in the project root for license information.
*/

import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";
import { useQuery } from "@tanstack/react-query";
import { getExperimentsClient } from "../experiments/client";
import { useCurrentProject } from "../projects/project-context";
Expand Down Expand Up @@ -40,7 +41,10 @@ export const useFeatureFlag = <K extends keyof FeatureFlags>(featureFlag: K): Fe

const query = useQuery(queryKey, async () => {
const flagValue = await getExperimentsClient().getValueAsync(featureFlag, featureFlags[featureFlag], {
user,
user: user && {
id: user.id,
email: getPrimaryEmail(user),
},
projectId: project?.id,
teamId: org?.id,
teamName: org?.name,
Expand Down
Loading