diff --git a/packages/admin/package.json b/packages/admin/package.json index 2bff7de6882..836f4a821a6 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -45,9 +45,11 @@ "@babel/preset-react": "^7.12.5", "@reach/router": "^1.3.4", "@rematch/core": "^1.4.0", + "@svgr/webpack": "^5.5.0", "babel-loader": "^8.2.1", "classnames": "^2.2.6", "css-loader": "^5.0.1", + "file-loader": "^6.2.0", "html-webpack-plugin": "^4.5.0", "marked": "^1.2.3", "md5": "^2.3.0", diff --git a/packages/admin/src/components/icon/github.svg b/packages/admin/src/components/icon/github.svg new file mode 100644 index 00000000000..95a1743dd5e --- /dev/null +++ b/packages/admin/src/components/icon/github.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/admin/src/pages/profile/index.js b/packages/admin/src/pages/profile/index.js index 6e27edad149..ad18ac5bca7 100644 --- a/packages/admin/src/pages/profile/index.js +++ b/packages/admin/src/pages/profile/index.js @@ -1,8 +1,11 @@ import React, { useState } from 'react'; +import cls from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import Header from '../../components/Header'; import { updateProfile } from '../../services/user'; +import { ReactComponent as GithubIcon } from '../../components/icon/github.svg'; + export default function() { const [isPasswordUpdating, setPasswordUpdating] = useState(false); const [isProfileUpdating, setProfileUpdating] = useState(false); @@ -41,6 +44,13 @@ export default function() { setPasswordUpdating(false); } + + let baseUrl = 'http://localhost:3000/'; globalThis.serverURL; + if(!baseUrl) { + const match = location.pathname.match(/(.*?\/)ui/); + baseUrl = match ? match[1] : '/'; + } + return ( <>
@@ -96,6 +106,25 @@ export default function() {
+
+

账号绑定

+
+
+ + + +
+ + + +
+
+
+
+

密码修改

diff --git a/packages/admin/src/style/custom.css b/packages/admin/src/style/custom.css index 55a453e1093..838cbe79b12 100644 --- a/packages/admin/src/style/custom.css +++ b/packages/admin/src/style/custom.css @@ -4,4 +4,32 @@ html, body { div[tabindex="-1"] { height: 100%; +} + +.account-item { + display: inline-block; + position: relative; +} + +.account-item .account-unbind svg { + background: #FFF; + border: 1px solid #999; + border-radius: 50%; + position: absolute; + top: -3px; + right: -3px; + display: none; + cursor: pointer; +} +.account-item:hover .account-unbind svg { + display: block; +} + + +.account-item.github path { + fill: grey; +} +.account-item.github:hover path, +.account-item.github.bind path { + fill: #1B1F23; } \ No newline at end of file diff --git a/packages/admin/src/utils/request.js b/packages/admin/src/utils/request.js index 3d2cf933a79..e1732212744 100644 --- a/packages/admin/src/utils/request.js +++ b/packages/admin/src/utils/request.js @@ -21,7 +21,7 @@ export default async function request(url, opts = {}) { opts.headers.Authorization = `Bearer ${token}`; } - let baseUrl = globalThis.serverURL; + let baseUrl = 'https://imnerd.vercel.app/'; //globalThis.serverURL; if(!baseUrl) { const match = location.pathname.match(/(.*?\/)ui/); baseUrl = match ? match[1] : '/'; diff --git a/packages/admin/webpack.config.js b/packages/admin/webpack.config.js index d89d95bac80..fb73061c825 100644 --- a/packages/admin/webpack.config.js +++ b/packages/admin/webpack.config.js @@ -21,6 +21,10 @@ module.exports = { 'css-loader' ] }, + { + test: /\.svg$/, + use: ['@svgr/webpack', 'file-loader'], + }, { test: /\.(png|jpe?g|gif)$/i, use: [ diff --git a/packages/server/src/config/middleware.js b/packages/server/src/config/middleware.js index 6a556cd5402..37f297077fa 100644 --- a/packages/server/src/config/middleware.js +++ b/packages/server/src/config/middleware.js @@ -29,6 +29,10 @@ module.exports = [ if(/favicon.ico$/.test(ctx.url)) { return; } + if (think.isPrevent(err)) { + return false; + } + console.error(err); } } diff --git a/packages/server/src/controller/oauth.js b/packages/server/src/controller/oauth.js new file mode 100644 index 00000000000..f9edd32e13b --- /dev/null +++ b/packages/server/src/controller/oauth.js @@ -0,0 +1,7 @@ +module.exports = class extends think.Controller { + async githubAction() { + const instance = this.service('auth/github', this); + const socialInfo = await instance.getUserInfo(); + return this.success(socialInfo); + } +}; diff --git a/packages/server/src/extend/controller.js b/packages/server/src/extend/controller.js new file mode 100644 index 00000000000..b5b0ce183a3 --- /dev/null +++ b/packages/server/src/extend/controller.js @@ -0,0 +1,10 @@ +module.exports = { + success(...args) { + this.ctx.success(...args); + return think.prevent(); + }, + fail(...args) { + this.ctx.fail(...args); + return think.prevent(); + } +}; \ No newline at end of file diff --git a/packages/server/src/extend/think.js b/packages/server/src/extend/think.js new file mode 100644 index 00000000000..a132485a287 --- /dev/null +++ b/packages/server/src/extend/think.js @@ -0,0 +1,9 @@ +const preventMessage = 'PREVENT_NEXT_PROCESS'; +module.exports = { + prevent() { + throw new Error(preventMessage); + }, + isPrevent(err) { + return think.isError(err) && err.message === preventMessage; + } +}; \ No newline at end of file diff --git a/packages/server/src/logic/oauth.js b/packages/server/src/logic/oauth.js new file mode 100644 index 00000000000..6cd53dd91ff --- /dev/null +++ b/packages/server/src/logic/oauth.js @@ -0,0 +1,4 @@ +const Base = require('./base'); + +module.exports = class extends Base { +} \ No newline at end of file diff --git a/packages/server/src/service/auth/base.js b/packages/server/src/service/auth/base.js new file mode 100644 index 00000000000..d7e6b28361b --- /dev/null +++ b/packages/server/src/service/auth/base.js @@ -0,0 +1,27 @@ +const { parse } = require('url'); + +module.exports = class extends think.Service { + constructor(app) { + super(app); + this.app = app; + } + + getCompleteUrl(url = '') { + if (url.slice(0, 4) === 'http') { + try { + const { host } = parse(url); + if (this.app.host.toLowerCase() !== host.toLowerCase()) { + throw new Error(403); + } + return url; + } catch (e) { + // ignore error + } + } + const protocol = this.app.header('x-forwarded-proto') || 'http'; + if (!/^\//.test(url)) { + url = '/' + url; + } + return protocol + '://' + this.app.ctx.host + url; + } +} \ No newline at end of file diff --git a/packages/server/src/service/auth/github.js b/packages/server/src/service/auth/github.js new file mode 100644 index 00000000000..e32e494a562 --- /dev/null +++ b/packages/server/src/service/auth/github.js @@ -0,0 +1,74 @@ +const qs = require('querystring'); +const request = require('request-promise-native'); +const Base = require('./base'); + +const OAUTH_URL = 'https://github.com/login/oauth/authorize'; +const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'; +const USER_INFO_URL = 'https://api.github.com/user'; +const {GITHUB_ID, GITHUB_SECRET} = process.env; +module.exports = class extends Base { + getAuthUrl(opts) { + const params = { + client_id: GITHUB_ID, + redirect_uri: opts.rdUrl, + scope: 'user' + }; + return OAUTH_URL + '?' + qs.stringify(params); + } + + async getAccessToken(opts) { + const params = { + client_id: GITHUB_ID, + client_secret: GITHUB_SECRET, + code: opts.code + }; + + const body = await request.post({ + url: ACCESS_TOKEN_URL, + headers: {'Accept': 'application/json'}, + form: params, + json: true + }); + return body; + } + + async getUserInfoByToken(opts) { + const userInfo = await request.get({ + url: USER_INFO_URL, + headers: { + 'User-Agent': '@waline', + 'Authorization': 'token ' + opts.access_token + }, + json: true + }); + + return { + id: userInfo.login, + name: userInfo.name, + desc: userInfo.bio, + avatar: userInfo.avatar_url + }; + } + + async redirect() { + const {app} = this; + const {type, rdurl} = app.get(); + const rdUrlAfterLogin = this.getCompleteUrl(rdurl); + + const params = { rdurl: rdUrlAfterLogin, type }; + const signinUrl = this.getCompleteUrl('/oauth') + '?' + qs.stringify(params); + const AUTH_URL = this.getAuthUrl({rdUrl: signinUrl}); + app.redirect(AUTH_URL); + return app.success(); + } + + async getUserInfo() { + const {code} = this.app.get(); + if (!code) { + return this.redirect(); + } + + const accessTokenInfo = await this.getAccessToken({code}); + return this.getUserInfoByToken(accessTokenInfo); + } +};