feat: complete Epic 1 production foundation
This commit is contained in:
+188
-5
@@ -1,6 +1,8 @@
|
||||
(function() {
|
||||
var API_BASE = '/api/admin';
|
||||
var AUTH_BASE = '/api/auth';
|
||||
var operationEtags = Object.create(null);
|
||||
var agentEtags = Object.create(null);
|
||||
|
||||
function headers(extra) {
|
||||
return Object.assign({
|
||||
@@ -8,6 +10,26 @@
|
||||
}, extra || {});
|
||||
}
|
||||
|
||||
function csrfExempt(path) {
|
||||
return /\/api\/auth\/(?:login|bootstrap\/complete|session\/csrf)$/.test(path);
|
||||
}
|
||||
|
||||
function attachCsrf(path, method, requestOptions) {
|
||||
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS' || csrfExempt(path)) {
|
||||
return;
|
||||
}
|
||||
if (!(path.indexOf('/api/auth/') === 0 || path.indexOf('/api/admin/') === 0)) {
|
||||
return;
|
||||
}
|
||||
var token = window.CrankAuth && typeof window.CrankAuth.getCsrfToken === 'function'
|
||||
? window.CrankAuth.getCsrfToken()
|
||||
: '';
|
||||
if (token) {
|
||||
requestOptions.headers = headers(requestOptions.headers);
|
||||
requestOptions.headers['x-csrf-token'] = token;
|
||||
}
|
||||
}
|
||||
|
||||
function attachCorrelation(error, response) {
|
||||
var requestId = response.headers.get('x-request-id');
|
||||
var traceId = response.headers.get('x-trace-id');
|
||||
@@ -20,13 +42,88 @@
|
||||
return error;
|
||||
}
|
||||
|
||||
function operationMutationResource(path, method) {
|
||||
if (!method || method === 'GET') {
|
||||
return null;
|
||||
}
|
||||
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/operations\/[^/?]+)(?:\/(publish|archive|versions))?(?:\?.*)?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
if (method === 'PATCH' || method === 'DELETE' || (method === 'POST' && match[2])) {
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function operationDetailResource(path, method) {
|
||||
if (method && method !== 'GET') {
|
||||
return null;
|
||||
}
|
||||
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/operations\/[^/?]+)$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function agentMutationResource(path, method) {
|
||||
if (!method || method === 'GET') {
|
||||
return null;
|
||||
}
|
||||
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/agents\/[^/?]+)(?:\/(bindings|publish|unpublish|archive))?(?:\?.*)?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
if (method === 'PATCH' || method === 'DELETE' || (method === 'POST' && match[2])) {
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function agentDetailResource(path, method) {
|
||||
if (method && method !== 'GET') {
|
||||
return null;
|
||||
}
|
||||
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/agents\/[^/?]+)$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function request(path, options) {
|
||||
var response = await fetch(path, Object.assign({
|
||||
var requestOptions = Object.assign({
|
||||
credentials: 'same-origin',
|
||||
headers: headers(),
|
||||
}, options || {}));
|
||||
}, options || {});
|
||||
var method = (requestOptions.method || 'GET').toUpperCase();
|
||||
attachCsrf(path, method, requestOptions);
|
||||
var mutationResource = operationMutationResource(path, method);
|
||||
var agentMutation = agentMutationResource(path, method);
|
||||
if (mutationResource) {
|
||||
if (!operationEtags[mutationResource]) {
|
||||
await request(mutationResource);
|
||||
}
|
||||
requestOptions.headers = headers(requestOptions.headers);
|
||||
requestOptions.headers['If-Match'] = operationEtags[mutationResource];
|
||||
}
|
||||
if (agentMutation) {
|
||||
if (!agentEtags[agentMutation]) {
|
||||
await request(agentMutation);
|
||||
}
|
||||
requestOptions.headers = headers(requestOptions.headers);
|
||||
requestOptions.headers['If-Match'] = agentEtags[agentMutation];
|
||||
}
|
||||
var response = await fetch(path, requestOptions);
|
||||
var detailResource = operationDetailResource(path, method);
|
||||
var agentDetail = agentDetailResource(path, method);
|
||||
var responseEtag = response.headers.get('etag');
|
||||
if (detailResource && responseEtag) {
|
||||
operationEtags[detailResource] = responseEtag;
|
||||
}
|
||||
if (agentDetail && responseEtag) {
|
||||
agentEtags[agentDetail] = responseEtag;
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
if (mutationResource) {
|
||||
delete operationEtags[mutationResource];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -40,6 +137,12 @@
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (mutationResource && (response.status === 409 || response.status === 428)) {
|
||||
delete operationEtags[mutationResource];
|
||||
}
|
||||
if (agentMutation && (response.status === 409 || response.status === 428)) {
|
||||
delete agentEtags[agentMutation];
|
||||
}
|
||||
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
|
||||
window.CrankAuth.handleUnauthorized();
|
||||
}
|
||||
@@ -54,9 +157,22 @@
|
||||
var error = new Error(message);
|
||||
error.status = response.status;
|
||||
error.payload = payload;
|
||||
var errorCode = payload && payload.error && typeof payload.error === 'object'
|
||||
? (payload.error.code || payload.error.error_code)
|
||||
: payload && (payload.code || payload.error_code);
|
||||
if (typeof errorCode === 'string' && /^[A-Za-z0-9._-]{1,128}$/.test(errorCode)) {
|
||||
error.code = errorCode;
|
||||
}
|
||||
throw attachCorrelation(error, response);
|
||||
}
|
||||
|
||||
if (mutationResource) {
|
||||
delete operationEtags[mutationResource];
|
||||
}
|
||||
if (agentMutation) {
|
||||
delete agentEtags[agentMutation];
|
||||
}
|
||||
|
||||
if (text && payload === null) {
|
||||
throw attachCorrelation(new Error('Backend returned a non-JSON response'), response);
|
||||
}
|
||||
@@ -137,6 +253,16 @@
|
||||
}
|
||||
|
||||
window.CrankApi = {
|
||||
getBootstrapStatus: function() {
|
||||
return request(AUTH_BASE + '/bootstrap/status');
|
||||
},
|
||||
completeBootstrap: function(payload) {
|
||||
return request(AUTH_BASE + '/bootstrap/complete', {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
login: function(payload) {
|
||||
return request(AUTH_BASE + '/login', {
|
||||
method: 'POST',
|
||||
@@ -154,6 +280,13 @@
|
||||
getSession: function() {
|
||||
return request(AUTH_BASE + '/session');
|
||||
},
|
||||
refreshSessionCsrf: function() {
|
||||
return request(AUTH_BASE + '/session/csrf', {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
},
|
||||
getProfile: function() {
|
||||
return request(AUTH_BASE + '/profile');
|
||||
},
|
||||
@@ -180,6 +313,21 @@
|
||||
getWorkspace: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId));
|
||||
},
|
||||
getOnboarding: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding');
|
||||
},
|
||||
recordOnboardingEvent: function(workspaceId, payload, options) {
|
||||
return request(API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding/events', Object.assign({}, options || {}, {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload),
|
||||
}));
|
||||
},
|
||||
resetOnboardingSelection: function(workspaceId, expectedRevision) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding/reset-selection', {
|
||||
expected_revision: expectedRevision,
|
||||
});
|
||||
},
|
||||
updateWorkspace: function(workspaceId, payload) {
|
||||
return patch('/workspaces/' + encodeURIComponent(workspaceId), payload);
|
||||
},
|
||||
@@ -238,15 +386,28 @@
|
||||
}
|
||||
);
|
||||
},
|
||||
importOperation: function(workspaceId, yamlDocument, mode) {
|
||||
return request(
|
||||
importOperation: async function(workspaceId, yamlDocument, mode, existingOperationId) {
|
||||
var importHeaders = headers({ 'Content-Type': 'application/yaml' });
|
||||
if (existingOperationId) {
|
||||
var resource = API_BASE + '/workspaces/' + encodeURIComponent(workspaceId)
|
||||
+ '/operations/' + encodeURIComponent(existingOperationId);
|
||||
if (!operationEtags[resource]) {
|
||||
await request(resource);
|
||||
}
|
||||
importHeaders['If-Match'] = operationEtags[resource];
|
||||
}
|
||||
var result = await request(
|
||||
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/operations/import' + query({ mode: mode }),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/yaml' }),
|
||||
headers: importHeaders,
|
||||
body: yamlDocument,
|
||||
}
|
||||
);
|
||||
if (existingOperationId) {
|
||||
delete operationEtags[resource];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
previewOpenApiImport: function(workspaceId, documentText) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/imports/openapi/preview', {
|
||||
@@ -331,6 +492,16 @@
|
||||
listLogs: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs' + query(params));
|
||||
},
|
||||
exportLogsCsv: function(workspaceId, params) {
|
||||
return requestText(
|
||||
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/logs/export.csv' + query(params)
|
||||
);
|
||||
},
|
||||
exportUsageCsv: function(workspaceId, params) {
|
||||
return requestText(
|
||||
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/usage/export.csv' + query(params)
|
||||
);
|
||||
},
|
||||
getLog: function(workspaceId, logId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
|
||||
},
|
||||
@@ -340,6 +511,18 @@
|
||||
getApproval: function(workspaceId, approvalId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId));
|
||||
},
|
||||
approveApproval: function(workspaceId, approvalId, payload) {
|
||||
return post(
|
||||
'/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId) + '/approve',
|
||||
payload || { approve: 'yes' }
|
||||
);
|
||||
},
|
||||
denyApproval: function(workspaceId, approvalId, payload) {
|
||||
return post(
|
||||
'/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId) + '/deny',
|
||||
payload || { approve: 'no' }
|
||||
);
|
||||
},
|
||||
getUsageOverview: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user