-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Implement user account verification with LinkedIn during onboarding #17074
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
Changes from all commits
26131d2
9e283ec
bec4742
8d5caee
5e00849
8404d01
605326b
65e0943
0f69fec
c1db45a
31b0c23
2e2995c
9e457aa
797c4ba
988c68a
4619e28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/** | ||
* 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 classNames from "classnames"; | ||
import { FC } from "react"; | ||
import { useLinkedIn } from "react-linkedin-login-oauth2"; | ||
import Alert from "../components/Alert"; | ||
import { Button } from "../components/Button"; | ||
import SignInWithLinkedIn from "../images/sign-in-with-linkedin.svg"; | ||
import { getGitpodService } from "../service/service"; | ||
import { LinkedInProfile } from "@gitpod/gitpod-protocol"; | ||
|
||
type Props = { | ||
onSuccess(profile: LinkedInProfile): void; | ||
}; | ||
export const LinkedInBanner: FC<Props> = ({ onSuccess }) => { | ||
const { | ||
data: clientID, | ||
isLoading, | ||
isError, | ||
} = useQuery( | ||
["linkedin-clientid"], | ||
async () => { | ||
return (await getGitpodService().server.getLinkedInClientId()) || ""; | ||
}, | ||
{ enabled: true }, | ||
); | ||
|
||
const { linkedInLogin } = useLinkedIn({ | ||
clientId: clientID || "", | ||
redirectUri: `${window.location.origin}/linkedin`, | ||
scope: "r_liteprofile r_emailaddress", | ||
onSuccess: (code) => { | ||
console.log("success", code); | ||
getGitpodService() | ||
.server.connectWithLinkedIn(code) | ||
.then((profile) => { | ||
onSuccess(profile); | ||
}) | ||
.catch((error) => { | ||
console.error("LinkedIn connection failed", error); | ||
}); | ||
}, | ||
onError: (error) => { | ||
console.error("error", error); | ||
}, | ||
}); | ||
|
||
return ( | ||
<> | ||
<div | ||
className={classNames( | ||
"mt-6 p-6", | ||
"border-2 border-dashed rounded-md space-y-4", | ||
"bg-gray-50 dark:bg-gray-800", | ||
)} | ||
> | ||
<div className="flex items-center justify-center space-x-6"> | ||
<span className="text-4xl">🎁</span> | ||
{/* TODO: Shouldn't need a fixed width here, but was hard to center otherwise */} | ||
<p className="w-64 text-base text-gray-500 dark:text-gray-100"> | ||
Receive <strong>500 credits</strong> per month by connecting your <strong>LinkedIn</strong>{" "} | ||
account. | ||
</p> | ||
</div> | ||
<Button | ||
className="w-full flex items-center justify-center space-x-2" | ||
onClick={(event) => { event.preventDefault(); linkedInLogin(); }} | ||
disabled={isLoading || !clientID} | ||
> | ||
<img src={SignInWithLinkedIn} width={20} height={20} alt="Sign in with Linked In" /> | ||
<span>Connect with LinkedIn</span> | ||
</Button> | ||
</div> | ||
{/* TODO: Figure out if there's a different way we want to handle an error getting the clientID */} | ||
{isError && ( | ||
<Alert className="mt-4" type="error"> | ||
We're sorry, there was a problem with the LinkedIn connection. | ||
</Alert> | ||
)} | ||
</> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** | ||
* 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 { LinkedInProfile } from "@gitpod/gitpod-protocol"; | ||
|
||
export const LinkedInProfileDB = Symbol("LinkedInProfileDB"); | ||
export interface LinkedInProfileDB { | ||
storeProfile(userId: string, profile: LinkedInProfile): Promise<void>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/** | ||
* 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 { Column, Entity, PrimaryColumn } from "typeorm"; | ||
import { TypeORM } from "../typeorm"; | ||
import { LinkedInProfile } from "@gitpod/gitpod-protocol"; | ||
|
||
@Entity() | ||
// on DB but not Typeorm: @Index("ind_lastModified", ["_lastModified"]) // DBSync | ||
export class DBLinkedInProfile { | ||
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE) | ||
id: string; | ||
|
||
@Column(TypeORM.UUID_COLUMN_TYPE) | ||
userId: string; | ||
|
||
@Column({ | ||
type: "simple-json", | ||
nullable: false, | ||
}) | ||
profile: LinkedInProfile; | ||
|
||
@Column("varchar") | ||
creationTime: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/** | ||
* 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 { LinkedInProfile } from "@gitpod/gitpod-protocol"; | ||
import { inject, injectable } from "inversify"; | ||
import { Repository } from "typeorm"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
import { LinkedInProfileDB } from "../linked-in-profile-db"; | ||
import { DBLinkedInProfile } from "./entity/db-linked-in-profile"; | ||
import { TypeORM } from "./typeorm"; | ||
|
||
@injectable() | ||
export class LinkedInProfileDBImpl implements LinkedInProfileDB { | ||
@inject(TypeORM) typeORM: TypeORM; | ||
|
||
protected async getEntityManager() { | ||
return (await this.typeORM.getConnection()).manager; | ||
} | ||
|
||
protected async getRepo(): Promise<Repository<DBLinkedInProfile>> { | ||
return (await this.getEntityManager()).getRepository<DBLinkedInProfile>(DBLinkedInProfile); | ||
} | ||
|
||
public async storeProfile(userId: string, profile: LinkedInProfile): Promise<void> { | ||
const repo = await this.getRepo(); | ||
const existingProfile = await repo | ||
.createQueryBuilder("p") | ||
.where('JSON_EXTRACT(`p`.`profile`, "$.id") = :id', { id: profile.id }) | ||
.getOne(); | ||
if (existingProfile && existingProfile.userId !== userId) { | ||
throw new Error(`LinkedIn profile ${profile.id} is already associated with another user.`); | ||
} | ||
|
||
await repo.save({ | ||
id: existingProfile?.id || uuidv4(), | ||
userId, | ||
profile, | ||
creationTime: existingProfile?.creationTime || new Date().toISOString(), | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/** | ||
* 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 { MigrationInterface, QueryRunner } from "typeorm"; | ||
import { tableExists } from "./helper/helper"; | ||
|
||
const TABLE_NAME = "d_b_linked_in_profile"; | ||
|
||
export class LinkedInProfile1680096507296 implements MigrationInterface { | ||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query( | ||
`CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (id char(36) NOT NULL, userId char(36) NOT NULL, profile text NOT NULL, creationTime varchar(255) NOT NULL, _lastModified timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this not be unique per user? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @svenefftinge it could be. Please explicitly mention what you're expecting to see different in this CREATE statement. 🙂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no id but making the userid the PK There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great idea, thanks! |
||
); | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<void> { | ||
if (await tableExists(queryRunner, TABLE_NAME)) { | ||
await queryRunner.query(`DROP TABLE ${TABLE_NAME}`); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.