diff --git a/build/build.ps1 b/build/build.ps1 index 652b18ffd..735961186 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -13,6 +13,10 @@ cd .. # build ui cd ui npm install + +# necessary to ensure native Node modules (e.g. keytar) are built against the correct version of Node) +node_modules\.bin\electron-rebuild + node extractLocals.js node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\ node_modules\.bin\webpack diff --git a/build/build.sh b/build/build.sh index 3bae4a960..e6214bd11 100755 --- a/build/build.sh +++ b/build/build.sh @@ -45,6 +45,13 @@ if [ "$FULL_BUILD" == "true" ]; then python "$BUILD_DIR/set_version.py" fi +libsecret="libsecret-1-dev" +if [ $LINUX -a -z "$(dpkg-query --show --showformat='${Status}\n' "$libsecret" 2>/dev/null | grep "install ok installed")" ]; then + # this is needed for keytar, which does secure password/token management + sudo apt-get install "$libsecret" +fi + + [ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist" mkdir -p "$ROOT/dist" [ -d "$ROOT/app/dist" ] && rm -rf "$ROOT/app/dist" @@ -61,6 +68,11 @@ npm install ( cd "$ROOT/ui" npm install + + # necessary to ensure native Node modules (e.g. keytar) are built against the correct version of Node) + # DEBUG=electron-rebuild node_modules/.bin/electron-rebuild . + node_modules/.bin/electron-rebuild "$ROOT/ui" + node extractLocals.js node_modules/.bin/node-sass --output dist/css --sourcemap=none scss/ node_modules/.bin/webpack diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index 972e167eb..ed35369e7 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -33,20 +33,20 @@ export function doUserFetch() { dispatch({ type: types.USER_FETCH_STARTED, }); - lbryio.setCurrentUser( - user => { + lbryio + .getCurrentUser() + .then(user => { dispatch({ type: types.USER_FETCH_SUCCESS, data: { user }, }); - }, - error => { + }) + .catch(error => { dispatch({ type: types.USER_FETCH_FAILURE, data: { error }, }); - } - ); + }); }; } @@ -56,32 +56,27 @@ export function doUserEmailNew(email) { type: types.USER_EMAIL_NEW_STARTED, email: email, }); - lbryio.call("user_email", "new", { email }, "post").then( - () => { + lbryio + .call("user_email", "new", { email: email, send_verification_email: true }, "post") + .catch(error => { + if (error.xhr && error.xhr.status == 409) { + return lbryio.call("user_email", "resend_token", { email: email, only_if_expired: true }, "post"); + } + throw error + }) + .then(() => { dispatch({ type: types.USER_EMAIL_NEW_SUCCESS, data: { email }, }); dispatch(doUserFetch()); - }, - error => { - if ( - error.xhr && - (error.xhr.status == 409 || - error.message == "This email is already in use") - ) { - dispatch({ - type: types.USER_EMAIL_NEW_EXISTS, - data: { email }, - }); - } else { - dispatch({ - type: types.USER_EMAIL_NEW_FAILURE, - data: { error: error.message }, - }); - } - } - ); + }) + .catch(error => { + dispatch({ + type: types.USER_EMAIL_NEW_FAILURE, + data: { error: error.message }, + }); + }); }; } @@ -111,12 +106,7 @@ export function doUserEmailVerify(verificationToken) { }; lbryio - .call( - "user_email", - "confirm", - { verification_token: verificationToken, email: email }, - "post" - ) + .call("user_email", "confirm", { verification_token: verificationToken, email: email }, "post") .then(userEmail => { if (userEmail.is_verified) { dispatch({ diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 11416fbd6..cc861875b 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -1,26 +1,23 @@ -import { getSession, setSession, setLocal } from "./utils.js"; import lbry from "./lbry.js"; const querystring = require("querystring"); +const keytar = require("keytar"); const lbryio = { - _accessToken: getSession("accessToken"), - _authenticationPromise: null, enabled: true, + _authenticationPromise: null, + _exchangePromise: null, + _exchangeLastFetched: null, }; const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end : "https://api.lbry.io/"; + const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; -lbryio._exchangePromise = null; -lbryio._exchangeLastFetched = null; lbryio.getExchangeRates = function() { - if ( - !lbryio._exchangeLastFetched || - Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT - ) { + if (!lbryio._exchangeLastFetched || Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT) { lbryio._exchangePromise = new Promise((resolve, reject) => { lbryio .call("lbc", "exchange_rate", {}, "get", true) @@ -46,9 +43,7 @@ lbryio.call = function(resource, action, params = {}, method = "get") { const xhr = new XMLHttpRequest(); xhr.addEventListener("error", function(event) { - reject( - new Error(__("Something went wrong making an internal API call.")) - ); + reject(new Error(__("Something went wrong making an internal API call."))); }); xhr.addEventListener("timeout", function() { @@ -60,7 +55,7 @@ lbryio.call = function(resource, action, params = {}, method = "get") { if (!response.success) { if (reject) { - let error = new Error(response.error); + const error = new Error(response.error); error.xhr = xhr; reject(error); } else { @@ -81,54 +76,38 @@ lbryio.call = function(resource, action, params = {}, method = "get") { } }); - // For social media auth: - //const accessToken = localStorage.getItem('accessToken'); - //const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}}; + lbryio + .getAuthToken() + .then(token => { + const fullParams = { auth_token: token, ...params }; - // Temp app ID based auth: - const fullParams = { app_id: lbryio.getAccessToken(), ...params }; - - if (method == "get") { - xhr.open( - "get", - CONNECTION_STRING + - resource + - "/" + - action + - "?" + - querystring.stringify(fullParams), - true - ); - xhr.send(); - } else if (method == "post") { - xhr.open("post", CONNECTION_STRING + resource + "/" + action, true); - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xhr.send(querystring.stringify(fullParams)); - } else { - reject(new Error(__("Invalid method"))); - } + if (method == "get") { + xhr.open("get", CONNECTION_STRING + resource + "/" + action + "?" + querystring.stringify(fullParams), true); + xhr.send(); + } else if (method == "post") { + xhr.open("post", CONNECTION_STRING + resource + "/" + action, true); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhr.send(querystring.stringify(fullParams)); + } else { + reject(new Error(__("Invalid method"))); + } + }) + .catch(reject); }); }; -lbryio.getAccessToken = () => { - const token = getSession("accessToken"); - return token ? token.toString().trim() : token; +lbryio.getAuthToken = () => { + return keytar.getPassword("LBRY", "auth_token").then(token => { + return token ? token.toString().trim() : null; + }); }; -lbryio.setAccessToken = token => { - setSession("accessToken", token ? token.toString().trim() : token); +lbryio.setAuthToken = token => { + return keytar.setPassword("LBRY", "auth_token", token ? token.toString().trim() : null); }; -lbryio.setCurrentUser = (resolve, reject) => { - lbryio - .call("user", "me") - .then(data => { - resolve(data); - }) - .catch(function(err) { - lbryio.setAccessToken(null); - reject(err); - }); +lbryio.getCurrentUser = () => { + return lbryio.call("user", "me"); }; lbryio.authenticate = function() { @@ -144,48 +123,56 @@ lbryio.authenticate = function() { }); }); } + if (lbryio._authenticationPromise === null) { lbryio._authenticationPromise = new Promise((resolve, reject) => { - lbry - .status() - .then(response => { - let installation_id = response.installation_id; - - if (!lbryio.getAccessToken()) { - lbryio - .call( - "user", - "new", - { - language: "en", - app_id: installation_id, - }, - "post" - ) - .then(function(responseData) { - if (!responseData.id) { - reject( - new Error("Received invalid authentication response.") - ); - } - lbryio.setAccessToken(installation_id); - lbryio.setCurrentUser(resolve, reject); - }) - .catch(function(error) { - /* - until we have better error code format, assume all errors are duplicate application id - if we're wrong, this will be caught by later attempts to make a valid call - */ - lbryio.setAccessToken(installation_id); - lbryio.setCurrentUser(resolve, reject); - }); - } else { - lbryio.setCurrentUser(resolve, reject); + lbryio + .getAuthToken() + .then(token => { + if (!token || token.length > 60) + { + return false; } + + // check that token works + return lbryio + .getCurrentUser() + .then(() => { return true; }) + .catch(() => { return false; }); }) - .catch(reject); + .then((isTokenValid) => { + if (isTokenValid) { + return; + } + + let app_id; + + return lbry + .status() + .then(status => { + // first try swapping + app_id = status.installation_id; + return lbryio.call("user", "token_swap", {auth_token: "", app_id: app_id}, "post"); + }) + .catch((err) => { + if (err.xhr.status == 403) { + // cannot swap. either app_id doesn't exist, or app_id already swapped. pretend its the former and create a new user. if we get another error, then its the latter + return lbryio.call("user", "new", {auth_token: "", language: "en", app_id: app_id}, "post"); + } + throw err; + }) + .then(response => { + if (!response.auth_token) { + throw new Error(__("auth_token is missing from response")); + } + return lbryio.setAuthToken(response.auth_token); + }); + }) + .then(lbryio.getCurrentUser()) + .then(resolve, reject); }); } + return lbryio._authenticationPromise; }; diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index f7104d3d4..d38ac3a14 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -69,7 +69,7 @@ export const selectUserIsVerificationCandidate = createSelector( selectEmailToVerify, selectUser, (isEligible, isApproved, emailToVerify, user) => - (isEligible && !isApproved) || (emailToVerify && user && !user.has_email) + emailToVerify && user ); export const selectUserIsAuthRequested = createSelector( diff --git a/ui/package.json b/ui/package.json index 2ffe670a8..4e3e4f87d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,11 +16,11 @@ "name": "LBRY Inc.", "email": "hello@lbry.io" }, - "license": "SEE LICENSE IN LICENSE.md", + "license": "MIT", "bugs": { "url": "https://github.com/lbryio/lbry-app/issues" }, - "homepage": "https://github.com/lbryio/lbry-app#readme", + "homepage": "https://github.com/lbryio/lbry-app", "dependencies": { "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.13.2", @@ -28,6 +28,7 @@ "from2": "^2.3.0", "jshashes": "^1.0.6", "localforage": "^1.5.0", + "keytar": "^4.0.3", "node-sass": "^3.8.0", "rc-progress": "^2.0.6", "react": "^15.4.0", @@ -54,6 +55,7 @@ "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-2": "^6.18.0", + "electron-rebuild": "^1.5.11", "eslint": "^3.10.2", "eslint-config-airbnb": "^13.0.0", "eslint-loader": "^1.6.1", @@ -64,6 +66,7 @@ "i18n-extract": "^0.4.4", "json-loader": "^0.5.4", "lint-staged": "^3.6.0", + "node-loader": "^0.6.0", "node-sass": "^3.13.0", "prettier": "^1.4.2", "webpack": "^2.6.1", diff --git a/ui/watch.sh b/ui/watch.sh index 91893b1c6..bb0cc6d7b 100755 --- a/ui/watch.sh +++ b/ui/watch.sh @@ -7,16 +7,19 @@ set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -mkdir -p $DIR/dist/css -mkdir -p $DIR/dist/js +( + cd "$DIR" + mkdir -p $DIR/dist/css + mkdir -p $DIR/dist/js -if [ ! -d "$DIR/node_modules" ]; then - echo "Installing NPM modules" - npm install -fi + if [ ! -d "$DIR/node_modules" ]; then + echo "Installing NPM modules" + npm install + fi -# run sass once without --watch to force update. then run with --watch to keep watching -$DIR/node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none $DIR/scss/ -$DIR/node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none --watch $DIR/scss/ & + # run sass once without --watch to force update. then run with --watch to keep watching + node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none $DIR/scss/ + node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none --watch $DIR/scss/ & -node_modules/.bin/webpack --config webpack.dev.config.js --progress --colors --watch + node_modules/.bin/webpack --config webpack.dev.config.js --progress --colors --watch +) \ No newline at end of file diff --git a/ui/webpack.config.js b/ui/webpack.config.js index b0ab6f08a..fd890022e 100644 --- a/ui/webpack.config.js +++ b/ui/webpack.config.js @@ -33,6 +33,10 @@ module.exports = { // define an include so we check just the files we need include: PATHS.app }, + { + test: /\.node$/, + use: ["node-loader"] + }, { test: /\.css$/, use: ["style-loader", "css-loader"] diff --git a/ui/webpack.dev.config.js b/ui/webpack.dev.config.js index 42107a3c5..604aa14d4 100644 --- a/ui/webpack.dev.config.js +++ b/ui/webpack.dev.config.js @@ -41,6 +41,10 @@ module.exports = { // define an include so we check just the files we need include: PATHS.app }, + { + test: /\.node$/, + use: ["node-loader"] + }, { test: /\.css$/, use: ["style-loader", "css-loader"]