mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
* Implement user account verification with LinkedIn during onboarding * updating connect with linked-in banner * removing unused imports * Store token, fix binding * Refactor LinkedInToken to LinkedInProfile * Actually write the LinkedIn secret to the server config * Fetch LinkedIn user profile and email address * Add creationTime column to d_b_linked_in_profile * Add more debug logging * Fix LinkedIn API calls, mount LinkedInProfileDB * Also bind LinkedInProfileDB * Add LinkedIn scope r_liteprofile * Enhance LinkedIn profile retrieval, store the profile, ensure uniqueness * Align with UX spec and complete onboarding flow * Prevent the LinkedIn button from auto-submitting the onboarding form * Address nits (LinkedInService to /src and minor spacing) --------- Co-authored-by: Brad Harris <bmharris@gmail.com>
137 lines
5.7 KiB
TypeScript
137 lines
5.7 KiB
TypeScript
/**
|
|
* 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 { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
|
|
import { LinkedInProfile, User } from "@gitpod/gitpod-protocol/src/protocol";
|
|
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
|
|
import { LinkedInProfileDB } from "@gitpod/gitpod-db/lib";
|
|
import { inject, injectable } from "inversify";
|
|
import fetch from "node-fetch";
|
|
import { ResponseError } from "vscode-jsonrpc";
|
|
import { Config } from "./config";
|
|
|
|
@injectable()
|
|
export class LinkedInService {
|
|
@inject(Config) protected readonly config: Config;
|
|
@inject(LinkedInProfileDB) protected readonly linkedInProfileDB: LinkedInProfileDB;
|
|
|
|
async connectWithLinkedIn(user: User, code: string): Promise<LinkedInProfile> {
|
|
const accessToken = await this.getAccessToken(code);
|
|
const profile = await this.getLinkedInProfile(accessToken);
|
|
// Note: The unique mapping from LinkedIn profile to Gitpod user is guaranteed by the DB.
|
|
await this.linkedInProfileDB.storeProfile(user.id, profile);
|
|
return profile;
|
|
}
|
|
|
|
private async getAccessToken(code: string) {
|
|
const { clientId, clientSecret } = this.config.linkedInSecrets || {};
|
|
if (!clientId || !clientSecret) {
|
|
throw new ResponseError(
|
|
ErrorCodes.INTERNAL_SERVER_ERROR,
|
|
"LinkedIn is not properly configured (no Client ID or Client Secret)",
|
|
);
|
|
}
|
|
const redirectUri = this.config.hostUrl.with({ pathname: "/linkedin" }).toString();
|
|
const url = `https://www.linkedin.com/oauth/v2/accessToken?grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}&client_id=${clientId}&client_secret=${clientSecret}`;
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
});
|
|
const data = await response.json();
|
|
if (data.error) {
|
|
throw new Error("Could not get LinkedIn access token: " + data.error_description);
|
|
}
|
|
return data.access_token;
|
|
}
|
|
|
|
// Retrieve the user's profile from LinkedIn using the following API:
|
|
// https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin
|
|
private async getLinkedInProfile(accessToken: string): Promise<LinkedInProfile> {
|
|
const profileUrl =
|
|
"https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))";
|
|
const emailUrl = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))";
|
|
|
|
// Fetch both the user's profile and email address in parallel
|
|
const [profileResponse, emailResponse] = await Promise.all([
|
|
fetch(profileUrl, {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
}),
|
|
fetch(emailUrl, {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const profileData = await profileResponse.json();
|
|
if (profileData.error) {
|
|
throw new Error("Could not get LinkedIn lite profile: " + profileData.error_description);
|
|
}
|
|
if (!profileData.id) {
|
|
throw new Error("Missing LinkedIn profile ID");
|
|
}
|
|
|
|
const emailData = await emailResponse.json();
|
|
if (emailData.error) {
|
|
throw new Error("Could not get LinkedIn email address: " + emailData.error_description);
|
|
}
|
|
if (!emailData.elements || emailData.elements.length < 1 || !emailData.elements[0]["handle~"]?.emailAddress) {
|
|
throw new Error("Missing LinkedIn email address");
|
|
}
|
|
|
|
const profile: LinkedInProfile = {
|
|
id: profileData.id,
|
|
firstName: "",
|
|
lastName: "",
|
|
profilePicture: "",
|
|
emailAddress: emailData.elements[0]["handle~"].emailAddress,
|
|
};
|
|
|
|
try {
|
|
if (
|
|
typeof profileData.firstName?.localized === "object" &&
|
|
Object.values(profileData.firstName?.localized).length > 0
|
|
) {
|
|
// If there are multiple first name localizations, just pick the first one
|
|
profile.firstName = String(Object.values(profileData.firstName.localized)[0]);
|
|
}
|
|
} catch (error) {
|
|
log.error("Error getting LinkedIn first name", error);
|
|
}
|
|
|
|
try {
|
|
if (
|
|
typeof profileData.lastName?.localized === "object" &&
|
|
Object.values(profileData.lastName?.localized).length > 0
|
|
) {
|
|
// If there are multiple last name localizations, just pick the first one
|
|
profile.lastName = String(Object.values(profileData.lastName.localized)[0]);
|
|
}
|
|
} catch (error) {
|
|
log.error("Error getting LinkedIn last name", error);
|
|
}
|
|
|
|
try {
|
|
if (profileData.profilePicture && profileData.profilePicture["displayImage~"]?.elements?.length > 0) {
|
|
// If there are multiple image sizes, just pick the first one (seems to be always the smallest size)
|
|
profile.profilePicture = String(
|
|
profileData.profilePicture["displayImage~"].elements[0].identifiers[0].identifier,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
log.error("Error getting LinkedIn profile picture", error);
|
|
}
|
|
|
|
return profile;
|
|
}
|
|
}
|