Merge branch 'feature/oauth' into develop

This commit is contained in:
EnixCoda 2019-08-24 22:46:12 +08:00
commit 76498d9ec9
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
10 changed files with 153 additions and 20 deletions

5
.env.example Normal file
View file

@ -0,0 +1,5 @@
# The client ID you received from GitHub for your GitHub App.
GITHUB_OAUTH_CLIENT_ID=GITHUB_OAUTH_CLIENT_ID
# The client secret you received from GitHub for your GitHub App.
GITHUB_OAUTH_CLIENT_SECRET=GITHUB_OAUTH_CLIENT_SECRET

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.vscode
.env
node_modules
tmp
dist

View file

@ -45,6 +45,7 @@
"babel-loader": "^8.0.5",
"copy-webpack-plugin": "^5.0.0",
"css-loader": "^2.1.0",
"dotenv-webpack": "^1.7.0",
"file-loader": "^3.0.1",
"fork-ts-checker-webpack-plugin": "^0.5.2",
"fsevents": "^1.2.4",

View file

@ -2,7 +2,7 @@ import * as React from 'react'
import Icon from 'components/Icon'
import configHelper, { configKeys } from 'utils/configHelper'
import keyHelper from 'utils/keyHelper'
import { friendlyFormatShortcut } from 'utils/general'
import { friendlyFormatShortcut, parseURLSearch, JSONRequest } from 'utils/general'
import { version } from '../../package.json'
const wikiLinks = {
@ -14,6 +14,11 @@ const wikiLinks = {
'https://github.com/EnixCoda/Gitako/wiki/How-to-create-access-token-for-Gitako%3F',
}
const oauth = {
clientId: process.env.GITHUB_OAUTH_CLIENT_ID,
clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
}
const ACCESS_TOKEN_REGEXP = /^[0-9a-f]{40}$/
type Props = {
@ -84,12 +89,33 @@ export default class SettingsBar extends React.PureComponent<Props, State> {
],
}
componentDidMount() {
if (!this.props.accessToken) this.trySetUpAccessTokenWithCode()
}
componentWillReceiveProps({ toggleShowSideBarShortcut }: Props) {
if (toggleShowSideBarShortcut !== this.props.toggleShowSideBarShortcut) {
this.setState({ toggleShowSideBarShortcut })
}
}
private async trySetUpAccessTokenWithCode() {
const search = parseURLSearch()
if ('code' in search) {
const res = await JSONRequest('https://github.com/login/oauth/access_token', {
code: search.code,
client_id: oauth.clientId,
client_secret: oauth.clientSecret,
})
const { access_token: accessToken, scope } = res
if (scope !== 'repo' || !accessToken) {
throw new Error(`Cannot resolve token response: '${JSON.stringify(res)}'`)
}
window.history.pushState({}, 'removed code', window.location.pathname.replace(/#.*$/, ''))
this.setState({ accessToken }, () => this.saveToken(''))
}
}
onInputAccessToken = (event: React.FormEvent<HTMLInputElement>) => {
const { value } = event.currentTarget
this.setState({
@ -105,7 +131,16 @@ export default class SettingsBar extends React.PureComponent<Props, State> {
}
}
saveToken = async () => {
saveToken = async (
hint: State['accessTokenHint'] = (
<span>
<a href="#" onClick={() => window.location.reload()}>
Reload
</a>{' '}
to activate!
</span>
),
) => {
const { onAccessTokenChange } = this.props
const { accessToken } = this.state
if (accessToken) {
@ -113,14 +148,7 @@ export default class SettingsBar extends React.PureComponent<Props, State> {
onAccessTokenChange(accessToken)
this.setState({
accessToken: '',
accessTokenHint: (
<span>
<a href="#" onClick={() => window.location.reload()}>
Reload
</a>{' '}
to activate!
</span>
),
accessTokenHint: hint,
})
}
}
@ -196,16 +224,31 @@ export default class SettingsBar extends React.PureComponent<Props, State> {
<div className={'gitako-settings-bar-content'}>
<div className={'shadow-shelter'} />
<div className={'gitako-settings-bar-content-section access-token'}>
<h4>Access Token</h4>
<a href={wikiLinks.createAccessToken} target="_blank">
Why & how to create it?
</a>
<br />
<h4>
Access Token
<a href={wikiLinks.createAccessToken} target="_blank">
&nbsp;(?)
</a>
</h4>
{!hasAccessToken && (
<a
href="#"
onClick={() => {
// use js here to make sure redirect_uri is latest url
const url = `https://github.com/login/oauth/authorize?client_id=${
oauth.clientId
}&scope=repo&redirect_uri=${encodeURIComponent(window.location.href)}`
window.location.href = url
}}
>
Create with OAuth (recommended)
</a>
)}
<div className={'access-token-input-control'}>
<input
className={'access-token-input form-control'}
disabled={hasAccessToken}
placeholder={hasAccessToken ? 'Your token is saved' : 'Input your token here'}
placeholder={hasAccessToken ? 'Your token is saved' : 'Or input here manually'}
value={accessToken}
onChange={this.onInputAccessToken}
onKeyPress={this.onPressAccessToken}

View file

@ -408,7 +408,7 @@
z-index: 1;
}
&-section {
padding-bottom: 10px;
padding-bottom: 16px;
}
/* inputs for access token & shortcut were too wide */

View file

@ -123,3 +123,9 @@ export default function connect<BaseP, ExtraP>(mapping: Sources<BaseP, ExtraP>)
}
}
}
export function promisifyGetState<Props, State>(
get: PreDispatch<Props, State>,
): () => Promise<State> {
return () => new Promise(resolve => get(state => resolve(state)))
}

View file

@ -11,7 +11,7 @@ import GitHubHelper, {
import configHelper from 'utils/configHelper'
import URLHelper from 'utils/URLHelper'
import keyHelper from 'utils/keyHelper'
import { MethodCreator } from 'driver/connect'
import { MethodCreator, promisifyGetState } from 'driver/connect'
import { Props } from 'components/SideBar'
import SettingsBar from 'components/SettingsBar'
@ -40,6 +40,7 @@ export type ConnectorState = {
copySnippetButton: boolean
logoContainerElement: Element | null
disabled: boolean
initializingPromise: Promise<void> | null
init: () => void
onPJAXEnd: () => void
@ -55,12 +56,24 @@ export type ConnectorState = {
}
const init: MethodCreator<Props, ConnectorState> = dispatch => async () => {
const { initializingPromise } = await promisifyGetState(dispatch.get)()
if (initializingPromise) await initializingPromise
let done: any = null // cannot use type `(() => void) | null` here
dispatch.set({
initializingPromise: new Promise(resolve => {
done = () => resolve()
}),
})
try {
if (!URLHelper.isInRepoPage()) {
dispatch.set({ disabled: true })
return
}
dispatch.set({
errorDueToAuth: false,
showSettings: false,
logoContainerElement: DOMHelper.insertLogoMountPoint(),
})
let detectedBranchName
@ -139,6 +152,8 @@ const init: MethodCreator<Props, ConnectorState> = dispatch => async () => {
DOMHelper.markGitakoReadyState()
} catch (err) {
dispatch.call(handleError, err)
} finally {
if (done) done()
}
}
@ -219,7 +234,13 @@ const onAccessTokenChange: MethodCreator<
Props,
ConnectorState,
[ConnectorState['accessToken']]
> = dispatch => accessToken => dispatch.set({ accessToken })
> = dispatch => accessToken => {
dispatch.set({ accessToken })
// reload when setting new accessToken
if (accessToken) {
dispatch.call(init)
}
}
const onShortcutChange: MethodCreator<
Props,

View file

@ -93,3 +93,38 @@ export function createStyleSheet(content: string) {
export function setStyleSheetMedia(style: HTMLStyleElement, media: string) {
style.setAttribute('media', media)
}
export function parseURLSearch(search: string = window.location.search) {
const parsed: any = {}
if (search.startsWith('?')) {
const pairs = search
.slice(1)
.split('&')
.map(rawPair => rawPair.split('=').map(raw => decodeURIComponent(raw))) // [key, value?][]
pairs.forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
if (Array.isArray(parsed[key])) parsed[key].push(value)
else parsed[key] = [parsed[key], value]
} else {
parsed[key] = value
}
})
}
return parsed
}
export async function JSONRequest(url: string, data: any, method = 'post') {
return (await fetch(url, {
method,
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
redirect: 'follow',
referrer: 'no-referrer',
body: JSON.stringify(data),
})).json()
}

View file

@ -2,6 +2,7 @@ const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const path = require('path')
const Dotenv = require('dotenv-webpack')
const srcPath = path.resolve(__dirname, 'src')
const packagesPath = path.resolve(__dirname, 'packages')
@ -18,6 +19,7 @@ const plugins = [
},
]),
new ForkTsCheckerWebpackPlugin(),
new Dotenv(),
]
const IN_PRODUCTION_MODE = process.env.NODE_ENV === 'production'
@ -27,7 +29,7 @@ if (IN_PRODUCTION_MODE) {
'process.env': {
NODE_ENV: JSON.stringify('production'),
},
})
}),
)
}

View file

@ -2197,6 +2197,25 @@ domutils@^1.7.0:
dom-serializer "0"
domelementtype "1"
dotenv-defaults@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/dotenv-defaults/-/dotenv-defaults-1.0.2.tgz#441cf5f067653fca4bbdce9dd3b803f6f84c585d"
integrity sha512-iXFvHtXl/hZPiFj++1hBg4lbKwGM+t/GlvELDnRtOFdjXyWP7mubkVr+eZGWG62kdsbulXAef6v/j6kiWc/xGA==
dependencies:
dotenv "^6.2.0"
dotenv-webpack@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-1.7.0.tgz#4384d8c57ee6f405c296278c14a9f9167856d3a1"
integrity sha512-wwNtOBW/6gLQSkb8p43y0Wts970A3xtNiG/mpwj9MLUhtPCQG6i+/DSXXoNN7fbPCU/vQ7JjwGmgOeGZSSZnsw==
dependencies:
dotenv-defaults "^1.0.2"
dotenv@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
duplexify@^3.4.2, duplexify@^3.6.0:
version "3.7.1"
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"