diff --git a/package.json b/package.json index bf94511b..98d31d54 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "dateformat": "^1.0.12", "deep-equal": "^1.0.1", "fuzzy": "^0.1.1", + "gotrue-js": "^0.9.3", "history": "^2.1.2", "immutability-helper": "^2.0.0", "immutable": "^3.7.6", @@ -114,7 +115,6 @@ "mdast-util-definitions": "^1.2.2", "mdast-util-to-string": "^1.0.4", "moment": "^2.11.2", - "netlify-auth-js": "^0.5.5", "normalize.css": "^4.2.0", "preliminaries": "1.1.0", "preliminaries-parser-toml": "1.1.0", diff --git a/src/backends/netlify-auth/AuthenticationPage.js b/src/backends/netlify-auth/AuthenticationPage.js index 4bb43591..d23aee69 100644 --- a/src/backends/netlify-auth/AuthenticationPage.js +++ b/src/backends/netlify-auth/AuthenticationPage.js @@ -6,12 +6,31 @@ import logo from "./netlify_logo.svg"; import styles from "./AuthenticationPage.css"; export default class AuthenticationPage extends React.Component { + constructor(props) { + super(props); + this.identity = window.netlifyIdentity; + this.state = {user: this.identity && this.identity.gotrue && this.identity.gotrue.currentUser()}; + } + + componentDidMount() { + if (this.identity && !this.state.user) { + this.identity.on('login', (user) => { + this.props.onLogin(user); + this.identity.close(); + }); + this.identity.on('signup', (user) => { + this.props.onLogin(user); + this.identity.close(); + }); + this.identity.open(); + } + } + static propTypes = { onLogin: React.PropTypes.func.isRequired, }; - state = { username: "", password: "", errors: {} }; - + state = { email: "", password: "", errors: {} }; handleChange = (name, value) => { this.setState({ ...this.state, [name]: value }); @@ -20,13 +39,13 @@ export default class AuthenticationPage extends React.Component { handleLogin = (e) => { e.preventDefault(); - const { username, password } = this.state; + const { email, password } = this.state; const errors = {}; - if (!username) { - errors.username = 'Make sure to enter your user name'; + if (!email) { + errors.email = 'Make sure to enter your email.'; } if (!password) { - errors.password = 'Please enter your password'; + errors.password = 'Please enter your password.'; } if (Object.keys(errors).length > 0) { @@ -34,11 +53,11 @@ export default class AuthenticationPage extends React.Component { return; } - AuthenticationPage.authClient.login(this.state.username, this.state.password, true) + AuthenticationPage.authClient.login(this.state.email, this.state.password, true) .then((user) => { this.props.onLogin(user); }) - .catch((error) => { + .catch((error) => { this.setState({ errors: { server: error.description || error.msg || error }, loggingIn: false }); }); }; @@ -46,39 +65,41 @@ export default class AuthenticationPage extends React.Component { render() { const { errors } = this.state; const { error } = this.props; + return (
- - {error &&

- {error} -

} - {errors.server &&

- {errors.server} -

} - - - +
+ + {error &&

+ {error} +

} + {errors.server &&

+ {errors.server} +

} + + + +
); diff --git a/src/backends/netlify-auth/implementation.js b/src/backends/netlify-auth/implementation.js index c15037ba..66776c5c 100644 --- a/src/backends/netlify-auth/implementation.js +++ b/src/backends/netlify-auth/implementation.js @@ -1,10 +1,27 @@ -import NetlifyAuthClient from "netlify-auth-js"; +import GoTrue from "gotrue-js"; import jwtDecode from 'jwt-decode'; +import {List} from 'immutable'; import { get, pick, intersection } from "lodash"; import GitHubBackend from "../github/implementation"; import API from "./API"; import AuthenticationPage from "./AuthenticationPage"; +const localHosts = { + localhost: true, + '127.0.0.1': true, + '0.0.0.0': true +} + +function getEndpoint(endpoint, netlifySiteURL) { + if (localHosts[document.location.host] && netlifySiteURL && endpoint.match(/^\/\.netlify\//)) { + const parts = [netlifySiteURL]; + if (!netlifySiteURL.match(/\/$/)) { parts.push("/"); } + parts.push(endpoint); + return parts.join(""); + } + return endpoint; +} + export default class NetlifyAuth extends GitHubBackend { constructor(config) { super(config, true); @@ -13,16 +30,13 @@ export default class NetlifyAuth extends GitHubBackend { 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"]); - if (config.getIn(["backend", "accept_roles"]) == null) { - throw new Error("The NetlifyAuth backend needs an \"accept_roles\" in the backend configuration."); - } - this.accept_roles = config.getIn(["backend", "accept_roles"]).toArray(); + this.accept_roles = (config.getIn(["backend", "accept_roles"]) || List()).toArray(); - this.authClient = new NetlifyAuthClient({ - APIUrl: config.getIn(["backend", "auth_url"]), - }); + const netlifySiteURL = localStorage.getItem("netlifySiteURL"); + const APIUrl = getEndpoint(config.getIn(["backend", "auth_url"]), netlifySiteURL); + this.github_proxy_url = getEndpoint(config.getIn(["backend", "github_proxy_url"]), netlifySiteURL); + this.authClient = new GoTrue({APIUrl}); AuthenticationPage.authClient = this.authClient; } @@ -37,10 +51,14 @@ export default class NetlifyAuth extends GitHubBackend { this.tokenPromise = user.jwt.bind(user); return this.tokenPromise() .then((token) => { + let validRole = true; + if (this.accept_roles && this.accept_roles.length > 0) { + validRole = intersection(userRoles, this.accept_roles).length > 0; + } const userRoles = get(jwtDecode(token), 'app_metadata.roles', []); - if (intersection(userRoles, this.accept_roles).length > 0) { + if (validRole) { const userData = { - name: `${ user.user_metadata.firstname } ${ user.user_metadata.lastname }`, + name: user.user_metadata.name, email: user.email, metadata: user.user_metadata, }; @@ -51,9 +69,9 @@ export default class NetlifyAuth extends GitHubBackend { }); return userData; } else { - throw new Error("User is not authorized"); + throw new Error("You don't have sufficient permissions to access Netlify CMS"); } - }); + }); } getToken() { diff --git a/src/integrations/providers/assetStore/implementation.js b/src/integrations/providers/assetStore/implementation.js index 0aa41841..a0dac10d 100644 --- a/src/integrations/providers/assetStore/implementation.js +++ b/src/integrations/providers/assetStore/implementation.js @@ -5,7 +5,7 @@ export default class AssetStore { throw 'The AssetStore integration needs the getSignedFormURL in the integration configuration.'; } this.getToken = getToken; - + this.shouldConfirmUpload = config.get('shouldConfirmUpload', false); this.getSignedFormURL = config.get('getSignedFormURL'); } @@ -69,9 +69,11 @@ export default class AssetStore { upload(file, privateUpload = false) { const fileData = { name: file.name, - size: file.size, - content_type: file.type, + size: file.size }; + if (file.type) { + fileData.content_type = file.type; + } if (privateUpload) { fileData.visibility = 'private'; @@ -91,7 +93,7 @@ export default class AssetStore { const formFields = response.form.fields; const assetID = response.asset.id; const assetURL = response.asset.url; - + const formData = new FormData(); Object.keys(formFields).forEach(key => formData.append(key, formFields[key])); formData.append('file', file, file.name); @@ -100,11 +102,10 @@ export default class AssetStore { method: 'POST', body: formData, }) - .then(() => { + .then(() => { if (this.shouldConfirmUpload) this.confirmRequest(assetID); return { success: true, assetURL }; }); }); } } - diff --git a/src/lib/urlHelper.js b/src/lib/urlHelper.js index accf7046..62dfb588 100644 --- a/src/lib/urlHelper.js +++ b/src/lib/urlHelper.js @@ -1,3 +1,5 @@ +import url from 'url'; + function getUrl(url, direct) { return `${ direct ? '/#' : '' }${ url }`; } @@ -9,3 +11,32 @@ export function getCollectionUrl(collectionName, direct) { export function getNewEntryUrl(collectionName, direct) { return getUrl(`/collections/${ collectionName }/entries/new`, direct); } + +export function urlize(string) { + const sanitized = makePathSanitized(string); + const parsedURL = url.parse(sanitized); + + return url.format(parsedURL); +} + +function makePathSanitized(string) { + return makePath(string.toLowerCase()); +} + +function makePath(string) { + return unicodeSanitize(string).trim().replace(/[\s]/g, '-').replace(/-+/g, '-'); +} + +function unicodeSanitize(string) { + let target = []; + const runes = string.split(''); + for (let i=0; i < string.length; i++) { + const r = runes[i]; + if (r == '%' && i+2 < string.length && string.substr(i+1, 2).match(/^[0-9a-f]+$/)) { + target = target.concat([r, runes[i+1], runes[i+2]]); + } else if (r.match(/[\w .\/\\_#\+-]/u)) { + target.push(r); + } + } + return target.join(''); +} diff --git a/yarn.lock b/yarn.lock index f2869818..0f367eef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2331,20 +2331,13 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" -doctrine@1.3.x: +doctrine@1.3.x, doctrine@^1.2.2: version "1.3.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.3.0.tgz#13e75682b55518424276f7c173783456ef913d26" dependencies: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^1.2.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -2402,20 +2395,13 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domutils@1.5.1: +domutils@1.5.1, domutils@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: dom-serializer "0" domelementtype "1" -domutils@^1.5.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" - dependencies: - dom-serializer "0" - domelementtype "1" - dot-prop@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" @@ -3481,6 +3467,13 @@ got@^6.7.1: unzip-response "^2.0.1" url-parse-lax "^1.0.0" +gotrue-js@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/gotrue-js/-/gotrue-js-0.9.3.tgz#5f8d4de45b10fd9e27b5739fe13e7568b9e9720b" + dependencies: + micro-api-client "^2.0.0" + prettier "^1.6.1" + graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -3801,14 +3794,10 @@ hyphenate-style-name@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b" -iconv-lite@0.4.13: +iconv-lite@0.4.13, iconv-lite@~0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" -iconv-lite@~0.4.13: - version "0.4.18" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" - icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -5348,7 +5337,7 @@ minimatch@3.0.3: dependencies: brace-expansion "^1.0.0" -minimist@0.0.8: +minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -5356,10 +5345,6 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1. version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - mixin-object@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" @@ -5421,12 +5406,6 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" -netlify-auth-js@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/netlify-auth-js/-/netlify-auth-js-0.5.5.tgz#8753018407c08757b941df9c1e8dd5fc99c4ad29" - dependencies: - micro-api-client "^2.0.0" - node-emoji@^1.0.3: version "1.8.1" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" @@ -6721,6 +6700,10 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" +prettier@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.6.1.tgz#850f411a3116226193e32ea5acfc21c0f9a76d7d" + pretty-format@^20.0.3: version "20.0.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14" @@ -9106,11 +9089,7 @@ whatwg-encoding@^1.0.1: dependencies: iconv-lite "0.4.13" -whatwg-fetch@>=0.10.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" - -whatwg-fetch@^1.0.0: +whatwg-fetch@>=0.10.0, whatwg-fetch@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz#ac3c9d39f320c6dce5339969d054ef43dd333319"