diff --git a/package-lock.json b/package-lock.json index 98b8b7ad..e54f6718 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4738,6 +4738,48 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "authing-js-sdk": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/authing-js-sdk/-/authing-js-sdk-4.19.5.tgz", + "integrity": "sha512-rV1l1w/Xw1nf5Zh3E8QzuTR2N4N+oOTA2YlDvv50beetpbutyD74bf6fgDvL0WP2Tbi1ek7ZNICiNVSmipkuqw==", + "requires": { + "axios": "^0.19.2", + "crypto-js": "^4.0.0", + "jsencrypt": "^3.1.0", + "jwt-decode": "^2.2.0" + }, + "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "author-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", @@ -7569,6 +7611,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", + "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" + }, "csp-html-webpack-plugin": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/csp-html-webpack-plugin/-/csp-html-webpack-plugin-5.1.0.tgz", @@ -14878,6 +14925,11 @@ } } }, + "jsencrypt": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.2.0.tgz", + "integrity": "sha512-Y/WBrCYRP1A2I1OEXxqurO+W3AC5uXhiArprpYQ0Y8/1Dc3NaiINAyCLx7HzXGwN7xvW3s5xpeOTdwD7lD1SQQ==" + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -15075,6 +15127,11 @@ "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", "dev": true }, + "jwt-decode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", + "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", diff --git a/package.json b/package.json index d99aa319..f2c8864b 100755 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@dnd-kit/sortable": "1.0.2", "@dnd-kit/utilities": "1.0.2", "@tiddlygit/tiddlywiki": "5.1.24-prerelease.20210331", + "authing-js-sdk": "4.19.5", "beautiful-react-hooks": "0.31.1", "bluebird": "3.7.2", "chokidar": "3.5.1", diff --git a/src/components/TokenForm/GitTokenForm.tsx b/src/components/TokenForm/GitTokenForm.tsx index a3431246..88454028 100644 --- a/src/components/TokenForm/GitTokenForm.tsx +++ b/src/components/TokenForm/GitTokenForm.tsx @@ -7,7 +7,7 @@ import { TextField, Button } from '@material-ui/core'; import { SupportedStorageServices } from '@services/types'; import { useUserInfoObservable } from '@services/auth/hooks'; import { ServiceEmailTypes, ServiceTokenTypes, ServiceUserNameTypes } from '@services/auth/interface'; -import { useAuthing } from './gitTokenHooks'; +import { useAuth } from './gitTokenHooks'; const AuthingLoginButton = styled(Button)` width: 100%; @@ -25,22 +25,7 @@ export function GitTokenForm(props: { const { children, storageService } = props; const { t } = useTranslation(); - const authing = useAuthing(); - - const onFailure = useCallback((error: Error) => { - console.error(error); - }, []); - - const onClickLogin = useCallback(async () => { - // clear token first, otherwise github login window won't give us a chance to see the form - // void this.auth.logout(); - // window.remote.clearStorageData(); - try { - await authing.login(); - } catch (error) { - onFailure(error); - } - }, [authing, onFailure]); + const [onClickLogin] = useAuth(storageService); const userInfo = useUserInfoObservable(); if (userInfo === undefined) { diff --git a/src/components/TokenForm/gitTokenHooks.ts b/src/components/TokenForm/gitTokenHooks.ts index 86b518b1..f8eddf15 100644 --- a/src/components/TokenForm/gitTokenHooks.ts +++ b/src/components/TokenForm/gitTokenHooks.ts @@ -1,84 +1,51 @@ import { useCallback, useEffect, useMemo } from 'react'; import AuthingSSO, { ITrackSessionResult } from '@authing/sso'; +import { AuthenticationClient } from 'authing-js-sdk'; import { ServiceTokenTypes, ServiceUserNameTypes, ServiceEmailTypes } from '@services/auth/interface'; import { SupportedStorageServices, IAuthingUserInfo } from '@services/types'; import { APP_ID, APP_DOMAIN } from '@/constants/auth'; -export function useTokenFromAuthingRedirect(authing: AuthingSSO, callback?: () => void): void { - const onLoginSuccessResponse = useCallback( - async (response: ITrackSessionResult) => { - // DEBUG: console - console.log(`response`, response); - if (!('userInfo' in response)) return; - const accessTokenToSet = response?.userInfo?.thirdPartyIdentity?.accessToken; - const provider = response?.userInfo?.thirdPartyIdentity?.provider as SupportedStorageServices; - if (accessTokenToSet !== undefined && provider !== undefined) { - if (!Object.values(SupportedStorageServices).includes(provider)) { - throw new Error(`${provider} not in SupportedStorageServices`); - } - // DEBUG: console - console.log(`provider`, provider); - const authDataString = response?.userInfo?.oauth; - // all data we need - if (accessTokenToSet !== undefined && authDataString !== undefined) { - const authData = JSON.parse(authDataString); - // DEBUG: console - console.log(`authData`, authData); - const nextUserInfo: IAuthingUserInfo = { - ...response.userInfo, - ...authData, - ...response.userInfo?.thirdPartyIdentity, - }; - // DEBUG: console - console.log(`nextUserInfo`, nextUserInfo); - await Promise.all([ - window.service.auth.set(`${provider}-token` as ServiceTokenTypes, accessTokenToSet), - window.service.auth.set(`${provider}-userName` as ServiceUserNameTypes, nextUserInfo.username), - window.service.auth.set(`${provider}-email` as ServiceEmailTypes, nextUserInfo.email), - ]); - callback?.(); - } - } - }, - [callback], - ); - - const onClickLogout = useCallback(async () => { - const { code, message } = await authing.logout(); - await window.service.window.clearStorageData(); - // DEBUG: console - console.log(`{ code, message }`, { code, message }); - if (code === 200) { - // TODO: clear the input - } else { - throw new Error(message); - } - }, [authing]); - - // after authing redirect to 3rd party page and success, it will redirect back, we then check if login is success on component mount - useEffect(() => { - void (async () => { - const response = await authing.trackSession(); - // we logout so login into github won't block use from login into gitlab - await onClickLogout(); - const isLogin = response?.session !== undefined && response?.session !== null; - if (isLogin) { - await onLoginSuccessResponse(response); - } - })(); - }, [authing, onLoginSuccessResponse, onClickLogout]); -} - -export function useAuthing(): AuthingSSO { +export function useAuth(storageService: SupportedStorageServices): [() => Promise, () => Promise] { const authing = useMemo( () => - new AuthingSSO({ + new AuthenticationClient({ appId: APP_ID, - appDomain: APP_DOMAIN, - redirectUrl: 'http://localhost:3000', + appHost: APP_DOMAIN, }), [], ); - return authing; + const onFailure = useCallback((error: Error) => { + console.error(error); + }, []); + const onClickLogout = useCallback(async () => { + await authing.logout(); + await window.service.window.clearStorageData(); + }, [authing]); + + const onClickLogin = useCallback(async () => { + // clear token first, otherwise github login window won't give us a chance to see the form + // void this.auth.logout(); + // window.remote.clearStorageData(); + try { + await authing.social.authorize(storageService, { + onSuccess: async (user) => { + // DEBUG: console + console.log('user', user); + const thirdPartyIdentity = user.identities?.find((identity) => identity?.provider === storageService); + await Promise.all([ + thirdPartyIdentity?.accessToken !== undefined && + window.service.auth.set(`${storageService}-token` as ServiceTokenTypes, thirdPartyIdentity.accessToken), + window.service.auth.set(`${storageService}-userName` as ServiceUserNameTypes, user.username), + window.service.auth.set(`${storageService}-email` as ServiceEmailTypes, user.email), + ]); + }, + onError: (code, message) => onFailure(new Error(message + String(code))), + }); + } catch (error) { + onFailure(error); + } + }, [authing.social, onFailure, storageService]); + + return [onClickLogin, onClickLogout]; } diff --git a/src/constants/auth.ts b/src/constants/auth.ts index 5aee41f1..891d7128 100644 --- a/src/constants/auth.ts +++ b/src/constants/auth.ts @@ -1,3 +1,3 @@ export const APP_ID = '5efdd30e56a87fb76b52809d'; -export const APP_DOMAIN = 'tiddlygit-desktop.authing.cn'; +export const APP_DOMAIN = 'https://tiddlygit-desktop.authing.cn'; export const GITHUB_GRAPHQL_API = 'https://api.github.com/graphql'; diff --git a/src/pages/AddWorkspace/index.tsx b/src/pages/AddWorkspace/index.tsx index cc052e1a..ad1d9ceb 100644 --- a/src/pages/AddWorkspace/index.tsx +++ b/src/pages/AddWorkspace/index.tsx @@ -18,7 +18,7 @@ import { CloneWikiDoneButton } from './CloneWikiDoneButton'; import { IErrorInWhichComponent, useIsCreateMainWorkspace, useIsCreateSyncedWorkspace, useWikiWorkspaceForm } from './useForm'; import { TokenForm } from '@/components/TokenForm'; -import { useAuthing, useTokenFromAuthingRedirect } from '@/components/TokenForm/gitTokenHooks'; +// import { useAuthing, useTokenFromAuthingRedirect } from '@/components/TokenForm/gitTokenHooks'; import { GitRepoUrlForm } from './GitRepoUrlForm'; enum CreateWorkspaceTabs { @@ -64,11 +64,7 @@ export default function AddWorkspace(): JSX.Element { } }, [isCreateSyncedWorkspace, storageProvider, storageProviderSetter]); - const authing = useAuthing(); - useTokenFromAuthingRedirect( - authing, - useCallback(() => isCreateSyncedWorkspaceSetter(true), [isCreateSyncedWorkspaceSetter]), - ); + // const [onClickLogin] = useAuth(storageService); const formProps = { form: form, diff --git a/src/services/auth/interface.ts b/src/services/auth/interface.ts index fe0989bb..480c9004 100644 --- a/src/services/auth/interface.ts +++ b/src/services/auth/interface.ts @@ -51,3 +51,38 @@ export const AuthenticationServiceIPCDescriptor = { reset: ProxyPropertyType.Function, }, }; + +export interface IGithubOAuthResult { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + name: any; + company: any; + blog: string; + location: any; + email: any; + hireable: any; + bio: any; + twitter_username: any; + public_repos: number; + public_gists: number; + followers: number; + following: number; + created_at: string; + updated_at: string; +}