mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
Merge branch 'feature/oauth' into develop
This commit is contained in:
commit
76498d9ec9
10 changed files with 153 additions and 20 deletions
5
.env.example
Normal file
5
.env.example
Normal 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
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
.vscode
|
||||
.env
|
||||
node_modules
|
||||
tmp
|
||||
dist
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
(?)
|
||||
</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}
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@
|
|||
z-index: 1;
|
||||
}
|
||||
&-section {
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
/* inputs for access token & shortcut were too wide */
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
19
yarn.lock
19
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue