@pranaysahith/decap-cms-backend-gitlab 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +416 -0
- package/README.md +13 -0
- package/dist/@pranaysahith/decap-cms-backend-gitlab.js +37 -0
- package/dist/@pranaysahith/decap-cms-backend-gitlab.js.LICENSE.txt +23 -0
- package/dist/@pranaysahith/decap-cms-backend-gitlab.js.map +1 -0
- package/dist/decap-cms-backend-gitlab.js +64 -0
- package/dist/decap-cms-backend-gitlab.js.LICENSE.txt +23 -0
- package/dist/decap-cms-backend-gitlab.js.map +1 -0
- package/dist/esm/API.js +802 -0
- package/dist/esm/AuthenticationPage.js +126 -0
- package/dist/esm/implementation.js +358 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/queries.js +64 -0
- package/package.json +47 -0
- package/src/API.ts +1029 -0
- package/src/AuthenticationPage.js +126 -0
- package/src/__tests__/API.spec.js +187 -0
- package/src/__tests__/gitlab.spec.js +552 -0
- package/src/implementation.ts +470 -0
- package/src/index.ts +10 -0
- package/src/queries.ts +73 -0
- package/webpack.config.js +3 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from '@emotion/styled';
|
|
4
|
+
import {
|
|
5
|
+
NetlifyAuthenticator,
|
|
6
|
+
ImplicitAuthenticator,
|
|
7
|
+
PkceAuthenticator,
|
|
8
|
+
} from 'decap-cms-lib-auth';
|
|
9
|
+
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
|
|
10
|
+
|
|
11
|
+
const LoginButtonIcon = styled(Icon)`
|
|
12
|
+
margin-right: 18px;
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const clientSideAuthenticators = {
|
|
16
|
+
pkce: ({
|
|
17
|
+
base_url,
|
|
18
|
+
auth_endpoint,
|
|
19
|
+
app_id,
|
|
20
|
+
auth_token_endpoint}) =>
|
|
21
|
+
new PkceAuthenticator({
|
|
22
|
+
base_url,
|
|
23
|
+
auth_endpoint,
|
|
24
|
+
app_id,
|
|
25
|
+
auth_token_endpoint,
|
|
26
|
+
auth_token_endpoint_content_type: 'application/json; charset=utf-8',
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
implicit: ({
|
|
30
|
+
base_url,
|
|
31
|
+
auth_endpoint,
|
|
32
|
+
app_id,
|
|
33
|
+
clearHash }) =>
|
|
34
|
+
new ImplicitAuthenticator({
|
|
35
|
+
base_url,
|
|
36
|
+
auth_endpoint,
|
|
37
|
+
app_id,
|
|
38
|
+
clearHash,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default class GitLabAuthenticationPage extends React.Component {
|
|
43
|
+
static propTypes = {
|
|
44
|
+
onLogin: PropTypes.func.isRequired,
|
|
45
|
+
inProgress: PropTypes.bool,
|
|
46
|
+
base_url: PropTypes.string,
|
|
47
|
+
siteId: PropTypes.string,
|
|
48
|
+
authEndpoint: PropTypes.string,
|
|
49
|
+
config: PropTypes.object.isRequired,
|
|
50
|
+
clearHash: PropTypes.func,
|
|
51
|
+
t: PropTypes.func.isRequired,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
state = {};
|
|
55
|
+
|
|
56
|
+
componentDidMount() {
|
|
57
|
+
// Manually validate PropTypes - React 19 breaking change
|
|
58
|
+
PropTypes.checkPropTypes(GitLabAuthenticationPage.propTypes, this.props, 'prop', 'GitLabAuthenticationPage');
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
auth_type: authType = '',
|
|
62
|
+
base_url = 'https://gitlab.com',
|
|
63
|
+
auth_endpoint = 'oauth/authorize',
|
|
64
|
+
app_id = '',
|
|
65
|
+
} = this.props.config.backend;
|
|
66
|
+
|
|
67
|
+
if (clientSideAuthenticators[authType]) {
|
|
68
|
+
this.auth = clientSideAuthenticators[authType]({
|
|
69
|
+
base_url,
|
|
70
|
+
auth_endpoint,
|
|
71
|
+
app_id,
|
|
72
|
+
auth_token_endpoint: 'oauth/token',
|
|
73
|
+
clearHash: this.props.clearHash,
|
|
74
|
+
});
|
|
75
|
+
// Complete implicit authentication if we were redirected back to from the provider.
|
|
76
|
+
this.auth.completeAuth((err, data) => {
|
|
77
|
+
if (err) {
|
|
78
|
+
this.setState({ loginError: err.toString() });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.props.onLogin(data);
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
this.auth = new NetlifyAuthenticator({
|
|
85
|
+
base_url: this.props.base_url,
|
|
86
|
+
site_id:
|
|
87
|
+
document.location.host.split(':')[0] === 'localhost'
|
|
88
|
+
? 'demo.decapcms.org'
|
|
89
|
+
: this.props.siteId,
|
|
90
|
+
auth_endpoint: this.props.authEndpoint,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
handleLogin = e => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
this.auth.authenticate({ provider: 'gitlab', scope: 'api' }, (err, data) => {
|
|
98
|
+
if (err) {
|
|
99
|
+
this.setState({ loginError: err.toString() });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.props.onLogin(data);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
render() {
|
|
107
|
+
const { inProgress, config, t } = this.props;
|
|
108
|
+
return (
|
|
109
|
+
<AuthenticationPage
|
|
110
|
+
onLogin={this.handleLogin}
|
|
111
|
+
loginDisabled={inProgress}
|
|
112
|
+
loginErrorMessage={this.state.loginError}
|
|
113
|
+
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
|
|
114
|
+
logo={config.logo}
|
|
115
|
+
siteUrl={config.site_url}
|
|
116
|
+
renderButtonContent={() => (
|
|
117
|
+
<React.Fragment>
|
|
118
|
+
<LoginButtonIcon type="gitlab" />{' '}
|
|
119
|
+
{inProgress ? t('auth.loggingIn') : t('auth.loginWithGitLab')}
|
|
120
|
+
</React.Fragment>
|
|
121
|
+
)}
|
|
122
|
+
t={t}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import API, { getMaxAccess } from '../API';
|
|
2
|
+
|
|
3
|
+
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
|
4
|
+
|
|
5
|
+
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
6
|
+
|
|
7
|
+
describe('GitLab API', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.resetAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('hasWriteAccess', () => {
|
|
13
|
+
test('should return true on project access_level >= 30', async () => {
|
|
14
|
+
const api = new API({ repo: 'repo' });
|
|
15
|
+
|
|
16
|
+
api.requestJSON = jest
|
|
17
|
+
.fn()
|
|
18
|
+
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 30 } } });
|
|
19
|
+
|
|
20
|
+
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should return false on project access_level < 30', async () => {
|
|
24
|
+
const api = new API({ repo: 'repo' });
|
|
25
|
+
|
|
26
|
+
api.requestJSON = jest
|
|
27
|
+
.fn()
|
|
28
|
+
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 10 } } });
|
|
29
|
+
|
|
30
|
+
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should return true on group access_level >= 30', async () => {
|
|
34
|
+
const api = new API({ repo: 'repo' });
|
|
35
|
+
|
|
36
|
+
api.requestJSON = jest
|
|
37
|
+
.fn()
|
|
38
|
+
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 30 } } });
|
|
39
|
+
|
|
40
|
+
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should return false on group access_level < 30', async () => {
|
|
44
|
+
const api = new API({ repo: 'repo' });
|
|
45
|
+
|
|
46
|
+
api.requestJSON = jest
|
|
47
|
+
.fn()
|
|
48
|
+
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 10 } } });
|
|
49
|
+
|
|
50
|
+
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should return true on shared group access_level >= 40', async () => {
|
|
54
|
+
const api = new API({ repo: 'repo' });
|
|
55
|
+
api.requestJSON = jest.fn().mockResolvedValueOnce({
|
|
56
|
+
permissions: { project_access: null, group_access: null },
|
|
57
|
+
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 40 }],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
|
61
|
+
|
|
62
|
+
expect(api.requestJSON).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should return true on shared group access_level >= 30, developers can merge and push', async () => {
|
|
66
|
+
const api = new API({ repo: 'repo' });
|
|
67
|
+
|
|
68
|
+
api.requestJSON = jest.fn();
|
|
69
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
70
|
+
permissions: { project_access: null, group_access: null },
|
|
71
|
+
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
|
72
|
+
});
|
|
73
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
74
|
+
developers_can_merge: true,
|
|
75
|
+
developers_can_push: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should return false on shared group access_level < 30,', async () => {
|
|
82
|
+
const api = new API({ repo: 'repo' });
|
|
83
|
+
|
|
84
|
+
api.requestJSON = jest.fn();
|
|
85
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
86
|
+
permissions: { project_access: null, group_access: null },
|
|
87
|
+
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 20 }],
|
|
88
|
+
});
|
|
89
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
90
|
+
developers_can_merge: true,
|
|
91
|
+
developers_can_push: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("should return false on shared group access_level >= 30, developers can't merge", async () => {
|
|
98
|
+
const api = new API({ repo: 'repo' });
|
|
99
|
+
|
|
100
|
+
api.requestJSON = jest.fn();
|
|
101
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
102
|
+
permissions: { project_access: null, group_access: null },
|
|
103
|
+
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
|
104
|
+
});
|
|
105
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
106
|
+
developers_can_merge: false,
|
|
107
|
+
developers_can_push: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("should return false on shared group access_level >= 30, developers can't push", async () => {
|
|
114
|
+
const api = new API({ repo: 'repo' });
|
|
115
|
+
|
|
116
|
+
api.requestJSON = jest.fn();
|
|
117
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
118
|
+
permissions: { project_access: null, group_access: null },
|
|
119
|
+
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
|
120
|
+
});
|
|
121
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
122
|
+
developers_can_merge: true,
|
|
123
|
+
developers_can_push: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should return false on shared group access_level >= 30, error getting branch', async () => {
|
|
130
|
+
const api = new API({ repo: 'repo' });
|
|
131
|
+
|
|
132
|
+
api.requestJSON = jest.fn();
|
|
133
|
+
api.requestJSON.mockResolvedValueOnce({
|
|
134
|
+
permissions: { project_access: null, group_access: null },
|
|
135
|
+
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const error = new Error('Not Found');
|
|
139
|
+
api.requestJSON.mockRejectedValue(error);
|
|
140
|
+
|
|
141
|
+
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
|
142
|
+
|
|
143
|
+
expect(console.log).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(console.log).toHaveBeenCalledWith('Failed getting default branch', error);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('getStatuses', () => {
|
|
149
|
+
test('should get preview statuses', async () => {
|
|
150
|
+
const api = new API({ repo: 'repo' });
|
|
151
|
+
|
|
152
|
+
const mr = { sha: 'sha' };
|
|
153
|
+
const statuses = [
|
|
154
|
+
{ name: 'deploy', status: 'success', target_url: 'deploy-url' },
|
|
155
|
+
{ name: 'build', status: 'pending' },
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
api.getBranchMergeRequest = jest.fn(() => Promise.resolve(mr));
|
|
159
|
+
api.getMergeRequestStatues = jest.fn(() => Promise.resolve(statuses));
|
|
160
|
+
|
|
161
|
+
const collectionName = 'posts';
|
|
162
|
+
const slug = 'title';
|
|
163
|
+
await expect(api.getStatuses(collectionName, slug)).resolves.toEqual([
|
|
164
|
+
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
|
|
165
|
+
{ context: 'build', state: 'other' },
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
expect(api.getBranchMergeRequest).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(api.getBranchMergeRequest).toHaveBeenCalledWith('cms/posts/title');
|
|
170
|
+
|
|
171
|
+
expect(api.getMergeRequestStatues).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(api.getMergeRequestStatues).toHaveBeenCalledWith(mr, 'cms/posts/title');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('getMaxAccess', () => {
|
|
177
|
+
it('should return group with max access level', () => {
|
|
178
|
+
const groups = [
|
|
179
|
+
{ group_access_level: 10 },
|
|
180
|
+
{ group_access_level: 5 },
|
|
181
|
+
{ group_access_level: 100 },
|
|
182
|
+
{ group_access_level: 1 },
|
|
183
|
+
];
|
|
184
|
+
expect(getMaxAccess(groups)).toBe(groups[2]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|