diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8b18cb0..ae7fdba4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,7 @@ on: branches: - main - beta + workflow_dispatch: jobs: setup: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d430669..bbad8c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,50 @@ > All notable changes to this project will be documented in this file +## [1.12.0](https://github.com/open-sauced/ai/compare/v1.11.1...v1.12.0) (2023-07-31) + + +### 🍕 Features + +* Login with pkce ([#230](https://github.com/open-sauced/ai/issues/230)) ([42e8d4a](https://github.com/open-sauced/ai/commit/42e8d4ad4d3bc47c4138b2eeec20920015183849)) +* view repos on opensauced ([#218](https://github.com/open-sauced/ai/issues/218)) ([f33d9fd](https://github.com/open-sauced/ai/commit/f33d9fd8ab66242aecf07be23d961c33b9779727)) + + +### 🐛 Bug Fixes + +* manual release.yml ([a41de3f](https://github.com/open-sauced/ai/commit/a41de3f288b1147e5cf9fc0139ac80fcd1c8e2ee)) +* post highlights from the extension ([#223](https://github.com/open-sauced/ai/issues/223)) ([7d91011](https://github.com/open-sauced/ai/commit/7d910114ed4c094e8040549496873c79aff2196e)) +* redirect to home page after posting a highlight ([#203](https://github.com/open-sauced/ai/issues/203)) ([4ebd2d1](https://github.com/open-sauced/ai/commit/4ebd2d15b8b3bae0ea6b563568b0b7b01a86f832)) +* view on open sauced location ([#225](https://github.com/open-sauced/ai/issues/225)) ([0ecfef0](https://github.com/open-sauced/ai/commit/0ecfef0dda3a83b43deaa12189a7dc0474f29d78)) + +## [1.12.0-beta.6](https://github.com/open-sauced/ai/compare/v1.12.0-beta.5...v1.12.0-beta.6) (2023-07-31) + + +### 🐛 Bug Fixes + +* manual release.yml ([a41de3f](https://github.com/open-sauced/ai/commit/a41de3f288b1147e5cf9fc0139ac80fcd1c8e2ee)) + +## [1.12.0-beta.5](https://github.com/open-sauced/ai/compare/v1.12.0-beta.4...v1.12.0-beta.5) (2023-07-28) + + +### 🍕 Features + +* Login with pkce ([#230](https://github.com/open-sauced/ai/issues/230)) ([42e8d4a](https://github.com/open-sauced/ai/commit/42e8d4ad4d3bc47c4138b2eeec20920015183849)) + +## [1.12.0-beta.4](https://github.com/open-sauced/ai/compare/v1.12.0-beta.3...v1.12.0-beta.4) (2023-07-25) + + +### 🐛 Bug Fixes + +* view on open sauced location ([#225](https://github.com/open-sauced/ai/issues/225)) ([0ecfef0](https://github.com/open-sauced/ai/commit/0ecfef0dda3a83b43deaa12189a7dc0474f29d78)) + +## [1.12.0-beta.3](https://github.com/open-sauced/ai/compare/v1.12.0-beta.2...v1.12.0-beta.3) (2023-07-20) + + +### 🐛 Bug Fixes + +* redirect to home page after posting a highlight ([#203](https://github.com/open-sauced/ai/issues/203)) ([4ebd2d1](https://github.com/open-sauced/ai/commit/4ebd2d15b8b3bae0ea6b563568b0b7b01a86f832)) + ## [1.12.0-beta.2](https://github.com/open-sauced/ai/compare/v1.12.0-beta.1...v1.12.0-beta.2) (2023-07-19) diff --git a/manifest.json b/manifest.json index faff589a..5429f2ef 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "OpenSauced.ai", - "version": "1.11.1", + "version": "1.12.0", "action": { "default_popup": "index.html" }, "content_scripts": [ { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 2aba3f74..19b17028 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "opensauced-browser-extension", - "version": "1.12.0-beta.2", + "version": "1.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opensauced-browser-extension", - "version": "1.12.0-beta.2", + "version": "1.12.0", "dependencies": { "date-fns": "^2.30.0", "gpt-tokenizer": "^1.0.5", diff --git a/package.json b/package.json index 0870dd69..5b4318dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "opensauced-browser-extension", "private": true, - "version": "1.12.0-beta.2", + "version": "1.12.0", "files": [ "dist" ], diff --git a/src/constants.ts b/src/constants.ts index 36c80e5c..6d5cc131 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,7 +2,8 @@ export const OPEN_SAUCED_INSIGHTS_DOMAIN = import.meta.env.VITE_OPEN_SAUCED_INSIGHTS_DOMAIN; export const OPEN_SAUCED_API_ENDPOINT = import.meta.env.VITE_OPEN_SAUCED_API_ENDPOINT; export const REPO_QUERY_API_ENDPOINT = "https://opensauced.tools"; -export const SUPABASE_LOGIN_URL = `https://${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}.supabase.co/auth/v1/authorize?provider=github&redirect_to=https://${OPEN_SAUCED_INSIGHTS_DOMAIN}/`; +export const SUPABASE_LOGIN_URL = `https://${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}.supabase.co/auth/v1/authorize`; + export const REPO_QUERY_EMBED_ENDPOINT = `${REPO_QUERY_API_ENDPOINT}/embed`; @@ -10,6 +11,7 @@ export const REPO_QUERY_QUERY_ENDPOINT = `${REPO_QUERY_API_ENDPOINT}/query`; export const REPO_QUERY_COLLECTION_ENDPOINT = `${REPO_QUERY_API_ENDPOINT}/collection`; export const SUPABASE_AUTH_COOKIE_NAME = `sb-${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}-auth-token`; +export const SUPABASE_PKCE_VERIFIER_COOKIE_NAME = `sb-${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}-auth-token-code-verifier`; export const OPEN_SAUCED_AUTH_TOKEN_KEY = "os-access-token"; export const OPEN_SAUCED_OPTED_LOG_OUT_KEY = "opted-log-out"; export const AI_PR_DESCRIPTION_CONFIG_KEY = "ai-pr-description-config"; @@ -32,6 +34,7 @@ export const OPEN_SAUCED_EMOJIS_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/emojis`; export const GITHUB_PROFILE_MENU_SELECTOR = ".p-nickname.vcard-username.d-block"; export const GITHUB_PROFILE_EDIT_MENU_SELECTOR = "button.js-profile-editable-edit-button"; export const GITHUB_PROFILE_USER_PROFILE_BIO_SELECTOR = ".p-note.user-profile-bio.mb-3.js-user-profile-bio.f4"; +export const GITHUB_PROFILE_USER_PROFILE_EDITABLE_AREA_SELECTOR = "js-profile-editable-area d-flex flex-column d-md-block"; export const GITHUB_PR_COMMENT_HEADER_SELECTOR = "timeline-comment-header clearfix d-flex"; export const GITHUB_NEW_PR_COMMENT_EDITOR_SELECTOR = "flex-nowrap d-none d-md-inline-block mr-md-0 mr-3"; export const GITHUB_PR_COMMENT_EDITOR_SELECTOR = "flex-nowrap d-inline-block mr-3"; diff --git a/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts b/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts index c5d76ddf..9c52ba7a 100644 --- a/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts +++ b/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts @@ -1,13 +1,10 @@ -import { - SUPABASE_LOGIN_URL, - GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR, -} from "../../../constants"; +import { GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR } from "../../../constants"; import { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert"; import { DescriptionConfig, getAIDescriptionConfig, } from "../../../utils/ai-utils/descriptionconfig"; -import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; +import { getAuthToken, isLoggedIn, optLogIn } from "../../../utils/checkAuthentication"; import { createHtmlElement } from "../../../utils/createHtmlElement"; import { isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; @@ -73,7 +70,7 @@ const handleSubmit = async ( try { if (!(await isLoggedIn())) { - return window.open(SUPABASE_LOGIN_URL, "_blank"); + return void optLogIn(); } if (!logo || !button) { diff --git a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts index f5374fca..e5dd01d5 100644 --- a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts +++ b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts @@ -3,10 +3,10 @@ import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg"; import { getPullRequestAPIURL } from "../../../utils/urlMatchers"; import { getDescriptionContext, isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; import { generateDescription } from "../../../utils/ai-utils/openai"; -import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR, SUPABASE_LOGIN_URL } from "../../../constants"; +import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR } from "../../../constants"; import { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert"; import { getAIDescriptionConfig } from "../../../utils/ai-utils/descriptionconfig"; -import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; +import { getAuthToken, isLoggedIn, optLogIn } from "../../../utils/checkAuthentication"; export const DescriptionGeneratorButton = () => { const descriptionGeneratorButton = createHtmlElement("a", { @@ -27,7 +27,7 @@ const handleSubmit = async () => { try { if (!(await isLoggedIn())) { - return window.open(SUPABASE_LOGIN_URL, "_blank"); + return void optLogIn(); } if (!logo || !button) { diff --git a/src/content-scripts/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts b/src/content-scripts/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts index 2e7f9a24..fac8aaea 100644 --- a/src/content-scripts/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts +++ b/src/content-scripts/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts @@ -8,7 +8,7 @@ export const ViewOnOpenSaucedButton = (username: string) => { id: "view-on-opensauced-button", href: `https://${OPEN_SAUCED_INSIGHTS_DOMAIN}/user/${username}/contributions`, className: - "inline-block mt-4 text-black bg-gh-white dark:bg-gh-gray dark:text-white rounded-md p-2 text-sm font-semibold text-center select-none w-full border hover:shadow-button hover:no-underline", + "inline-block my-4 text-black bg-gh-white dark:bg-gh-gray dark:text-white rounded-md p-2 text-sm font-semibold text-center select-none w-full border hover:shadow-button hover:no-underline", target: "_blank", rel: "noopener noreferrer", innerHTML: ` diff --git a/src/popup/pages/home.tsx b/src/popup/pages/home.tsx index 4d0b7451..f56451b8 100644 --- a/src/popup/pages/home.tsx +++ b/src/popup/pages/home.tsx @@ -25,7 +25,7 @@ import type { Highlight } from "../../ts/types"; import { usGetGitHubPageInfo } from "../../hooks/useGetGitHubPageInfo"; import { HighlightSlide } from "../components/HighlightSlide"; -const Home = () => { +const Home = ({ forceRefresh }: { forceRefresh: boolean } = { forceRefresh: false }) => { const { user } = useAuth(); const { currentTabIsOpensaucedUser, checkedUser } = useOpensaucedUserCheck(); const { pageUrl, pageTitle, type: GitHubPageType } = usGetGitHubPageInfo(); @@ -37,7 +37,7 @@ const Home = () => { useEffect(() => { const fetchHighlights = async () => { try { - const userHighlightsData = await getHighlights(); + const userHighlightsData = await getHighlights(forceRefresh); if (!userHighlightsData) { return; diff --git a/src/popup/pages/posthighlight.tsx b/src/popup/pages/posthighlight.tsx index 7006ee19..0498ff7e 100644 --- a/src/popup/pages/posthighlight.tsx +++ b/src/popup/pages/posthighlight.tsx @@ -4,9 +4,10 @@ import OpenSaucedLogo from "../../assets/opensauced-logo.svg"; import { useAuth } from "../../hooks/useAuth"; import toast, { Toaster } from "react-hot-toast"; import { createHighlight } from "../../utils/fetchOpenSaucedApiData"; -import { goBack } from "react-chrome-extension-router"; +import { goBack, goTo } from "react-chrome-extension-router"; import { OPEN_SAUCED_INSIGHTS_DOMAIN } from "../../constants"; import { getAiDescription } from "../../content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton"; +import Home from "./home"; const PostOnHighlight = ({ prUrl, prTitle }: { prUrl: string, prTitle: string }) => { const { authToken, user } = useAuth(); @@ -45,8 +46,9 @@ const PostOnHighlight = ({ prUrl, prTitle }: { prUrl: string, prTitle: string }) success: data => { enableSendButton(true); if (!data.ok) { - throw new Error(`Statues code ${data.status}`); + throw new Error(`Status code ${data.status}`); } + goTo(Home, { forceRefresh: true }); return ( => Object.entries(await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY)).length !== 0; export const getAuthToken = async (): Promise => (await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY))[OPEN_SAUCED_AUTH_TOKEN_KEY]; @@ -13,14 +13,66 @@ export const optLogOut = () => { void chrome.storage.local.set({ [OPEN_SAUCED_OPTED_LOG_OUT_KEY]: true }); }; -export const optLogIn = () => { +export const optLogIn = async () => { if (typeof window === "undefined") { return; } void chrome.storage.local.set({ [OPEN_SAUCED_OPTED_LOG_OUT_KEY]: false }); - window.open(SUPABASE_LOGIN_URL, "_blank"); + + const verifier = generatePKCEVerifier(); + const challenge = await generatePKCEChallenge(verifier); + + const loginURL = new URL(SUPABASE_LOGIN_URL); + + loginURL.searchParams.append("provider", "github"); + loginURL.searchParams.append( + "redirect_to", + `https://${OPEN_SAUCED_INSIGHTS_DOMAIN}`, + ); + loginURL.searchParams.append("code_challenge_method", "s256"); + loginURL.searchParams.append("code_challenge", challenge); + + await chrome.cookies.set({ + url: `https://${OPEN_SAUCED_INSIGHTS_DOMAIN}`, + name: SUPABASE_PKCE_VERIFIER_COOKIE_NAME, + value: verifier, + }); + window.open(loginURL, "_blank"); }; export const hasOptedLogOut = async (): Promise => (await chrome.storage.local.get(OPEN_SAUCED_OPTED_LOG_OUT_KEY))[OPEN_SAUCED_OPTED_LOG_OUT_KEY] === true; export const removeAuthTokenFromStorage = async (): Promise => chrome.storage.sync.remove(OPEN_SAUCED_AUTH_TOKEN_KEY); + +// Custom browser only PKCE implementation based on https://github.com/supabase/gotrue-js + +const dec2hex = (dec: number) => (`0${dec.toString(16)}`).substring(-2); + +const generatePKCEVerifier = () => { + const verifierLength = 56; + const array = new Uint32Array(verifierLength); + + crypto.getRandomValues(array); + return Array.from(array, dec2hex).join(""); +}; + +const sha256 = async (randomString: string) => { + const encoder = (new TextEncoder); + const encodedData = encoder.encode(randomString); + const hash = await window.crypto.subtle.digest("SHA-256", encodedData); + const bytes = new Uint8Array(hash); + + return Array.from(bytes) + .map(c => String.fromCharCode(c)) + .join(""); +}; + +const base64urlencode = (str: string) => btoa(str).replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/[=]+$/, ""); + +const generatePKCEChallenge = async (verifier: string) => { + const hashed = await sha256(verifier); + + return base64urlencode(hashed); +}; diff --git a/src/utils/dom-utils/viewOnOpenSauced.ts b/src/utils/dom-utils/viewOnOpenSauced.ts index 8653e877..d82bbf4e 100644 --- a/src/utils/dom-utils/viewOnOpenSauced.ts +++ b/src/utils/dom-utils/viewOnOpenSauced.ts @@ -1,7 +1,4 @@ -import { - GITHUB_PROFILE_USER_PROFILE_BIO_SELECTOR, - GITHUB_PROFILE_EDIT_MENU_SELECTOR, -} from "../../constants"; +import { GITHUB_PROFILE_USER_PROFILE_EDITABLE_AREA_SELECTOR } from "../../constants"; import { ViewOnOpenSaucedButton } from "../../content-scripts/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton"; const injectViewOnOpenSaucedButton = (username: string) => { @@ -11,11 +8,12 @@ const injectViewOnOpenSaucedButton = (username: string) => { const viewOnOpenSaucedButton = ViewOnOpenSaucedButton(username); - const userBio = document.querySelector( - `${GITHUB_PROFILE_USER_PROFILE_BIO_SELECTOR}, ${GITHUB_PROFILE_EDIT_MENU_SELECTOR}`, + const userEditableArea = document.getElementsByClassName( + GITHUB_PROFILE_USER_PROFILE_EDITABLE_AREA_SELECTOR, ); + const editableAreaElement = userEditableArea[0]; - userBio?.append(viewOnOpenSaucedButton); + editableAreaElement.parentNode?.insertBefore(viewOnOpenSaucedButton, editableAreaElement); }; export default injectViewOnOpenSaucedButton; diff --git a/src/utils/fetchOpenSaucedApiData.ts b/src/utils/fetchOpenSaucedApiData.ts index 79d5028c..02026ef7 100644 --- a/src/utils/fetchOpenSaucedApiData.ts +++ b/src/utils/fetchOpenSaucedApiData.ts @@ -171,12 +171,13 @@ export const updateInsight = async (userToken: string, repoId: string, checked: return response.status === 200; }; -export const getHighlights = async (): Promise => { +export const getHighlights = async (forceRefresh?: boolean): Promise => { const response = await cachedFetch( `${OPEN_SAUCED_HIGHLIGHTS_LIST_ENDPOINT}?limit=10`, { method: "GET", expireInSeconds: 300, + forceRefresh, }, );