Netlify auth (#194)
This commit is contained in:
parent
578ffc91df
commit
1efc59a9fb
@ -104,6 +104,7 @@
|
|||||||
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"moment": "^2.11.2",
|
"moment": "^2.11.2",
|
||||||
|
"netlify-auth-js": "^0.5.2",
|
||||||
"normalize.css": "^4.2.0",
|
"normalize.css": "^4.2.0",
|
||||||
"pluralize": "^3.0.0",
|
"pluralize": "^3.0.0",
|
||||||
"prismjs": "^1.5.1",
|
"prismjs": "^1.5.1",
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
import yaml from 'js-yaml';
|
import yaml from "js-yaml";
|
||||||
import { set, defaultsDeep } from 'lodash';
|
import { set, defaultsDeep } from "lodash";
|
||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from "../backends/backend";
|
||||||
import { authenticate } from '../actions/auth';
|
import { authenticate } from "../actions/auth";
|
||||||
import * as MediaProxy from '../valueObjects/MediaProxy';
|
import * as MediaProxy from "../valueObjects/MediaProxy";
|
||||||
import * as publishModes from '../constants/publishModes';
|
import * as publishModes from "../constants/publishModes";
|
||||||
|
|
||||||
export const CONFIG_REQUEST = 'CONFIG_REQUEST';
|
export const CONFIG_REQUEST = "CONFIG_REQUEST";
|
||||||
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
|
export const CONFIG_SUCCESS = "CONFIG_SUCCESS";
|
||||||
export const CONFIG_FAILURE = 'CONFIG_FAILURE';
|
export const CONFIG_FAILURE = "CONFIG_FAILURE";
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
publish_mode: publishModes.SIMPLE,
|
publish_mode: publishModes.SIMPLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function applyDefaults(config) {
|
export function applyDefaults(config) {
|
||||||
if (!('media_folder' in config)) {
|
if (!("media_folder" in config)) {
|
||||||
throw new Error('Error in configuration file: A `media_folder` wasn\'t found. Check your config.yml file.');
|
throw new Error("Error in configuration file: A `media_folder` wasn't found. Check your config.yml file.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there is a public folder
|
// Make sure there is a public folder
|
||||||
set(defaults,
|
set(defaults,
|
||||||
'public_folder',
|
"public_folder",
|
||||||
config.media_folder.charAt(0) === '/' ? config.media_folder : `/${ config.media_folder }`);
|
config.media_folder.charAt(0) === "/" ? config.media_folder : `/${ config.media_folder }`);
|
||||||
|
|
||||||
return defaultsDeep(config, defaults);
|
return defaultsDeep(config, defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseConfig(data) {
|
function parseConfig(data) {
|
||||||
const config = yaml.safeLoad(data);
|
const config = yaml.safeLoad(data);
|
||||||
if (typeof CMS_ENV === 'string' && config[CMS_ENV]) {
|
if (typeof CMS_ENV === "string" && config[CMS_ENV]) {
|
||||||
// TODO: Add tests and refactor
|
// TODO: Add tests and refactor
|
||||||
for (const key in config[CMS_ENV]) { // eslint-disable-line no-restricted-syntax
|
for (const key in config[CMS_ENV]) { // eslint-disable-line no-restricted-syntax
|
||||||
if (config[CMS_ENV].hasOwnProperty(key)) { // eslint-disable-line no-prototype-builtins
|
if (config[CMS_ENV].hasOwnProperty(key)) { // eslint-disable-line no-prototype-builtins
|
||||||
@ -57,7 +57,7 @@ export function configLoading() {
|
|||||||
export function configFailed(err) {
|
export function configFailed(err) {
|
||||||
return {
|
return {
|
||||||
type: CONFIG_FAILURE,
|
type: CONFIG_FAILURE,
|
||||||
error: 'Error loading config',
|
error: "Error loading config",
|
||||||
payload: err,
|
payload: err,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ export function loadConfig() {
|
|||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(configLoading());
|
dispatch(configLoading());
|
||||||
|
|
||||||
fetch('config.yml').then((response) => {
|
fetch("config.yml").then((response) => {
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw new Error(`Failed to load config.yml (${ response.status })`);
|
throw new Error(`Failed to load config.yml (${ response.status })`);
|
||||||
}
|
}
|
||||||
@ -88,7 +88,9 @@ export function loadConfig() {
|
|||||||
.then((config) => {
|
.then((config) => {
|
||||||
dispatch(configDidLoad(config));
|
dispatch(configDidLoad(config));
|
||||||
const backend = currentBackend(config);
|
const backend = currentBackend(config);
|
||||||
const user = backend && backend.currentUser();
|
return backend && backend.currentUser();
|
||||||
|
})
|
||||||
|
.then((user) => {
|
||||||
if (user) dispatch(authenticate(user));
|
if (user) dispatch(authenticate(user));
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import TestRepoBackend from './test-repo/implementation';
|
import TestRepoBackend from "./test-repo/implementation";
|
||||||
import GitHubBackend from './github/implementation';
|
import GitHubBackend from "./github/implementation";
|
||||||
import NetlifyGitBackend from './netlify-git/implementation';
|
import NetlifyGitBackend from "./netlify-git/implementation";
|
||||||
import { resolveFormat } from '../formats/formats';
|
import NetlifyAuthBackend from "./netlify-auth/implementation";
|
||||||
import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries } from '../reducers/collections';
|
import { resolveFormat } from "../formats/formats";
|
||||||
import { createEntry } from '../valueObjects/Entry';
|
import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries } from "../reducers/collections";
|
||||||
|
import { createEntry } from "../valueObjects/Entry";
|
||||||
|
|
||||||
class LocalStorageAuthStore {
|
class LocalStorageAuthStore {
|
||||||
storageKey = 'nf-cms-user';
|
storageKey = "nf-cms-user";
|
||||||
|
|
||||||
retrieve() {
|
retrieve() {
|
||||||
const data = window.localStorage.getItem(this.storageKey);
|
const data = window.localStorage.getItem(this.storageKey);
|
||||||
@ -22,21 +23,21 @@ class LocalStorageAuthStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const slugFormatter = (template = '{{slug}}', entryData) => {
|
const slugFormatter = (template = "{{slug}}", entryData) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const identifier = entryData.get('title', entryData.get('path'));
|
const identifier = entryData.get("title", entryData.get("path"));
|
||||||
return template.replace(/\{\{([^\}]+)\}\}/g, (_, field) => {
|
return template.replace(/\{\{([^\}]+)\}\}/g, (_, field) => {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'year':
|
case "year":
|
||||||
return date.getFullYear();
|
return date.getFullYear();
|
||||||
case 'month':
|
case "month":
|
||||||
return (`0${ date.getMonth() + 1 }`).slice(-2);
|
return (`0${ date.getMonth() + 1 }`).slice(-2);
|
||||||
case 'day':
|
case "day":
|
||||||
return (`0${ date.getDate() }`).slice(-2);
|
return (`0${ date.getDate() }`).slice(-2);
|
||||||
case 'slug':
|
case "slug":
|
||||||
return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, '-');
|
return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, "-");
|
||||||
default:
|
default:
|
||||||
return entryData.get(field, '').trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, '-');
|
return entryData.get(field, "").trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, "-");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -46,7 +47,7 @@ class Backend {
|
|||||||
this.implementation = implementation;
|
this.implementation = implementation;
|
||||||
this.authStore = authStore;
|
this.authStore = authStore;
|
||||||
if (this.implementation === null) {
|
if (this.implementation === null) {
|
||||||
throw new Error('Cannot instantiate a Backend with no implementation');
|
throw new Error("Cannot instantiate a Backend with no implementation");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,10 +55,9 @@ class Backend {
|
|||||||
if (this.user) { return this.user; }
|
if (this.user) { return this.user; }
|
||||||
const stored = this.authStore && this.authStore.retrieve();
|
const stored = this.authStore && this.authStore.retrieve();
|
||||||
if (stored) {
|
if (stored) {
|
||||||
this.implementation.setUser(stored);
|
return Promise.resolve(this.implementation.setUser(stored)).then(() => stored);
|
||||||
return stored;
|
|
||||||
}
|
}
|
||||||
return null;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
authComponent() {
|
authComponent() {
|
||||||
@ -75,7 +75,7 @@ class Backend {
|
|||||||
if (this.authStore) {
|
if (this.authStore) {
|
||||||
this.authStore.logout();
|
this.authStore.logout();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('User isn\'t authenticated.');
|
throw new Error("User isn't authenticated.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class Backend {
|
|||||||
return listMethod.call(this.implementation, collection)
|
return listMethod.call(this.implementation, collection)
|
||||||
.then(loadedEntries => (
|
.then(loadedEntries => (
|
||||||
loadedEntries.map(loadedEntry => createEntry(
|
loadedEntries.map(loadedEntry => createEntry(
|
||||||
collection.get('name'),
|
collection.get("name"),
|
||||||
selectEntrySlug(collection, loadedEntry.file.path),
|
selectEntrySlug(collection, loadedEntry.file.path),
|
||||||
loadedEntry.file.path,
|
loadedEntry.file.path,
|
||||||
{ raw: loadedEntry.data, label: loadedEntry.file.label }
|
{ raw: loadedEntry.data, label: loadedEntry.file.label }
|
||||||
@ -100,7 +100,7 @@ class Backend {
|
|||||||
getEntry(collection, slug) {
|
getEntry(collection, slug) {
|
||||||
return this.implementation.getEntry(collection, slug, selectEntryPath(collection, slug))
|
return this.implementation.getEntry(collection, slug, selectEntryPath(collection, slug))
|
||||||
.then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry(
|
.then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry(
|
||||||
collection.get('name'),
|
collection.get("name"),
|
||||||
slug,
|
slug,
|
||||||
loadedEntry.file.path,
|
loadedEntry.file.path,
|
||||||
{ raw: loadedEntry.data, label: loadedEntry.file.label }
|
{ raw: loadedEntry.data, label: loadedEntry.file.label }
|
||||||
@ -123,21 +123,21 @@ class Backend {
|
|||||||
.then(loadedEntries => loadedEntries.filter(entry => entry !== null))
|
.then(loadedEntries => loadedEntries.filter(entry => entry !== null))
|
||||||
.then(entries => (
|
.then(entries => (
|
||||||
entries.map((loadedEntry) => {
|
entries.map((loadedEntry) => {
|
||||||
const entry = createEntry('draft', loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data });
|
const entry = createEntry("draft", loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data });
|
||||||
entry.metaData = loadedEntry.metaData;
|
entry.metaData = loadedEntry.metaData;
|
||||||
return entry;
|
return entry;
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
.then(entries => ({
|
.then(entries => ({
|
||||||
pagination: 0,
|
pagination: 0,
|
||||||
entries: entries.map(this.entryWithFormat('editorialWorkflow')),
|
entries: entries.map(this.entryWithFormat("editorialWorkflow")),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
unpublishedEntry(collection, slug) {
|
unpublishedEntry(collection, slug) {
|
||||||
return this.implementation.unpublishedEntry(collection, slug)
|
return this.implementation.unpublishedEntry(collection, slug)
|
||||||
.then((loadedEntry) => {
|
.then((loadedEntry) => {
|
||||||
const entry = createEntry('draft', loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data });
|
const entry = createEntry("draft", loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data });
|
||||||
entry.metaData = loadedEntry.metaData;
|
entry.metaData = loadedEntry.metaData;
|
||||||
return entry;
|
return entry;
|
||||||
})
|
})
|
||||||
@ -145,20 +145,20 @@ class Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
persistEntry(config, collection, entryDraft, MediaFiles, options) {
|
persistEntry(config, collection, entryDraft, MediaFiles, options) {
|
||||||
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
const newEntry = entryDraft.getIn(["entry", "newRecord"]) || false;
|
||||||
|
|
||||||
const parsedData = {
|
const parsedData = {
|
||||||
title: entryDraft.getIn(['entry', 'data', 'title'], 'No Title'),
|
title: entryDraft.getIn(["entry", "data", "title"], "No Title"),
|
||||||
description: entryDraft.getIn(['entry', 'data', 'description'], 'No Description!'),
|
description: entryDraft.getIn(["entry", "data", "description"], "No Description!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const entryData = entryDraft.getIn(['entry', 'data']).toJS();
|
const entryData = entryDraft.getIn(["entry", "data"]).toJS();
|
||||||
let entryObj;
|
let entryObj;
|
||||||
if (newEntry) {
|
if (newEntry) {
|
||||||
if (!selectAllowNewEntries(collection)) {
|
if (!selectAllowNewEntries(collection)) {
|
||||||
throw (new Error('Not allowed to create new entries in this collection'));
|
throw (new Error("Not allowed to create new entries in this collection"));
|
||||||
}
|
}
|
||||||
const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data']));
|
const slug = slugFormatter(collection.get("slug"), entryDraft.getIn(["entry", "data"]));
|
||||||
const path = selectEntryPath(collection, slug);
|
const path = selectEntryPath(collection, slug);
|
||||||
entryObj = {
|
entryObj = {
|
||||||
path,
|
path,
|
||||||
@ -166,20 +166,20 @@ class Backend {
|
|||||||
raw: this.entryToRaw(collection, entryData),
|
raw: this.entryToRaw(collection, entryData),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const path = entryDraft.getIn(['entry', 'path']);
|
const path = entryDraft.getIn(["entry", "path"]);
|
||||||
entryObj = {
|
entryObj = {
|
||||||
path,
|
path,
|
||||||
slug: entryDraft.getIn(['entry', 'slug']),
|
slug: entryDraft.getIn(["entry", "slug"]),
|
||||||
raw: this.entryToRaw(collection, entryData),
|
raw: this.entryToRaw(collection, entryData),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitMessage = `${ (newEntry ? 'Created ' : 'Updated ') +
|
const commitMessage = `${ (newEntry ? "Created " : "Updated ") +
|
||||||
collection.get('label') } “${ entryObj.slug }”`;
|
collection.get("label") } “${ entryObj.slug }”`;
|
||||||
|
|
||||||
const mode = config.get('publish_mode');
|
const mode = config.get("publish_mode");
|
||||||
|
|
||||||
const collectionName = collection.get('name');
|
const collectionName = collection.get("name");
|
||||||
|
|
||||||
return this.implementation.persistEntry(entryObj, MediaFiles, {
|
return this.implementation.persistEntry(entryObj, MediaFiles, {
|
||||||
newEntry, parsedData, commitMessage, collectionName, mode, ...options,
|
newEntry, parsedData, commitMessage, collectionName, mode, ...options,
|
||||||
@ -206,20 +206,22 @@ class Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveBackend(config) {
|
export function resolveBackend(config) {
|
||||||
const name = config.getIn(['backend', 'name']);
|
const name = config.getIn(["backend", "name"]);
|
||||||
if (name == null) {
|
if (name == null) {
|
||||||
throw new Error('No backend defined in configuration');
|
throw new Error("No backend defined in configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
const authStore = new LocalStorageAuthStore();
|
const authStore = new LocalStorageAuthStore();
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'test-repo':
|
case "test-repo":
|
||||||
return new Backend(new TestRepoBackend(config), authStore);
|
return new Backend(new TestRepoBackend(config), authStore);
|
||||||
case 'github':
|
case "github":
|
||||||
return new Backend(new GitHubBackend(config), authStore);
|
return new Backend(new GitHubBackend(config), authStore);
|
||||||
case 'netlify-git':
|
case "netlify-git":
|
||||||
return new Backend(new NetlifyGitBackend(config), authStore);
|
return new Backend(new NetlifyGitBackend(config), authStore);
|
||||||
|
case "netlify-auth":
|
||||||
|
return new Backend(new NetlifyAuthBackend(config), authStore);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Backend not found: ${ name }`);
|
throw new Error(`Backend not found: ${ name }`);
|
||||||
}
|
}
|
||||||
@ -230,7 +232,7 @@ export const currentBackend = (function () {
|
|||||||
|
|
||||||
return (config) => {
|
return (config) => {
|
||||||
if (backend) { return backend; }
|
if (backend) { return backend; }
|
||||||
if (config.get('backend')) {
|
if (config.get("backend")) {
|
||||||
return backend = resolveBackend(config);
|
return backend = resolveBackend(config);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,29 +1,34 @@
|
|||||||
import LocalForage from 'localforage';
|
import LocalForage from "localforage";
|
||||||
import MediaProxy from '../../valueObjects/MediaProxy';
|
import { Base64 } from "js-base64";
|
||||||
import { Base64 } from 'js-base64';
|
import _ from "lodash";
|
||||||
import _ from 'lodash';
|
import MediaProxy from "../../valueObjects/MediaProxy";
|
||||||
import { SIMPLE, EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
import { SIMPLE, EDITORIAL_WORKFLOW, status } from "../../constants/publishModes";
|
||||||
|
|
||||||
const API_ROOT = 'https://api.github.com';
|
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
constructor(token, repo, branch) {
|
constructor(config) {
|
||||||
this.token = token;
|
this.api_root = config.api_root || "https://api.github.com";
|
||||||
this.repo = repo;
|
this.token = config.token || false;
|
||||||
this.branch = branch;
|
this.branch = config.branch || "master";
|
||||||
|
this.repo = config.repo || "";
|
||||||
this.repoURL = `/repos/${ this.repo }`;
|
this.repoURL = `/repos/${ this.repo }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
user() {
|
user() {
|
||||||
return this.request('/user');
|
return this.request("/user");
|
||||||
}
|
}
|
||||||
|
|
||||||
requestHeaders(headers = {}) {
|
requestHeaders(headers = {}) {
|
||||||
return {
|
const baseHeader = {
|
||||||
Authorization: `token ${ this.token }`,
|
"Content-Type": "application/json",
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers,
|
...headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.token) {
|
||||||
|
baseHeader.Authorization = `token ${ this.token }`;
|
||||||
|
return baseHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseJsonResponse(response) {
|
parseJsonResponse(response) {
|
||||||
@ -44,16 +49,16 @@ export default class API {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (params.length) {
|
if (params.length) {
|
||||||
path += `?${ params.join('&') }`;
|
path += `?${ params.join("&") }`;
|
||||||
}
|
}
|
||||||
return API_ROOT + path;
|
return this.api_root + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
request(path, options = {}) {
|
request(path, options = {}) {
|
||||||
const headers = this.requestHeaders(options.headers || {});
|
const headers = this.requestHeaders(options.headers || {});
|
||||||
const url = this.urlFor(path, options);
|
const url = this.urlFor(path, options);
|
||||||
return fetch(url, { ...options, headers }).then((response) => {
|
return fetch(url, { ...options, headers }).then((response) => {
|
||||||
const contentType = response.headers.get('Content-Type');
|
const contentType = response.headers.get("Content-Type");
|
||||||
if (contentType && contentType.match(/json/)) {
|
if (contentType && contentType.match(/json/)) {
|
||||||
return this.parseJsonResponse(response);
|
return this.parseJsonResponse(response);
|
||||||
}
|
}
|
||||||
@ -64,27 +69,28 @@ export default class API {
|
|||||||
|
|
||||||
checkMetadataRef() {
|
checkMetadataRef() {
|
||||||
return this.request(`${ this.repoURL }/git/refs/meta/_netlify_cms?${ Date.now() }`, {
|
return this.request(`${ this.repoURL }/git/refs/meta/_netlify_cms?${ Date.now() }`, {
|
||||||
cache: 'no-store',
|
cache: "no-store",
|
||||||
})
|
})
|
||||||
.then(response => response.object)
|
.then(response => response.object)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
// Meta ref doesn't exist
|
// Meta ref doesn't exist
|
||||||
const readme = {
|
const readme = {
|
||||||
raw: '# Netlify CMS\n\nThis tree is used by the Netlify CMS to store metadata information for specific files and branches.',
|
raw: "# Netlify CMS\n\nThis tree is used by the Netlify CMS to store metadata information for specific files and branches.",
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.uploadBlob(readme)
|
return this.uploadBlob(readme)
|
||||||
.then(item => this.request(`${ this.repoURL }/git/trees`, {
|
.then(item => this.request(`${ this.repoURL }/git/trees`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({ tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }] }),
|
body: JSON.stringify({ tree: [{ path: "README.md", mode: "100644", type: "blob", sha: item.sha }] }),
|
||||||
}))
|
}))
|
||||||
.then(tree => this.commit('First Commit', tree))
|
.then(tree => this.commit("First Commit", tree))
|
||||||
.then(response => this.createRef('meta', '_netlify_cms', response.sha))
|
.then(response => this.createRef("meta", "_netlify_cms", response.sha))
|
||||||
.then(response => response.object);
|
.then(response => response.object);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
storeMetadata(key, data) {
|
storeMetadata(key, data) {
|
||||||
|
console.log('Trying to store Metadata');
|
||||||
return this.checkMetadataRef()
|
return this.checkMetadataRef()
|
||||||
.then((branchData) => {
|
.then((branchData) => {
|
||||||
const fileTree = {
|
const fileTree = {
|
||||||
@ -96,9 +102,9 @@ export default class API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return this.uploadBlob(fileTree[`${ key }.json`])
|
return this.uploadBlob(fileTree[`${ key }.json`])
|
||||||
.then(item => this.updateTree(branchData.sha, '/', fileTree))
|
.then(item => this.updateTree(branchData.sha, "/", fileTree))
|
||||||
.then(changeTree => this.commit(`Updating “${ key }” metadata`, changeTree))
|
.then(changeTree => this.commit(`Updating “${ key }” metadata`, changeTree))
|
||||||
.then(response => this.patchRef('meta', '_netlify_cms', response.sha))
|
.then(response => this.patchRef("meta", "_netlify_cms", response.sha))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
LocalForage.setItem(`gh.meta.${ key }`, {
|
LocalForage.setItem(`gh.meta.${ key }`, {
|
||||||
expires: Date.now() + 300000, // In 5 minutes
|
expires: Date.now() + 300000, // In 5 minutes
|
||||||
@ -114,9 +120,9 @@ export default class API {
|
|||||||
if (cached && cached.expires > Date.now()) { return cached.data; }
|
if (cached && cached.expires > Date.now()) { return cached.data; }
|
||||||
console.log("%c Checking for MetaData files", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line
|
console.log("%c Checking for MetaData files", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line
|
||||||
return this.request(`${ this.repoURL }/contents/${ key }.json`, {
|
return this.request(`${ this.repoURL }/contents/${ key }.json`, {
|
||||||
params: { ref: 'refs/meta/_netlify_cms' },
|
params: { ref: "refs/meta/_netlify_cms" },
|
||||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
headers: { Accept: "application/vnd.github.VERSION.raw" },
|
||||||
cache: 'no-store',
|
cache: "no-store",
|
||||||
})
|
})
|
||||||
.then(response => JSON.parse(response))
|
.then(response => JSON.parse(response))
|
||||||
.catch(error => console.log("%c %s does not have metadata", "line-height: 30px;text-align: center;font-weight: bold", key)); // eslint-disable-line
|
.catch(error => console.log("%c %s does not have metadata", "line-height: 30px;text-align: center;font-weight: bold", key)); // eslint-disable-line
|
||||||
@ -129,7 +135,7 @@ export default class API {
|
|||||||
if (cached) { return cached; }
|
if (cached) { return cached; }
|
||||||
|
|
||||||
return this.request(`${ this.repoURL }/contents/${ path }`, {
|
return this.request(`${ this.repoURL }/contents/${ path }`, {
|
||||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
headers: { Accept: "application/vnd.github.VERSION.raw" },
|
||||||
params: { ref: branch },
|
params: { ref: branch },
|
||||||
cache: false,
|
cache: false,
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
@ -155,9 +161,7 @@ export default class API {
|
|||||||
return this.readFile(data.objects.entry, null, data.branch);
|
return this.readFile(data.objects.entry, null, data.branch);
|
||||||
})
|
})
|
||||||
.then(fileData => ({ metaData, fileData }))
|
.then(fileData => ({ metaData, fileData }))
|
||||||
.catch((error) => {
|
.catch(error => null);
|
||||||
return null;
|
|
||||||
});
|
|
||||||
return unpublishedPromise;
|
return unpublishedPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +182,7 @@ export default class API {
|
|||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
if (file.uploaded) { return; }
|
if (file.uploaded) { return; }
|
||||||
uploadPromises.push(this.uploadBlob(file));
|
uploadPromises.push(this.uploadBlob(file));
|
||||||
parts = file.path.split('/').filter(part => part);
|
parts = file.path.split("/").filter(part => part);
|
||||||
filename = parts.pop();
|
filename = parts.pop();
|
||||||
subtree = fileTree;
|
subtree = fileTree;
|
||||||
while (part = parts.shift()) {
|
while (part = parts.shift()) {
|
||||||
@ -191,7 +195,7 @@ export default class API {
|
|||||||
return Promise.all(uploadPromises).then(() => {
|
return Promise.all(uploadPromises).then(() => {
|
||||||
if (!options.mode || (options.mode && options.mode === SIMPLE)) {
|
if (!options.mode || (options.mode && options.mode === SIMPLE)) {
|
||||||
return this.getBranch()
|
return this.getBranch()
|
||||||
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
|
.then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree))
|
||||||
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
||||||
.then(response => this.patchBranch(this.branch, response.sha));
|
.then(response => this.patchBranch(this.branch, response.sha));
|
||||||
} else if (options.mode && options.mode === EDITORIAL_WORKFLOW) {
|
} else if (options.mode && options.mode === EDITORIAL_WORKFLOW) {
|
||||||
@ -212,16 +216,13 @@ export default class API {
|
|||||||
const branchName = `cms/${ contentKey }`;
|
const branchName = `cms/${ contentKey }`;
|
||||||
|
|
||||||
return this.getBranch()
|
return this.getBranch()
|
||||||
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
|
.then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree))
|
||||||
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
||||||
.then(commitResponse => this.createBranch(branchName, commitResponse.sha))
|
.then(commitResponse => this.createBranch(branchName, commitResponse.sha))
|
||||||
.then(branchResponse => this.createPR(options.commitMessage, branchName))
|
.then(branchResponse => this.createPR(options.commitMessage, branchName))
|
||||||
.then((prResponse) => {
|
.then(prResponse => this.user().then(user => user.name ? user.name : user.login)
|
||||||
return this.user().then((user) => {
|
|
||||||
return user.name ? user.name : user.login;
|
|
||||||
})
|
|
||||||
.then(username => this.storeMetadata(contentKey, {
|
.then(username => this.storeMetadata(contentKey, {
|
||||||
type: 'PR',
|
type: "PR",
|
||||||
pr: {
|
pr: {
|
||||||
number: prResponse.number,
|
number: prResponse.number,
|
||||||
head: prResponse.head && prResponse.head.sha,
|
head: prResponse.head && prResponse.head.sha,
|
||||||
@ -237,19 +238,16 @@ export default class API {
|
|||||||
files: filesList,
|
files: filesList,
|
||||||
},
|
},
|
||||||
timeStamp: new Date().toISOString(),
|
timeStamp: new Date().toISOString(),
|
||||||
}));
|
})));
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Entry is already on editorial review workflow - just update metadata and commit to existing branch
|
// Entry is already on editorial review workflow - just update metadata and commit to existing branch
|
||||||
return this.getBranch(branchName)
|
return this.getBranch(branchName)
|
||||||
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
|
.then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree))
|
||||||
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const contentKey = entry.slug;
|
const contentKey = entry.slug;
|
||||||
const branchName = `cms/${ contentKey }`;
|
const branchName = `cms/${ contentKey }`;
|
||||||
return this.user().then((user) => {
|
return this.user().then(user => user.name ? user.name : user.login)
|
||||||
return user.name ? user.name : user.login;
|
|
||||||
})
|
|
||||||
.then(username => this.retrieveMetadata(contentKey))
|
.then(username => this.retrieveMetadata(contentKey))
|
||||||
.then((metadata) => {
|
.then((metadata) => {
|
||||||
let files = metadata.objects && metadata.objects.files || [];
|
let files = metadata.objects && metadata.objects.files || [];
|
||||||
@ -275,12 +273,10 @@ export default class API {
|
|||||||
updateUnpublishedEntryStatus(collection, slug, status) {
|
updateUnpublishedEntryStatus(collection, slug, status) {
|
||||||
const contentKey = slug;
|
const contentKey = slug;
|
||||||
return this.retrieveMetadata(contentKey)
|
return this.retrieveMetadata(contentKey)
|
||||||
.then((metadata) => {
|
.then(metadata => ({
|
||||||
return {
|
...metadata,
|
||||||
...metadata,
|
status,
|
||||||
status,
|
}))
|
||||||
};
|
|
||||||
})
|
|
||||||
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata));
|
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,21 +293,21 @@ export default class API {
|
|||||||
|
|
||||||
createRef(type, name, sha) {
|
createRef(type, name, sha) {
|
||||||
return this.request(`${ this.repoURL }/git/refs`, {
|
return this.request(`${ this.repoURL }/git/refs`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({ ref: `refs/${ type }/${ name }`, sha }),
|
body: JSON.stringify({ ref: `refs/${ type }/${ name }`, sha }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
patchRef(type, name, sha) {
|
patchRef(type, name, sha) {
|
||||||
return this.request(`${ this.repoURL }/git/refs/${ type }/${ name }`, {
|
return this.request(`${ this.repoURL }/git/refs/${ type }/${ name }`, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
body: JSON.stringify({ sha }),
|
body: JSON.stringify({ sha }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRef(type, name, sha) {
|
deleteRef(type, name, sha) {
|
||||||
return this.request(`${ this.repoURL }/git/refs/${ type }/${ name }`, {
|
return this.request(`${ this.repoURL }/git/refs/${ type }/${ name }`, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,30 +316,30 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createBranch(branchName, sha) {
|
createBranch(branchName, sha) {
|
||||||
return this.createRef('heads', branchName, sha);
|
return this.createRef("heads", branchName, sha);
|
||||||
}
|
}
|
||||||
|
|
||||||
patchBranch(branchName, sha) {
|
patchBranch(branchName, sha) {
|
||||||
return this.patchRef('heads', branchName, sha);
|
return this.patchRef("heads", branchName, sha);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBranch(branchName) {
|
deleteBranch(branchName) {
|
||||||
return this.deleteRef('heads', branchName);
|
return this.deleteRef("heads", branchName);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPR(title, head, base = 'master') {
|
createPR(title, head, base = "master") {
|
||||||
const body = 'Automatically generated by Netlify CMS';
|
const body = "Automatically generated by Netlify CMS";
|
||||||
return this.request(`${ this.repoURL }/pulls`, {
|
return this.request(`${ this.repoURL }/pulls`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({ title, body, head, base }),
|
body: JSON.stringify({ title, body, head, base }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mergePR(headSha, number) {
|
mergePR(headSha, number) {
|
||||||
return this.request(`${ this.repoURL }/pulls/${ number }/merge`, {
|
return this.request(`${ this.repoURL }/pulls/${ number }/merge`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
commit_message: 'Automatically generated. Merged on Netlify CMS.',
|
commit_message: "Automatically generated. Merged on Netlify CMS.",
|
||||||
sha: headSha,
|
sha: headSha,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -362,19 +358,17 @@ export default class API {
|
|||||||
uploadBlob(item) {
|
uploadBlob(item) {
|
||||||
const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw);
|
const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw);
|
||||||
|
|
||||||
return content.then((contentBase64) => {
|
return content.then(contentBase64 => this.request(`${ this.repoURL }/git/blobs`, {
|
||||||
return this.request(`${ this.repoURL }/git/blobs`, {
|
method: "POST",
|
||||||
method: 'POST',
|
body: JSON.stringify({
|
||||||
body: JSON.stringify({
|
content: contentBase64,
|
||||||
content: contentBase64,
|
encoding: "base64",
|
||||||
encoding: 'base64',
|
}),
|
||||||
}),
|
}).then((response) => {
|
||||||
}).then((response) => {
|
item.sha = response.sha;
|
||||||
item.sha = response.sha;
|
item.uploaded = true;
|
||||||
item.uploaded = true;
|
return item;
|
||||||
return item;
|
}));
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTree(sha, path, fileTree) {
|
updateTree(sha, path, fileTree) {
|
||||||
@ -402,19 +396,15 @@ export default class API {
|
|||||||
if (added[filename]) { continue; }
|
if (added[filename]) { continue; }
|
||||||
updates.push(
|
updates.push(
|
||||||
fileOrDir.file ?
|
fileOrDir.file ?
|
||||||
{ path: filename, mode: '100644', type: 'blob', sha: fileOrDir.sha } :
|
{ path: filename, mode: "100644", type: "blob", sha: fileOrDir.sha } :
|
||||||
this.updateTree(null, filename, fileOrDir)
|
this.updateTree(null, filename, fileOrDir)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Promise.all(updates)
|
return Promise.all(updates)
|
||||||
.then((updates) => {
|
.then(updates => this.request(`${ this.repoURL }/git/trees`, {
|
||||||
return this.request(`${ this.repoURL }/git/trees`, {
|
method: "POST",
|
||||||
method: 'POST',
|
body: JSON.stringify({ base_tree: sha, tree: updates }),
|
||||||
body: JSON.stringify({ base_tree: sha, tree: updates }),
|
})).then(response => ({ path, mode: "040000", type: "tree", sha: response.sha, parentSha: sha }));
|
||||||
});
|
|
||||||
}).then((response) => {
|
|
||||||
return { path, mode: '040000', type: 'tree', sha: response.sha, parentSha: sha };
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +412,7 @@ export default class API {
|
|||||||
const tree = changeTree.sha;
|
const tree = changeTree.sha;
|
||||||
const parents = changeTree.parentSha ? [changeTree.parentSha] : [];
|
const parents = changeTree.parentSha ? [changeTree.parentSha] : [];
|
||||||
return this.request(`${ this.repoURL }/git/commits`, {
|
return this.request(`${ this.repoURL }/git/commits`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({ message, tree, parents }),
|
body: JSON.stringify({ message, tree, parents }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import semaphore from 'semaphore';
|
import semaphore from "semaphore";
|
||||||
import AuthenticationPage from './AuthenticationPage';
|
import AuthenticationPage from "./AuthenticationPage";
|
||||||
import API from './API';
|
import API from "./API";
|
||||||
|
|
||||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||||
|
|
||||||
export default class GitHub {
|
export default class GitHub {
|
||||||
constructor(config) {
|
constructor(config, proxied = false) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
if (config.getIn(['backend', 'repo']) == null) {
|
|
||||||
throw new Error('The GitHub backend needs a "repo" in the backend configuration.');
|
if (!proxied && config.getIn(["backend", "repo"]) == null) {
|
||||||
|
throw new Error("The GitHub backend needs a \"repo\" in the backend configuration.");
|
||||||
}
|
}
|
||||||
this.repo = config.getIn(['backend', 'repo']);
|
|
||||||
this.branch = config.getIn(['backend', 'branch']) || 'master';
|
this.repo = config.getIn(["backend", "repo"], "");
|
||||||
|
this.branch = config.getIn(["backend", "branch"], "master");
|
||||||
}
|
}
|
||||||
|
|
||||||
authComponent() {
|
authComponent() {
|
||||||
@ -19,11 +21,11 @@ export default class GitHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setUser(user) {
|
setUser(user) {
|
||||||
this.api = new API(user.token, this.repo, this.branch || 'master');
|
this.api = new API({ token: user.token, branch: this.branch, repo: this.repo });
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate(state) {
|
authenticate(state) {
|
||||||
this.api = new API(state.token, this.repo, this.branch || 'master');
|
this.api = new API({ token: state.token, branch: this.branch, repo: this.repo });
|
||||||
return this.api.user().then((user) => {
|
return this.api.user().then((user) => {
|
||||||
user.token = state.token;
|
user.token = state.token;
|
||||||
return user;
|
return user;
|
||||||
@ -31,14 +33,14 @@ export default class GitHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entriesByFolder(collection) {
|
entriesByFolder(collection) {
|
||||||
return this.api.listFiles(collection.get('folder'))
|
return this.api.listFiles(collection.get("folder"))
|
||||||
.then(this.fetchFiles);
|
.then(this.fetchFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
entriesByFiles(collection) {
|
entriesByFiles(collection) {
|
||||||
const files = collection.get('files').map(collectionFile => ({
|
const files = collection.get("files").map(collectionFile => ({
|
||||||
path: collectionFile.get('file'),
|
path: collectionFile.get("file"),
|
||||||
label: collectionFile.get('label'),
|
label: collectionFile.get("label"),
|
||||||
}));
|
}));
|
||||||
return this.fetchFiles(files);
|
return this.fetchFiles(files);
|
||||||
}
|
}
|
||||||
@ -78,7 +80,7 @@ export default class GitHub {
|
|||||||
const promises = [];
|
const promises = [];
|
||||||
branches.map((branch) => {
|
branches.map((branch) => {
|
||||||
promises.push(new Promise((resolve, reject) => {
|
promises.push(new Promise((resolve, reject) => {
|
||||||
const slug = branch.ref.split('refs/heads/cms/').pop();
|
const slug = branch.ref.split("refs/heads/cms/").pop();
|
||||||
return sem.take(() => this.api.readUnpublishedBranchFile(slug).then((data) => {
|
return sem.take(() => this.api.readUnpublishedBranchFile(slug).then((data) => {
|
||||||
if (data === null || data === undefined) {
|
if (data === null || data === undefined) {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@ -102,7 +104,7 @@ export default class GitHub {
|
|||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.message === 'Not Found') {
|
if (error.message === "Not Found") {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
return error;
|
return error;
|
||||||
|
74
src/backends/netlify-auth/API.js
Normal file
74
src/backends/netlify-auth/API.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import GithubAPI from "../github/API";
|
||||||
|
|
||||||
|
export default class API extends GithubAPI {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.api_root = config.api_root;
|
||||||
|
this.jwtToken = config.jwtToken;
|
||||||
|
this.commitAuthor = config.commitAuthor;
|
||||||
|
this.repoURL = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
requestHeaders(headers = {}) {
|
||||||
|
const baseHeader = {
|
||||||
|
Authorization: `Bearer ${ this.jwtToken }`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
return baseHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
urlFor(path, options) {
|
||||||
|
const params = [];
|
||||||
|
if (options.params) {
|
||||||
|
for (const key in options.params) {
|
||||||
|
params.push(`${ key }=${ encodeURIComponent(options.params[key]) }`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params.length) {
|
||||||
|
path += `?${ params.join("&") }`;
|
||||||
|
}
|
||||||
|
return this.api_root + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
user() {
|
||||||
|
return Promise.resolve(this.commitAuthor);
|
||||||
|
}
|
||||||
|
|
||||||
|
request(path, options = {}) {
|
||||||
|
const headers = this.requestHeaders(options.headers || {});
|
||||||
|
const url = this.urlFor(path, options);
|
||||||
|
return fetch(url, { ...options, headers }).then((response) => {
|
||||||
|
const contentType = response.headers.get("Content-Type");
|
||||||
|
if (contentType && contentType.match(/json/)) {
|
||||||
|
return this.parseJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
commit(message, changeTree) {
|
||||||
|
const commitParams = {
|
||||||
|
message,
|
||||||
|
tree: changeTree.sha,
|
||||||
|
parents: changeTree.parentSha ? [changeTree.parentSha] : [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.commitAuthor) {
|
||||||
|
commitParams.author = {
|
||||||
|
...this.commitAuthor,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request("/git/commits", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(commitParams),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
src/backends/netlify-auth/AuthenticationPage.css
Normal file
21
src/backends/netlify-auth/AuthenticationPage.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 350px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card img {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: .25em 1em;
|
||||||
|
height: auto;
|
||||||
|
}
|
50
src/backends/netlify-auth/AuthenticationPage.js
Normal file
50
src/backends/netlify-auth/AuthenticationPage.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Input from "react-toolbox/lib/input";
|
||||||
|
import Button from "react-toolbox/lib/button";
|
||||||
|
import { Card, Icon } from "../../components/UI";
|
||||||
|
import logo from "./netlify_logo.svg";
|
||||||
|
import styles from "./AuthenticationPage.css";
|
||||||
|
|
||||||
|
export default class AuthenticationPage extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onLogin: React.PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = { username: "", password: "" };
|
||||||
|
|
||||||
|
handleChange = (name, value) => {
|
||||||
|
this.setState({ ...this.state, [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLogin = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
AuthenticationPage.authClient.login(this.state.username, this.state.password, true)
|
||||||
|
.then((user) => {
|
||||||
|
this.props.onLogin(user);
|
||||||
|
})
|
||||||
|
.catch((err) => { throw err; });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loginError } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.root}>
|
||||||
|
{loginError && <p>{loginError}</p>}
|
||||||
|
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<img src={logo} width={100} />
|
||||||
|
<Input type="text" label="Username" name="username" value={this.state.username} onChange={this.handleChange.bind(this, "username")} />
|
||||||
|
<Input type="password" label="Password" name="password" value={this.state.password} onChange={this.handleChange.bind(this, "password")} />
|
||||||
|
<Button
|
||||||
|
className={styles.button}
|
||||||
|
raised
|
||||||
|
onClick={this.handleLogin}
|
||||||
|
>
|
||||||
|
<Icon type="login" /> Login
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
src/backends/netlify-auth/implementation.js
Normal file
48
src/backends/netlify-auth/implementation.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import NetlifyAuthClient from "netlify-auth-js";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
import GitHubBackend from "../github/implementation";
|
||||||
|
import API from "./API";
|
||||||
|
import AuthenticationPage from "./AuthenticationPage";
|
||||||
|
|
||||||
|
export default class NetlifyAuth extends GitHubBackend {
|
||||||
|
constructor(config) {
|
||||||
|
super(config, true);
|
||||||
|
if (config.getIn(["backend", "auth_url"]) == null) { throw new Error("The NetlifyAuth backend needs an \"auth_url\" in the backend configuration."); }
|
||||||
|
|
||||||
|
if (config.getIn(["backend", "github_proxy_url"]) == null) {
|
||||||
|
throw new Error("The NetlifyAuth backend needs an \"github_proxy_url\" in the backend configuration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.github_proxy_url = config.getIn(["backend", "github_proxy_url"]);
|
||||||
|
|
||||||
|
this.authClient = new NetlifyAuthClient({
|
||||||
|
APIUrl: config.getIn(["backend", "auth_url"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
AuthenticationPage.authClient = this.authClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser() {
|
||||||
|
const user = this.authClient.currentUser();
|
||||||
|
if (!user) return Promise.reject();
|
||||||
|
|
||||||
|
return this.authenticate(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate(user) {
|
||||||
|
return user.jwt().then((token) => {
|
||||||
|
const userData = {
|
||||||
|
name: `${ user.user_metadata.firstname } ${ user.user_metadata.lastname }`,
|
||||||
|
email: user.email,
|
||||||
|
metadata: user.user_metadata,
|
||||||
|
};
|
||||||
|
this.api = new API({ api_root: this.github_proxy_url, jwtToken: token, commitAuthor: omit(userData, ["metadata"]) });
|
||||||
|
return userData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
authComponent() {
|
||||||
|
return AuthenticationPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
src/backends/netlify-auth/netlify_logo.svg
Normal file
23
src/backends/netlify-auth/netlify_logo.svg
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="295px" height="284px" viewBox="0 0 295 284" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<title>Netlify</title>
|
||||||
|
<g transform="translate(149.500000, 142.500000) rotate(-315.000000) translate(-149.500000, -142.500000) translate(45.000000, 38.000000)">
|
||||||
|
<g transform="translate(4.000000, 4.000000)" fill="#3FB5A0">
|
||||||
|
<path d="M0,0 L200,0 L200,200 L0,200 L0,0 L0,0 Z" id="Shape"></path>
|
||||||
|
</g>
|
||||||
|
<g stroke="#FFFFFF" stroke-width="8">
|
||||||
|
<path d="M209,70 L0,209 L209,70 Z" id="Shape"></path>
|
||||||
|
<path d="M209,6 L0,93 L209,6 Z" id="Shape"></path>
|
||||||
|
<path d="M209,180 L0,145 L209,180 Z" id="Shape"></path>
|
||||||
|
<path d="M88,209 L43,0 L88,209 Z" id="Shape"></path>
|
||||||
|
<path d="M209,172 L89,0 L209,172 Z" id="Shape"></path>
|
||||||
|
<path d="M137,0 L57,209 L137,0 Z" id="Shape"></path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(43.000000, 33.000000)" fill="#FFFFFF">
|
||||||
|
<circle id="Oval" cx="14" cy="38" r="14"></circle>
|
||||||
|
<circle id="Oval" cx="77" cy="12" r="12"></circle>
|
||||||
|
<circle id="Oval" cx="116" cy="70" r="12"></circle>
|
||||||
|
<circle id="Oval" cx="35" cy="125" r="16"></circle>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -1,13 +1,14 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from "react";
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from "react-immutable-proptypes";
|
||||||
import pluralize from 'pluralize';
|
import pluralize from "pluralize";
|
||||||
import { IndexLink } from 'react-router';
|
import { IndexLink } from "react-router";
|
||||||
import { IconMenu, Menu, MenuItem } from 'react-toolbox/lib/menu';
|
import { IconMenu, Menu, MenuItem } from "react-toolbox/lib/menu";
|
||||||
import Avatar from 'react-toolbox/lib/avatar';
|
import Avatar from "react-toolbox/lib/avatar";
|
||||||
import AppBar from 'react-toolbox/lib/app_bar';
|
import AppBar from "react-toolbox/lib/app_bar";
|
||||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
import FontIcon from "react-toolbox/lib/font_icon";
|
||||||
import FindBar from '../FindBar/FindBar';
|
import FindBar from "../FindBar/FindBar";
|
||||||
import styles from './AppHeader.css';
|
import { stringToRGB } from "../../lib/textHelper";
|
||||||
|
import styles from "./AppHeader.css";
|
||||||
|
|
||||||
export default class AppHeader extends React.Component {
|
export default class AppHeader extends React.Component {
|
||||||
|
|
||||||
@ -68,6 +69,10 @@ export default class AppHeader extends React.Component {
|
|||||||
userMenuActive,
|
userMenuActive,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const avatarStyle = {
|
||||||
|
backgroundColor: `#${ stringToRGB(user.get("name")) }`,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
fixed
|
fixed
|
||||||
@ -76,8 +81,9 @@ export default class AppHeader extends React.Component {
|
|||||||
rightIcon={
|
rightIcon={
|
||||||
<div>
|
<div>
|
||||||
<Avatar
|
<Avatar
|
||||||
title={user.get('name')}
|
style={avatarStyle}
|
||||||
image={user.get('avatar_url')}
|
title={user.get("name")}
|
||||||
|
image={user.get("avatar_url")}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
active={userMenuActive}
|
active={userMenuActive}
|
||||||
@ -102,7 +108,6 @@ export default class AppHeader extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<IconMenu
|
<IconMenu
|
||||||
theme={styles}
|
theme={styles}
|
||||||
active={createMenuActive}
|
|
||||||
icon="create"
|
icon="create"
|
||||||
onClick={this.handleCreateButtonClick}
|
onClick={this.handleCreateButtonClick}
|
||||||
onHide={this.handleCreateMenuHide}
|
onHide={this.handleCreateMenuHide}
|
||||||
@ -110,10 +115,10 @@ export default class AppHeader extends React.Component {
|
|||||||
{
|
{
|
||||||
collections.valueSeq().map(collection =>
|
collections.valueSeq().map(collection =>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={collection.get('name')}
|
key={collection.get("name")}
|
||||||
value={collection.get('name')}
|
value={collection.get("name")}
|
||||||
onClick={this.handleCreatePostClick.bind(this, collection.get('name'))} // eslint-disable-line
|
onClick={this.handleCreatePostClick.bind(this, collection.get('name'))} // eslint-disable-line
|
||||||
caption={pluralize(collection.get('label'), 1)}
|
caption={pluralize(collection.get("label"), 1)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
exports[`EntryEditorToolbar should disable and update label of Save button when persisting 1`] = `"<div><button disabled=\"\" class=\"\" data-react-toolbox=\"button\">Saving...</button> <button class=\"\" data-react-toolbox=\"button\">Cancel</button></div>"`;
|
exports[`EntryEditorToolbar should disable and update label of Save button when persisting 1`] = `"<div><button disabled=\"\" class=\"\" type=\"button\" data-react-toolbox=\"button\">Saving...</button> <button class=\"\" type=\"button\" data-react-toolbox=\"button\">Cancel</button></div>"`;
|
||||||
|
|
||||||
exports[`EntryEditorToolbar should have both buttons enabled initially 1`] = `"<div><button class=\"\" data-react-toolbox=\"button\">Save</button> <button class=\"\" data-react-toolbox=\"button\">Cancel</button></div>"`;
|
exports[`EntryEditorToolbar should have both buttons enabled initially 1`] = `"<div><button class=\"\" type=\"button\" data-react-toolbox=\"button\">Save</button> <button class=\"\" type=\"button\" data-react-toolbox=\"button\">Cancel</button></div>"`;
|
||||||
|
@ -1,6 +1,20 @@
|
|||||||
export function truncateMiddle(string = '', size) {
|
export function truncateMiddle(string = "", size) {
|
||||||
if (string.length <= size) {
|
if (string.length <= size) {
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
return string.substring(0, size / 2) + '\u2026' + string.substring(string.length - size / 2 + 1, string.length);
|
return `${ string.substring(0, size / 2) }\u2026${ string.substring(string.length - size / 2 + 1, string.length) }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringToRGB(str) {
|
||||||
|
if (!str) return "000000";
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = (hash & 0x00FFFFFF)
|
||||||
|
.toString(16)
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
return "00000".substring(0, 6 - c.length) + c;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user