import utils from '../../utils'; 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 appDataRepo = 'stackedit-app-data'; const request = (token, options) => networkSvc.request({ ...options, headers: { ...options.headers || {}, }, params: { ...options.params || {}, t: Date.now(), // Prevent from caching access_token: token.accessToken, }, }); const repoRequest = (token, owner, repo, options) => request(token, { ...options, url: `https://gitee.com/api/v5/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${options.url}`, }) .then(res => res.body); const getCommitMessage = (name, path) => { const message = store.getters['data/computedSettings'].git[name]; return message.replace(/{{path}}/g, path); }; /** * Getting a user from its userId is not feasible with API v3. * Using an undocumented endpoint... */ const subPrefix = 'ge'; userSvc.setInfoResolver('gitee', subPrefix, async (sub) => { try { const user = (await networkSvc.request({ url: `https://gitee.com/api/v5/users/${sub}`, params: { t: Date.now(), // Prevent from caching }, })).body; if (user.avatar_url && user.avatar_url.endsWith('.png') && !user.avatar_url.endsWith('no_portrait.png')) { user.avatar_url = `${user.avatar_url}!avatar60`; } return { id: `${subPrefix}:${user.login}`, name: user.login, imageUrl: user.avatar_url || '', }; } catch (err) { if (err.status !== 404) { throw new Error('RETRY'); } throw err; } }); export default { subPrefix, /** * https://developer.gitee.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/ */ async startOauth2(lastToken, silent = false, isMain, randomClientId) { let tokenBody; if (!silent) { const clientId = (await networkSvc.request({ method: 'GET', url: 'giteeClientId', params: { random: randomClientId }, })).body; // Get an OAuth2 code const { code } = await networkSvc.startOauth2( 'https://gitee.com/oauth/authorize', { client_id: clientId, scope: 'projects pull_requests', response_type: 'code', }, silent, ); // Exchange code with token tokenBody = (await networkSvc.request({ method: 'GET', url: 'oauth2/giteeToken', params: { clientId, code, oauth2RedirectUri: constants.oauth2RedirectUri, }, })).body; } else { // grant_type=refresh_token&refresh_token={refresh_token} tokenBody = (await networkSvc.request({ method: 'POST', url: 'https://gitee.com/oauth/token', params: { grant_type: 'refresh_token', refresh_token: lastToken.refreshToken, }, })).body; } const accessToken = tokenBody.access_token; // Call the user info endpoint let user = null; try { user = (await networkSvc.request({ method: 'GET', url: 'https://gitee.com/api/v5/user', params: { access_token: accessToken, }, })).body; } catch (err) { if (err.status === 401) { this.startOauth2(null, false, isMain, 1); } throw err; } if (user.avatar_url && user.avatar_url.endsWith('.png') && !user.avatar_url.endsWith('no_portrait.png')) { user.avatar_url = `${user.avatar_url}!avatar60`; } userSvc.addUserInfo({ id: `${subPrefix}:${user.login}`, name: user.login, imageUrl: user.avatar_url || '', }); // 获取同一个用户的登录token const existingToken = store.getters['data/giteeTokensBySub'][user.login]; // Build token object including sub 在token失效后刷新token 如果刷新失败则触发重新授权 const token = { accessToken, // 主文档空间的登录 标识登录 isLogin: !!isMain || (existingToken && !!existingToken.isLogin), refreshToken: tokenBody.refresh_token, expiresOn: Date.now() + (tokenBody.expires_in * 1000), name: user.login, sub: `${user.login}`, }; if (isMain) { // 检查 stackedit-app-data 仓库是否已经存在 如果不存在则创建该仓库 await this.checkAndCreateRepo(token); } // Add token to gitee tokens store.dispatch('data/addGiteeToken', token); return token; }, // 刷新token async refreshToken(token) { const { sub } = token; const lastToken = store.getters['data/giteeTokensBySub'][sub]; // 兼容旧的没有过期时间 if (!lastToken.expiresOn) { await store.dispatch('modal/open', { type: 'providerRedirection', name: 'Gitee', }); return this.startOauth2(); } // 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(lastToken, true); } catch (err) { // If it fails try to popup a window if (store.state.offline) { throw err; } await store.dispatch('modal/open', { type: 'providerRedirection', name: 'Gitee', }); return this.startOauth2(); } }, signin() { return this.startOauth2(null, false, true); }, async addAccount() { const token = await this.startOauth2(); badgeSvc.addBadge('addGiteeAccount'); return token; }, /** * https://developer.gitee.com/v3/repos/commits/#get-a-single-commit * https://developer.gitee.com/v3/git/trees/#get-a-tree */ async getTree({ token, owner, repo, branch, }) { try { const refreshedToken = await this.refreshToken(token); const { commit } = await repoRequest(refreshedToken, owner, repo, { url: `commits/${encodeURIComponent(branch)}`, }); const { tree, truncated } = await repoRequest(refreshedToken, owner, repo, { url: `git/trees/${encodeURIComponent(commit.tree.sha)}?recursive=1`, }); if (truncated) { throw new Error('Git tree too big. Please remove some files in the repository.'); } return tree; } catch (err) { if (err.status === 401) { this.startOauth2(null, false, null, 1); } throw err; } }, async checkAndCreateRepo(token) { const url = `https://gitee.com/api/v5/repos/${encodeURIComponent(token.name)}/${encodeURIComponent(appDataRepo)}`; try { await request(token, { url }); } catch (err) { // 不存在则创建 if (err.status === 404) { await request(token, { method: 'POST', url: 'https://gitee.com/api/v5/user/repos', params: { name: appDataRepo, auto_init: true, }, }); } else { throw err; } } }, /** * https://developer.gitee.com/v3/repos/commits/#list-commits-on-a-repository */ async getCommits({ token, owner, repo, sha, path, }) { const refreshedToken = await this.refreshToken(token); return repoRequest(refreshedToken, owner, repo, { url: 'commits', params: { sha, path }, }); }, /** * https://developer.gitee.com/v3/repos/contents/#create-a-file * https://developer.gitee.com/v3/repos/contents/#update-a-file */ async uploadFile({ token, owner, repo, branch, path, content, sha, isImg, commitMessage, }) { let uploadContent = content; if (isImg && typeof content !== 'string') { uploadContent = await utils.encodeFiletoBase64(content); } const refreshedToken = await this.refreshToken(token); return repoRequest(refreshedToken, owner, repo, { method: sha ? 'PUT' : 'POST', url: `contents/${encodeURIComponent(path)}`, body: { message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path), content: isImg ? uploadContent : utils.encodeBase64(content || ' '), sha, branch, }, }); }, /** * https://developer.gitee.com/v3/repos/contents/#delete-a-file */ async removeFile({ token, owner, repo, branch, path, sha, }) { const refreshedToken = await this.refreshToken(token); return repoRequest(refreshedToken, owner, repo, { method: 'DELETE', url: `contents/${encodeURIComponent(path)}`, body: { message: getCommitMessage('deleteFileMessage', path), sha, branch, }, }); }, /** * https://developer.gitee.com/v3/repos/contents/#get-contents */ async downloadFile({ token, owner, repo, branch, path, isImg, }) { const refreshedToken = await this.refreshToken(token); const { sha, content } = await repoRequest(refreshedToken, owner, repo, { url: `contents/${encodeURIComponent(path)}`, params: { ref: branch }, }); if (sha) { const data = !isImg ? utils.decodeBase64(content) : content; return { sha, data: data === ' ' ? '' : data, }; } return {}; }, /** * https://developer.gitee.com/v3/gists/#create-a-gist * https://developer.gitee.com/v3/gists/#edit-a-gist */ async uploadGist({ token, description, filename, content, isPublic, gistId, }) { const refreshedToken = await this.refreshToken(token); const { body } = await request(refreshedToken, gistId ? { method: 'PATCH', url: `https://gitee.com/api/v5/gists/${gistId}`, body: { description, files: { [filename]: { content, }, }, }, } : { method: 'POST', url: 'https://gitee.com/api/v5/gists', body: { description, files: { [filename]: { content, }, }, public: isPublic, }, }); return body; }, /** * https://developer.gitee.com/v3/gists/#get-a-single-gist */ async downloadGist({ token, gistId, filename, }) { const refreshedToken = await this.refreshToken(token); const result = (await request(refreshedToken, { url: `https://gitee.com/api/v5/gists/${gistId}`, })).body.files[filename]; if (!result) { throw new Error('Gist file not found.'); } return result.content; }, /** * https://developer.gitee.com/v3/gists/#list-gist-commits */ async getGistCommits({ token, gistId, }) { const refreshedToken = await this.refreshToken(token); const { body } = await request(refreshedToken, { url: `https://gitee.com/api/v5/gists/${gistId}/commits`, }); return body; }, /** * https://developer.gitee.com/v3/gists/#get-a-specific-revision-of-a-gist */ async downloadGistRevision({ token, gistId, filename, sha, }) { const refreshedToken = await this.refreshToken(token); const result = (await request(refreshedToken, { url: `https://gitee.com/api/v5/gists/${gistId}/${sha}`, })).body.files[filename]; if (!result) { throw new Error('Gist file not found.'); } return result.content; }, };