feat: use oauth url to sign in github and get token

This commit is contained in:
linonetwo 2023-12-29 17:42:06 +08:00 committed by lin onetwo
parent 98bc7f1f6c
commit 9f8b24354f
11 changed files with 124 additions and 214 deletions

View file

@ -66,7 +66,6 @@
"menubar": "9.3.0",
"nanoid": "^4.0.2",
"node-fetch": "3.3.2",
"oidc-client-ts": "^2.4.0",
"reflect-metadata": "0.1.13",
"registry-js": "1.15.1",
"rxjs": "7.8.1",

19
pnpm-lock.yaml generated
View file

@ -110,9 +110,6 @@ dependencies:
node-fetch:
specifier: 3.3.2
version: 3.3.2
oidc-client-ts:
specifier: ^2.4.0
version: 2.4.0
reflect-metadata:
specifier: 0.1.13
version: 0.1.13
@ -5911,10 +5908,6 @@ packages:
randomfill: 1.0.4
dev: true
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/csp-html-webpack-plugin@5.1.0(html-webpack-plugin@5.5.3)(webpack@5.88.1):
resolution: {integrity: sha512-6l/s6hACE+UA01PLReNKZfgLZWM98f7ewWmE79maDWIbEXiPcIWQGB3LQR/Zw+hPBj4XPZZ5zNrrO+aygqaLaQ==}
peerDependencies:
@ -9241,10 +9234,6 @@ packages:
engines: {node: '>=8'}
dev: true
/jwt-decode@3.1.2:
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
dev: false
/keycode@2.2.1:
resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==}
dev: true
@ -10307,14 +10296,6 @@ packages:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
dev: true
/oidc-client-ts@2.4.0:
resolution: {integrity: sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==}
engines: {node: '>=12.13.0'}
dependencies:
crypto-js: 4.2.0
jwt-decode: 3.1.2
dev: false
/omggif@1.0.10:
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
dev: false

View file

@ -6,7 +6,7 @@ import { Button, TextField } from '@mui/material';
import { useUserInfoObservable } from '@services/auth/hooks';
import { getServiceBranchTypes, getServiceEmailTypes, getServiceTokenTypes, getServiceUserNameTypes } from '@services/auth/interface';
import { SupportedStorageServices } from '@services/types';
import { useAuth } from './gitTokenHooks';
import { useAuth, useGetGithubUserInfoOnLoad } from './gitTokenHooks';
const AuthingLoginButton = styled(Button)`
width: 100%;
@ -33,9 +33,10 @@ export function GitTokenForm(props: {
const { children, storageService } = props;
const { t } = useTranslation();
const [onClickLogin] = useAuth(storageService);
const userInfo = useUserInfoObservable();
const [onClickLogin] = useAuth(storageService);
useGetGithubUserInfoOnLoad();
if (userInfo === undefined) {
return <div>{t('Loading')}</div>;
}

View file

@ -1,69 +1,58 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { IGitUserInfosWithoutToken } from '@services/git/interface';
import { SupportedStorageServices } from '@services/types';
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
import { useCallback, useEffect, useState } from 'react';
export function useAuth(storageService: SupportedStorageServices, userInfo: IGitUserInfosWithoutToken): [() => Promise<void>, () => Promise<void>] {
const [userManager, setUserManager] = useState<UserManager>();
useEffect(() => {
void window.service.context.get('LOGIN_REDIRECT_PATH').then( (LOGIN_REDIRECT_PATH) => {
const settings = {
authority: 'https://github.com/',
client_id: userInfo.gitUserName,
redirect_uri: LOGIN_REDIRECT_PATH,
response_type: 'code',
scope: 'openid',
// post_logout_redirect_uri: '<YOUR_POST_LOGOUT_REDIRECT_URI>',
userStore: new WebStorageStateStore({ store: window.localStorage }),
};
const userManager = new UserManager(settings);
setUserManager(userManager);
userManager.events.addUserLoaded(async user => {
// Handle the loaded user information here
// Save the access token and other user details
if (user.access_token) {
await window.service.auth.set(`${storageService}-token`, user.access_token);
}
if (userInfo.gitUserName) {
await window.service.auth.set(`${storageService}-userName`, userInfo.gitUserName);
}
if (userInfo.email) {
await window.service.auth.set(`${storageService}-email`, userInfo.email);
}
// Additional user information might be available in the user.profile object
});
userManager.events.addSilentRenewError(error => {
console.error('Silent renew error', error);
});
// userManager.events.addUserSignedOut(async () => {
// // Handle user sign-out event
// await window.service.window.clearStorageData();
// });
});
}, [storageService, userInfo.email, userInfo.gitUserName]);
const onClickLogin = useCallback(async () => {
try {
await userManager?.signinRedirect();
} catch (error) {
console.error(error);
}
}, [userManager]);
import { useCallback, useEffect } from 'react';
export function useAuth(storageService: SupportedStorageServices): [() => Promise<void>, () => Promise<void>] {
const onClickLogout = useCallback(async () => {
try {
await userManager?.signoutRedirect();
await window.service.auth.set(`${storageService}-token`, '');
await window.service.window.clearStorageData();
} catch (error) {
console.error(error);
}
}, [userManager]);
}, [storageService]);
const onClickLogin = useCallback(async () => {
await onClickLogout();
try {
// redirect current page to oauth login page
switch (storageService) {
case SupportedStorageServices.github: {
location.href = await window.service.context.get('GITHUB_OAUTH_PATH');
}
}
} catch (error) {
console.error(error);
}
}, [onClickLogout, storageService]);
return [onClickLogin, onClickLogout];
}
export function useGetGithubUserInfoOnLoad(): void {
useEffect(() => {
void window.service.auth.get(`${SupportedStorageServices.github}-token`).then(async (githubToken) => {
try {
// DEBUG: console githubToken
console.log(`githubToken`, githubToken);
if (githubToken) {
// get user name and email using github api
const response = await fetch('https://api.github.com/user', {
method: 'GET',
headers: {
Authorization: `Bearer ${githubToken}`,
},
});
const userInfo = await (response.json() as Promise<{ email: string; login: string; name: string }>);
// DEBUG: console userInfo
console.log(`userInfo`, userInfo);
await window.service.auth.set(`${SupportedStorageServices.github}-userName`, userInfo.login);
await window.service.auth.set('userName', userInfo.name);
await window.service.auth.set(`${SupportedStorageServices.github}-email`, userInfo.email);
}
} catch (error) {
console.error(error);
}
});
}, []);
}

View file

@ -2,3 +2,15 @@ export const GITHUB_GRAPHQL_API = 'https://api.github.com/graphql';
export const TIDGI_AUTH_TOKEN_HEADER = 'x-tidgi-auth-token';
export const getTidGiAuthHeaderWithToken = (authToken: string) => `${TIDGI_AUTH_TOKEN_HEADER}-${authToken}`;
export const DEFAULT_USER_NAME = 'TidGi User';
/**
* Github OAuth Apps TidGi-SignIn Setting in https://github.com/organizations/tiddly-gittly/settings/applications/1326590
*/
export const GITHUB_LOGIN_REDIRECT_PATH = 'http://127.0.0.1:3012/tidgi-auth/github';
export const GITHUB_OAUTH_APP_CLIENT_ID = '7b6e0fc33f4afd71a4bb';
export const GITHUB_OAUTH_APP_CLIENT_SECRET = 'e356c4499e1e38548a44da5301ef42c11ec14173';
const GITHUB_SCOPES = 'user:email,read:user,repo,workflow';
/**
* Will redirect to `http://127.0.0.1:3012/tidgi-auth/github?code=65xxxxxxx` after login, which is 404, and handled by src/preload/common/authRedirect.ts
*/
export const GITHUB_OAUTH_PATH =
`https://github.com/login/oauth/authorize?client_id=${GITHUB_OAUTH_APP_CLIENT_ID}&scope=${GITHUB_SCOPES}&redirect_uri=${GITHUB_LOGIN_REDIRECT_PATH}`;

View file

@ -1,6 +1,7 @@
import os from 'os';
import path from 'path';
import { isMac } from '../helpers/system';
import { GITHUB_LOGIN_REDIRECT_PATH, GITHUB_OAUTH_APP_CLIENT_ID } from './auth';
import { isDevelopmentOrTest } from './environment';
import { developmentWikiFolderName, localizationFolderName } from './fileNames';
@ -19,7 +20,6 @@ const menuBarIconFileName = isMac ? 'menubarTemplate@2x.png' : 'menubar@2x.png';
export const MENUBAR_ICON_PATH = path.resolve(isDevelopmentOrTest ? buildResourcePath : process.resourcesPath, menuBarIconFileName);
export const CHROME_ERROR_PATH = 'chrome-error://chromewebdata/';
export const LOGIN_REDIRECT_PATH = 'http://127.0.0.1:3012/tidgi-github-auth';
export const DESKTOP_PATH = path.join(os.homedir(), 'Desktop');
export const PACKAGE_PATH_BASE = isDevelopmentOrTest

View file

@ -1,57 +1,78 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
// on production build, if we try to redirect to http://localhost:3012 , we will reach chrome-error://chromewebdata/ , but we can easily get back
// this happens when we are redirected by OAuth login
import { SupportedStorageServices } from '@services/types';
import { WindowNames } from '@services/windows/WindowProperties';
import { windowName } from './browserViewMetaData';
import { context, window as windowService } from './services';
const CHECK_LOADED_INTERVAL = 500;
let constantsFetched = false;
let CHROME_ERROR_PATH: string | undefined;
let LOGIN_REDIRECT_PATH: string | undefined;
let GITHUB_LOGIN_REDIRECT_PATH: string | undefined;
let MAIN_WINDOW_WEBPACK_ENTRY: string | undefined;
let GITHUB_OAUTH_APP_CLIENT_SECRET: string | undefined;
let GITHUB_OAUTH_APP_CLIENT_ID: string | undefined;
async function refresh(): Promise<void> {
if (CHROME_ERROR_PATH === undefined || MAIN_WINDOW_WEBPACK_ENTRY === undefined || LOGIN_REDIRECT_PATH === undefined) {
// get path from src/constants/paths.ts
if (!constantsFetched) {
await Promise.all([
context.get('CHROME_ERROR_PATH').then((pathName) => {
CHROME_ERROR_PATH = pathName;
}),
context.get('LOGIN_REDIRECT_PATH').then((pathName) => {
LOGIN_REDIRECT_PATH = pathName;
}),
context.get('MAIN_WINDOW_WEBPACK_ENTRY').then((pathName) => {
MAIN_WINDOW_WEBPACK_ENTRY = pathName;
}),
context.get('GITHUB_LOGIN_REDIRECT_PATH').then((pathName) => {
GITHUB_LOGIN_REDIRECT_PATH = pathName;
}),
context.get('GITHUB_OAUTH_APP_CLIENT_SECRET').then((pathName) => {
GITHUB_OAUTH_APP_CLIENT_SECRET = pathName;
}),
context.get('GITHUB_OAUTH_APP_CLIENT_ID').then((pathName) => {
GITHUB_OAUTH_APP_CLIENT_ID = pathName;
}),
]);
setTimeout(() => void refresh(), CHECK_LOADED_INTERVAL);
constantsFetched = true;
await refresh();
return;
}
if (window.location.href === CHROME_ERROR_PATH || window.location.href.startsWith(`${LOGIN_REDIRECT_PATH}/?code=`)) {
if (window.location.href.startsWith(GITHUB_LOGIN_REDIRECT_PATH!)) {
// currently content will be something like `/tidgi-auth/github 404 not found`, we need to write something to tell user we are handling login, this is normal.
// center the text and make it large
document.body.innerHTML = '<div style="text-align: center; font-size: 2rem;">Handling Github login, please wait...</div>';
// get the code
const code = window.location.href.split('code=')[1];
if (code) {
// exchange the code for an access token in github
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: GITHUB_OAUTH_APP_CLIENT_ID,
client_secret: GITHUB_OAUTH_APP_CLIENT_SECRET,
code,
}),
});
// get the access token from the response
const { access_token: token } = await (response.json() as Promise<{ access_token: string }>);
await window.service.auth.set(`${SupportedStorageServices.github}-token`, token);
}
await windowService.loadURL(windowName, MAIN_WINDOW_WEBPACK_ENTRY);
} else if (window.location.href === CHROME_ERROR_PATH) {
await windowService.loadURL(windowName, MAIN_WINDOW_WEBPACK_ENTRY);
} else {
setTimeout(() => void refresh(), CHECK_LOADED_INTERVAL);
}
}
interface IAuthingPostMessageEvent {
code?: number;
data?: {
token?: string;
};
from?: string;
}
/**
* Setting window and add workspace window may be used to login, and will be redirect, we catch it and redirect back.
*/
if (![WindowNames.main, WindowNames.view].includes(windowName)) {
setTimeout(() => void refresh(), CHECK_LOADED_INTERVAL);
// https://stackoverflow.com/questions/55544936/communication-between-preload-and-client-given-context-isolation-in-electron
window.addEventListener(
'message',
(event: MessageEvent<IAuthingPostMessageEvent>) => {
if (typeof event?.data?.code === 'number' && typeof event?.data?.data?.token === 'string' && event?.data.from !== 'preload') {
// This message will be catch by this handler again, so we add a 'from' to indicate that it is re-send by ourself
// we re-send this, so authing in this window can catch it
window.postMessage({ ...event.data, from: 'preload' }, '*');
}
},
false,
);
}

View file

@ -2,7 +2,7 @@
/* eslint-disable unicorn/no-null */
import { IGitUserInfos } from '@services/git/interface';
import { logger } from '@services/libs/log';
import { IAuthingUserInfo, SupportedStorageServices } from '@services/types';
import { SupportedStorageServices } from '@services/types';
import { IWorkspace } from '@services/workspaces/interface';
import settings from 'electron-settings';
import { injectable } from 'inversify';
@ -13,7 +13,6 @@ import { IAuthenticationService, IUserInfos, ServiceBranchTypes, ServiceEmailTyp
const defaultUserInfos = {
userName: '',
authing: undefined as IAuthingUserInfo | undefined,
};
@injectable()

View file

@ -6,14 +6,16 @@ import os from 'os';
import process from 'process';
import * as appPaths from '@/constants/appPaths';
import * as auth from '@/constants/auth';
import { supportedLanguagesMap, tiddlywikiLanguagesMap } from '@/constants/languages';
import * as paths from '@/constants/paths';
import { IConstants, IContext, IContextService, IPaths } from './interface';
import { IAuthConstants, IConstants, IContext, IContextService, IPaths } from './interface';
@injectable()
export class ContextService implements IContextService {
// @ts-expect-error Property 'MAIN_WINDOW_WEBPACK_ENTRY' is missing, esbuild will make it `pathConstants = { ..._constants_paths__WEBPACK_IMPORTED_MODULE_4__, ..._constants_appPaths__WEBPACK_IMPORTED_MODULE_5__, 'http://localhost:3012/main_window' };`
private readonly pathConstants: IPaths = { ...paths, ...appPaths };
private readonly authConstants: IAuthConstants = { ...auth };
private readonly constants: IConstants = {
isDevelopment: isElectronDevelopment,
platform: process.platform,
@ -31,6 +33,7 @@ export class ContextService implements IContextService {
this.context = {
...this.pathConstants,
...this.constants,
...this.authConstants,
};
}

View file

@ -10,7 +10,6 @@ export interface IPaths {
HTTPS_CERT_KEY_FOLDER: string;
LANGUAGE_MODEL_FOLDER: string;
LOCALIZATION_FOLDER: string;
LOGIN_REDIRECT_PATH: string;
LOG_FOLDER: string;
MAIN_WINDOW_WEBPACK_ENTRY: string;
MENUBAR_ICON_PATH: string;
@ -19,6 +18,13 @@ export interface IPaths {
TIDDLYWIKI_TEMPLATE_FOLDER_PATH: string;
V8_CACHE_FOLDER: string;
}
export interface IAuthConstants {
GITHUB_LOGIN_REDIRECT_PATH: string;
GITHUB_OAUTH_APP_CLIENT_ID: string;
GITHUB_OAUTH_APP_CLIENT_SECRET: string;
GITHUB_OAUTH_PATH: string;
}
/**
* Available values about running environment
*/
@ -33,7 +39,7 @@ export interface IConstants {
tiddlywikiLanguagesMap: Record<string, string | undefined>;
}
export interface IContext extends IPaths, IConstants {}
export interface IContext extends IPaths, IConstants, IAuthConstants {}
/**
* Manage constant value like `isDevelopment` and many else, so you can know about about running environment in main and renderer process easily.

View file

@ -14,104 +14,3 @@ export enum SupportedStorageServices {
/** SocialLinkedData, a privacy first DApp platform leading by Tim Berners-Lee, you can run a server by you own */
solid = 'solid',
}
export interface IAuthingResponse {
session?: null | { appId: string; type: string; userId: string };
urlParams?: {
access_token: string;
code: string;
// 这些参数是从 url 中获取到的,需要开发者自己存储以备使用
id_token: string;
};
userInfo?: {
oauth?: string;
thirdPartyIdentity?: {
accessToken?: string;
};
};
}
// {"email":"linonetwo012@gmail.com","phone":"","emailVerified":false,"phoneVerified":false,"username":"lin onetwo","nickname":"","company":"ByteDance","photo":"https://avatars1.githubusercontent.com/u/3746270?v=4","browser":"","device":"","loginsCount":26,"registerMethod":"oauth:github","blocked":false,"isDeleted":false,"phoneCode":"","name":"lin onetwo","givenName":"","familyName":"","middleName":"","profile":"","preferredUsername":"","website":"","gender":"","birthdate":"","zoneinfo":"","locale":"","address":"","formatted":"","streetAddress":"","locality":"","region":"","postalCode":"","country":"","updatedAt":"2020-07-04T05:08:53.472Z","metadata":"","_operate_history":[],"sendSMSCount":0,"sendSMSLimitCount":1000,"_id":"5efdd5475b9f7bc0990d7377","unionid":"3746270","lastIP":"61.223.86.45","registerInClient":"5efdd30d48432dfae5d047da","lastLogin":"2020-07-04T05:08:53.665Z","signedUp":"2020-07-02T12:38:31.485Z","__v":0,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImVtYWlsIjoibGlub2","tokenExpiredAt":"2020-07-19T05:08:53.000Z","__Token 验证方式说明":"https://docs.authing.cn/authing/advanced/authentication/verify-jwt-token#fa-song-token-gei-authing-fu-wu-qi-yan-zheng","login":"linonetwo","id":3746270,"node_id":"MDQ6VXNlcjM3NDYyNzA=","avatar_url":"https://avatars1.githubusercontent.com/u/3746270?v=4","gravatar_id":"","url":"https://api.github.com/users/linonetwo","html_url":"https://github.com/linonetwo","followers_url":"https://api.github.com/users/linonetwo/followers","following_url":"https://api.github.com/users/linonetwo/following{/other_user}","gists_url":"https://api.github.com/users/linonetwo/gists{/gist_id}","starred_url":"https://api.github.com/users/linonetwo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/linonetwo/subscriptions","organizations_url":"https://api.github.com/users/linonetwo/orgs","repos_url":"https://api.github.com/users/linonetwo/repos","events_url":"https://api.github.com/users/linonetwo/events{/privacy}","received_events_url":"https://api.github.com/users/linonetwo/received_events","type":"User","site_admin":false,"blog":"https://onetwo.ren","location":"Shanghaitech University","hireable":null,"bio":"Use Web technology to create dev-tool and knowledge tools for procedural content generation. Hopefully will create a knowledge-driven PCG in game cosmos one day","twitter_username":null,"public_repos":146,"public_gists":13,"followers":167,"following":120,"created_at":"2013-03-02T07:09:13Z","updated_at":"2020-07-02T12:36:46Z","accessToken":"f5610134da3c51632e43e8a2413863987e8ad16e","scope":"repo","provider":"github","expiresIn":null}
export interface IAuthingUserInfo {
'__Token 验证方式说明': string;
__v: number;
_id: string;
_operate_history: any[];
accessToken: string;
address: string;
avatar_url: string;
bio: string;
birthdate: string;
blocked: boolean;
blog: string;
browser: string;
company: string;
country: string;
created_at: string;
device: string;
email: string;
emailVerified: boolean;
events_url: string;
expiresIn: null;
familyName: string;
followers: number;
followers_url: string;
following: number;
following_url: string;
formatted: string;
gender: string;
gists_url: string;
givenName: string;
gravatar_id: string;
hireable: null;
html_url: string;
id: number;
isDeleted: boolean;
lastIP: string;
lastLogin: string;
locale: string;
locality: string;
location: string;
login: string;
loginsCount: number;
metadata: string;
middleName: string;
name: string;
nickname: string;
node_id: string;
organizations_url: string;
phone: string;
phoneCode: string;
phoneVerified: boolean;
photo: string;
postalCode: string;
preferredUsername: string;
profile: string;
provider: string;
public_gists: number;
public_repos: number;
received_events_url: string;
region: string;
registerInClient: string;
registerMethod: string;
repos_url: string;
scope: string;
sendSMSCount: number;
sendSMSLimitCount: number;
signedUp: string;
site_admin: boolean;
starred_url: string;
streetAddress: string;
subscriptions_url: string;
token: string;
tokenExpiredAt: string;
twitter_username: null;
type: string;
unionid: string;
updatedAt: string;
updated_at: string;
url: string;
username: string;
website: string;
zoneinfo: string;
}