GitLab授权调整
This commit is contained in:
parent
80e0e3bc99
commit
12e4befa96
@ -77,6 +77,7 @@ StackEdit中文版
|
||||
- 导出HTML、PDF支持带预览主题导出(2023-02-26)
|
||||
- 支持分享文档(2023-03-30)
|
||||
- 支持ChatGPT生成内容(2023-04-10)
|
||||
- GitLab授权接口调整(2023-08-26)
|
||||
|
||||
## 国外开源版本弊端:
|
||||
- 作者已经不维护了
|
||||
@ -113,6 +114,7 @@ services:
|
||||
- GITEA_CLIENT_SECRET=【不需要支持则删掉】
|
||||
- GITEA_URL=【不需要支持则删掉】
|
||||
- GITLAB_CLIENT_ID=【不需要支持则删掉】
|
||||
- GITLAB_CLIENT_SECRET=【不需要支持则删掉】
|
||||
- GITLAB_URL=【不需要支持则删掉】
|
||||
ports:
|
||||
- 8080:8080/tcp
|
||||
@ -149,6 +151,7 @@ docker run -itd --name stackedit \
|
||||
-e GITEA_CLIENT_SECRET=【不需要支持则删掉】 \
|
||||
-e GITEA_URL=【不需要支持则删掉】 \
|
||||
-e GITLAB_CLIENT_ID=【不需要支持则删掉】 \
|
||||
-e GITLAB_CLIENT_SECRET=【不需要支持则删掉】 \
|
||||
-e GITLAB_URL=【不需要支持则删掉】 \
|
||||
mafgwo/stackedit:【docker中央仓库找到最新版本】
|
||||
|
||||
@ -163,7 +166,7 @@ docker run -itd --name stackedit \
|
||||
|
||||
- Gitea可选择性配置环境变量(未配置则在关联时前端指定,有配置则仅允许配置的应用信息):GITEA_CLIENT_ID、GITEA_CLIENT_SECRET、GITEA_URL,**[如何创建Gitea应用](./docs/部署之Gitea应用创建.md)**
|
||||
|
||||
- Gitlab可选择性配置环境变量(未配置则在关联时前端指定,有配置则仅允许配置的应用信息):GITLAB_CLIENT_ID、GITLAB_URL **如何创建Gitlab应用(待补充文档)**
|
||||
- Gitlab可选择性配置环境变量(未配置则在关联时前端指定,有配置则仅允许配置的应用信息):GITLAB_CLIENT_ID、GITLAB_CLIENT_SECRET、GITLAB_URL **如何创建Gitlab应用(待补充文档)**
|
||||
|
||||
(特别说明:自建的Gitea、Gitlab要能接入stackedit必须支持跨域)
|
||||
|
||||
|
@ -12,6 +12,7 @@ module.exports = merge(prodEnv, {
|
||||
// GITEA_CLIENT_ID: '"fe30f8f9-b1e8-4531-8f72-c1a5d3912805"',
|
||||
// GITEA_CLIENT_SECRET: '"lus7oMnb3H6M1hsChndphArE20Txr7erwJLf7SDBQWTw"',
|
||||
// GITEA_URL: '"https://gitea.test.com"',
|
||||
// GITLAB_CLIENT_ID: '"33e01128c27fe75df3e5b35218d710c7df280e6ee9c90b6ca27ac9d9fdfb92f7"',
|
||||
// GITLAB_URL: '"http://gitlab.qicoder.com"',
|
||||
GITLAB_CLIENT_ID: '"074cd5103c62dea0f479dac861039656ac80935e304c8113a02cc64c629496ae"',
|
||||
GITLAB_CLIENT_SECRET: '"6f406f24216b686d55d28313dec1913c2a8e599afdb08380d5e8ce838e16e41e"',
|
||||
GITLAB_URL: '"http://gitlab.qicoder.com"',
|
||||
})
|
@ -15,6 +15,7 @@ const giteaClientId = process.env.GITEA_CLIENT_ID;
|
||||
const giteaClientSecret = process.env.GITEA_CLIENT_SECRET;
|
||||
const giteaUrl = process.env.GITEA_URL;
|
||||
const gitlabClientId = process.env.GITLAB_CLIENT_ID;
|
||||
const gitlabClientSecret = process.env.GITLAB_CLIENT_SECRET;
|
||||
const gitlabUrl = process.env.GITLAB_URL;
|
||||
|
||||
exports.values = {
|
||||
@ -33,6 +34,9 @@ exports.values = {
|
||||
giteaClientId,
|
||||
giteaClientSecret,
|
||||
giteaUrl,
|
||||
gitlabClientId,
|
||||
gitlabClientSecret,
|
||||
gitlabUrl,
|
||||
};
|
||||
|
||||
exports.publicValues = {
|
||||
|
@ -1,4 +1,3 @@
|
||||
const qs = require('qs');
|
||||
const request = require('request');
|
||||
const conf = require('./conf');
|
||||
|
||||
|
40
server/gitlab.js
Normal file
40
server/gitlab.js
Normal file
@ -0,0 +1,40 @@
|
||||
const request = require('request');
|
||||
const conf = require('./conf');
|
||||
|
||||
function gitlabToken(queryParam) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: `${conf.values.gitlabUrl}/oauth/token`,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
json: true,
|
||||
qs: {
|
||||
...queryParam,
|
||||
client_id: conf.values.gitlabClientId,
|
||||
client_secret: conf.values.gitlabClientSecret,
|
||||
},
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
const token = body.access_token;
|
||||
if (token) {
|
||||
resolve(body);
|
||||
} else {
|
||||
reject(res.statusCode + ',body:' + JSON.stringify(body));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.gitlabToken = (req, res) => {
|
||||
gitlabToken(req.query)
|
||||
.then(
|
||||
tokenBody => res.send(tokenBody),
|
||||
err => res
|
||||
.status(400)
|
||||
.send(err ? err.message || err.toString() : 'bad_code'),
|
||||
);
|
||||
};
|
@ -5,6 +5,7 @@ const path = require('path');
|
||||
const github = require('./github');
|
||||
const gitee = require('./gitee');
|
||||
const gitea = require('./gitea');
|
||||
const gitlab = require('./gitlab');
|
||||
const pdf = require('./pdf');
|
||||
const pandoc = require('./pandoc');
|
||||
const conf = require('./conf');
|
||||
@ -28,6 +29,7 @@ module.exports = (app) => {
|
||||
app.get('/oauth2/githubToken', github.githubToken);
|
||||
app.get('/oauth2/giteeToken', gitee.giteeToken);
|
||||
app.get('/oauth2/giteaToken', gitea.giteaToken);
|
||||
app.get('/oauth2/gitlabToken', gitlab.gitlabToken);
|
||||
app.get('/conf', (req, res) => res.send(conf.publicValues));
|
||||
app.post('/pdfExport', pdf.generate);
|
||||
app.post('/pandocExport', pandoc.generate);
|
||||
|
@ -263,8 +263,8 @@ export default {
|
||||
},
|
||||
async addGitlabAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGiteaAccount() {
|
||||
|
@ -239,8 +239,8 @@ export default {
|
||||
},
|
||||
async addGitlabAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGiteaAccount() {
|
||||
|
@ -85,8 +85,8 @@ export default {
|
||||
},
|
||||
async addGitlabWorkspace() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
const token = await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
const token = await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
store.dispatch('modal/open', {
|
||||
type: 'gitlabWorkspace',
|
||||
token,
|
||||
|
@ -248,8 +248,8 @@ export default {
|
||||
},
|
||||
async addGitlabAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGiteaAccount() {
|
||||
|
@ -18,6 +18,9 @@
|
||||
</form-entry>
|
||||
<form-entry label="Application ID" error="applicationId">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="applicationId" @keydown.enter="resolve()">
|
||||
</form-entry>
|
||||
<form-entry label="Application Secret" error="applicationSecret">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="applicationSecret" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
您必须使用重定向url <b>{{redirectUrl}}</b>配置OAuth2应用程序
|
||||
</div>
|
||||
@ -47,6 +50,7 @@ export default modalTemplate({
|
||||
computedLocalSettings: {
|
||||
serverUrl: 'gitlabServerUrl',
|
||||
applicationId: 'gitlabApplicationId',
|
||||
applicationSecret: 'gitlabApplicationSecret',
|
||||
},
|
||||
computed: {
|
||||
httpAppUrl() {
|
||||
@ -78,7 +82,10 @@ export default modalTemplate({
|
||||
if (!this.applicationId) {
|
||||
this.setError('applicationId');
|
||||
}
|
||||
if (serverUrl && this.applicationId) {
|
||||
if (!this.applicationSecret) {
|
||||
this.setError('applicationSecret');
|
||||
}
|
||||
if (serverUrl && this.applicationId && this.applicationSecret) {
|
||||
const parsedUrl = serverUrl.match(/^(http[s]?:\/\/[^/]+)/);
|
||||
if (!parsedUrl) {
|
||||
this.setError('serverUrl');
|
||||
@ -86,6 +93,7 @@ export default modalTemplate({
|
||||
this.config.resolve({
|
||||
serverUrl: parsedUrl[1],
|
||||
applicationId: this.applicationId,
|
||||
applicationSecret: this.applicationSecret,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ export default () => ({
|
||||
giteePublishTemplate: 'jekyllSite',
|
||||
gitlabServerUrl: '',
|
||||
gitlabApplicationId: '',
|
||||
gitlabApplicationSecret: '',
|
||||
gitlabProjectUrl: '',
|
||||
gitlabWorkspaceProjectUrl: '',
|
||||
gitlabPublishTemplate: 'plainText',
|
||||
|
@ -75,11 +75,11 @@ export default new Provider({
|
||||
const sub = workspace ? workspace.sub : utils.queryParams.sub;
|
||||
let token = store.getters['data/gitlabTokensBySub'][sub];
|
||||
if (!token) {
|
||||
const { applicationId } = await store.dispatch('modal/open', {
|
||||
const { applicationId, applicationSecret } = await store.dispatch('modal/open', {
|
||||
type: 'gitlabAccount',
|
||||
forceServerUrl: serverUrl,
|
||||
});
|
||||
token = await gitlabHelper.addAccount(serverUrl, applicationId, sub);
|
||||
token = await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret, sub);
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
|
@ -3,6 +3,9 @@ import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
import userSvc from '../../userSvc';
|
||||
import badgeSvc from '../../badgeSvc';
|
||||
import constants from '../../../data/constants';
|
||||
|
||||
const tokenExpirationMargin = 5 * 60 * 1000;
|
||||
|
||||
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
|
||||
...options,
|
||||
@ -50,34 +53,90 @@ export default {
|
||||
/**
|
||||
* https://docs.gitlab.com/ee/api/oauth2.html
|
||||
*/
|
||||
async startOauth2(serverUrl, applicationId, sub = null, silent = false) {
|
||||
async startOauth2(
|
||||
serverUrl, applicationId, applicationSecret,
|
||||
sub = null, silent = false, refreshToken,
|
||||
) {
|
||||
let apiUrl = serverUrl;
|
||||
let clientId = applicationId;
|
||||
// 获取gitea配置的参数
|
||||
let useServerConf = false;
|
||||
// 获取gitlab配置的参数
|
||||
await networkSvc.getServerConf();
|
||||
const confClientId = store.getters['data/serverConf'].gitlabClientId;
|
||||
const confServerUrl = store.getters['data/serverConf'].gitlabUrl;
|
||||
// 存在gitea配置则使用后端配置
|
||||
// 存在gitlab配置则使用后端配置
|
||||
if (confClientId && confServerUrl) {
|
||||
apiUrl = confServerUrl;
|
||||
clientId = confClientId;
|
||||
useServerConf = true;
|
||||
}
|
||||
let tokenBody;
|
||||
if (!silent) {
|
||||
// Get an OAuth2 code
|
||||
const { code } = await networkSvc.startOauth2(
|
||||
`${apiUrl}/oauth/authorize`,
|
||||
{
|
||||
client_id: clientId,
|
||||
response_type: 'code',
|
||||
redirect_uri: constants.oauth2RedirectUri,
|
||||
},
|
||||
silent,
|
||||
);
|
||||
if (useServerConf) {
|
||||
tokenBody = (await networkSvc.request({
|
||||
method: 'GET',
|
||||
url: 'oauth2/gitlabToken',
|
||||
params: {
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: constants.oauth2RedirectUri,
|
||||
},
|
||||
})).body;
|
||||
} else {
|
||||
// Exchange code with token
|
||||
tokenBody = (await networkSvc.request({
|
||||
method: 'POST',
|
||||
url: `${apiUrl}/oauth/token`,
|
||||
params: {
|
||||
client_id: clientId,
|
||||
client_secret: applicationSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: constants.oauth2RedirectUri,
|
||||
},
|
||||
})).body;
|
||||
}
|
||||
} else if (useServerConf) {
|
||||
tokenBody = (await networkSvc.request({
|
||||
method: 'GET',
|
||||
url: 'oauth2/gitlabToken',
|
||||
params: {
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
redirect_uri: constants.oauth2RedirectUri,
|
||||
},
|
||||
})).body;
|
||||
} else {
|
||||
// Exchange refreshToken with token
|
||||
tokenBody = (await networkSvc.request({
|
||||
method: 'POST',
|
||||
url: `${apiUrl}/oauth/token`,
|
||||
body: {
|
||||
client_id: clientId,
|
||||
client_secret: applicationSecret,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
redirect_uri: constants.oauth2RedirectUri,
|
||||
},
|
||||
})).body;
|
||||
}
|
||||
// Get an OAuth2 code
|
||||
const { accessToken } = await networkSvc.startOauth2(
|
||||
`${apiUrl}/oauth/authorize`,
|
||||
{
|
||||
client_id: clientId,
|
||||
response_type: 'token',
|
||||
scope: 'api',
|
||||
},
|
||||
silent,
|
||||
);
|
||||
|
||||
const accessToken = tokenBody.access_token;
|
||||
// Call the user info endpoint
|
||||
const user = await request({ accessToken, serverUrl }, {
|
||||
const user = await request({ accessToken, serverUrl: apiUrl }, {
|
||||
url: 'user',
|
||||
});
|
||||
const uniqueSub = `${serverUrl}/${user.id}`;
|
||||
const uniqueSub = `${apiUrl}/${user.id}`;
|
||||
userSvc.addUserInfo({
|
||||
id: `${subPrefix}:${uniqueSub}`,
|
||||
name: user.username,
|
||||
@ -89,11 +148,17 @@ export default {
|
||||
throw new Error('GitLab account ID not expected.');
|
||||
}
|
||||
|
||||
const oldToken = store.getters['data/gitlabTokensBySub'][uniqueSub];
|
||||
// Build token object including scopes and sub
|
||||
const token = {
|
||||
accessToken,
|
||||
name: user.username,
|
||||
serverUrl,
|
||||
applicationId: clientId,
|
||||
applicationSecret,
|
||||
imgStorages: oldToken && oldToken.imgStorages,
|
||||
refreshToken: tokenBody.refresh_token,
|
||||
expiresOn: Date.now() + ((tokenBody.expires_in || 7200) * 1000),
|
||||
serverUrl: apiUrl,
|
||||
sub: uniqueSub,
|
||||
};
|
||||
|
||||
@ -101,12 +166,58 @@ export default {
|
||||
store.dispatch('data/addGitlabToken', token);
|
||||
return token;
|
||||
},
|
||||
async addAccount(serverUrl, applicationId, sub = null) {
|
||||
const token = await this.startOauth2(serverUrl, applicationId, sub);
|
||||
async addAccount(serverUrl, applicationId, applicationSecret, sub = null) {
|
||||
const token = await this.startOauth2(serverUrl, applicationId, applicationSecret, sub);
|
||||
badgeSvc.addBadge('addGitLabAccount');
|
||||
return token;
|
||||
},
|
||||
// 刷新token
|
||||
async refreshToken(token) {
|
||||
const {
|
||||
serverUrl,
|
||||
applicationId,
|
||||
applicationSecret,
|
||||
sub,
|
||||
} = token;
|
||||
const lastToken = store.getters['data/gitlabTokensBySub'][sub];
|
||||
// 兼容旧的没有过期时间
|
||||
if (!lastToken.expiresOn || !lastToken.refreshToken) {
|
||||
await store.dispatch('modal/open', {
|
||||
type: 'providerRedirection',
|
||||
name: 'Gitlab',
|
||||
});
|
||||
return this.startOauth2(serverUrl, applicationId, applicationSecret, sub);
|
||||
}
|
||||
// lastToken is not expired
|
||||
if (lastToken.expiresOn > Date.now() + tokenExpirationMargin) {
|
||||
return lastToken;
|
||||
}
|
||||
|
||||
// existing token is about to expire.
|
||||
// Try to get a new token in background
|
||||
try {
|
||||
return await this.startOauth2(
|
||||
serverUrl, applicationId, applicationSecret,
|
||||
sub, true, lastToken.refreshToken,
|
||||
);
|
||||
} catch (err) {
|
||||
// If it fails try to popup a window
|
||||
if (store.state.offline) {
|
||||
throw err;
|
||||
}
|
||||
await store.dispatch('modal/open', {
|
||||
type: 'providerRedirection',
|
||||
name: 'Gitlab',
|
||||
});
|
||||
return this.startOauth2(serverUrl, applicationId, applicationSecret, sub);
|
||||
}
|
||||
},
|
||||
// 带刷新token
|
||||
async requestWithRefreshToken(token, options) {
|
||||
const refreshedToken = await this.refreshToken(token);
|
||||
const result = await request(refreshedToken, options);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* https://docs.gitlab.com/ee/api/projects.html#get-single-project
|
||||
*/
|
||||
@ -114,8 +225,7 @@ export default {
|
||||
if (projectId) {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
const project = await request(token, {
|
||||
const project = await this.requestWithRefreshToken(token, {
|
||||
url: `projects/${encodeURIComponent(projectPath)}`,
|
||||
});
|
||||
return project.id;
|
||||
@ -129,7 +239,7 @@ export default {
|
||||
projectId,
|
||||
branch,
|
||||
}) {
|
||||
return request(token, {
|
||||
return this.requestWithRefreshToken(token, {
|
||||
url: `projects/${encodeURIComponent(projectId)}/repository/tree`,
|
||||
params: {
|
||||
ref: branch,
|
||||
@ -148,7 +258,7 @@ export default {
|
||||
branch,
|
||||
path,
|
||||
}) {
|
||||
return request(token, {
|
||||
return this.requestWithRefreshToken(token, {
|
||||
url: `projects/${encodeURIComponent(projectId)}/repository/commits`,
|
||||
params: {
|
||||
ref_name: branch,
|
||||
@ -175,7 +285,7 @@ export default {
|
||||
if (isImg && typeof content !== 'string') {
|
||||
uploadContent = await utils.encodeFiletoBase64(content);
|
||||
}
|
||||
return request(token, {
|
||||
return this.requestWithRefreshToken(token, {
|
||||
method: sha ? 'PUT' : 'POST',
|
||||
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
|
||||
body: {
|
||||
@ -198,7 +308,7 @@ export default {
|
||||
path,
|
||||
sha,
|
||||
}) {
|
||||
return request(token, {
|
||||
return this.requestWithRefreshToken(token, {
|
||||
method: 'DELETE',
|
||||
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
|
||||
body: {
|
||||
@ -219,7 +329,7 @@ export default {
|
||||
path,
|
||||
isImg,
|
||||
}) {
|
||||
const res = await request(token, {
|
||||
const res = await this.requestWithRefreshToken(token, {
|
||||
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
|
||||
params: { ref: branch },
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user