GitLab授权调整

This commit is contained in:
xiaoqi.cxq 2023-08-26 01:15:54 +08:00
parent 80e0e3bc99
commit 12e4befa96
14 changed files with 208 additions and 40 deletions

View File

@ -77,6 +77,7 @@ StackEdit中文版
- 导出HTML、PDF支持带预览主题导出2023-02-26 - 导出HTML、PDF支持带预览主题导出2023-02-26
- 支持分享文档2023-03-30 - 支持分享文档2023-03-30
- 支持ChatGPT生成内容2023-04-10 - 支持ChatGPT生成内容2023-04-10
- GitLab授权接口调整2023-08-26
## 国外开源版本弊端: ## 国外开源版本弊端:
- 作者已经不维护了 - 作者已经不维护了
@ -113,6 +114,7 @@ services:
- GITEA_CLIENT_SECRET=【不需要支持则删掉】 - GITEA_CLIENT_SECRET=【不需要支持则删掉】
- GITEA_URL=【不需要支持则删掉】 - GITEA_URL=【不需要支持则删掉】
- GITLAB_CLIENT_ID=【不需要支持则删掉】 - GITLAB_CLIENT_ID=【不需要支持则删掉】
- GITLAB_CLIENT_SECRET=【不需要支持则删掉】
- GITLAB_URL=【不需要支持则删掉】 - GITLAB_URL=【不需要支持则删掉】
ports: ports:
- 8080:8080/tcp - 8080:8080/tcp
@ -149,6 +151,7 @@ docker run -itd --name stackedit \
-e GITEA_CLIENT_SECRET=【不需要支持则删掉】 \ -e GITEA_CLIENT_SECRET=【不需要支持则删掉】 \
-e GITEA_URL=【不需要支持则删掉】 \ -e GITEA_URL=【不需要支持则删掉】 \
-e GITLAB_CLIENT_ID=【不需要支持则删掉】 \ -e GITLAB_CLIENT_ID=【不需要支持则删掉】 \
-e GITLAB_CLIENT_SECRET=【不需要支持则删掉】 \
-e GITLAB_URL=【不需要支持则删掉】 \ -e GITLAB_URL=【不需要支持则删掉】 \
mafgwo/stackedit:【docker中央仓库找到最新版本】 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)** - 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必须支持跨域 特别说明自建的Gitea、Gitlab要能接入stackedit必须支持跨域

View File

@ -12,6 +12,7 @@ module.exports = merge(prodEnv, {
// GITEA_CLIENT_ID: '"fe30f8f9-b1e8-4531-8f72-c1a5d3912805"', // GITEA_CLIENT_ID: '"fe30f8f9-b1e8-4531-8f72-c1a5d3912805"',
// GITEA_CLIENT_SECRET: '"lus7oMnb3H6M1hsChndphArE20Txr7erwJLf7SDBQWTw"', // GITEA_CLIENT_SECRET: '"lus7oMnb3H6M1hsChndphArE20Txr7erwJLf7SDBQWTw"',
// GITEA_URL: '"https://gitea.test.com"', // GITEA_URL: '"https://gitea.test.com"',
// GITLAB_CLIENT_ID: '"33e01128c27fe75df3e5b35218d710c7df280e6ee9c90b6ca27ac9d9fdfb92f7"', GITLAB_CLIENT_ID: '"074cd5103c62dea0f479dac861039656ac80935e304c8113a02cc64c629496ae"',
// GITLAB_URL: '"http://gitlab.qicoder.com"', GITLAB_CLIENT_SECRET: '"6f406f24216b686d55d28313dec1913c2a8e599afdb08380d5e8ce838e16e41e"',
GITLAB_URL: '"http://gitlab.qicoder.com"',
}) })

View File

@ -15,6 +15,7 @@ const giteaClientId = process.env.GITEA_CLIENT_ID;
const giteaClientSecret = process.env.GITEA_CLIENT_SECRET; const giteaClientSecret = process.env.GITEA_CLIENT_SECRET;
const giteaUrl = process.env.GITEA_URL; const giteaUrl = process.env.GITEA_URL;
const gitlabClientId = process.env.GITLAB_CLIENT_ID; const gitlabClientId = process.env.GITLAB_CLIENT_ID;
const gitlabClientSecret = process.env.GITLAB_CLIENT_SECRET;
const gitlabUrl = process.env.GITLAB_URL; const gitlabUrl = process.env.GITLAB_URL;
exports.values = { exports.values = {
@ -33,6 +34,9 @@ exports.values = {
giteaClientId, giteaClientId,
giteaClientSecret, giteaClientSecret,
giteaUrl, giteaUrl,
gitlabClientId,
gitlabClientSecret,
gitlabUrl,
}; };
exports.publicValues = { exports.publicValues = {

View File

@ -1,4 +1,3 @@
const qs = require('qs');
const request = require('request'); const request = require('request');
const conf = require('./conf'); const conf = require('./conf');

40
server/gitlab.js Normal file
View 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'),
);
};

View File

@ -5,6 +5,7 @@ const path = require('path');
const github = require('./github'); const github = require('./github');
const gitee = require('./gitee'); const gitee = require('./gitee');
const gitea = require('./gitea'); const gitea = require('./gitea');
const gitlab = require('./gitlab');
const pdf = require('./pdf'); const pdf = require('./pdf');
const pandoc = require('./pandoc'); const pandoc = require('./pandoc');
const conf = require('./conf'); const conf = require('./conf');
@ -28,6 +29,7 @@ module.exports = (app) => {
app.get('/oauth2/githubToken', github.githubToken); app.get('/oauth2/githubToken', github.githubToken);
app.get('/oauth2/giteeToken', gitee.giteeToken); app.get('/oauth2/giteeToken', gitee.giteeToken);
app.get('/oauth2/giteaToken', gitea.giteaToken); app.get('/oauth2/giteaToken', gitea.giteaToken);
app.get('/oauth2/gitlabToken', gitlab.gitlabToken);
app.get('/conf', (req, res) => res.send(conf.publicValues)); app.get('/conf', (req, res) => res.send(conf.publicValues));
app.post('/pdfExport', pdf.generate); app.post('/pdfExport', pdf.generate);
app.post('/pandocExport', pandoc.generate); app.post('/pandocExport', pandoc.generate);

View File

@ -263,8 +263,8 @@ export default {
}, },
async addGitlabAccount() { async addGitlabAccount() {
try { try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' }); const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
await gitlabHelper.addAccount(serverUrl, applicationId); await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async addGiteaAccount() { async addGiteaAccount() {

View File

@ -239,8 +239,8 @@ export default {
}, },
async addGitlabAccount() { async addGitlabAccount() {
try { try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' }); const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
await gitlabHelper.addAccount(serverUrl, applicationId); await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async addGiteaAccount() { async addGiteaAccount() {

View File

@ -85,8 +85,8 @@ export default {
}, },
async addGitlabWorkspace() { async addGitlabWorkspace() {
try { try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' }); const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
const token = await gitlabHelper.addAccount(serverUrl, applicationId); const token = await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
store.dispatch('modal/open', { store.dispatch('modal/open', {
type: 'gitlabWorkspace', type: 'gitlabWorkspace',
token, token,

View File

@ -248,8 +248,8 @@ export default {
}, },
async addGitlabAccount() { async addGitlabAccount() {
try { try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' }); const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
await gitlabHelper.addAccount(serverUrl, applicationId); await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async addGiteaAccount() { async addGiteaAccount() {

View File

@ -18,6 +18,9 @@
</form-entry> </form-entry>
<form-entry label="Application ID" error="applicationId"> <form-entry label="Application ID" error="applicationId">
<input slot="field" class="textfield" type="text" v-model.trim="applicationId" @keydown.enter="resolve()"> <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"> <div class="form-entry__info">
您必须使用重定向url <b>{{redirectUrl}}</b>配置OAuth2应用程序 您必须使用重定向url <b>{{redirectUrl}}</b>配置OAuth2应用程序
</div> </div>
@ -47,6 +50,7 @@ export default modalTemplate({
computedLocalSettings: { computedLocalSettings: {
serverUrl: 'gitlabServerUrl', serverUrl: 'gitlabServerUrl',
applicationId: 'gitlabApplicationId', applicationId: 'gitlabApplicationId',
applicationSecret: 'gitlabApplicationSecret',
}, },
computed: { computed: {
httpAppUrl() { httpAppUrl() {
@ -78,7 +82,10 @@ export default modalTemplate({
if (!this.applicationId) { if (!this.applicationId) {
this.setError('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]?:\/\/[^/]+)/); const parsedUrl = serverUrl.match(/^(http[s]?:\/\/[^/]+)/);
if (!parsedUrl) { if (!parsedUrl) {
this.setError('serverUrl'); this.setError('serverUrl');
@ -86,6 +93,7 @@ export default modalTemplate({
this.config.resolve({ this.config.resolve({
serverUrl: parsedUrl[1], serverUrl: parsedUrl[1],
applicationId: this.applicationId, applicationId: this.applicationId,
applicationSecret: this.applicationSecret,
}); });
} }
} }

View File

@ -24,6 +24,7 @@ export default () => ({
giteePublishTemplate: 'jekyllSite', giteePublishTemplate: 'jekyllSite',
gitlabServerUrl: '', gitlabServerUrl: '',
gitlabApplicationId: '', gitlabApplicationId: '',
gitlabApplicationSecret: '',
gitlabProjectUrl: '', gitlabProjectUrl: '',
gitlabWorkspaceProjectUrl: '', gitlabWorkspaceProjectUrl: '',
gitlabPublishTemplate: 'plainText', gitlabPublishTemplate: 'plainText',

View File

@ -75,11 +75,11 @@ export default new Provider({
const sub = workspace ? workspace.sub : utils.queryParams.sub; const sub = workspace ? workspace.sub : utils.queryParams.sub;
let token = store.getters['data/gitlabTokensBySub'][sub]; let token = store.getters['data/gitlabTokensBySub'][sub];
if (!token) { if (!token) {
const { applicationId } = await store.dispatch('modal/open', { const { applicationId, applicationSecret } = await store.dispatch('modal/open', {
type: 'gitlabAccount', type: 'gitlabAccount',
forceServerUrl: serverUrl, forceServerUrl: serverUrl,
}); });
token = await gitlabHelper.addAccount(serverUrl, applicationId, sub); token = await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret, sub);
} }
if (!workspace) { if (!workspace) {

View File

@ -3,6 +3,9 @@ import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import userSvc from '../../userSvc'; import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc'; import badgeSvc from '../../badgeSvc';
import constants from '../../../data/constants';
const tokenExpirationMargin = 5 * 60 * 1000;
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({ const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
...options, ...options,
@ -50,34 +53,90 @@ export default {
/** /**
* https://docs.gitlab.com/ee/api/oauth2.html * 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 apiUrl = serverUrl;
let clientId = applicationId; let clientId = applicationId;
// 获取gitea配置的参数 let useServerConf = false;
// 获取gitlab配置的参数
await networkSvc.getServerConf(); await networkSvc.getServerConf();
const confClientId = store.getters['data/serverConf'].gitlabClientId; const confClientId = store.getters['data/serverConf'].gitlabClientId;
const confServerUrl = store.getters['data/serverConf'].gitlabUrl; const confServerUrl = store.getters['data/serverConf'].gitlabUrl;
// 存在gitea配置则使用后端配置 // 存在gitlab配置则使用后端配置
if (confClientId && confServerUrl) { if (confClientId && confServerUrl) {
apiUrl = confServerUrl; apiUrl = confServerUrl;
clientId = confClientId; 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 // Call the user info endpoint
const user = await request({ accessToken, serverUrl }, { const user = await request({ accessToken, serverUrl: apiUrl }, {
url: 'user', url: 'user',
}); });
const uniqueSub = `${serverUrl}/${user.id}`; const uniqueSub = `${apiUrl}/${user.id}`;
userSvc.addUserInfo({ userSvc.addUserInfo({
id: `${subPrefix}:${uniqueSub}`, id: `${subPrefix}:${uniqueSub}`,
name: user.username, name: user.username,
@ -89,11 +148,17 @@ export default {
throw new Error('GitLab account ID not expected.'); throw new Error('GitLab account ID not expected.');
} }
const oldToken = store.getters['data/gitlabTokensBySub'][uniqueSub];
// Build token object including scopes and sub // Build token object including scopes and sub
const token = { const token = {
accessToken, accessToken,
name: user.username, 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, sub: uniqueSub,
}; };
@ -101,12 +166,58 @@ export default {
store.dispatch('data/addGitlabToken', token); store.dispatch('data/addGitlabToken', token);
return token; return token;
}, },
async addAccount(serverUrl, applicationId, sub = null) { async addAccount(serverUrl, applicationId, applicationSecret, sub = null) {
const token = await this.startOauth2(serverUrl, applicationId, sub); const token = await this.startOauth2(serverUrl, applicationId, applicationSecret, sub);
badgeSvc.addBadge('addGitLabAccount'); badgeSvc.addBadge('addGitLabAccount');
return token; 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 * https://docs.gitlab.com/ee/api/projects.html#get-single-project
*/ */
@ -114,8 +225,7 @@ export default {
if (projectId) { if (projectId) {
return projectId; return projectId;
} }
const project = await this.requestWithRefreshToken(token, {
const project = await request(token, {
url: `projects/${encodeURIComponent(projectPath)}`, url: `projects/${encodeURIComponent(projectPath)}`,
}); });
return project.id; return project.id;
@ -129,7 +239,7 @@ export default {
projectId, projectId,
branch, branch,
}) { }) {
return request(token, { return this.requestWithRefreshToken(token, {
url: `projects/${encodeURIComponent(projectId)}/repository/tree`, url: `projects/${encodeURIComponent(projectId)}/repository/tree`,
params: { params: {
ref: branch, ref: branch,
@ -148,7 +258,7 @@ export default {
branch, branch,
path, path,
}) { }) {
return request(token, { return this.requestWithRefreshToken(token, {
url: `projects/${encodeURIComponent(projectId)}/repository/commits`, url: `projects/${encodeURIComponent(projectId)}/repository/commits`,
params: { params: {
ref_name: branch, ref_name: branch,
@ -175,7 +285,7 @@ export default {
if (isImg && typeof content !== 'string') { if (isImg && typeof content !== 'string') {
uploadContent = await utils.encodeFiletoBase64(content); uploadContent = await utils.encodeFiletoBase64(content);
} }
return request(token, { return this.requestWithRefreshToken(token, {
method: sha ? 'PUT' : 'POST', method: sha ? 'PUT' : 'POST',
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`, url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
body: { body: {
@ -198,7 +308,7 @@ export default {
path, path,
sha, sha,
}) { }) {
return request(token, { return this.requestWithRefreshToken(token, {
method: 'DELETE', method: 'DELETE',
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`, url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
body: { body: {
@ -219,7 +329,7 @@ export default {
path, path,
isImg, isImg,
}) { }) {
const res = await request(token, { const res = await this.requestWithRefreshToken(token, {
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`, url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
params: { ref: branch }, params: { ref: branch },
}); });