BitBucket backend
This commit is contained in:
parent
9838673929
commit
9894b3e805
@ -18,6 +18,7 @@ import { sanitizeSlug } from "Lib/urlHelper";
|
||||
import TestRepoBackend from "./test-repo/implementation";
|
||||
import GitHubBackend from "./github/implementation";
|
||||
import GitLabBackend from "./gitlab/implementation";
|
||||
import BitBucketBackend from "./bitbucket/implementation";
|
||||
import GitGatewayBackend from "./git-gateway/implementation";
|
||||
import { registerBackend, getBackend } from 'Lib/registry';
|
||||
import Cursor, { CURSOR_COMPATIBILITY_SYMBOL } from '../valueObjects/Cursor';
|
||||
@ -28,6 +29,7 @@ import Cursor, { CURSOR_COMPATIBILITY_SYMBOL } from '../valueObjects/Cursor';
|
||||
registerBackend('git-gateway', GitGatewayBackend);
|
||||
registerBackend('github', GitHubBackend);
|
||||
registerBackend('gitlab', GitLabBackend);
|
||||
registerBackend('bitbucket', BitBucketBackend);
|
||||
registerBackend('test-repo', TestRepoBackend);
|
||||
|
||||
|
||||
@ -124,8 +126,8 @@ const sortByScore = (a, b) => {
|
||||
};
|
||||
|
||||
class Backend {
|
||||
constructor(implementation, backendName, authStore = null) {
|
||||
this.implementation = implementation;
|
||||
constructor(implementation, { authStore = null, backendName, config } = {}) {
|
||||
this.implementation = implementation.init(config, { updateUserCredentials: this.updateUserCredentials });
|
||||
this.backendName = backendName;
|
||||
this.authStore = authStore;
|
||||
if (this.implementation === null) {
|
||||
@ -147,6 +149,15 @@ class Backend {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
updateUserCredentials = updatedCredentials => {
|
||||
const storedUser = this.authStore && this.authStore.retrieve();
|
||||
if (storedUser && storedUser.backendName === this.backendName) {
|
||||
const newUser = { ...storedUser, ...updatedCredentials };
|
||||
this.authStore.store(newUser);
|
||||
return newUser;
|
||||
}
|
||||
};
|
||||
|
||||
authComponent() {
|
||||
return this.implementation.authComponent();
|
||||
}
|
||||
@ -476,7 +487,7 @@ export function resolveBackend(config) {
|
||||
if (!getBackend(name)) {
|
||||
throw new Error(`Backend not found: ${ name }`);
|
||||
} else {
|
||||
return new Backend(getBackend(name).init(config), name, authStore);
|
||||
return new Backend(getBackend(name), { backendName: name, authStore, config });
|
||||
}
|
||||
}
|
||||
|
||||
|
149
src/backends/bitbucket/API.js
Normal file
149
src/backends/bitbucket/API.js
Normal file
@ -0,0 +1,149 @@
|
||||
import { flow } from "lodash";
|
||||
import LocalForage from "Lib/LocalForage";
|
||||
import unsentRequest from "Lib/unsentRequest";
|
||||
import { responseParser } from "Lib/backendHelper";
|
||||
import { then } from "Lib/promiseHelper";
|
||||
import { basename } from "Lib/pathHelper";
|
||||
import AssetProxy from "ValueObjects/AssetProxy";
|
||||
import Cursor from "ValueObjects/Cursor";
|
||||
import { APIError } from "ValueObjects/errors";
|
||||
|
||||
export default class API {
|
||||
constructor(config) {
|
||||
this.api_root = config.api_root || "https://api.bitbucket.org/2.0";
|
||||
this.branch = config.branch || "master";
|
||||
this.repo = config.repo || "";
|
||||
this.requestFunction = config.requestFunction || unsentRequest.performRequest;
|
||||
this.repoURL = `/repositories/${ this.repo }`;
|
||||
this.repoURL = this.repo ? `/repositories/${ this.repo }` : "";
|
||||
}
|
||||
|
||||
buildRequest = req => flow([
|
||||
unsentRequest.withRoot(this.api_root),
|
||||
unsentRequest.withTimestamp,
|
||||
])(req);
|
||||
|
||||
request = req => flow([
|
||||
this.buildRequest,
|
||||
this.requestFunction,
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
|
||||
])(req);
|
||||
|
||||
requestJSON = req => flow([
|
||||
unsentRequest.withDefaultHeaders({ "Content-Type": "application/json" }),
|
||||
this.request,
|
||||
then(responseParser({ format: "json" })),
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
|
||||
])(req);
|
||||
requestText = req => flow([
|
||||
unsentRequest.withDefaultHeaders({ "Content-Type": "text/plain" }),
|
||||
this.request,
|
||||
then(responseParser({ format: "text" })),
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
|
||||
])(req);
|
||||
|
||||
user = () => this.request("/user");
|
||||
hasWriteAccess = user => this.request(this.repoURL).then(res => res.ok);
|
||||
|
||||
isFile = ({ type }) => type === "commit_file";
|
||||
processFile = file => ({
|
||||
...file,
|
||||
name: basename(file.path),
|
||||
download_url: file.links.self.href,
|
||||
|
||||
// BitBucket does not return file SHAs, but it does give us the
|
||||
// commit SHA. Since the commit SHA will change if any files do,
|
||||
// we can construct an ID using the commit SHA and the file path
|
||||
// that will help with caching (though not as well as a normal
|
||||
// SHA, since it will change even if the individual file itself
|
||||
// doesn't.)
|
||||
...(file.commit && file.commit.hash
|
||||
? { id: `${ file.commit.hash }/${ file.path }` }
|
||||
: {}),
|
||||
});
|
||||
processFiles = files => files.filter(this.isFile).map(this.processFile);
|
||||
|
||||
readFile = async (path, sha, { ref = this.branch, parseText = true } = {}) => {
|
||||
const cacheKey = parseText ? `bb.${ sha }` : `bb.${ sha }.blob`;
|
||||
const cachedFile = sha ? await LocalForage.getItem(cacheKey) : null;
|
||||
if (cachedFile) { return cachedFile; }
|
||||
const result = await this.request({
|
||||
url: `${ this.repoURL }/src/${ ref }/${ path }`,
|
||||
cache: "no-store",
|
||||
}).then(parseText ? responseParser({ format: "text" }) : responseParser({ format: "blob" }));
|
||||
if (sha) { LocalForage.setItem(cacheKey, result); }
|
||||
return result;
|
||||
}
|
||||
|
||||
getEntriesAndCursor = jsonResponse => {
|
||||
const { size: count, page: index, pagelen: pageSize, next, previous: prev, values: entries } = jsonResponse;
|
||||
const pageCount = (pageSize && count) ? Math.ceil(count / pageSize) : undefined;
|
||||
return {
|
||||
entries,
|
||||
cursor: Cursor.create({
|
||||
actions: [...(next ? ["next"] : []), ...(prev ? ["prev"] : [])],
|
||||
meta: { index, count, pageSize, pageCount },
|
||||
data: { links: { next, prev } },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
listFiles = async path => {
|
||||
const { entries, cursor } = await flow([
|
||||
// sort files by filename ascending
|
||||
unsentRequest.withParams({ sort: "-path" }),
|
||||
this.requestJSON,
|
||||
then(this.getEntriesAndCursor),
|
||||
])(`${ this.repoURL }/src/${ this.branch }/${ path }`);
|
||||
return { entries: this.processFiles(entries), cursor };
|
||||
}
|
||||
|
||||
traverseCursor = async (cursor, action) => flow([
|
||||
this.requestJSON,
|
||||
then(this.getEntriesAndCursor),
|
||||
then(({ cursor: newCursor, entries }) => ({ cursor: newCursor, entries: this.processFiles(entries) })),
|
||||
])(cursor.data.getIn(["links", action]));
|
||||
|
||||
listAllFiles = async path => {
|
||||
const { cursor: initialCursor, entries: initialEntries } = await this.listFiles(path);
|
||||
const entries = [...initialEntries];
|
||||
let currentCursor = initialCursor;
|
||||
while (currentCursor && currentCursor.actions.has("next")) {
|
||||
const { cursor: newCursor, entries: newEntries } = await this.traverseCursor(currentCursor, "next");
|
||||
entries.push(...newEntries);
|
||||
currentCursor = newCursor;
|
||||
}
|
||||
return this.processFiles(entries);
|
||||
};
|
||||
|
||||
uploadBlob = async item => {
|
||||
const contentBase64 = await (item instanceof AssetProxy ? item.toBase64() : Promise.resolve(item.raw));
|
||||
const formData = new FormData();
|
||||
formData.append(item.path, contentBase64);
|
||||
|
||||
return flow([
|
||||
unsentRequest.withMethod("POST"),
|
||||
unsentRequest.withBody(formData),
|
||||
this.request,
|
||||
then(() => ({ ...item, uploaded: true })),
|
||||
])(`${ this.repoURL }/src`);
|
||||
};
|
||||
|
||||
persistFiles = (files, { commitMessage, newEntry }) => Promise.all(
|
||||
files.filter(({ uploaded }) => !uploaded).map(this.uploadBlob)
|
||||
);
|
||||
|
||||
deleteFile = (path, message, options={}) => {
|
||||
const branch = options.branch || this.branch;
|
||||
const body = new FormData();
|
||||
body.append('files', path);
|
||||
if (message && message !== "") {
|
||||
body.append("message", message);
|
||||
}
|
||||
return flow([
|
||||
unsentRequest.withMethod("POST"),
|
||||
unsentRequest.withBody(body),
|
||||
this.request,
|
||||
])(`${ this.repoURL }/src`);
|
||||
};
|
||||
}
|
50
src/backends/bitbucket/AuthenticationPage.js
Normal file
50
src/backends/bitbucket/AuthenticationPage.js
Normal file
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Authenticator from 'Lib/netlify-auth';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
export default class AuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
const cfg = {
|
||||
base_url: this.props.base_url,
|
||||
site_id: (document.location.host.split(':')[0] === 'localhost') ? 'cms.netlify.com' : this.props.site_id,
|
||||
auth_endpoint: this.props.authEndpoint,
|
||||
};
|
||||
const auth = new Authenticator(cfg);
|
||||
|
||||
auth.authenticate({ provider: 'bitbucket', scope: 'repo' }, (err, data) => {
|
||||
if (err) {
|
||||
this.setState({ loginError: err.toString() });
|
||||
return;
|
||||
}
|
||||
this.props.onLogin(data);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loginError } = this.state;
|
||||
const { inProgress } = this.props;
|
||||
|
||||
return (
|
||||
<section className="nc-githubAuthenticationPage-root">
|
||||
<Icon className="nc-githubAuthenticationPage-logo" size="500px" type="netlify-cms"/>
|
||||
{loginError && <p>{loginError}</p>}
|
||||
<button
|
||||
className="nc-githubAuthenticationPage-button"
|
||||
disabled={inProgress}
|
||||
onClick={this.handleLogin}
|
||||
>
|
||||
<Icon type="bitbucket" /> {inProgress ? "Logging in..." : "Login with Bitbucket"}
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
222
src/backends/bitbucket/implementation.js
Normal file
222
src/backends/bitbucket/implementation.js
Normal file
@ -0,0 +1,222 @@
|
||||
import semaphore from "semaphore";
|
||||
import { flow, trimStart } from "lodash";
|
||||
import { EDITORIAL_WORKFLOW } from "Constants/publishModes";
|
||||
import { CURSOR_COMPATIBILITY_SYMBOL } from "ValueObjects/Cursor";
|
||||
import { filterByPropExtension } from "Lib/backendHelper";
|
||||
import { resolvePromiseProperties, then } from "Lib/promiseHelper";
|
||||
import unsentRequest from "Lib/unsentRequest";
|
||||
import AuthenticationPage from "./AuthenticationPage";
|
||||
import Authenticator from 'Lib/netlify-auth';
|
||||
import API from "./API";
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
// Implementation wrapper class
|
||||
export default class Bitbucket {
|
||||
constructor(config, options={}) {
|
||||
this.config = config;
|
||||
this.options = {
|
||||
proxied: false,
|
||||
API: null,
|
||||
updateUserCredentials: async () => null,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (config.getIn(["publish_mode"]) === EDITORIAL_WORKFLOW) {
|
||||
throw new Error("The BitBucket backend does not support the Editorial Workflow.");
|
||||
}
|
||||
|
||||
if (!this.options.proxied && !config.getIn(["backend", "repo"], false)) {
|
||||
throw new Error("The BitBucket backend needs a \"repo\ in the backend configuration.");
|
||||
}
|
||||
|
||||
this.api = this.options.API || null;
|
||||
|
||||
this.updateUserCredentials = this.options.updateUserCredentials;
|
||||
|
||||
this.repo = config.getIn(["backend", "repo"], "");
|
||||
this.branch = config.getIn(["backend", "branch"], "master");
|
||||
this.api_root = config.getIn(["backend", "api_root"], "https://api.bitbucket.org/2.0");
|
||||
this.base_url = config.get("base_url");
|
||||
this.site_id = config.get("site_id");
|
||||
this.token = "";
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
return AuthenticationPage;
|
||||
}
|
||||
|
||||
setUser(user) {
|
||||
this.token = user.token;
|
||||
this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo });
|
||||
}
|
||||
|
||||
restoreUser(user) {
|
||||
return this.authenticate(user);
|
||||
}
|
||||
|
||||
authenticate(state) {
|
||||
this.token = state.token;
|
||||
this.refreshToken = state.refresh_token;
|
||||
this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo, api_root: this.api_root });
|
||||
|
||||
return this.api.user().then(user =>
|
||||
this.api.hasWriteAccess(user).then(isCollab => {
|
||||
if (!isCollab) throw new Error("Your BitBucker user account does not have access to this repo.");
|
||||
return Object.assign({}, user, { token: state.token, refresh_token: state.refresh_token });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getRefreshedAccessToken() {
|
||||
if (this.refreshedTokenPromise) {
|
||||
return this.refreshedTokenPromise;
|
||||
}
|
||||
|
||||
// instantiating a new Authenticator on each refresh isn't ideal,
|
||||
if (!this.auth) {
|
||||
const cfg = {
|
||||
base_url: this.base_url,
|
||||
site_id: this.site_id,
|
||||
};
|
||||
this.authenticator = new Authenticator(cfg);
|
||||
}
|
||||
|
||||
this.refreshedTokenPromise = this.authenticator.refresh({ provider: "bitbucket", refresh_token: this.refreshToken })
|
||||
.then(({ token, refresh_token }) => {
|
||||
this.token = token;
|
||||
this.refreshToken = refresh_token;
|
||||
this.refreshedTokenPromise = undefined;
|
||||
this.updateUserCredentials({ token, refresh_token });
|
||||
return token;
|
||||
});
|
||||
|
||||
return this.refreshedTokenPromise;
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
return;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
if (this.refreshedTokenPromise) {
|
||||
return this.refreshedTokenPromise;
|
||||
}
|
||||
|
||||
return Promise.resolve(this.token);
|
||||
}
|
||||
|
||||
apiRequestFunction = async req => {
|
||||
const token = this.refreshedTokenPromise ? await this.refreshedTokenPromise : this.token;
|
||||
return flow([
|
||||
unsentRequest.withHeaders({ Authorization: `Bearer ${ token }` }),
|
||||
unsentRequest.performRequest,
|
||||
then(async res => {
|
||||
if (res.status === 401) {
|
||||
const json = (await res.json().catch(() => null));
|
||||
if (json && json.type === "error" && /^access token expired/i.test(json.error.message)) {
|
||||
const newToken = await this.getRefreshedAccessToken();
|
||||
const reqWithNewToken = unsentRequest.withHeaders({ Authorization: `Bearer ${ newToken }` }, req);
|
||||
return unsentRequest.performRequest(reqWithNewToken);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}),
|
||||
])(req);
|
||||
};
|
||||
|
||||
entriesByFolder(collection, extension) {
|
||||
const listPromise = this.api.listFiles(collection.get("folder"));
|
||||
return resolvePromiseProperties({
|
||||
files: listPromise
|
||||
.then(({ entries }) => entries)
|
||||
.then(filterByPropExtension(extension, "path"))
|
||||
.then(this.fetchFiles),
|
||||
cursor: listPromise.then(({ cursor }) => cursor),
|
||||
}).then(({ files, cursor }) => {
|
||||
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
||||
return files;
|
||||
});
|
||||
}
|
||||
|
||||
allEntriesByFolder(collection, extension) {
|
||||
return this.api.listAllFiles(collection.get("folder"))
|
||||
.then(filterByPropExtension(extension, "path"))
|
||||
.then(this.fetchFiles);
|
||||
}
|
||||
|
||||
entriesByFiles(collection) {
|
||||
const files = collection.get("files").map(collectionFile => ({
|
||||
path: collectionFile.get("file"),
|
||||
label: collectionFile.get("label"),
|
||||
}));
|
||||
return this.fetchFiles(files);
|
||||
}
|
||||
|
||||
fetchFiles = (files) => {
|
||||
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
const promises = [];
|
||||
files.forEach((file) => {
|
||||
promises.push(new Promise((resolve, reject) => (
|
||||
sem.take(() => this.api.readFile(file.path, file.id).then((data) => {
|
||||
resolve({ file, data });
|
||||
sem.leave();
|
||||
}).catch((error = true) => {
|
||||
sem.leave();
|
||||
console.error(`failed to load file from BitBucket: ${ file.path }`);
|
||||
resolve({ error });
|
||||
}))
|
||||
)));
|
||||
});
|
||||
return Promise.all(promises)
|
||||
.then(loadedEntries => loadedEntries.filter(loadedEntry => !loadedEntry.error));
|
||||
}
|
||||
|
||||
getEntry(collection, slug, path) {
|
||||
return this.api.readFile(path).then(data => ({
|
||||
file: { path },
|
||||
data,
|
||||
}));
|
||||
}
|
||||
|
||||
getMedia() {
|
||||
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
|
||||
return this.api.listAllFiles(this.config.get("media_folder"))
|
||||
.then(files => files.map(({ id, name, download_url, path }) => {
|
||||
const getBlobPromise = () => new Promise((resolve, reject) =>
|
||||
sem.take(() =>
|
||||
this.api.readFile(path, id, { parseText: false })
|
||||
.then(resolve, reject)
|
||||
.finally(() => sem.leave())
|
||||
)
|
||||
);
|
||||
|
||||
return { id, name, getBlobPromise, url: download_url, path };
|
||||
}));
|
||||
}
|
||||
|
||||
persistEntry(entry, mediaFiles, options = {}) {
|
||||
return this.api.persistFiles([entry], options);
|
||||
}
|
||||
|
||||
async persistMedia(mediaFile, options = {}) {
|
||||
await this.api.persistFiles([mediaFile], options);
|
||||
const { value, path, fileObj } = mediaFile;
|
||||
const getBlobPromise = () => Promise.resolve(fileObj);
|
||||
return { name: value, size: fileObj.size, getBlobPromise, path: trimStart(path, '/k') };
|
||||
}
|
||||
|
||||
deleteFile(path, commitMessage, options) {
|
||||
return this.api.deleteFile(path, commitMessage, options);
|
||||
}
|
||||
|
||||
traverseCursor(cursor, action) {
|
||||
return this.api.traverseCursor(cursor, action)
|
||||
.then(async ({ entries, cursor: newCursor }) => ({
|
||||
entries: await Promise.all(entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data })))),
|
||||
cursor: newCursor,
|
||||
}));
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import iconAdd from './add.svg';
|
||||
import iconAddWith from './add-with.svg';
|
||||
import iconArrow from './arrow.svg';
|
||||
import iconBitbucket from './bitbucket.svg';
|
||||
import iconBold from './bold.svg';
|
||||
import iconCheck from './check.svg';
|
||||
import iconChevron from './chevron.svg';
|
||||
@ -44,6 +45,7 @@ const images = {
|
||||
'add': iconAdd,
|
||||
'add-with': iconAddWith,
|
||||
'arrow': iconArrow,
|
||||
'bitbucket': iconBitbucket,
|
||||
'bold': iconBold,
|
||||
'check': iconCheck,
|
||||
'chevron': iconChevron,
|
||||
|
3
src/components/UI/Icon/images/bitbucket.svg
Normal file
3
src/components/UI/Icon/images/bitbucket.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="26px" height="26px" viewBox="0 0 26 26" version="1.1">
|
||||
<path d="M2.77580579,3.0000546 C2.58222841,2.99755793 2.39745454,3.08078757 2.27104968,3.2274172 C2.14464483,3.37404683 2.08954809,3.5690671 2.12053915,3.76016391 L4.90214605,20.6463853 C4.97368482,21.0729296 5.34116371,21.38653 5.77365069,21.3901129 L19.1181559,21.3901129 C19.4427702,21.3942909 19.7215068,21.1601522 19.7734225,20.839689 L22.5550294,3.76344024 C22.5860205,3.57234343 22.5309237,3.37732317 22.4045189,3.23069353 C22.278114,3.0840639 22.0933402,3.00083426 21.8997628,3.00333094 L2.77580579,3.0000546 Z M14.488697,15.2043958 L10.2294639,15.2043958 L9.07619457,9.17921905 L15.520742,9.17921905 L14.488697,15.2043958 Z" id="Shape" fill="#2684FF" fill-rule="nonzero"/>
|
||||
</svg>
|
After Width: | Height: | Size: 757 B |
44
src/lib/backendHelper.js
Normal file
44
src/lib/backendHelper.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { get } from "lodash";
|
||||
import { fromJS } from "immutable";
|
||||
import { fileExtension } from "Lib/pathHelper";
|
||||
import unsentRequest from "Lib/unsentRequest";
|
||||
import { CURSOR_COMPATIBILITY_SYMBOL } from "../valueObjects/Cursor";
|
||||
|
||||
export const filterByPropExtension = (extension, propName) => arr =>
|
||||
arr.filter(el => fileExtension(get(el, propName)) === extension);
|
||||
|
||||
const catchFormatErrors = (format, formatter) => res => {
|
||||
try {
|
||||
return formatter(res);
|
||||
} catch (err) {
|
||||
throw new Error(`Response cannot be parsed into the expected format (${ format }): ${ err.message }`);
|
||||
}
|
||||
};
|
||||
|
||||
const responseFormatters = fromJS({
|
||||
json: async res => {
|
||||
const contentType = res.headers.get("Content-Type");
|
||||
if (!contentType.startsWith("application/json") && !contentType.startsWith("text/json")) {
|
||||
throw new Error(`${ contentType } is not a valid JSON Content-Type`);
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
text: async res => res.text(),
|
||||
blob: async res => res.blob(),
|
||||
}).mapEntries(
|
||||
([format, formatter]) => [format, catchFormatErrors(format, formatter)]
|
||||
);
|
||||
|
||||
export const parseResponse = async (res, { expectingOk = true, format = "text" } = {}) => {
|
||||
if (expectingOk && !res.ok) {
|
||||
throw new Error(`Expected an ok response, but received an error status: ${ res.status }.`);
|
||||
}
|
||||
const formatter = responseFormatters.get(format, false);
|
||||
if (!formatter) {
|
||||
throw new Error(`${ format } is not a supported response format.`);
|
||||
}
|
||||
const body = await formatter(res);
|
||||
return body;
|
||||
};
|
||||
|
||||
export const responseParser = options => res => parseResponse(res, options);
|
@ -51,17 +51,17 @@ class Authenticator {
|
||||
|
||||
authorizeCallback(options, cb) {
|
||||
const fn = (e) => {
|
||||
var data, err;
|
||||
if (e.origin !== this.base_url) { return; }
|
||||
|
||||
if (e.data.indexOf('authorization:' + options.provider + ':success:') === 0) {
|
||||
data = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':success:(.+)$'))[1]);
|
||||
const data = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':success:(.+)$'))[1]);
|
||||
window.removeEventListener('message', fn, false);
|
||||
this.authWindow.close();
|
||||
cb(null, data);
|
||||
}
|
||||
if (e.data.indexOf('authorization:' + options.provider + ':error:') === 0) {
|
||||
console.log('Got authorization error');
|
||||
err = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':error:(.+)$'))[1]);
|
||||
const err = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':error:(.+)$'))[1]);
|
||||
window.removeEventListener('message', fn, false);
|
||||
this.authWindow.close();
|
||||
cb(new NetlifyError(err));
|
||||
@ -75,29 +75,29 @@ class Authenticator {
|
||||
return this.site_id;
|
||||
}
|
||||
const host = document.location.host.split(':')[0];
|
||||
return host === 'localhost' ? null : host;
|
||||
return host === 'localhost' ? 'cms.netlify.com' : host;
|
||||
}
|
||||
|
||||
authenticate(options, cb) {
|
||||
var left, top, url,
|
||||
siteID = this.getSiteID(),
|
||||
provider = options.provider;
|
||||
const { provider } = options;
|
||||
const siteID = this.getSiteID();
|
||||
|
||||
if (!provider) {
|
||||
return cb(new NetlifyError({
|
||||
message: 'You must specify a provider when calling netlify.authenticate'
|
||||
message: 'You must specify a provider when calling netlify.authenticate',
|
||||
}));
|
||||
}
|
||||
if (!siteID) {
|
||||
return cb(new NetlifyError({
|
||||
message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make authentication work from localhost'
|
||||
message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make authentication work from localhost',
|
||||
}));
|
||||
}
|
||||
|
||||
const conf = PROVIDERS[provider] || PROVIDERS.github;
|
||||
left = (screen.width / 2) - (conf.width / 2);
|
||||
top = (screen.height / 2) - (conf.height / 2);
|
||||
const left = (screen.width / 2) - (conf.width / 2);
|
||||
const top = (screen.height / 2) - (conf.height / 2);
|
||||
window.addEventListener('message', this.handshakeCallback(options, cb), false);
|
||||
url = `${this.base_url}/${this.auth_endpoint}?provider=${options.provider}&site_id=${siteID}`;
|
||||
let url = `${ this.base_url }/${ this.auth_endpoint }?provider=${ options.provider }&site_id=${ siteID }`;
|
||||
if (options.scope) {
|
||||
url += '&scope=' + options.scope;
|
||||
}
|
||||
@ -118,6 +118,33 @@ class Authenticator {
|
||||
);
|
||||
this.authWindow.focus();
|
||||
}
|
||||
|
||||
refresh(options, cb) {
|
||||
const { provider, refresh_token } = options;
|
||||
const siteID = this.getSiteID();
|
||||
const onError = cb || Promise.reject.bind(Promise);
|
||||
|
||||
if (!provider || !refresh_token) {
|
||||
return onError(new NetlifyError({
|
||||
message: 'You must specify a provider and refresh token when calling netlify.refresh',
|
||||
}));
|
||||
}
|
||||
if (!siteID) {
|
||||
return onError(new NetlifyError({
|
||||
message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make token refresh work from localhost',
|
||||
}));
|
||||
}
|
||||
const url = `${ this.base_url }/${ this.auth_endpoint }/refresh?provider=${ provider }&site_id=${ siteID }&refresh_token=${ refresh_token }`;
|
||||
const refreshPromise = fetch(url, { method: "POST", body: "" }).then(res => res.json());
|
||||
|
||||
// Return a promise if a callback wasn't provided
|
||||
if (!cb) {
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
// Otherwise, use the provided callback.
|
||||
refreshPromise.then(data => cb(null, data)).catch(cb);
|
||||
}
|
||||
}
|
||||
|
||||
export default Authenticator;
|
||||
|
@ -104,7 +104,7 @@ export function registerBackend(name, BackendClass) {
|
||||
console.error(`Backend [${ name }] already registered. Please choose a different name.`);
|
||||
} else {
|
||||
registry.backends[name] = {
|
||||
init: config => new BackendClass(config),
|
||||
init: (...args) => new BackendClass(...args),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ const getPropMergeFunctions = path => [
|
||||
];
|
||||
|
||||
const [withMethod, withDefaultMethod] = getPropSetFunctions(["method"]);
|
||||
const [withBody, withDefaultBody] = getPropSetFunctions(["method"]);
|
||||
const [withBody, withDefaultBody] = getPropSetFunctions(["body"]);
|
||||
const [withParams, withDefaultParams] = getPropMergeFunctions(["params"]);
|
||||
const [withHeaders, withDefaultHeaders] = getPropMergeFunctions(["headers"]);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user