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
- 支持分享文档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必须支持跨域

View File

@ -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"',
})

View File

@ -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 = {

View File

@ -1,4 +1,3 @@
const qs = require('qs');
const request = require('request');
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 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);

View File

@ -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() {

View File

@ -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() {

View File

@ -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,

View File

@ -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() {

View File

@ -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,
});
}
}

View File

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

View File

@ -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) {

View File

@ -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 },
});