diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c0b199b --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-0", "react"], + "plugins": ["transform-runtime", "transform-decorators-legacy"] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6cb772b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +tmp diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6051e92 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.json] +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bf61c02 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,67 @@ +/* +# Airbnb is putting everything on error which makes it hard to spot JavaScript errors: https://github.com/airbnb/javascript/issues/853 +# Work-around, move everything from error to warn: +find node_modules/eslint-config-airbnb -name '*.js'|xargs sed -i 's/\[2,/\[1,/' +find node_modules/eslint-config-airbnb -name '*.js'|xargs sed -i "s/': 2/': 1/" +*/ +{ + "extends": "airbnb", + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true, + "es6": true + }, + "rules": { + "max-len": 0, /*[1, 160, 4],*/ + "comma-dangle": 0, + "semi": [0], + "curly": [0], + "one-var": [0], + "eqeqeq": 0, + "no-cond-assign": 0, + "react/wrap-multilines": [0], + "one-var-declaration-per-line": [0], + "no-param-reassign": [0], + "no-nested-ternary": [0], + "no-undef": 2, + "camelcase": [0], + "no-console": [0], + "padded-blocks": 0, + "object-curly-spacing": [0], + "react/jsx-indent": [1,4], + "react/jsx-indent-props": [1,4], + "react/jsx-closing-bracket-location": [0], + "no-use-before-define": [0, {"functions": false, "classes": false}], + "spaced-comment": [0], + "prefer-template": [0], + "new-cap": [0], + "arrow-body-style": [0], + "func-names": [0], + "no-return-assign": 0, + "no-redeclare": 1, + "eol-last": 0, + "no-loop-func": 0, + "no-unneeded-ternary": 0, // false trigger const b = b ? b : a + + /* Nice to haves */ + /*"quotes": [1, "single", "avoid-escape"],*/ "quotes": 0, + /*"indent", [1,4], */ "indent": 0, + "brace-style": 0, + "space-infix-ops": 0, + "keyword-spacing": 0, + "no-confusing-arrow": 0, + "space-in-parens": 0, + "no-throw-literal": 0, + "react/sort-comp": [1, { "order": [ "lifecycle" ] }], + "react/prefer-stateless-function": 0, + "react/prop-types": 0, + "radix": 0, + + "jsx-a11y/href-no-hash": "off", + "jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }], + "import/no-extraneous-dependencies": 0, + "import/no-unresolved": 0, + "import/extensions": 0 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0391319 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.idea +coverage +node_modules +*.log +dist +vendor/* +tmp/* +.vagrant +lib + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4d0ed73 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Please Read + +Please read these instructions before submitting issues to the Steemit.com GitHub repository. The issue tracker is for bugs and specific implementation discussion **only**. It should not be used for other purposes, as described below. + +## Bug Reports + +If there is an existing feature that is not working correctly, or a glitch in the website that is impacting user behaviour - please open an issue to report variance. Include as much relevant information as you can, including screen shots and steps to reproduce the issue. + +## Technical Support and Signup/Login Issues + +If you are having trouble using the website but it is not an error with the website (this includes signup/login issues), do **not** open a GitHub issue. Please request help from the users in the [steemit.chat help](https://steemit.chat/channel/help) channel. + +## Enhancement Suggestions + +Do **not** use the issue tracker to suggest enhancements or improvements to the platform. The best place for these discussions is on Steemit.com. If there is a well vetted idea that has the support of the community that you feel should be considered by the development team, please email it to [sneak@steemit.com](mailto:sneak@steemit.com) for review. + +## Implementation Discussion + +The developers frequently open issues to discuss changes that are being worked on. This is to inform the community of the changes being worked on, and to get input from the community and other developers on the implementation. + +Issues opened that devolve into lengthy discussion of minor site features will be closed or locked. The issue tracker is not a general purpose discussion forum. + +This is not the place to make suggestions for product improvement (please see the Enhancement Suggestions section above for this). If you are not planning to work on the change yourself - do not open an issue for it. + +## Duplicate Issues + +Please do a keyword search to see if there is already an existing issue before opening a new one. + +## Steemit.com vs. Steem Blockchain + +This issue tracker is only intended to track issues for the Steemit.com website. If the issue is with the Steem blockchain, please open an issue in the [Steem Repository](https://github.com/steemit/steem). + +## Pull Requests + +Anybody in the community is welcome and encouraged to submit pull requests with any desired changes to the platform! + +Requests to make changes to steemit.com that include working, tested pull requests jump to the top of the queue. There is not a guarantee that all functionality submitted as a PR will be accepted and merged, however. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4cc9e00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM node:7.5 + +# yarn > npm +#RUN npm install --global yarn + +RUN npm install -g yarn + +WORKDIR /var/app +RUN mkdir -p /var/app +ADD package.json /var/app/package.json +RUN yarn + +COPY . /var/app + +# FIXME TODO: fix eslint warnings + +#RUN mkdir tmp && \ +# npm test && \ +# ./node_modules/.bin/eslint . && \ +# npm run build + +RUN mkdir tmp && \ + npm test && \ + npm run-script build + +ENV PORT 8080 +ENV NODE_ENV production + +EXPOSE 8080 + +CMD [ "yarn", "run", "production" ] + +# uncomment the lines below to run it in development mode +# ENV NODE_ENV development +# CMD [ "yarn", "run", "start" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1afba09 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +default: test + +test: node_modules + npm test + +node_modules: + yarn install + +build: + docker build -t steemit/steemit.com . + +clean: + rm -rf node_modules *.log tmp npm-debug.log.* + +vagrant: + vagrant destroy -f + vagrant up diff --git a/README.md b/README.md new file mode 100644 index 0000000..aca00ee --- /dev/null +++ b/README.md @@ -0,0 +1,226 @@ + +# Condenser + + +Condenser is the react.js web interface to the world's first and best blockchain-based social media platform, steemit.com. It uses [STEEM](https://github.com/steemit/steem), a blockchain powered by Graphene 2.0 technology to store JSON-based content for a plethora of web applications. + +## Why would I want to use Condenser (steemit.com front-end)? +* Learning how to build blockchain-based web applications using STEEM as a content storage mechanism in react.js +* Reviewing the inner workings of the steemit.com social media platform +* Assisting with software development for steemit.com + +## Installation + +#### Docker + +We highly recommend using docker to run condenser. This is how we run the live steemit.com site and it is the most supported (and fastest) method of both building and running condenser. We will always have the latest version of condenser (master branch) available on dockerhub. Configuration settings can be set using environment variables (see configuration section below for more information). If you need to install docker, you can get it at https://get.docker.com + +To bring up a running container it's as simple as this: + +```bash +docker run -it -p 8080:8080 steemit/condenser +``` + +Environment variables can be added like this: + +```bash +docker run -it --env SDC_DATABASE_URL="mysql://user:pass@hostname/databasename" -p 8080:8080 steemit/condenser +``` + +If you would like to modify, build, and run condenser using docker, it's as simple as pulling in the github repo and issuing one command to build it, like this: + +```bash +git clone https://github.com/steemit/condenser +cd condenser +docker build -t="myname/condenser:mybranch" . +docker run -it -p 8080:8080 myname/condenser:mybranch +``` + +## Building from source without docker (the 'traditional' way): + +#### Clone the repository and make a tmp folder +```bash +git clone https://github.com/steemit/condenser +cd condenser +mkdir tmp +``` + +#### Install dependencies + +Install at least Node v7.5 if you don't already have it. We recommend using `nvm` to do this as it's both the simplest way to install and manage installed version(s) of node. If you need `nvm`, you can get it at [https://github.com/creationix/nvm](https://github.com/creationix/nvm). + +Condenser is known to successfully build using node 7.5, npm 4.1.2, and yarn 1.1.0. + +Using nvm, you would install like this: +```bash +nvm install v7.5 +``` + +We use the yarn package manager instead of the default `npm`. There are multiple reasons for this, one being that we have `steem-js` built from source pulling the github repo as part of the build process and yarn supports this. This way the library that handles keys can be loaded by commit hash instead of a version name and cryptographically verified to be exactly what we expect it to be. Yarn can be installed with `npm`, but afterwards you will not need to use `npm` further. + +```bash +npm install -g yarn +yarn global add babel-cli +yarn install +yarn run build +``` +To run condenser in production mode, run: + +```bash +yarn run production +``` + +When launching condenser in production mode it will automatically use 1 process per available core. You will be able to access the front-end at http://localhost:8080 by default. + +To run condenser in development mode, run: + +```bash +yarn run start +``` + +It will take quite a bit longer to start in this mode (~60s) as it needs to build and start the webpack-dev-server. + +By default you will be connected to steemit.com's public steem node at `wss://steemd.steeemit.com`. This is actually on the real blockchain and you would use your regular account name and credentials to login - there is not an official separate testnet at this time. If you intend to run a full-fledged site relying on your own, we recommend looking into running a copy of `steemd` locally instead [https://github.com/steemit/steem](https://github.com/steemit/steem). + +#### Configuration + +The intention is to configure condenser using environment variables. You can see the names of all of the available configuration environment variables in `config/custom-environment-variables.json`. Default values are stored in `config/defaults.json`. + +Environment variables using an example like this: + +```bash +export SDC_CLIENT_STEEMD_URL="wss://steemd.steemit.com" +export SDC_SERVER_STEEMD_URL="wss://steemd.steemit.com" +``` +Keep in mind environment variables only exist in your active session, so if you wish to save them for later use you can put them all in a file and `source` them in. + +If you'd like to statically configure condenser without variables you can edit the settings directly in `config/production.json`. If you're running in development mode, copy `config/production.json` to `config/dev.json` with `cp config/production.json config/dev.json` and adjust settings in `dev.json`. + +If you're intending to run condenser in a production environment one configuration option that you will definitely want to edit is `server_session_secret` which can be set by the environment variable `SDC_SESSION_SECRETKEY`. To generate a new value for this setting, you can do this: + +```bash +node +> crypto.randomBytes(32).toString('base64') +> .exit +``` + +#### Install mysql server + +If you've followed the instructions up until this point you will already have a running condenser installation which is entirely acceptable for development purposes. It is *not required to run a SQL server for development*. If you're running a full-fledged site however, you will want to set one up. + +Once set up, you can set the mysql server configuration option for condenser using the environment variable `SDC_DATABASE_URL`, or alternatively by editing it in `config/production.json`. You will use the format `mysql://user:pass@hostname/databasename`. + +Example: + +```bash +export SDC_DATABASE_URL="mysql://root:password@127.0.0.1/steemit_dev" +``` + +Here are instructions for setting up a mysql server and running the necessary migrations by operating system: + +OS X: + +```bash +brew update +brew doctor +brew upgrade +brew install mysql +mysql.server restart +``` + +Debian based Linux: + +```bash +sudo apt-get update +sudo apt-get install mysql-server +``` + +On Ubuntu 16.04+ you may be unable to connect to mysql without root access, if +so update the mysql root user as follows: + +``` +sudo mysql -u root +DROP USER 'root'@'localhost'; +CREATE USER 'root'@'%' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'; +FLUSH PRIVILEGES; +``` + +Now launch mysql client and create steemit_dev database: +```bash +mysql -u root +> create database steemit_dev; +> quit +``` + +#### Database migrations + +This is a required step in order for the database to be 'ready' for condenser's use. + + +Edit the file `src/db/config/config.json` using your favorite command line text editor being sure that the username, password, host, and database name are set correctly and match your newly configured mysql setup. + +Run `sequelize db:migrate` in `src/db` directory, like this: + +```bash +cd src/db +yarn exec sequelize db:migrate +``` + +#### Install Tarantool - Production Only + +Tarantool similarly to mysql is not required for development but if you're running a full-fledged site with condenser you will want to run one. + +OS X: + +```bash +brew install tarantool +``` + +Debian based Linux: + +```bash +sudo apt-get install tarantool +``` + +Test the interactive console: + +```bash +user@example:~$ tarantool +``` + +#### Style Guides For Submitting Pull Requests + +##### File naming and location + +- Prefer CamelCase js and jsx file names +- Prefer lower case one word directory names +- Keep stylesheet files close to components +- Component's stylesheet file name should match component name + +##### Js & Jsx +We are using _[Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)_ with some modifications (see .eslintrc). +Please run _eslint_ in the working directory before committing your changes and make sure you didn't introduce any new styling issues. + +##### CSS & SCSS +If a component requires a css rule, please use its uppercase name for the class, e.g. "Header" class for the header's root div. +We adhere to BEM methodology with exception for Foundation classes, here is an example for the Header component: + +```html + +
{alert || warning || success}
+{tt('g.read_only_mode')}
+{description}+ +
[\S\s]*<\/p>/.test(text) + } + + // Strip out HTML comments. "JS-DOS" bug. + text = text.replace(/|$)/g, '(html comment removed: $1)') + + let renderedText = html ? text : remarkable.render(text) + + // Embed videos, link mentions and hashtags, etc... + if(renderedText) renderedText = HtmlReady(renderedText, {hideImages}).html + + // Complete removal of javascript and other dangerous tags.. + // The must remain as close as possible to dangerouslySetInnerHTML + let cleanText = renderedText + if (this.props.allowDangerousHTML === true) { + console.log('WARN\tMarkdownViewer rendering unsanitized content') + } else { + cleanText = sanitize(renderedText, sanitizeConfig({large, highQualityPost, noImage: noImage && allowNoImage})) + } + + if(/<\s*script/ig.test(cleanText)) { + // Not meant to be complete checking, just a secondary trap and red flag (code can change) + console.error('Refusing to render script tag in post text', cleanText) + return
+ } + + const noImageActive = cleanText.indexOf(noImageText) !== -1 + + // In addition to inserting the youtube compoennt, this allows react to compare separately preventing excessive re-rendering. + let idx = 0 + const sections = [] + + // HtmlReady inserts ~~~ embed:${id} type ~~~ + for(let section of cleanText.split('~~~ embed:')) { + const match = section.match(/^([A-Za-z0-9\_\-]+) (youtube|vimeo) ~~~/) + if(match && match.length >= 3) { + const id = match[1] + const type = match[2] + const w = large ? 640 : 480, + h = large ? 360 : 270 + if(type === 'youtube') { + sections.push( ++ {content.root_title} +
+
+
+ //
+ // This feature will add a Steemit account as an additional owner on your account. This is a service that can be used by yourself and Steemit to recover your account should it get compromised or you loose your password. + //
+ // @Steemit + // } + //
+ {tt('g.warning')}:
+ {tt('checkloginowner_jsx.your_password_permissions_were_reduced')}
+
+ {tt('checkloginowner_jsx.ownership_changed_on')}
+ {tt('checkloginowner_jsx.deadline_for_recovery_is')}
+ + {tt('checkloginowner_jsx.i_understand_dont_show_again')} +
+{tt('g.date')} | +{tt('g.price')} | +{LIQUID_TOKEN} | +{`${DEBT_TOKEN_SHORT} (${CURRENCY_SIGN})`} | +
---|
+ {suggestedPassword}
+
+ //
+ // {print &&
}
+ // {suggestedPassword &&
{keyObj.pubkey}
+ {keyObj.wif}
+ {tt('voting_jsx.flagging_post_can_remove_rewards_the_flag_should_be_used_for_the_following')}
+{ABOUT_FLAG}
+ Flag +{('loading')}...
+{tt('loginform_jsx.due_to_server_maintenance')}
{tt('loginform_jsx.you_account_has_been_successfully_created')}
+{tt('loginform_jsx.you_account_has_been_successfully_recovered')}
+{tt('loginform_jsx.password_update_succes', {accountName: username.value})}
+
+ {tt('powerdown_jsx.amount')}
+
+ {LIQUID_TICKER}
+
Due to server maintenance we are running in read only mode. We are sorry for the inconvenience.
Membership to Steemit.com is now under invitation only because of unexpectedly high sign up rate. + Submit your email to get on the waiting list.
+Steemit funds each account with over {this.props.signup_bonus} worth of Steem Power; to prevent abuse, we
+ require new users to login via social media.
+ Your personal information will be kept private.
+
By verifying your account you agree to the Steemit terms and conditions.
+Last Updated April 28, 2016
Welcome to Steemit! These Terms of Service (“Terms”) apply to your access to and use of Steemit.com and any other products or services that link to these Terms (“Steemit”). Steemit is provided by Steemit, Inc. (“Steemit”, “we” + or “us”). By accessing or using Steemit, you agree to be bound by these Terms. If you do not agree to these Terms, including the mandatory arbitration provision and class action waiver in Section 14, do not access or use Steemit. If we make changes to these Terms, we will provide notice of those changes by updating the “Last Updated” date above or posting notice on Steemit. Your continued use of Steemit will confirm your acceptance of the changes. +
You understand and agree that these Terms apply solely to your access to, and use of, Steemit and that, when you use other Steemit services such as Steemit.com or Steemitgifts.com, the terms and policies particular to those services apply. +
1. Privacy Policy
Please refer to our Privacy Policy for information about how we collect, use and disclose information about you. +
2. Eligibility
Steemit is not targeted towards, nor intended for use by, anyone under the age of 13. You must be at least 13 years of age to access or use Steemit. If you are between 13 and 18 years of age (or the age of legal majority where you reside), you may only access or use Steemit under the supervision of a parent or legal guardian who agrees to be bound by these Terms. +
3. Copyright and Limited License
Steemit contains data, text, photographs, images, video, audio, graphics, articles, comments, software, code, scripts and other content supplied by us, the Steem blockchain or our licensors, which we call “Steemit Content.” Steemit Content is protected by intellectual property laws, including copyright and other proprietary rights of the United States and foreign countries. Except as explicitly stated in these Terms, Steemit does not grant any express or implied rights to use Steemit Content. +
You are granted a limited, nonexclusive, non-transferable, and non-sublicensable license to access and use Steemit and Steemit Content for your personal, non-commercial use. This license is subject to these Terms and does not include any right to: (a) distribute, publicly perform or publicly display any Steemit Content; (b) modify or otherwise make any derivative uses of Steemit or Steemit Content, or any portion thereof; (c) use any data mining, robots or similar data gathering or extraction methods; and (d) use Steemit or Steemit Content other than for their intended purposes. Any use of Steemit or Steemit Content other than as authorized in these Terms is strictly prohibited and will terminate the license granted herein. This license is revocable at any time. +
4. Adult-Oriented Content
Steemit is intended for a general audience and, as a result, some Steemit Content may discuss or depict adult-oriented topics. We realize that this content may not be appropriate or desirable for some of our readers depending on their current location, age, background or personal views. As a result, we mark this content as Not Safe For Work (“NSFW”).
Marking Steemit Content as NSFW does not prevent you from being able to access this content but, instead, helps you make informed decisions about the type of content you view on Steemit. You understand and agree that you access content marked as NSFW at your own risk. +
5. Trademarks
“Steem,” “ + Steemit,” the Steemit logo and any other product or service names, logos or slogans that may appear on Steemit are trademarks of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. You may not use any metatags or other “ + hidden text” utilizing “Steemit” or any other name, trademark or product or service name of Steemit without our prior written permission. In addition, the look and feel of Steemit, including, without limitation, all page headers, custom graphics, button icons and scripts, constitute the service mark, trademark or trade dress of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. All other trademarks, registered trademarks, product names and company names or logos mentioned or used on Steemit are the property of their respective owners and may not be copied, imitated or used, in whole or in part, without the permission of the applicable trademark holder. Reference to any products, services, processes or other information by name, trademark, manufacturer, supplier or otherwise does not constitute or imply endorsement, sponsorship or recommendation by Steemit. +
6. Assumption of Risk, Limitations on Liability & Indemnity
6.1. + You accept and acknowledge that there are risks associated with utilizing an Internet- based STEEM account service including, but not limited to, the risk of failure of hardware, software and Internet connections, the risk of malicious software introduction, and the risk that third parties may obtain unauthorized access to information stored within your Account, including, but not limited to your Private Key (as defined below at 10.2.). You accept and acknowledge that Steemit will not be responsible for any communication failures, disruptions, errors, distortions or delays you may experience when using the Services, however caused. +
6.2. We will use reasonable endeavours to verify the accuracy of any information on the Service but we make no representation or warranty of any kind, express or implied, statutory or otherwise, regarding the contents of the Service, information and functions made accessible through the Service, any hyperlinks to third party websites, nor for any breach of security associated with the transmission of information through the Service or any website linked to by the Service. +
6.3. We will not be responsible or liable to you for any loss and take no responsibility for and will not be liable to you for any use of our Services, including but not limited to any losses, damages or claims arising from: (a) User error such as forgotten passwords, incorrectly constructed transactions, or mistyped STEEM addresses; (b) Server failure or data loss; (c) Corrupted Account files; (d) Unauthorized access to applications; (e) Any unauthorized third party activities, including without limitation the use of viruses, phishing, bruteforcing or other means of attack against the Service or Services. +
6.4. We make no warranty that the Service or the server that makes it available, are free of viruses or errors, that its content is accurate, that it will be uninterrupted, or that defects will be corrected. We will not be responsible or liable to you for any loss of any kind, from action taken, or taken in reliance on material, or information, contained on the Service. +
6.5. Subject to 7.1 below, any and all indemnities, warranties, terms and conditions (whether express or implied) are hereby excluded to the fullest extent permitted under Luxembourg law. +
6.6. We will not be liable, in contract, or tort (including, without limitation, negligence), other than where we have been fraudulent or made negligent misrepresentations. +
6.7. Nothing in these Terms excludes or limits liability for death or personal injury caused by negligence, fraudulent misrepresentation, or any other liability which may not otherwise be limited or excluded under United States law. +
7. Agreement to Hold Steemit Harmless
7.1. You agree to hold harmless Steemit (and each of our officers, directors, members, employees, agents and affiliates) from any claim, demand, action, damage, loss, cost or expense, including without limitation reasonable legal fees, arising out or relating to: +
7.1.1. Your use of, or conduct in connection with, our Services;
7.1.2. Any feedback or submissions you provide (see 19 below);
7.1.3. + Your violation of these Terms; or
7.1.4. Violation of any rights of any other person or entity. +
7.2. If you are obligated to indemnify us, we will have the right, in our sole discretion, to control any action or proceeding (at our expense) and determine whether we wish to settle it. +
8. No Liability For Third Party Services And Content
8.1. In using our Services, you may view content or utilize services provided by third parties, including links to web pages and services of such parties (“ + Third Party Content”). We do not control, endorse or adopt any Third-Party Content and will have no responsibility for Third Party Content including, without limitation, material that may be misleading, incomplete, erroneous, offensive, indecent or otherwise objectionable in your jurisdiction. In addition, your dealings or correspondence with such third parties are solely between you and the third parties. We are not responsible or liable for any loss or damage of any sort incurred as a result of any such dealings and you understand that your use of Third Party Content, and your interactions with third parties, is at your own risk. +
9. Account Registration
9.1. You need not use a Steemit Account. If you wish to use an Account, you must create an Account with Steemit to access the Services (“ + Account”). When you create an Account, you are strongly advised to take the following precautions, as failure to do so may result in loss of access to, and/or control over, your Wallet: (a) Create a strong password that you do not use for any other website or online service; (b) Provide accurate and truthful information; (c) Maintain and promptly update your Account information; (d) maintain the security of your Account by protecting your Account password and access to your computer and your Account; (e) Promptly notify us if you discover or otherwise suspect any security breaches related to your Account. +
9.2. You hereby accept and acknowledge that you take responsibility for all activities that occur under your Account and accept all risks of any authorized or unauthorized access to your Account, to the maximum extent permitted by law. +
10. The Steemit Services +
10.1. As described in more detail below, the Services, among other things, provide in-browser (or otherwise local) software that (a) generates and stores STEEM ACCOUNT Addresses and encrypted Private Keys (defined below), and (b) Facilitates the submission of STEEM transaction data to the Steem network without requiring you to access the STEEM command line interface. +
10.2. Account Names and Private Keys. When you create an Account, the Services generate and store a cryptographic private and public key pair that you may use to send and receive STEEM and Steem Dollars via the Steem network.. The Private Key uniquely matches the Account Name and must be used in connection with the Account Name to authorize the transfer of STEEM and Steem Dollars from that Account. You are solely responsible for maintaining the security of your Private Key and any password phrase associated with your wallet. You must keep your Account, password phrase and Private Key access information secure. Failure to do so may result in the loss of control of Steem, Steem Power and Steem Dollars associated with the Wallet. +
10.3. No Password Retrieval. Steemit does not receive or store your Account password, nor the unencrypted keys and addresses. Therefore, we cannot assist you with Account password retrieval. Our Services provide you with tools to help you remember or recover your password, including by allowing you to set password hints, but the Services cannot generate a new password for your Account. You are solely responsible for remembering your Account password. If you have not safely stored a backup of any Account Names and password pairs maintained in your Account, you accept and acknowledge that any STEEM, Steem Dollars and Steem Power you have associated with such Account will become inaccessible if you do not have your Account password. +
10.4. Transactions. In order to be completed, all proposed Steem transactions must be confirmed and recorded in the Steem public ledger via the Steem distributed consensus network (a peer-to-peer economic network that operates on a cryptographic protocol), which is not owned, controlled or operated by Steemit. The Steem Network is operated by a decentralized network of independent third parties. Blockchain has no control over the Steem network and therefore cannot and does not ensure that any transaction details you submit via the Services will be confirmed via the Steem network. You acknowledge and agree that the transaction details you submit via the Services may not be completed, or may be substantially delayed, by the Steem network. You may use the Services to submit these details to the network. +
10.5. No Storage or Transmission of STEEM, Steem Dollars or Steem Power. Steem, in any of its forms (STEEM, Steem Dollars and Steem Power) is an intangible, digital asset. They exist only by virtue of the ownership record maintained in the Steem network. The Service does not store, send or receive Steem. Any transfer of title that might occur in any STEEM, Steem Dollars or Steem Power occurs on the decentralized ledger within the Steem network and not within the Services. We do not guarantee that the Service can effect the transfer of title or right in any Steem, Steem Dollars or Steem Power. +
10.6. Relationship. Nothing in these Terms is intended to nor shall create any partnership, joint venture, agency, consultancy or trusteeship, you and Steemit being with respect to one another independent contractors. +
10.7. Accuracy of Information. You represent and warrant that any information you provide via the Services is accurate and complete. You accept and acknowledge that Steemit is not responsible for any errors or omissions that you make in connection with any Steem transaction initiated via the Services, for instance, if you mistype an Account Name or otherwise provide incorrect information. We strongly encourage you to review your transaction details carefully before completing them via the Services. +
10.8. No Cancellations or Modifications. Once transaction details have been submitted to the Steem network via the Services, The Services cannot assist you to cancel or otherwise modify your transaction details. Steemit has no control over the Steem Network and does not have the ability to facilitate any cancellation or modification requests. +
10.9. Taxes. It is your responsibility to determine what, if any, taxes apply to the transactions you for which you have submitted transaction details via the Services, and it is your responsibility to report and remit the correct tax to the appropriate tax authority. You agree that Steemit is not responsible for determining whether taxes apply to your Steem transactions or for collecting, reporting, withholding or remitting any taxes arising from any Steem transactions. +
11. Fees for Using the Steemit Services
11.1. Company Fees Creating an Account. Steemit does not currently charge fees for any Services, however we reserve the right to do so in future, and in such case any applicable fees will be displayed prior to you using any Service to which a fee applies. +
12. No Right To Cancel And/Or Reverse Steem Transactions
14.1. If you use a Service to which Steem, Steem Dollars or Steem Power is transacted, you will not be able to change your mind once you have confirmed that you wish to proceed with the Service or transaction. +
15. Discontinuation of Services +
15.1. We may, in our sole discretion and without cost to you, with or without prior notice and at any time, modify or discontinue, temporarily or permanently, any portion of our Services. You are solely responsible for storing, outside of the Services, a backup of any Account and Private Key pair that you maintain in your Wallet. +
15.2. If you do not maintain a backup of your Account data outside of the Services, you will be may not be able to access Steem, Steem Dollars and Steem Power associated with any Account Name maintained in your Account in the event that we discontinue or deprecate the Services. +
16. Suspension or Termination of Service.
16.1. We may suspend or terminate your access to the Services in our sole discretion, immediately and without prior notice, and delete or deactivate your Account and all related information and files in such without cost to you, including, for instance, in the event that you breach any term of these Terms. In the event of termination, your access to funds will depend on your access to your backup of your Account data including your Account Name and Private Keys. +
17. User Conduct
17.1. When accessing or using the Services, you agree that you will not commit any unlawful act, and that you are solely responsible for your conduct while using our Services. Without limiting the generality of the foregoing, you agree that you will not: +
17.1.1. Use our Services in any manner that could interfere with, disrupt, negatively affect or inhibit other users from fully enjoying our Services, or that could damage, disable, overburden or impair the functioning of our Services in any manner; +
17.1.2. Use our Services to pay for, support or otherwise engage in any illegal activities, including, but not limited to illegal gambling, fraud, money- laundering, or terrorist activities. +
17.1.3. Use any robot, spider, crawler, scraper or other automated means or interface not provided by us to access our Services or to extract data; +
17.1.4. Use or attempt to use another user’s Wallet without authorization;
17.1.5. Attempt to circumvent any content filtering techniques we employ, or attempt to access any service or area of our Services that you are not authorized to access; +
17.1.6. + Introduce to the Services any virus, Trojan, worms, logic bombs or other harmful material;
17.1.7. + Develop any third-party applications that interact with our Services without our prior written consent; +
17.1.8. + Provide false, inaccurate, or misleading information; or +
17.1.9. Encourage or induce any third party to engage in any of the activities prohibited under this Section. +
17.1.10. Reverse engineer any aspect of Steemit or do anything that might discover source code or bypass or circumvent measures employed to prevent or limit access to any Steemit Content, area or code of Steemit. +
18. Third-Party Content and Sites
+ Steemit may include links and other content owned or operated by third parties, including advertisements and social “ + widgets” (we call these “Third-Party Content“). You agree that Steemit is not responsible or liable for Third-Party Content and that you access and use Third-Party Content at your own risk. Your interactions with Third-Party Content are solely between you and the third party providing the content. When you leave Steemit, you should understand that these Terms no longer govern and that the terms and policies of those third-party sites or services will then apply. +
19. Feedback
You may submit questions, comments, feedback, suggestions, and other information regarding Steemit (we call this “Feedback“). You acknowledge and agree that Feedback is non-confidential and will become the sole property of Steemit. Steemit shall own exclusive rights, including, without limitation, all intellectual property rights, in and to such Feedback and is entitled to the unrestricted use and dissemination of this Feedback for any purpose, without acknowledgment or compensation to you. You agree to execute any documentation required by Steemit to confirm such assignment to Steemit. +
20. Copyright Complaints
Steemit respects the intellectual property of others by not reading infringed content from the Steem blockchain. If you believe that your work has been copied in a way that constitutes copyright infringement, you may notify Steemit’ + s Designated Agent by contacting:
contact@steemit.com
Please see 17 + U.S.C. §512(c)(3) for the requirements of a proper notification. You should note that if you knowingly misrepresent in your notification that the material or activity is infringing, you may be liable for any damages, including costs and attorneys’ + fees, incurred by Steemit or the alleged infringer, as the result of Steemit’s relying upon such misrepresentation in removing or disabling access to the material or activity claimed to be infringing. +
21. Disclaimers
TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, STEEMIT AND THE STEEMIT CONTENT ARE PROVIDED ON AN “ + AS IS” AND “AS AVAILABLE” BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT AND ANY WARRANTIES IMPLIED BY ANY COURSE OF PERFORMANCE OR USAGE OF TRADE. STEEMIT DOES NOT REPRESENT OR WARRANT THAT STEEMIT AND THE STEEMIT CONTENT: (A) WILL BE SECURE OR AVAILABLE AT ANY PARTICULAR TIME OR LOCATION; (B) ARE ACCURATE, COMPLETE, RELIABLE, CURRENT OR ERROR-FREE OR THAT ANY DEFECTS OR ERRORS WILL BE CORRECTED; AND (C) ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. YOUR USE OF STEEMIT AND THE STEEMIT CONTENT IS SOLELY AT YOUR OWN RISK. SOME JURISDICTIONS DO NOT ALLOW THE DISCLAIMER OF IMPLIED TERMS IN CONTRACTS WITH CONSUMERS, SO SOME OR ALL OF THE DISCLAIMERS IN THIS SECTION MAY NOT APPLY TO YOU. +
22. Limitation of Liability
TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL STEEMIT OR THE STEEMIT PARTIES BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY OR PUNITIVE DAMAGES, OR ANY OTHER DAMAGES OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, LOSS OF PROFITS OR LOSS OF DATA, WHETHER IN AN ACTION IN CONTRACT, TORT (INCLUDING, BUT NOT LIMITED TO, NEGLIGENCE) OR OTHERWISE, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH, THE USE OF, OR INABILITY TO USE, STEEMIT OR THE STEEMIT CONTENT. TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE AGGREGATE LIABILITY OF STEEMIT OR THE STEEMIT PARTIES, WHETHER IN CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE, WHETHER ACTIVE, PASSIVE OR IMPUTED), PRODUCT LIABILITY, STRICT LIABILITY OR OTHER THEORY, ARISING OUT OF OR RELATING TO: (A) THE USE OF OR INABILITY TO USE STEEMIT OR THE STEEMIT CONTENT; OR (B) THESE TERMS EXCEED ANY COMPENSATION YOU PAY, IF ANY, TO STEEMIT FOR ACCESS TO OR USE OF STEEMIT. +
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS IN THIS SECTION MAY NOT APPLY TO YOU. +
23. Modifications to Steemit
Steemit reserves the right to modify or discontinue, temporarily or permanently, Steemit, or any features or portions of Steemit, without prior notice. You agree that Steemit will not be liable for any modification, suspension or discontinuance of Steemit, or any part of Steemit. +
24. Arbitration
PLEASE READ THE FOLLOWING SECTION CAREFULLY BECAUSE IT REQUIRES YOU TO ARBITRATE CERTAIN DISPUTES WITH STEEMIT AND LIMITS THE MANNER IN WHICH YOU CAN SEEK RELIEF FROM STEEMIT. +
24.1. Binding Arbitration
Except for disputes in which either party seeks to bring an individual action in small claims court or seeks injunctive or other equitable relief for the alleged unlawful use of copyrights, trademarks, trade names, logos, trade secrets or patents, you and Steemit: (a) waive your right to have any and all disputes or Claims arising from these Terms or Steemit (collectively, “Disputes“) resolved in a court; and (b) waive your right to a jury trial. Instead, you and Steemit will arbitrate Disputes through binding arbitration (which is the referral of a Dispute to one or more persons charged with reviewing the Dispute and making a final and binding determination to resolve it, instead of having the Dispute decided by a judge or jury in court). +
24.2. No Class Arbitrations, Class Actions or Representative Actions
YOU AND STEEMIT AGREE THAT ANY DISPUTE IS PERSONAL TO YOU AND STEEMIT AND THAT ANY SUCH DISPUTE WILL BE RESOLVED SOLELY THROUGH INDIVIDUAL ARBITRATION AND WILL NOT BE BROUGHT AS A CLASS ARBITRATION, CLASS ACTION OR ANY OTHER TYPE OF REPRESENTATIVE PROCEEDING. NEITHER PARTY AGREES TO CLASS ARBITRATION OR TO AN ARBITRATION IN WHICH AN INDIVIDUAL ATTEMPTS TO RESOLVE A DISPUTE AS A REPRESENTATIVE OF ANOTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. FURTHER, YOU AND STEEMIT AGREE THAT A DISPUTE CANNOT BE BROUGHT AS A CLASS, OR OTHER TYPE OF REPRESENTATIVE ACTION, WHETHER WITHIN OR OUTSIDE OF ARBITRATION, OR ON BEHALF OF ANY OTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. +
24.3. Federal Arbitration Act
You and Steemit agree that these Terms affect interstate commerce and that the enforceability of this Section 14 shall be governed by, construed and enforced, both substantively and procedurally, by the Federal Arbitration Act, 9 U.S.C. § + 1 et seq. (the “FAA”) to the maximum extent permitted by applicable law.
24.4. Process +
You and Steemit agree that you will notify each other in writing of any Dispute within thirty (30) days of when it arises so that the parties can attempt, in good faith, to resolve the Dispute informally. Notice to Steemit shall be provided by filling out the form available at https://steemit.com/contact/ or sending an email to contact@Steemit.com. Your notice must include: (1) your name, postal address and email address; (2) a description of the nature or basis of the Dispute; and (3) the specific relief that you are seeking. If you and Steemit cannot agree how to resolve the Dispute within thirty (30) days of Steemit receiving the notice, either you or Steemit may, as appropriate pursuant to this Section 14, commence an arbitration proceeding or file a claim in court. You and Steemit agree that any arbitration or claim must be commenced or filed within one (1) year after the Dispute arose; otherwise, you and Steemit agree that the claim is permanently barred (which means that you will no longer have the right to assert a claim regarding the Dispute). You and Steemit agree that: (a) any arbitration will occur in San Francisco County, California; (b) arbitration will be conducted confidentially by a single arbitrator in accordance with the rules of JAMS; and (c) the state or federal courts in California will have exclusive jurisdiction over the enforcement of an arbitration award and over any Dispute between the parties that is not subject to arbitration. You may also litigate a Dispute in small claims court located in the county where you reside if the Dispute meets the requirements to be heard in small claims court. +
24.5. Authority of Arbitrator
As limited by the FAA, these Terms and applicable JAMS rules, the arbitrator will have: (a) the exclusive authority and jurisdiction to make all procedural and substantive decisions regarding a Dispute; and (b) the authority to grant any remedy that would otherwise be available in court. The arbitrator may only conduct an individual arbitration and may not consolidate more than one individual’ + s claims, preside over any type of class or representative proceeding or preside over any proceeding involving more than one individual. +
24.6. Rules of JAMS
The rules of, and additional information about, JAMS are available on the JAMS website at http://www.jamsadr.com/, as may be updated from time to time. By agreeing to be bound by these Terms, you either: (a) acknowledge and agree that you have read and understand the rules of JAMS; or (b) waive your opportunity to read the rules of JAMS and any claim that the rules of JAMS are unfair or should not apply for any reason. +
24.7. Severability
If any term, clause or provision of this Section 14 is held invalid or unenforceable, it will be so held to the minimum extent required by law and all other terms, clauses or provisions will remain valid and enforceable. Further, the waivers set forth in Section 24.2 are severable from the other provisions of these Terms and will remain valid and enforceable, except as prohibited by applicable law. +
25. Applicable Law and Venue
These Terms and your access to and use of Steemit and the Steemit Content will be governed by, and construed in accordance with, the laws of California, without resort to its conflict of law provisions. To the extent the arbitration provision in Section 14 does not apply and the Dispute cannot be heard in small claims court, you agree that any action at law or in equity arising out of, or relating to, these Terms shall be filed only in the state and federal courts located in San Francisco County, California and you hereby irrevocably and unconditionally consent and submit to the exclusive jurisdiction of such courts over any suit, action or proceeding arising out of these Terms. +
26. Termination
Steemit reserves the right, without notice and in our sole discretion, to terminate your license to access and use Steemit and to block or prevent your future access to, and use of, Steemit. +
27. Severability
If any term, clause or provision of these Terms is deemed to be unlawful, void or for any reason unenforceable, then that term, clause or provision shall be deemed severable from these Terms and shall not affect the validity and enforceability of any remaining provisions. +
28. Questions & Contact Information
Questions or comments about Steemit may be directed to contact@steemit.com
+{total_value}
; + if (isMyAccount) { + estimate_output ={total_value}
; + } + + /// transfer log + let idx = 0 + const transfer_log = account.get('transfer_history') + .map(item => { + const data = item.getIn([1, 'op', 1]); + const type = item.getIn([1, 'op', 0]); + + // Filter out rewards + if (type === "curation_reward" || type === "author_reward" || type === "comment_benefactor_reward") { + return null; + } + + if(data.sbd_payout === '0.000 SBD' && data.vesting_payout === '0.000000 VESTS') + return null + return+ {tt('about_jsx.about_app_details')} + {tt('about_jsx.learn_more_at_app_url', {APP_URL})}. +
+After validating your sign up request with us we'll look it over for approval. As soon as your turn is up and you're approved, you'll be sent a link to finalize your account!
+You'll be among the earliest members of the Steemit community!
+You're a few steps away from getting to the top of the list. Check your email and click the email validation link.
+After validating your sign up request with us we'll look it over for approval. As soon as your turn is up and you're approved, you'll be sent a link to finalize your account!
+You'll be among the earliest members of the Steemit community!
+LOADING..
+Membership to Steemit.com is now under invitation only because of unexpectedly high sign up rate.
+You need to Logout before you can create another account.
+Please note that Steemit can only register one account per verified user.
+It looks like your sign up request is not approved yet or you already created an account.
+ Please try again later or contact support@steemit.com for the status of your request.
+ If you didn't submit your sign up application yet, apply now!
+
{server_error}
+
+ The first rule of Steemit is: Do not lose your password.
+ The second rule of Steemit is: Do not lose your password.
+ The third rule of Steemit is: We cannot recover your password, or your account if you lose your password.
+ The forth rule: Do not tell anyone your password.
+ The fifth rule: Always back up your password.
+
+
+ Seriously, we are, for technical reasons, entirely unable to gain
+ access to an account without knowing the password. Steemit is a
+ new model, entirely unlike other sites on the Internet. It's not
+ simply policy: We cannot recover your account or password
+ if you lose it.
+
+ Print out your password or write it down in a safe place.
+
{tt('market_jsx.date_created')} | +{tt('g.type')} | +{tt('g.price')} | +{LIQUID_TOKEN} | +{`${DEBT_TOKEN_SHORT} (${CURRENCY_SIGN})`} | +{tt('market_jsx.action')} | +
---|
Not to worry. You can head back to our homepage, + or check out some great posts. +
+LOADING..
+The creation of new accounts is temporarily disabled.
+You need to Logout before you can create an additional account.
+Please note that Steemit can only register one account per verified user.
+Your sign up request is being processed and you will receive an email from us when it is ready.
+Signup requests can take up to 7 days to be processed, but usually complete in a day or two.
+Congratulations! Your sign up request has been approved.
+Let's get your account created!
+Our records indicate that you already have steem account: {existingUserAccount}
+ //In order to prevent abuse Steemit can only register one account per verified user.
+ //You can either login to your existing account + // or send us email if you need a new account.
+ //{server_error}
+Your account name is how you will be known on steemit.com.
+ {/*Your account name can never be changed, so please choose carefully.*/}
Got an account? Login
+{tt('promote_post_jsx.this_post_was_hidden_due_to_low_ratings')}.{' '} +
++ {showNegativeComments ? tt('post_jsx.now_showing_comments_with_low_ratings') : tt('post_jsx.comments_were_hidden_due_to_low_ratings')}.{' '} + +
+Not to worry. You can head back to our homepage, + or check out some great posts. +
++ 1 + We want you to understand how and why Steemit, Inc (“Steemit,” “we” or “us”) collects, uses, and shares information about you when + you access and use Steemit’s websites, mobile apps, widgets, and other online products and services (collectively, the "Services") + or when you otherwise interact with us. +
++ 2 + We collect information you provide directly to us when you use our Services. Depending on which Service you use, we may collect + different information about you. This includes: +
++ 3 + We collect the content and other information you provide when you use our Services. This includes information used to create your + account (e.g., a username, an email address, phone number), account preferences, and the content of information you post to the + Services (e.g., text, photos, videos, links). +
++ 4 + If you purchase products or services from us (e.g., Steem Power), we will collect certain information from you, including your name, + address, email address, and information about the product or service you are purchasing. Payments are processed by third-party + payment processors (e.g., Stripe and PayPal), so please refer to the applicable processor’s terms and privacy policy for more + information about how payment information is processed and stored. +
++ 5 + You may choose to provide other information directly to us. For example, we may collect information when you fill out a form, + participate in contests, sweepstakes or promotions, apply for a job, communicate with us via third-party sites and services, request + customer support or otherwise communicate with us. +
++ 6 + When you access or use our Services, we may also automatically collect information about you. This includes: +
++ 7 + We may log information when you access and use the Services. This may include your IP address, user-agent string, browser type, + operating system, referral URLs, device information (e.g., device IDs), pages visited, links clicked, user interactions (e.g., + voting data), the requested URL, hardware settings, and search terms. +
++ 8 + We may receive information from cookies, which are pieces of data your browser stores and sends back to us when making requests. We + use this information to improve your experience, understand user activity, and improve the quality of our Services. For + example, we store and retrieve information about your preferred language and other settings. For more information on how you can + disable cookies, please see “Your Choices” below. +
++ 9 + With your consent, we may collect information about the specific location of your mobile device (for example, by using GPS or + Bluetooth). You can revoke this consent at any time by changing the preferences on your device, but doing so may affect your ability + to use all of the features and functionality of our Services. +
++ 10 + We may offer social sharing features or other integrated tools that let you share content or actions you take on our Services with + other media. Your use of these features enables the sharing of certain information with your friends or the public, depending on the + settings you establish with the third party that provides the social sharing feature. For more information about the purpose and + scope of data collection and processing in connection with social sharing features, please visit the privacy policies of the third + parties that provide these social sharing features (e.g., Tumblr, Facebook, Reddit, Pinterest, and Twitter). +
++ 13 + Please note that, even when you delete your account, the posts, comments and messages you submit through the Services may still be + viewable or available on our servers. For more information, see “Your Choices” below. +
++ 15 + We may share aggregated or de-identified information, which cannot reasonably be used to identify you. +
++ 16 + We may partner with third-party advertisers, ad networks, and analytics providers to deliver advertising and content targeted to + your interests and to better understand your use of the Services. These third parties may collect information sent by your computer, + browser, or mobile device in response to a request for content, such as unique identifiers, your IP address, or other information + about your computer or device. For example: +
++ 17 + Our ad partners and network may use cookies and use related technologies to collect information when ads are delivered to you on our + Services, but Steemit does not link to or provide your actual Steemit account details to these advertising partners. This means that + Steemit does not share your individual account browsing habits with advertisers. Steemit cannot see advertisers’ cookies and + advertisers will not see Steemit cookies. +
++ 18 + We use analytics partners (such as Google Analytics) to help analyze usage and traffic for our Services. As an example, we may use + analytics partners to analyze and measure, in the aggregate, the number of unique visitors to our Services. +
++ 19 + For more information about how you may control the collection and/or use of information for advertising and analytics purposes, + please see “Your Choices.” +
++ 20 + We take reasonable measures to help protect information about you from loss, theft, misuse and unauthorized access, disclosure, + alteration, and destruction. +
++ 21 + Although we welcome users from all walks of life, Steemit is not intended or directed at individuals under the age of 13. Therefore, + individuals under the age of 13 may not create an account or otherwise access or use the Services. +
++ 22 + As a Steemit user, you have choices about how to protect and limit the collection, use, and disclosure of, information about you. + This includes: +
++ 23 + We may provide you with tools and preference settings that allow you to access, correct, delete, and modify information associated + with your account. +
++ 24 + You may delete your account information at any time by logging into your account and following the steps under “Preferences.” When + you delete your account, your profile is no longer visible to other users and disassociated from content you posted under that + account. Please note, however, that the posts, comments, and messages you submitted prior to deleting your account will still be + visible to others, unless you delete such content. We may also retain certain information about you as required by law or for + legitimate business purposes after you delete your account. Any information that has been published to the Steem blockchain will + also remain indefinitely. +
++ 25 + Most web browsers are set to accept cookies by default. If you prefer, you can usually choose to set your browser to remove or + reject first- and third-party cookies. Please note that if you choose to remove or reject cookies, this could affect the + availability and functionality of our Services. +
++ 26 + Some analytics providers may provide specific opt-out mechanisms (e.g. Google Analytics Opt-out), and we will provide, as needed, + additional tools and third-party services that allow you to better understand cookies and how you can opt-out. You have the ability + to opt out of having your web browsing information used for + behavioral advertising purposes. For more information about behavioral advertising, or to opt out, please visit + www.aboutads.info/choices. +
++ 27 + Do Not Track (“DNT”) is a privacy preference that you can set in most web browsers. We support DNT in our Services. There is no + accepted standard on how to respond to web browsers’ DNT signals. When you have DNT enabled, we may still use information collected + for analytics and measurement purposes or to otherwise provide our Services (e.g., Steemit.com buttons), but we will not load any + third-party trackers. You may, however, opt out of having information about you collected and used for behavioral advertising + purposes, as described above. +
++ 28 + You may opt out of receiving promotional communications from us by following the instructions in those communications. If you opt + out, we may still send you non-promotional communications, such as information about your account or your use of our Services. +
++ 29 + With your consent, we may send promotional and non-promotional push notifications or alerts to your mobile device. You can + deactivate these messages at any time by changing the notification settings on your mobile device. +
++ 30 + We are based in the United States and the information we collect is governed by U.S. law. By accessing or using the Services or + otherwise providing information to us, you consent to the processing, transfer and storage of information in and to the U.S. and + other countries, where you may not have the same rights as you do under local law. +
++ 31 + Steemit complies with the U.S.-E.U. and U.S.-Swiss Safe Harbor Privacy Principles of notice, choice, onward transfer, security, data + integrity, access, and enforcement. Despite an adverse judgment by the European Court of Justice on October 6, 2015, the U.S. + Department of Commerce has advised that it continues to administer the Safe Harbor program until further notice. To learn more about + the Safe Harbor program, and to view our certification, please visit the U.S. Department of Commerce website. If you have concerns + about our compliance with the Safe Harbor program, you should contact us. If you are unable to resolve your concern or dispute with + us, you may submit complaints to JAMS for mediation pursuant to the JAMS International Mediation Rules. +
++ 32 + We may change this Privacy Policy from time to time. If we do, we will let you know by revising the date at the top of the policy. + If we make a change to this policy that, in our sole discretion, is material, we will provide you with additional notice (such as + adding a statement to steemouncements, the front page of the Services or sending you a notification). We encourage you to review the + Privacy Policy whenever you access or use our Services or otherwise interact with us to stay informed about our information + practices and the ways you can help protect your privacy. If you continue to use our Services after Privacy Policy changes go into + effect, you consent to the revised policy. +
++ 33 + If you have any questions about this Privacy Policy, please email contact@steemit.com. +
++ {tt('recoveraccountstep1_jsx.recover_account_intro', {APP_URL: APP_DOMAIN, APP_NAME})} +
+ ++ {tt('g.please_email_questions_to')} contact@steemit.com. +
+Last Updated April 28, 2016
Welcome to Steemit! These Terms of Service (“Terms”) apply to your access to and use of Steemit.com and any other products or services that link to these Terms (“Steemit”). Steemit is provided by Steemit, Inc. (“Steemit”, “we” + or “us”). By accessing or using Steemit, you agree to be bound by these Terms. If you do not agree to these Terms, including the mandatory arbitration provision and class action waiver in Section 14, do not access or use Steemit. If we make changes to these Terms, we will provide notice of those changes by updating the “Last Updated” date above or posting notice on Steemit. Your continued use of Steemit will confirm your acceptance of the changes. +
You understand and agree that these Terms apply solely to your access to, and use of, Steemit and that, when you use other Steemit services such as Steemit.com or Steemitgifts.com, the terms and policies particular to those services apply. +
1. Privacy Policy
Please refer to our Privacy Policy for information about how we collect, use and disclose information about you. +
2. Eligibility
Steemit is not targeted towards, nor intended for use by, anyone under the age of 13. You must be at least 13 years of age to access or use Steemit. If you are between 13 and 18 years of age (or the age of legal majority where you reside), you may only access or use Steemit under the supervision of a parent or legal guardian who agrees to be bound by these Terms. +
3. Copyright and Limited License
Steemit contains data, text, photographs, images, video, audio, graphics, articles, comments, software, code, scripts and other content supplied by us, the Steem blockchain or our licensors, which we call “Steemit Content.” Steemit Content is protected by intellectual property laws, including copyright and other proprietary rights of the United States and foreign countries. Except as explicitly stated in these Terms, Steemit does not grant any express or implied rights to use Steemit Content. +
You are granted a limited, nonexclusive, non-transferable, and non-sublicensable license to access and use Steemit and Steemit Content for your personal, non-commercial use. This license is subject to these Terms and does not include any right to: (a) distribute, publicly perform or publicly display any Steemit Content; (b) modify or otherwise make any derivative uses of Steemit or Steemit Content, or any portion thereof; (c) use any data mining, robots or similar data gathering or extraction methods; and (d) use Steemit or Steemit Content other than for their intended purposes. Any use of Steemit or Steemit Content other than as authorized in these Terms is strictly prohibited and will terminate the license granted herein. This license is revocable at any time. +
4. Adult-Oriented Content
Steemit is intended for a general audience and, as a result, some Steemit Content may discuss or depict adult-oriented topics. We realize that this content may not be appropriate or desirable for some of our readers depending on their current location, age, background or personal views. As a result, we mark this content as Not Safe For Work (“NSFW”).
Marking Steemit Content as NSFW does not prevent you from being able to access this content but, instead, helps you make informed decisions about the type of content you view on Steemit. You understand and agree that you access content marked as NSFW at your own risk. +
5. Trademarks
“Steem,” “ + Steemit,” the Steemit logo and any other product or service names, logos or slogans that may appear on Steemit are trademarks of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. You may not use any metatags or other “ + hidden text” utilizing “Steemit” or any other name, trademark or product or service name of Steemit without our prior written permission. In addition, the look and feel of Steemit, including, without limitation, all page headers, custom graphics, button icons and scripts, constitute the service mark, trademark or trade dress of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. All other trademarks, registered trademarks, product names and company names or logos mentioned or used on Steemit are the property of their respective owners and may not be copied, imitated or used, in whole or in part, without the permission of the applicable trademark holder. Reference to any products, services, processes or other information by name, trademark, manufacturer, supplier or otherwise does not constitute or imply endorsement, sponsorship or recommendation by Steemit. +
6. Assumption of Risk, Limitations on Liability & Indemnity
6.1. + You accept and acknowledge that there are risks associated with utilizing an Internet- based STEEM account service including, but not limited to, the risk of failure of hardware, software and Internet connections, the risk of malicious software introduction, and the risk that third parties may obtain unauthorized access to information stored within your Account, including, but not limited to your Private Key (as defined below at 10.2.). You accept and acknowledge that Steemit will not be responsible for any communication failures, disruptions, errors, distortions or delays you may experience when using the Services, however caused. +
6.2. We will use reasonable endeavours to verify the accuracy of any information on the Service but we make no representation or warranty of any kind, express or implied, statutory or otherwise, regarding the contents of the Service, information and functions made accessible through the Service, any hyperlinks to third party websites, nor for any breach of security associated with the transmission of information through the Service or any website linked to by the Service. +
6.3. We will not be responsible or liable to you for any loss and take no responsibility for and will not be liable to you for any use of our Services, including but not limited to any losses, damages or claims arising from: (a) User error such as forgotten passwords, incorrectly constructed transactions, or mistyped STEEM addresses; (b) Server failure or data loss; (c) Corrupted Account files; (d) Unauthorized access to applications; (e) Any unauthorized third party activities, including without limitation the use of viruses, phishing, bruteforcing or other means of attack against the Service or Services. +
6.4. We make no warranty that the Service or the server that makes it available, are free of viruses or errors, that its content is accurate, that it will be uninterrupted, or that defects will be corrected. We will not be responsible or liable to you for any loss of any kind, from action taken, or taken in reliance on material, or information, contained on the Service. +
6.5. Subject to 7.1 below, any and all indemnities, warranties, terms and conditions (whether express or implied) are hereby excluded to the fullest extent permitted under Luxembourg law. +
6.6. We will not be liable, in contract, or tort (including, without limitation, negligence), other than where we have been fraudulent or made negligent misrepresentations. +
6.7. Nothing in these Terms excludes or limits liability for death or personal injury caused by negligence, fraudulent misrepresentation, or any other liability which may not otherwise be limited or excluded under United States law. +
7. Agreement to Hold Steemit Harmless
7.1. You agree to hold harmless Steemit (and each of our officers, directors, members, employees, agents and affiliates) from any claim, demand, action, damage, loss, cost or expense, including without limitation reasonable legal fees, arising out or relating to: +
7.1.1. Your use of, or conduct in connection with, our Services;
7.1.2. Any feedback or submissions you provide (see 19 below);
7.1.3. + Your violation of these Terms; or
7.1.4. Violation of any rights of any other person or entity. +
7.2. If you are obligated to indemnify us, we will have the right, in our sole discretion, to control any action or proceeding (at our expense) and determine whether we wish to settle it. +
8. No Liability For Third Party Services And Content
8.1. In using our Services, you may view content or utilize services provided by third parties, including links to web pages and services of such parties (“ + Third Party Content”). We do not control, endorse or adopt any Third-Party Content and will have no responsibility for Third Party Content including, without limitation, material that may be misleading, incomplete, erroneous, offensive, indecent or otherwise objectionable in your jurisdiction. In addition, your dealings or correspondence with such third parties are solely between you and the third parties. We are not responsible or liable for any loss or damage of any sort incurred as a result of any such dealings and you understand that your use of Third Party Content, and your interactions with third parties, is at your own risk. +
9. Account Registration
9.1. You need not use a Steemit Account. If you wish to use an Account, you must create an Account with Steemit to access the Services (“ + Account”). When you create an Account, you are strongly advised to take the following precautions, as failure to do so may result in loss of access to, and/or control over, your Wallet: (a) Create a strong password that you do not use for any other website or online service; (b) Provide accurate and truthful information; (c) Maintain and promptly update your Account information; (d) maintain the security of your Account by protecting your Account password and access to your computer and your Account; (e) Promptly notify us if you discover or otherwise suspect any security breaches related to your Account. +
9.2. You hereby accept and acknowledge that you take responsibility for all activities that occur under your Account and accept all risks of any authorized or unauthorized access to your Account, to the maximum extent permitted by law. +
10. The Steemit Services +
10.1. As described in more detail below, the Services, among other things, provide in-browser (or otherwise local) software that (a) generates and stores STEEM ACCOUNT Addresses and encrypted Private Keys (defined below), and (b) Facilitates the submission of STEEM transaction data to the Steem network without requiring you to access the STEEM command line interface. +
10.2. Account Names and Private Keys. When you create an Account, the Services generate and store a cryptographic private and public key pair that you may use to send and receive STEEM and Steem Dollars via the Steem network.. The Private Key uniquely matches the Account Name and must be used in connection with the Account Name to authorize the transfer of STEEM and Steem Dollars from that Account. You are solely responsible for maintaining the security of your Private Key and any password phrase associated with your wallet. You must keep your Account, password phrase and Private Key access information secure. Failure to do so may result in the loss of control of Steem, Steem Power and Steem Dollars associated with the Wallet. +
10.3. No Password Retrieval. Steemit does not receive or store your Account password, nor the unencrypted keys and addresses. Therefore, we cannot assist you with Account password retrieval. Our Services provide you with tools to help you remember or recover your password, including by allowing you to set password hints, but the Services cannot generate a new password for your Account. You are solely responsible for remembering your Account password. If you have not safely stored a backup of any Account Names and password pairs maintained in your Account, you accept and acknowledge that any STEEM, Steem Dollars and Steem Power you have associated with such Account will become inaccessible if you do not have your Account password. +
10.4. Transactions. In order to be completed, all proposed Steem transactions must be confirmed and recorded in the Steem public ledger via the Steem distributed consensus network (a peer-to-peer economic network that operates on a cryptographic protocol), which is not owned, controlled or operated by Steemit. The Steem Network is operated by a decentralized network of independent third parties. Blockchain has no control over the Steem network and therefore cannot and does not ensure that any transaction details you submit via the Services will be confirmed via the Steem network. You acknowledge and agree that the transaction details you submit via the Services may not be completed, or may be substantially delayed, by the Steem network. You may use the Services to submit these details to the network. +
10.5. No Storage or Transmission of STEEM, Steem Dollars or Steem Power. Steem, in any of its forms (STEEM, Steem Dollars and Steem Power) is an intangible, digital asset. They exist only by virtue of the ownership record maintained in the Steem network. The Service does not store, send or receive Steem. Any transfer of title that might occur in any STEEM, Steem Dollars or Steem Power occurs on the decentralized ledger within the Steem network and not within the Services. We do not guarantee that the Service can effect the transfer of title or right in any Steem, Steem Dollars or Steem Power. +
10.6. Relationship. Nothing in these Terms is intended to nor shall create any partnership, joint venture, agency, consultancy or trusteeship, you and Steemit being with respect to one another independent contractors. +
10.7. Accuracy of Information. You represent and warrant that any information you provide via the Services is accurate and complete. You accept and acknowledge that Steemit is not responsible for any errors or omissions that you make in connection with any Steem transaction initiated via the Services, for instance, if you mistype an Account Name or otherwise provide incorrect information. We strongly encourage you to review your transaction details carefully before completing them via the Services. +
10.8. No Cancellations or Modifications. Once transaction details have been submitted to the Steem network via the Services, The Services cannot assist you to cancel or otherwise modify your transaction details. Steemit has no control over the Steem Network and does not have the ability to facilitate any cancellation or modification requests. +
10.9. Taxes. It is your responsibility to determine what, if any, taxes apply to the transactions you for which you have submitted transaction details via the Services, and it is your responsibility to report and remit the correct tax to the appropriate tax authority. You agree that Steemit is not responsible for determining whether taxes apply to your Steem transactions or for collecting, reporting, withholding or remitting any taxes arising from any Steem transactions. +
11. Fees for Using the Steemit Services
11.1. Company Fees Creating an Account. Steemit does not currently charge fees for any Services, however we reserve the right to do so in future, and in such case any applicable fees will be displayed prior to you using any Service to which a fee applies. +
12. No Right To Cancel And/Or Reverse Steem Transactions
14.1. If you use a Service to which Steem, Steem Dollars or Steem Power is transacted, you will not be able to change your mind once you have confirmed that you wish to proceed with the Service or transaction. +
15. Discontinuation of Services +
15.1. We may, in our sole discretion and without cost to you, with or without prior notice and at any time, modify or discontinue, temporarily or permanently, any portion of our Services. You are solely responsible for storing, outside of the Services, a backup of any Account and Private Key pair that you maintain in your Wallet. +
15.2. If you do not maintain a backup of your Account data outside of the Services, you will be may not be able to access Steem, Steem Dollars and Steem Power associated with any Account Name maintained in your Account in the event that we discontinue or deprecate the Services. +
16. Suspension or Termination of Service.
16.1. We may suspend or terminate your access to the Services in our sole discretion, immediately and without prior notice, and delete or deactivate your Account and all related information and files in such without cost to you, including, for instance, in the event that you breach any term of these Terms. In the event of termination, your access to funds will depend on your access to your backup of your Account data including your Account Name and Private Keys. +
17. User Conduct
17.1. When accessing or using the Services, you agree that you will not commit any unlawful act, and that you are solely responsible for your conduct while using our Services. Without limiting the generality of the foregoing, you agree that you will not: +
17.1.1. Use our Services in any manner that could interfere with, disrupt, negatively affect or inhibit other users from fully enjoying our Services, or that could damage, disable, overburden or impair the functioning of our Services in any manner; +
17.1.2. Use our Services to pay for, support or otherwise engage in any illegal activities, including, but not limited to illegal gambling, fraud, money- laundering, or terrorist activities. +
17.1.3. Use any robot, spider, crawler, scraper or other automated means or interface not provided by us to access our Services or to extract data; +
17.1.4. Use or attempt to use another user’s Wallet without authorization;
17.1.5. Attempt to circumvent any content filtering techniques we employ, or attempt to access any service or area of our Services that you are not authorized to access; +
17.1.6. + Introduce to the Services any virus, Trojan, worms, logic bombs or other harmful material;
17.1.7. + Develop any third-party applications that interact with our Services without our prior written consent; +
17.1.8. + Provide false, inaccurate, or misleading information; or +
17.1.9. Encourage or induce any third party to engage in any of the activities prohibited under this Section. +
17.1.10. Reverse engineer any aspect of Steemit or do anything that might discover source code or bypass or circumvent measures employed to prevent or limit access to any Steemit Content, area or code of Steemit. +
18. Third-Party Content and Sites
+ Steemit may include links and other content owned or operated by third parties, including advertisements and social “ + widgets” (we call these “Third-Party Content“). You agree that Steemit is not responsible or liable for Third-Party Content and that you access and use Third-Party Content at your own risk. Your interactions with Third-Party Content are solely between you and the third party providing the content. When you leave Steemit, you should understand that these Terms no longer govern and that the terms and policies of those third-party sites or services will then apply. +
19. Feedback
You may submit questions, comments, feedback, suggestions, and other information regarding Steemit (we call this “Feedback“). You acknowledge and agree that Feedback is non-confidential and will become the sole property of Steemit. Steemit shall own exclusive rights, including, without limitation, all intellectual property rights, in and to such Feedback and is entitled to the unrestricted use and dissemination of this Feedback for any purpose, without acknowledgment or compensation to you. You agree to execute any documentation required by Steemit to confirm such assignment to Steemit. +
20. Copyright Complaints
Steemit respects the intellectual property of others by not reading infringed content from the Steem blockchain. If you believe that your work has been copied in a way that constitutes copyright infringement, you may notify Steemit’ + s Designated Agent by contacting:
contact@steemit.com
Please see 17 + U.S.C. §512(c)(3) for the requirements of a proper notification. You should note that if you knowingly misrepresent in your notification that the material or activity is infringing, you may be liable for any damages, including costs and attorneys’ + fees, incurred by Steemit or the alleged infringer, as the result of Steemit’s relying upon such misrepresentation in removing or disabling access to the material or activity claimed to be infringing. +
21. Disclaimers
TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, STEEMIT AND THE STEEMIT CONTENT ARE PROVIDED ON AN “ + AS IS” AND “AS AVAILABLE” BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT AND ANY WARRANTIES IMPLIED BY ANY COURSE OF PERFORMANCE OR USAGE OF TRADE. STEEMIT DOES NOT REPRESENT OR WARRANT THAT STEEMIT AND THE STEEMIT CONTENT: (A) WILL BE SECURE OR AVAILABLE AT ANY PARTICULAR TIME OR LOCATION; (B) ARE ACCURATE, COMPLETE, RELIABLE, CURRENT OR ERROR-FREE OR THAT ANY DEFECTS OR ERRORS WILL BE CORRECTED; AND (C) ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. YOUR USE OF STEEMIT AND THE STEEMIT CONTENT IS SOLELY AT YOUR OWN RISK. SOME JURISDICTIONS DO NOT ALLOW THE DISCLAIMER OF IMPLIED TERMS IN CONTRACTS WITH CONSUMERS, SO SOME OR ALL OF THE DISCLAIMERS IN THIS SECTION MAY NOT APPLY TO YOU. +
22. Limitation of Liability
TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL STEEMIT OR THE STEEMIT PARTIES BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY OR PUNITIVE DAMAGES, OR ANY OTHER DAMAGES OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, LOSS OF PROFITS OR LOSS OF DATA, WHETHER IN AN ACTION IN CONTRACT, TORT (INCLUDING, BUT NOT LIMITED TO, NEGLIGENCE) OR OTHERWISE, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH, THE USE OF, OR INABILITY TO USE, STEEMIT OR THE STEEMIT CONTENT. TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE AGGREGATE LIABILITY OF STEEMIT OR THE STEEMIT PARTIES, WHETHER IN CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE, WHETHER ACTIVE, PASSIVE OR IMPUTED), PRODUCT LIABILITY, STRICT LIABILITY OR OTHER THEORY, ARISING OUT OF OR RELATING TO: (A) THE USE OF OR INABILITY TO USE STEEMIT OR THE STEEMIT CONTENT; OR (B) THESE TERMS EXCEED ANY COMPENSATION YOU PAY, IF ANY, TO STEEMIT FOR ACCESS TO OR USE OF STEEMIT. +
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS IN THIS SECTION MAY NOT APPLY TO YOU. +
23. Modifications to Steemit
Steemit reserves the right to modify or discontinue, temporarily or permanently, Steemit, or any features or portions of Steemit, without prior notice. You agree that Steemit will not be liable for any modification, suspension or discontinuance of Steemit, or any part of Steemit. +
24. Arbitration
PLEASE READ THE FOLLOWING SECTION CAREFULLY BECAUSE IT REQUIRES YOU TO ARBITRATE CERTAIN DISPUTES WITH STEEMIT AND LIMITS THE MANNER IN WHICH YOU CAN SEEK RELIEF FROM STEEMIT. +
24.1. Binding Arbitration
Except for disputes in which either party seeks to bring an individual action in small claims court or seeks injunctive or other equitable relief for the alleged unlawful use of copyrights, trademarks, trade names, logos, trade secrets or patents, you and Steemit: (a) waive your right to have any and all disputes or Claims arising from these Terms or Steemit (collectively, “Disputes“) resolved in a court; and (b) waive your right to a jury trial. Instead, you and Steemit will arbitrate Disputes through binding arbitration (which is the referral of a Dispute to one or more persons charged with reviewing the Dispute and making a final and binding determination to resolve it, instead of having the Dispute decided by a judge or jury in court). +
24.2. No Class Arbitrations, Class Actions or Representative Actions
YOU AND STEEMIT AGREE THAT ANY DISPUTE IS PERSONAL TO YOU AND STEEMIT AND THAT ANY SUCH DISPUTE WILL BE RESOLVED SOLELY THROUGH INDIVIDUAL ARBITRATION AND WILL NOT BE BROUGHT AS A CLASS ARBITRATION, CLASS ACTION OR ANY OTHER TYPE OF REPRESENTATIVE PROCEEDING. NEITHER PARTY AGREES TO CLASS ARBITRATION OR TO AN ARBITRATION IN WHICH AN INDIVIDUAL ATTEMPTS TO RESOLVE A DISPUTE AS A REPRESENTATIVE OF ANOTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. FURTHER, YOU AND STEEMIT AGREE THAT A DISPUTE CANNOT BE BROUGHT AS A CLASS, OR OTHER TYPE OF REPRESENTATIVE ACTION, WHETHER WITHIN OR OUTSIDE OF ARBITRATION, OR ON BEHALF OF ANY OTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. +
24.3. Federal Arbitration Act
You and Steemit agree that these Terms affect interstate commerce and that the enforceability of this Section 14 shall be governed by, construed and enforced, both substantively and procedurally, by the Federal Arbitration Act, 9 U.S.C. § + 1 et seq. (the “FAA”) to the maximum extent permitted by applicable law.
24.4. Process +
You and Steemit agree that you will notify each other in writing of any Dispute within thirty (30) days of when it arises so that the parties can attempt, in good faith, to resolve the Dispute informally. Notice to Steemit shall be provided by filling out the form available at https://steemit.com/contact/ or sending an email to contact@Steemit.com. Your notice must include: (1) your name, postal address and email address; (2) a description of the nature or basis of the Dispute; and (3) the specific relief that you are seeking. If you and Steemit cannot agree how to resolve the Dispute within thirty (30) days of Steemit receiving the notice, either you or Steemit may, as appropriate pursuant to this Section 14, commence an arbitration proceeding or file a claim in court. You and Steemit agree that any arbitration or claim must be commenced or filed within one (1) year after the Dispute arose; otherwise, you and Steemit agree that the claim is permanently barred (which means that you will no longer have the right to assert a claim regarding the Dispute). You and Steemit agree that: (a) any arbitration will occur in San Francisco County, California; (b) arbitration will be conducted confidentially by a single arbitrator in accordance with the rules of JAMS; and (c) the state or federal courts in California will have exclusive jurisdiction over the enforcement of an arbitration award and over any Dispute between the parties that is not subject to arbitration. You may also litigate a Dispute in small claims court located in the county where you reside if the Dispute meets the requirements to be heard in small claims court. +
24.5. Authority of Arbitrator
As limited by the FAA, these Terms and applicable JAMS rules, the arbitrator will have: (a) the exclusive authority and jurisdiction to make all procedural and substantive decisions regarding a Dispute; and (b) the authority to grant any remedy that would otherwise be available in court. The arbitrator may only conduct an individual arbitration and may not consolidate more than one individual’ + s claims, preside over any type of class or representative proceeding or preside over any proceeding involving more than one individual. +
24.6. Rules of JAMS
The rules of, and additional information about, JAMS are available on the JAMS website at http://www.jamsadr.com/, as may be updated from time to time. By agreeing to be bound by these Terms, you either: (a) acknowledge and agree that you have read and understand the rules of JAMS; or (b) waive your opportunity to read the rules of JAMS and any claim that the rules of JAMS are unfair or should not apply for any reason. +
24.7. Severability
If any term, clause or provision of this Section 14 is held invalid or unenforceable, it will be so held to the minimum extent required by law and all other terms, clauses or provisions will remain valid and enforceable. Further, the waivers set forth in Section 24.2 are severable from the other provisions of these Terms and will remain valid and enforceable, except as prohibited by applicable law. +
25. Applicable Law and Venue
These Terms and your access to and use of Steemit and the Steemit Content will be governed by, and construed in accordance with, the laws of California, without resort to its conflict of law provisions. To the extent the arbitration provision in Section 14 does not apply and the Dispute cannot be heard in small claims court, you agree that any action at law or in equity arising out of, or relating to, these Terms shall be filed only in the state and federal courts located in San Francisco County, California and you hereby irrevocably and unconditionally consent and submit to the exclusive jurisdiction of such courts over any suit, action or proceeding arising out of these Terms. +
26. Termination
Steemit reserves the right, without notice and in our sole discretion, to terminate your license to access and use Steemit and to block or prevent your future access to, and use of, Steemit. +
27. Severability
If any term, clause or provision of these Terms is deemed to be unlawful, void or for any reason unenforceable, then that term, clause or provision shall be deemed severable from these Terms and shall not affect the validity and enforceability of any remaining provisions. +
28. Questions & Contact Information
Questions or comments about Steemit may be directed to contact@steemit.com
+{about}
} +
+ {location &&
+ {tt('g.sorry_your_reddit_account_doesnt_have_enough_karma')} +
+{tt('g.or_click_the_button_below_to_register_with_facebook')}
+ {tt('g.register_with_facebook')} ++ {tt('witnesses_jsx.you_have_votes_remaining', {count: witness_vote_count})}.{' '} + {tt('witnesses_jsx.you_can_vote_for_maximum_of_witnesses')}. +
} ++ | {tt('witnesses_jsx.witness')} | +{tt('witnesses_jsx.information')} | +
---|
{tt('witnesses_jsx.if_you_want_to_vote_outside_of_top_enter_account_name')}.
+ +{current_proxy ? tt('witnesses_jsx.witness_set') : tt('witnesses_jsx.set_witness_proxy')}
+ {current_proxy ? +{tt('witnesses_jsx.proxy_update_error')}.
} +* |
Благодарим Вас за отправку запроса на восстановление аккаунта используя основанную на блокчейне мультифакторную аутентификацию %(APP_NAME)s’a.
Мы ответим Вам как можно быстрее, однако, пожалуйста, ожидайте что может быть некоторая задержка из-за большого объема писем.
Пожалуйста, будьте готовы подтвердить свою личность.
С уважением,
Ned Scott
CEO Steemit
", + "recovering_account": "Восстанавливаем аккаунт", + "recover_account": "Восстановить аккаунт", + "checking_account_owner": "Проверяем владельца аккаунта", + "sending_recovery_request": "Отправляем запрос восстановления", + "cant_confirm_account_ownership": "Мы не можем подтвердить владение аккаунтом. Проверьте ваш пароль", + "account_recovery_request_not_confirmed": "Запрос восстановления аккаунта еще не подтвержден, пожалуйста проверьте позднее. Спасибо за ваше терпение." + }, + "user_profile": { + "unknown_account": "Неизвестный аккаунт", + "user_hasnt_made_any_posts_yet": "Похоже, что %(name)s еще не написал постов!", + "user_hasnt_started_bloggin_yet": "Похоже. что %(name)s еще не завёл блог!", + "user_hasnt_followed_anything_yet": "Похоже, что %(name)s еще ни на кого не подписан! Если, %(name)s недавно подписался на новых пользователей, их персонализированный канал будет заполняться сразу после появления нового контента.", + "user_hasnt_had_any_replies_yet": "%(name)s еще не получил ответов", + "looks_like_you_havent_posted_anything_yet": "Похоже, ты еще ничего не опубликовал.", + "create_a_post": "Создать сообщение", + "explore_trending_articles": "Посмотреть статьи, набирающие популярность", + "read_the_quick_start_guide": "Прочтите краткое руководство", + "browse_the_faq": "Просмотреть ЧаВО", + "followers": "Подписчики", + "this_is_users_reputations_score_it_is_based_on_history_of_votes": "Это репутация %(name)s.\n\nРепутация рассчитывается на основе истории полученных голосов и используется для сокрытия низкокачественного контента.", + "follower_count": { + "zero": "Нет подписчиков", + "one": "1 подписчик", + "few": "%(count)s подписчика", + "many": "%(count)s подписчиков", + "other": "%(count)s подписчиков" + }, + "followed_count": { + "zero": "Ни на кого не подписан", + "one": "1 подписок", + "few": "%(count)s подписки", + "many": "%(count)s подписок", + "other": "%(count)s подписок" + }, + "post_count": { + "zero": "Постов нет", + "one": "1 пост", + "few": "%(count)s поста", + "many": "%(count)s постов", + "other": "%(count)s постов" + } + }, + "authorrewards_jsx": { + "estimated_author_rewards_last_week": "Оценочные авторские вознаграждения за прошлую неделю", + "author_rewards_history": "История авторских наград" + }, + "curationrewards_jsx": { + "estimated_curation_rewards_last_week": "Оценочные кураторские вознаграждения за последнюю неделю", + "curation_rewards_history": "История кураторских наград" + }, + "post_jsx": { + "now_showing_comments_with_low_ratings": "Отображаем комментарии с низким рейтингом", + "sort_order": "Порядок сортировки", + "comments_were_hidden_due_to_low_ratings": "Комментарии были скрыты из-за низкого рейтинга" + }, + "voting_jsx": { + "flagging_post_can_remove_rewards_the_flag_should_be_used_for_the_following": "Голосование против уменьшает вознаграждение и снижает позицию в рейтинге.", + "disagreement_on_rewards": "Несогласие с вознаграждением", + "fraud_or_plagiarism": "Мошенничество или плагиат", + "hate_speech_or_internet_trolling": "Разжигание ненависти или Интернет троллинг", + "intentional_miss_categorized_content_or_spam": "Преднамеренная неправильная категоризация контента или спам", + "pending_payout": "Ожидаемая выплата", + "payout_declined": "Автор отказался от вознаграждения", + "max_accepted_payout": "Максимально допустимое вознаграждение: $%(value)s", + "promotion_cost": "Цена продвижения: $%(value)s", + "past_payouts": "Предыдущие выплаты $%(value)s", + "past_payouts_author": " - Авторские $%(value)s", + "past_payouts_curators": " - Кураторские $%(value)s", + "we_will_reset_curation_rewards_for_this_post": "это сбросит выплаты за курирование", + "removing_your_vote": "Удаление голоса", + "changing_to_an_upvote": "Изменить на голосование 'за'", + "changing_to_a_downvote": "Изменить на голосование 'против'", + "confirm_flag": "Подтвердить голос 'против'", + "and_more": "и %(count)s больше", + "votes_plural": { + "one": "%(count)s голоса", + "few": "%(count)s голоса", + "many": "%(count)s голосов", + "other": "%(count)s голосов" + } + }, + "witnesses_jsx": { + "witness_thread": "пост делегата", + "top_witnesses": "Топ делегатов", + "you_have_votes_remaining": { + "zero": "У Вас не осталось голосов", + "one": "У Вас остался 1 голос", + "few": "У Вас осталось %(count)s голоса", + "many": "У Вас осталось %(count)s голосов", + "other": "У Вас осталось %(count)s голосов" + }, + "you_can_vote_for_maximum_of_witnesses": "Вы можете голосовать максимум за 30 делегатов", + "witness": "Делегаты", + "information": "Информация", + "if_you_want_to_vote_outside_of_top_enter_account_name": "Если вы хотите проголосовать за делегата вне top 50, введите имя аккаунта ниже", + "set_witness_proxy": "Вы также можете выбрать прокси, который будет вместо вас голосовать за делегатов. Это сбросит ваш текущий выбор делегата.", + "witness_set": "Вы установили прокси для голосования. Если вы хотите голосовать вручную, пожалуйста, очистите ваш прокси.", + "witness_proxy_current": "Ваш текущий прокси", + "witness_proxy_set": "Установить прокси", + "witness_proxy_clear": "Очистить прокси", + "proxy_update_error": "Ваш прокси не был обновлен" + }, + "votesandcomments_jsx": { + "no_responses_yet_click_to_respond": "Ответов пока нет. Нажмите чтобы ответить.", + "response_count_tooltip": { + "zero": "ответов пока нет. Нажмите чтобы ответить.", + "one": "1 ответ. Нажмите чтобы ответить.", + "few": "%(count)s ответа. Нажмите чтобы ответить.", + "many": "%(count)s ответов. Нажмите чтобы ответить.", + "other": "%(count)s ответов. Нажмите чтобы ответить." + }, + "vote_count": { + "zero": "нет голосов", + "one": "1 голос", + "few": "%(count)s голоса", + "many": "%(count)s голосов", + "other": "%(count)s голосов" + } + }, + "userkeys_jsx": { + "public": "Публичное", + "private": "Приватное", + "public_something_key": "Публичный %(key)s ключ", + "private_something_key": "Приватный %(key)s ключ", + "posting_key_is_required_it_should_be_different": "Постинг ключ используется для постинга и голосования. Он должен отличаться от активного ключа и ключа владельца.", + "the_active_key_is_used_to_make_transfers_and_place_orders": "Активный ключ используется для переводов и размещения заказов на внутреннем рынке.", + "the_owner_key_is_required_to_change_other_keys": "Ключ владельца это главный ключ ко всему аккаунта, он необходим для изменения других ключей.", + "the_private_key_or_password_should_be_kept_offline": "Приватный ключ или пароль должен храниться в оффлайне так часто насколько возможно.", + "the_memo_key_is_used_to_create_and_read_memos": "Ключ примечаний используется для создания и чтения примечаний." + }, + "suggestpassword_jsx": { + "APP_NAME_cannot_recover_passwords_keep_this_page_in_a_secure_location": "%(APP_NAME)s не может восстановить пароли. Сохраните эту страницу в безопасном месте, например, в огнестойком сейфе или в депозитарной ячейке.", + "APP_NAME_password_backup": "%(APP_NAME)s резервное копирование пароля", + "APP_NAME_password_backup_required": "%(APP_NAME)s резервное копирование пароля (обязательно!)", + "after_printing_write_down_your_user_name": "После печати запишите ваше имя пользователя" + }, + "converttosteem_jsx": { + "your_existing_DEBT_TOKEN_are_liquid_and_transferable": "Ваши %(DEBT_TOKEN)s токены ликвидны. Вы можете конвертировать %(DEBT_TOKEN)s на этом сайте (%(link)s) или вывести на биржу/обменник.", + "this_is_a_price_feed_conversion": "Конвертация на сайте выполняется три с половиной дня - это требуется, чтобы исключить манипуляции с ценами.", + "convert_to_LIQUID_TOKEN": "Ковертировать в %(LIQUID_TOKEN)s", + "DEBT_TOKEN_will_be_unavailable": "Эта операция будет проходить 3-5 дней от настоящего момента и ее нельзя отменить. Эти %(DEBT_TOKEN)s мгновенно станут недоступны" + }, + "tips_js": { + "liquid_token": "Ликвидные цифровые токены, которые могут переданы куда угодно в любой момент.+ type = checked (for checkbox or radio) + type = selected (for seelct option) + type = string ++ @return {string} type +*/ +function t(field) { + const [, type = 'string'] = field.split(':') + return type +} + +/** + @return {string} name +*/ +function n(field) { + const [name] = field.split(':') + return name +} + +const hasValue = v => v == null ? false : (typeof v === 'string' ? v.trim() : v) === '' ? false : true +const toString = v => hasValue(v) ? v : '' +const toBoolean = v => hasValue(v) ? JSON.parse(v) : '' diff --git a/src/app/utils/ReduxForms.js b/src/app/utils/ReduxForms.js new file mode 100644 index 0000000..8e8e2a0 --- /dev/null +++ b/src/app/utils/ReduxForms.js @@ -0,0 +1,9 @@ +export const cleanReduxInput = i => { + // Remove all props that don't belong. Triggers React warnings. + const {name, placeholder, label, value, checked, onChange, onBlur, onFocus} = i + const ret = {name, placeholder, label, value, checked, onChange, onBlur, onFocus} + if(ret.value == null) delete ret.value + if(ret.label == null) delete ret.label + if(ret.type == null) delete ret.type + return ret +} diff --git a/src/app/utils/RegisterServiceWorker.js b/src/app/utils/RegisterServiceWorker.js new file mode 100644 index 0000000..4da112e --- /dev/null +++ b/src/app/utils/RegisterServiceWorker.js @@ -0,0 +1,34 @@ +export default function registerServiceWorker() { + if (!navigator.serviceWorker) return Promise.resolve(false); + return navigator.serviceWorker.register('/service-worker.js', {scope: '/'}) + .then(function (registration) { + navigator.serviceWorker.ready.catch(e => console.error('-- registerServiceWorker error -->', e)); + return navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { + let subscription = serviceWorkerRegistration.pushManager.getSubscription(); + return subscription.then(function (subscription) { + if (subscription) { + return subscription; + } + return serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true + }); + }); + }); + }).then(function (subscription) { + const rawKey = subscription.getKey ? subscription.getKey('p256dh') : ''; + const key = rawKey ? + btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : + ''; + const rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : ''; + const authSecret = rawAuthSecret ? + btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : + ''; + return { + endpoint: subscription.endpoint, + keys: { + p256dh: key, + auth: authSecret + } + }; + }); +} diff --git a/src/app/utils/RemarkablePlugin.js b/src/app/utils/RemarkablePlugin.js new file mode 100644 index 0000000..61bceda --- /dev/null +++ b/src/app/utils/RemarkablePlugin.js @@ -0,0 +1,180 @@ +// import {validate_account_name} from 'app/utils/ChainValidation' +// import linksRe from 'app/utils/Links' +// import config from 'config' + +/** usage +
+import {plugin, tagRules} from 'app/utils/RemarkablePlugin'
+import Remarkable from 'remarkable'
+const remarkable = new Remarkable()
+remarkable.use(plugin(tagRules))
+const renderedText = remarkable.render(`#jsc @user`)
+console.log('hashtags', tagRules.hashtags())
+console.log('usertags', tagRules.usertags())
+console.log('renderedText', renderedText)
+
+*/
+// export const tagRules = {
+// done: () => {
+// const ret = {
+// tags: Array.from(hashtags),
+// users: Array.from(usertags),
+// }
+// hashtags = new Set()
+// usertags = new Set()
+// return ret
+// },
+// link_open: (tokens, i, options, env, renderer) => {
+// tagLinkOpen = true
+// const link = tokens[i + 1].content
+// tagRules.youtubeId = null
+// tagRules.youtubeTime = null
+// if(link) link.replace(linksRe.youTube, url => {
+// const match = url.match(linksRe.youTubeId)
+// if(match && match.length >= 2) {
+// const id = match[1]
+// tagRules.youtubeId = id
+// const [, query] = link.split('?')
+// if(query) {
+// const params = query.split('&')
+// for(const param of params) {
+// if(/^t=/.test(param)) tagRules.youtubeTime = param.substring(2)
+// }
+// }
+// return
+// }
+// console.log("Youtube link without ID?", url);
+// })
+// if(tagRules.youtubeId) return ''
+// return renderer.rules.link_open(tokens, i, options, env, renderer)
+// },
+// text: (tokens, i, options, env, renderer) => {
+// if(tagRules.youtubeId)
+// return `youtube:${tagRules.youtubeId}${tagRules.youtubeTime ? ',' + tagRules.youtubeTime : ''}`
+//
+// if(tagLinkOpen)
+// return renderer.rules.text(tokens, i, options, env, renderer)
+// let content = tokens[i].content
+// // hashtag
+// content = content.replace(/(^|\s)(#[-a-z\d]+)/ig, tag => {
+// if(/#[\d]+$/.test(tag)) return tag // Don't allow numbers to be tags
+// const space = /^\s/.test(tag) ? tag[0] : ''
+// tag = tag.trim().substring(1)
+// hashtags.add(tag)
+// return space + `#${tag}`
+// })
+// // usertag (mention)
+// content = content.replace(/(^|\s)(@[-\.a-z\d]+)/ig, user => {
+// const space = /^\s/.test(user) ? user[0] : ''
+// user = user.trim().substring(1)
+// const valid = validate_account_name(user) == null
+// if(valid) usertags.add(user)
+// return space + (valid ?
+// `@${user}` :
+// '@' + user
+// )
+// })
+// // unescapted ipfs links (temp, until the reply editor categorizes the image)
+// //if(config.ipfs_prefix)
+// // content = content.replace(linksRe.ipfsPrefix, config.ipfs_prefix)
+//
+// return content
+// },
+// link_close: (tokens, i, options, env, renderer) => {
+// tagLinkOpen = false
+// if(tagRules.youtubeId) {
+// tagRules.youtubeId = null
+// tagRules.youtubeTime = null
+// return ''
+// }
+// return renderer.rules.link_close(tokens, i, options, env, renderer)
+// },
+// }
+// let hashtags = new Set()
+// let usertags = new Set()
+// let tagLinkOpen
+//
+// export const imageLinks = {
+// done: () => {
+// if(image.length > 1) links.delete(image[1])
+// const ret = {
+// image: Array.from(image),
+// links: Array.from(links)
+// }
+// image = []
+// links = new Set()
+// return ret
+// },
+// image: (tokens, i) => {
+// // 
+// const token = tokens[i]
+// const {content} = token
+// if(image.length) return content // only first one
+// image.push(token.src)
+// return content
+// },
+// link_open: (tokens, i) => {
+// // [inline link](http://www.duckduckgo.com "Example Title")
+// const token = tokens[i]
+// const {content} = token
+// // console.log('token(link_open)', token)
+// const {href} = token
+// if(linksRe.image.test(href) && !image.length)
+// // looks like an image link, no markup though
+// image.push(href)
+// else
+// links.add(href)
+// // [](http://google.com.au/)
+// if(image.length) return content // only the first image and link combo
+// // link around an image
+// const next = tokens[i + 1]
+// if(next && next.type === 'image') {
+// const {src} = next
+// image.push(src)
+// image.push(href)
+// }
+// return content
+// }
+// }
+// let image = []
+// let links = new Set()
+
+/**
+Usage...
+
+import Remarkable from 'remarkable'
+const remarkable = new Remarkable()
+const customRules = {
+ text: (tokens, i, options, env, ctx) => {
+ const token = tokens[i]
+ const content = token.content
+ console.log('token(2)', token)
+ return content
+ }
+}
+remarkable.use(plugin(customRules))
+const renderedText = remarkable.render(`#jsc`)
+console.log('renderedText', renderedText)
+
+*/
+// export const plugin = rules => md => {
+// // const render = md.renderer.render
+// md.renderer.render = (tokens, options, env) => {
+// let str = ''
+// for (let i = 0; i < tokens.length; i++) {
+// if (tokens[i].type === 'inline') {
+// str += md.renderer.render(tokens[i].children, options, env);
+// } else {
+// const token = tokens[i]
+// // console.log('token(plugin)', token, rules)
+// const customRule = rules[token.type]
+// if(customRule)
+// str += customRule(tokens, i, options, env, md.renderer)
+// else {
+// str += md.renderer.rules[token.type](tokens, i, options, env, md.renderer)
+// }
+// }
+// }
+// return str
+// }
+// }
diff --git a/src/app/utils/RemarkableStripper.js b/src/app/utils/RemarkableStripper.js
new file mode 100644
index 0000000..f0c2a82
--- /dev/null
+++ b/src/app/utils/RemarkableStripper.js
@@ -0,0 +1,23 @@
+import Remarkable from 'remarkable'
+
+const remarkable = new Remarkable()
+export default remarkable
+
+/** Removes all markdown leaving just plain text */
+const remarkableStripper = md => {
+ md.renderer.render = (tokens, options, env) => {
+ let str = ''
+ for (let i = 0; i < tokens.length; i++) {
+ if (tokens[i].type === 'inline') {
+ str += md.renderer.render(tokens[i].children, options, env);
+ } else {
+ // console.log('content', tokens[i])
+ const content = tokens[i].content
+ str += (content || '') + ' '
+ }
+ }
+ return str
+ }
+}
+
+remarkable.use(remarkableStripper) // removes all markdown
diff --git a/src/app/utils/SanitizeConfig.js b/src/app/utils/SanitizeConfig.js
new file mode 100644
index 0000000..b4206a9
--- /dev/null
+++ b/src/app/utils/SanitizeConfig.js
@@ -0,0 +1,138 @@
+import { getPhishingWarningMessage } from 'shared/HtmlReady'; // the only allowable title attribute for a div
+
+const iframeWhitelist = [
+ {
+ re: /^(https?:)?\/\/player.vimeo.com\/video\/.*/i,
+ fn: (src) => {
+ //
+ if(!src) return null
+ const m = src.match(/https:\/\/player\.vimeo\.com\/video\/([0-9]+)/)
+ if(!m || m.length !== 2) return null
+ return 'https://player.vimeo.com/video/' + m[1]
+ }
+ },
+ { re: /^(https?:)?\/\/www.youtube.com\/embed\/.*/i,
+ fn: (src) => {
+ return src.replace(/\?.+$/, ''); // strip query string (yt: autoplay=1,controls=0,showinfo=0, etc)
+ }
+ },
+ {
+ re: /^https:\/\/w.soundcloud.com\/player\/.*/i,
+ fn: (src) => {
+ if(!src) return null
+ //
+ const m = src.match(/url=(.+?)&/)
+ if(!m || m.length !== 2) return null
+ return 'https://w.soundcloud.com/player/?url=' + m[1] +
+ '&auto_play=false&hide_related=false&show_comments=true' +
+ '&show_user=true&show_reposts=false&visual=true'
+ }
+ }
+];
+export const noImageText = '(Image not shown due to low ratings)'
+export const allowedTags = `
+ div, iframe, del,
+ a, p, b, i, q, br, ul, li, ol, img, h1, h2, h3, h4, h5, h6, hr,
+ blockquote, pre, code, em, strong, center, table, thead, tbody, tr, th, td,
+ strike, sup, sub
+`.trim().split(/,\s*/)
+
+// Medium insert plugin uses: div, figure, figcaption, iframe
+export default ({large = true, highQualityPost = true, noImage = false, sanitizeErrors = []}) => ({
+ allowedTags,
+ // figure, figcaption,
+
+ // SEE https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
+ allowedAttributes: {
+ // "src" MUST pass a whitelist (below)
+ iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen',
+ 'webkitallowfullscreen', 'mozallowfullscreen'],
+
+ // class attribute is strictly whitelisted (below)
+ // and title is only set in the case of a phishing warning
+ div: ['class', 'title'],
+
+ // style is subject to attack, filtering more below
+ td: ['style'],
+ img: ['src', 'alt'],
+ a: ['href', 'rel'],
+ },
+ transformTags: {
+ iframe: (tagName, attribs) => {
+ const srcAtty = attribs.src;
+ for(const item of iframeWhitelist)
+ if(item.re.test(srcAtty)) {
+ const src = typeof item.fn === 'function' ? item.fn(srcAtty, item.re) : srcAtty
+ if(!src) break
+ return {
+ tagName: 'iframe',
+ attribs: {
+ frameborder: '0',
+ allowfullscreen: 'allowfullscreen',
+ webkitallowfullscreen: 'webkitallowfullscreen', // deprecated but required for vimeo : https://vimeo.com/forums/help/topic:278181
+ mozallowfullscreen: 'mozallowfullscreen', // deprecated but required for vimeo
+ src,
+ width: large ? '640' : '480',
+ height: large ? '360' : '270',
+ },
+ }
+ }
+ console.log('Blocked, did not match iframe "src" white list urls:', tagName, attribs)
+ sanitizeErrors.push('Invalid iframe URL: ' + srcAtty)
+ return {tagName: 'div', text: `(Unsupported ${srcAtty})`}
+ },
+ img: (tagName, attribs) => {
+ if(noImage) return {tagName: 'div', text: noImageText}
+ //See https://github.com/punkave/sanitize-html/issues/117
+ let {src, alt} = attribs
+ if(!/^(https?:)?\/\//i.test(src)) {
+ console.log('Blocked, image tag src does not appear to be a url', tagName, attribs)
+ sanitizeErrors.push('An image in this post did not save properly.')
+ return {tagName: 'img', attribs: {src: 'brokenimg.jpg'}}
+ }
+
+ // replace http:// with // to force https when needed
+ src = src.replace(/^http:\/\//i, '//')
+ let atts = {src}
+ if(alt && alt !== '') atts.alt = alt
+ return {tagName, attribs: atts}
+ },
+ div: (tagName, attribs) => {
+ const attys = {}
+ const classWhitelist = ['pull-right', 'pull-left', 'text-justify', 'text-rtl', 'text-center', 'text-right', 'videoWrapper', 'phishy']
+ const validClass = classWhitelist.find(e => attribs.class == e)
+ if(validClass)
+ attys.class = validClass
+ if (validClass === 'phishy' && attribs.title === getPhishingWarningMessage())
+ attys.title = attribs.title
+ return {
+ tagName,
+ attribs: attys
+ }
+ },
+ td: (tagName, attribs) => {
+ const attys = {}
+ if(attribs.style === 'text-align:right')
+ attys.style = 'text-align:right'
+ return {
+ tagName,
+ attribs: attys
+ }
+ },
+ a: (tagName, attribs) => {
+ let {href} = attribs
+ if(!href) href = '#'
+ href = href.trim()
+ const attys = {href}
+ // If it's not a (relative or absolute) steemit URL...
+ if (!href.match(/^(\/(?!\/)|https:\/\/steemit.com)/)) {
+ // attys.target = '_blank' // pending iframe impl https://mathiasbynens.github.io/rel-noopener/
+ attys.rel = highQualityPost ? 'noopener' : 'nofollow noopener'
+ }
+ return {
+ tagName,
+ attribs: attys
+ }
+ },
+ }
+})
diff --git a/src/app/utils/ServerApiClient.js b/src/app/utils/ServerApiClient.js
new file mode 100644
index 0000000..d4bae6d
--- /dev/null
+++ b/src/app/utils/ServerApiClient.js
@@ -0,0 +1,101 @@
+import {NTYPES, notificationsArrayToMap} from 'app/utils/Notifications';
+import {api} from 'steem';
+
+const request_base = {
+ method: 'post',
+ mode: 'no-cors',
+ credentials: 'same-origin',
+ headers: {
+ Accept: 'application/json',
+ 'Content-type': 'application/json'
+ }
+};
+
+export function serverApiLogin(account, signatures) {
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return;
+ const request = Object.assign({}, request_base, {body: JSON.stringify({account, signatures, csrf: $STM_csrf})});
+ fetch('/api/v1/login_account', request);
+}
+
+export function serverApiLogout() {
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return;
+ const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf})});
+ fetch('/api/v1/logout_account', request);
+}
+
+let last_call;
+export function serverApiRecordEvent(type, val, rate_limit_ms = 5000) {
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return;
+ if (last_call && (new Date() - last_call) < rate_limit_ms) return;
+ last_call = new Date();
+ const value = val && val.stack ? `${val.toString()} | ${val.stack}` : val;
+ const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, type, value})});
+ fetch('/api/v1/record_event', request);
+ api.call('overseer.collect', {collection: 'event', metadata: {type, value}}, (error) => {
+ // if (error) console.warn('overseer error', error, error.data);
+ });
+}
+
+export function getNotifications(account) {
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(null);
+ const request = Object.assign({}, request_base, {method: 'get'});
+ return fetch(`/api/v1/notifications/${account}`, request).then(r => r.json()).then(res => {
+ return notificationsArrayToMap(res);
+});
+}
+
+export function markNotificationRead(account, fields) {
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(null);
+ const request = Object.assign({}, request_base, {method: 'put', mode: 'cors'});
+ const field_nums_str = fields.map(f => NTYPES.indexOf(f)).join('-');
+ return fetch(`/api/v1/notifications/${account}/${field_nums_str}`, request).then(r => r.json()).then(res => {
+ return notificationsArrayToMap(res);
+});
+}
+
+let last_page, last_views, last_page_promise;
+export function recordPageView(page, ref, account) {
+ if (last_page_promise && page === last_page) return last_page_promise;
+ if (window.ga) { // virtual pageview
+ window.ga('set', 'page', page);
+ window.ga('send', 'pageview');
+ }
+ api.call('overseer.pageview', {page, referer: ref, account}, (error) => {
+ // if (error) console.warn('overseer error', error, error.data);
+ });
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(0);
+ const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, page, ref})});
+ last_page_promise = fetch(`/api/v1/page_view`, request).then(r => r.json()).then(res => {
+ last_views = res.views;
+ return last_views;
+});
+ last_page = page;
+ return last_page_promise;
+}
+
+export function webPushRegister(account, webpush_params) {
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return;
+ const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, account, webpush_params})});
+ fetch('/api/v1/notifications/register', request);
+}
+
+export function sendConfirmEmail(account) {
+ const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, account})});
+ fetch('/api/v1/notifications/send_confirm', request);
+}
+
+export function saveCords(x, y) {
+ const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, x: x, y: y})});
+ fetch('/api/v1/save_cords', request);
+}
+
+export function setUserPreferences(payload) {
+ if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve();
+ const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: window.$STM_csrf, payload})});
+ return fetch('/api/v1/setUserPreferences', request);
+}
+
+if (process.env.BROWSER) {
+ window.getNotifications = getNotifications;
+ window.markNotificationRead = markNotificationRead;
+}
diff --git a/src/app/utils/SlateEditor/Align.js b/src/app/utils/SlateEditor/Align.js
new file mode 100644
index 0000000..b1c7d15
--- /dev/null
+++ b/src/app/utils/SlateEditor/Align.js
@@ -0,0 +1,26 @@
+import React from 'react'
+
+export default class Align extends React.Component {
+
+ getAlignClass = () => {
+ const {node} = this.props
+ switch(node.data.get('align')) {
+ case 'text-right': return 'text-right';
+ case 'text-left': return 'text-left';
+ case 'text-center': return 'text-center';
+ case 'pull-right': return 'pull-right';
+ case 'pull-left': return 'pull-left';
+ }
+ }
+
+ render = () => {
+ const { node, attributes, children } = this.props
+ const className = this.getAlignClass();
+
+ return (
+ + + const rect = node.getBoundingClientRect(); + return rect; +} diff --git a/src/app/utils/SlateEditor/Iframe.js b/src/app/utils/SlateEditor/Iframe.js new file mode 100644 index 0000000..f1dd15e --- /dev/null +++ b/src/app/utils/SlateEditor/Iframe.js @@ -0,0 +1,114 @@ +import React from 'react' +import linksRe from 'app/utils/Links' + +export default class Iframe extends React.Component { + + normalizeEmbedUrl = (url) => { + let match; + + // Detect youtube URLs + match = url.match(linksRe.youTubeId) + if(match && match.length >= 2) { + return 'https://www.youtube.com/embed/' + match[1] + } + + // Detect vimeo + match = url.match(linksRe.vimeoId) + if(match && match.length >= 2) { + return 'https://player.vimeo.com/video/' + match[1] + } + + console.log("unable to auto-detect embed url", url) + return null + } + + onChange = (e) => { + const { node, state, editor } = this.props + const value = e.target.value + + const src = this.normalizeEmbedUrl(value) || value + + const next = editor + .getState() + .transform() + .setNodeByKey(node.key, {data: {src}}) + .apply() + + editor.onChange(next) + } + + onClick = (e) => { + // stop propagation so that the void node itself isn't focused, since that would unfocus the input. + e.stopPropagation() + } + + render = () => { + const { node, state, attributes } = this.props + const isFocused = state.selection.hasEdgeIn(node) + const className = isFocused ? 'active' : null + + const lockStyle = { + position: 'absolute', + top: '0px', + left: '0px', + width: '100%', + height: '100%', + background: 'rgba(0,0,0,0.1)', + } + + return ( +
{'{}'}
},
+ { type: 'sup', label: x2 },
+ { type: 'sub', label: x2 },
+ ],
+
+ // blockTypes: {...Blocks,},
+ // toolbarTypes: [],
+ // sidebarTypes: [],
+
+ nodes: {
+ 'paragraph': ({ children, attributes }) => {children}
, + 'code-block': ({ children, attributes }) =>{children}
,
+ 'block-quote': ({ children, attributes }) => {children}, + 'bulleted-list': ({ children, attributes }) =>
{props.children}
,
+ italic: props => {props.children},
+ underline: props => {props.children},
+ strike: props => : ignore its inner element.
+ const code = el.tagName == 'pre' ? el.children[0] : null
+ let children = code && code.tagName == 'code' ? code.children : el.children
+
+ // due to disabled/broken whitespace normalization in cheerio/htmlparser2, perform basic cleaning...
+ // i.e. removal of text nodes where they are invalid -- otherwise they may convert to
s in bad places
+ const noTextChildren = 'ol,ul,table,thead,tbody,tr'.split(',')
+ if(noTextChildren.includes(el.tagName)) {
+ children = children.filter(el => el.type !== 'text')
+ }
+
+ // If this block-level node contains *any* tags, strip them out and wrap-align node
+ let center = false;
+ children = children.reduce( (out, child) => {
+ if(child.tagName == 'center') {
+ center = true;
+ //child.children.map(c => out.push(c))
+ out.push(...child.children)
+ } else {
+ out.push(child)
+ }
+ return out
+ }, [])
+
+ // Generate output block with clean children
+ const block = {
+ kind: 'block',
+ type: type,
+ isVoid: (type == 'hr'),
+ nodes: next(children)
+ }
+
+ // Wrap output block with align node if needed
+ if(center) {
+ console.log("** force-centering node")
+ return {
+ kind: 'block',
+ type: 'align',
+ data: {align: 'text-center'},
+ nodes: [block]
+ }
+ }
+
+ // Otherwise return plain block
+ return block
+ },
+
+ serialize: (object, children) => {
+ if(object.kind !== 'block') return
+ switch(object.type) {
+ case 'paragraph': return {children}
+ case 'block-quote': return {children}
+ case 'code-block': return {children}
+ case 'heading-one': return {children}
+ case 'heading-two': return {children}
+ case 'heading-three': return {children}
+ case 'heading-four': return {children}
+ case 'bulleted-list': return {children}
+ case 'numbered-list': return {children}
+ case 'list-item': return - {children}
+ case 'hr': return
+ case 'table': return {children}
+ case 'thead': return {children}
+ case 'tbody': return {children}
+ case 'tr': return {children}
+ case 'td': return {children}
+ case 'th': return {children}
+ }
+ }
+ },
+
+ // Mark rules
+ {
+ deserialize: (el, next) => {
+ const type = MARK_TAGS[el.tagName]
+ if (!type) return
+ return {
+ kind: 'mark',
+ type: type,
+ nodes: next(el.children)
+ }
+ },
+ serialize: (object, children) => {
+ if(object.kind !== 'mark') return;
+ switch(object.type) {
+ case 'bold': return {children}
+ case 'italic': return {children}
+ case 'underline': return {children}
+ case 'strike': return {children}
+ case 'code': return {children}
+ case 'sup': return {children}
+ case 'sub': return {children}
+ }
+ }
+ },
+
+ // Custom
+ {
+ deserialize: (el, next) => {
+ switch(el.tagName) {
+ case 'iframe':
+ return {
+ kind: 'block',
+ type: 'embed',
+ isVoid: true,
+ data: {src: el.attribs.src},
+ nodes: next(el.children)
+ }
+ case 'img':
+ return {
+ kind: 'inline',
+ type: 'image',
+ isVoid: true,
+ data: {src: el.attribs.src,
+ alt: el.attribs.alt},
+ nodes: next(el.children)
+ }
+ case 'a':
+ return {
+ kind: 'inline',
+ type: 'link',
+ data: {href: el.attribs.href},
+ nodes: next(el.children)
+ }
+ case 'br':
+ return {
+ "kind": "text",
+ "ranges": [{"text": "\n"}]
+ }
+ case 'code':
+ // may not be necessary after pr #406
+ if($(el).closest('pre').length == 0) {
+ return {
+ kind: 'mark',
+ type: 'code',
+ nodes: next(el.children)
+ }
+ } else {
+ console.log("** skipping within a ")
+ }
+ }
+ },
+
+ serialize: (object, children) => {
+ if(object.kind == 'string') return;
+ if(object.kind == 'inline' && object.type == 'link') {
+ const href = object.data.get('href')
+ return {children}
+ }
+ if(object.kind == 'block' && object.type == 'embed') {
+ const src = object.data.get('src')
+ return
+ }
+ if(object.kind == 'inline' && object.type == 'image') {
+ const src = object.data.get('src')
+ const alt = object.data.get('alt')
+ if(!src) console.log("** ERR: serializing image with no src...", JSON.stringify(object))
+ return
+ }
+ }
+ },
+
+ // debug uncaught nodes/elements
+ {
+ deserialize: (el, next) => {if(el.type !== 'text') console.log("** no deserializer for: ", $.html(el).replace(/\n/g, "\\n"))},
+ serialize: (object, children) => {if(object.kind != 'string') console.log("** no serializer for:", object.type, object.kind, 'data:', JSON.stringify(object))}
+ },
+]
+
+export const getMarkdownType = (chars) => {
+ switch (chars) {
+ case '1.':
+ case '*':
+ case '-': return 'list-item';
+ case '>': return 'block-quote';
+ case '#': return 'heading-one';
+ case '##': return 'heading-two';
+ case '###': return 'heading-three';
+ case '####': return 'heading-four';
+ case ' ': return 'code-block';
+ case '---': return 'hr';
+ default: return null;
+ }
+}
+
diff --git a/src/app/utils/StateFunctions.js b/src/app/utils/StateFunctions.js
new file mode 100644
index 0000000..d15eb2d
--- /dev/null
+++ b/src/app/utils/StateFunctions.js
@@ -0,0 +1,147 @@
+import assert from 'assert';
+import constants from 'app/redux/constants';
+import {parsePayoutAmount, repLog10} from 'app/utils/ParsersAndFormatters';
+import {Long} from 'bytebuffer';
+import {VEST_TICKER, LIQUID_TICKER} from 'app/client_config'
+import {fromJS} from 'immutable';
+
+export const numberWithCommas = (x) => x.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
+
+export function vestsToSpf(state, vesting_shares) {
+ const {global} = state
+ let vests = vesting_shares
+ if (typeof vesting_shares === 'string') {
+ vests = assetFloat(vesting_shares, VEST_TICKER)
+ }
+ const total_vests = assetFloat(global.getIn(['props', 'total_vesting_shares']), VEST_TICKER)
+ const total_vest_steem = assetFloat(global.getIn(['props', 'total_vesting_fund_steem']), LIQUID_TICKER)
+ return total_vest_steem * (vests / total_vests)
+}
+
+export function vestsToSp(state, vesting_shares) {
+ return vestsToSpf(state, vesting_shares).toFixed(3)
+}
+
+export function spToVestsf(state, steem_power) {
+ const {global} = state
+ let power = steem_power
+ if (typeof power === 'string') {
+ power = assetFloat(power, LIQUID_TICKER)
+ }
+ const total_vests = assetFloat(global.getIn(['props', 'total_vesting_shares']), VEST_TICKER)
+ const total_vest_steem = assetFloat(global.getIn(['props', 'total_vesting_fund_steem']), LIQUID_TICKER)
+ return (steem_power / total_vest_steem) * total_vests
+}
+
+export function spToVests(state, vesting_shares) {
+ return spToVestsf(state, vesting_shares).toFixed(6)
+}
+
+export function vestingSteem(account, gprops) {
+ const vests = parseFloat(account.vesting_shares.split( ' ' )[0]);
+ const total_vests = parseFloat(gprops.total_vesting_shares.split( ' ' )[0]);
+ const total_vest_steem = parseFloat(gprops.total_vesting_fund_steem.split( ' ' )[0]);
+ const vesting_steemf = total_vest_steem * (vests / total_vests);
+ return vesting_steemf;
+}
+
+// How much STEEM this account has delegated out (minus received).
+export function delegatedSteem(account, gprops) {
+ const delegated_vests = parseFloat(account.delegated_vesting_shares.split( ' ' )[0]);
+ const received_vests = parseFloat(account.received_vesting_shares.split( ' ' )[0]);
+ const vests = delegated_vests - received_vests;
+ const total_vests = parseFloat(gprops.total_vesting_shares.split( ' ' )[0]);
+ const total_vest_steem = parseFloat(gprops.total_vesting_fund_steem.split( ' ' )[0]);
+ const vesting_steemf = total_vest_steem * (vests / total_vests);
+ return vesting_steemf;
+}
+
+export function assetFloat(str, asset) {
+ try {
+ assert.equal(typeof str, 'string')
+ assert.equal(typeof asset, 'string')
+ assert(new RegExp(`^\\d+(\\.\\d+)? ${asset}$`).test(str), 'Asset should be formatted like 99.99 ' + asset + ': ' + str)
+ return parseFloat(str.split(' ')[0])
+ } catch(e) {
+ console.log(e);
+ return undefined
+ }
+}
+
+export function isFetchingOrRecentlyUpdated(global_status, order, category) {
+ const status = global_status ? global_status.getIn([category || '', order]) : null;
+ if (!status) return false;
+ if (status.fetching) return true;
+ if (status.last_fetch) {
+ const res = new Date() - status.last_fetch < constants.FETCH_DATA_EXPIRE_SEC * 1000;
+ return res;
+ }
+ return false;
+}
+
+export function contentStats(content) {
+ if(!content) return {}
+ if(!(content instanceof Map)) content = fromJS(content);
+
+ let net_rshares_adj = Long.ZERO
+ let neg_rshares = Long.ZERO
+ let total_votes = 0;
+ let up_votes = 0;
+
+ content.get('active_votes').forEach((v) => {
+ const sign = Math.sign(v.get('percent'))
+ if(sign === 0) return;
+ total_votes += 1
+ if(sign > 0) up_votes += 1
+
+ const rshares = String(v.get('rshares'))
+
+ // For flag weight: count total neg rshares
+ if(sign < 0) {
+ neg_rshares = neg_rshares.add(rshares)
+ }
+
+ // For graying: sum up total rshares from voters with non-neg reputation.
+ if(String(v.get('reputation')).substring(0, 1) !== '-') {
+ // And also ignore tiny downvotes (9 digits or less)
+ if(!(rshares.substring(0, 1) === '-' && rshares.length < 11)) {
+ net_rshares_adj = net_rshares_adj.add(rshares)
+ }
+ }
+ });
+
+ // take negative rshares, divide by 2, truncate 10 digits (plus neg sign), count digits.
+ // creates a cheap log10, stake-based flag weight. 1 = approx $400 of downvoting stake; 2 = $4,000; etc
+ const flagWeight = Math.max(String(neg_rshares.div(2)).length - 11, 0)
+
+ // post must have non-trivial negative rshares to be grayed out. (more than 10 digits)
+ const grayThreshold = -9999999999
+ const meetsGrayThreshold = net_rshares_adj.compare(grayThreshold) < 0
+
+ // to be eligible for deletion, a comment must have non-positive rshares and no replies
+ const hasPositiveRshares = Long.fromString(String(content.get('net_rshares'))).gt(Long.ZERO)
+ const allowDelete = !hasPositiveRshares && content.get('children') === 0
+ const hasPendingPayout = parsePayoutAmount(content.get('pending_payout_value')) >= 0.02
+ const authorRepLog10 = repLog10(content.get('author_reputation'))
+
+ const gray = !hasPendingPayout && (authorRepLog10 < 1 || meetsGrayThreshold)
+ const hide = !hasPendingPayout && (authorRepLog10 < 0) // rephide
+
+ // Combine tags+category to check nsfw status
+ const json = content.get('json_metadata')
+ let tags = []
+ try {
+ tags = (json && JSON.parse(json).tags) || [];
+ if(typeof tags == 'string') {
+ tags = [tags];
+ } if(!Array.isArray(tags)) {
+ tags = [];
+ }
+ } catch(e) {
+ tags = []
+ }
+ tags.push(content.get('category'))
+ const isNsfw = tags.filter(tag => tag && tag.match(/^nsfw$/i)).length > 0;
+
+ return {hide, gray, authorRepLog10, allowDelete, isNsfw, flagWeight, total_votes, up_votes, hasPendingPayout}
+}
diff --git a/src/app/utils/VerifiedExchangeList.js b/src/app/utils/VerifiedExchangeList.js
new file mode 100644
index 0000000..0a86b57
--- /dev/null
+++ b/src/app/utils/VerifiedExchangeList.js
@@ -0,0 +1,6 @@
+const list = `
+poloniex
+bittrex
+`.trim().split('\n');
+
+export default list;
diff --git a/src/app/utils/shouldComponentUpdate.js b/src/app/utils/shouldComponentUpdate.js
new file mode 100644
index 0000000..fc4b0cf
--- /dev/null
+++ b/src/app/utils/shouldComponentUpdate.js
@@ -0,0 +1,50 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin'
+import {Iterable} from 'immutable'
+
+/**
+ Wrapper for PureRenderMixin.
+ This allows debugging that will show which properties changed.
+*/
+export default function (instance, name) {
+ const mixin = PureRenderMixin.shouldComponentUpdate.bind(instance)
+ if (process.env.BROWSER && window.steemDebug_shouldComponentUpdate === undefined) {
+ window.steemDebug_shouldComponentUpdate = false // console command line completion
+ }
+ return (nextProps, nextState) => {
+ const upd = mixin(nextProps, nextState)
+ // Usage: steemDebug_shouldComponentUpdate = true
+ // Or: steemDebug_shouldComponentUpdate = /Comment/
+ if (upd && process.env.BROWSER && window.steemDebug_shouldComponentUpdate) {
+ const filter = window.steemDebug_shouldComponentUpdate
+ if(filter.test) {
+ if(!filter.test(name))
+ return upd
+ }
+ compare(name, instance.props, nextProps)
+ compare(name, instance.state, nextState)
+ }
+ return upd
+ }
+}
+
+export function compare(name, a, b) {
+ const aKeys = new Set(a && Object.keys(a))
+ const bKeys = new Set(b && Object.keys(b))
+ const ab = new Set([...aKeys, ...aKeys])
+ ab.forEach(key => {
+ const hasA = aKeys.has(key)
+ const hasB = bKeys.has(key)
+ if (!hasA && !hasB) return
+ if (hasA && hasB && a[key] === b[key]) return
+ const desc = !hasA ? 'added' : !hasB ? 'removed' : 'changed'
+ console.log(name, key, desc)
+ const aKey = a[key]
+ const bKey = b[key]
+ if (typeof aKey !== 'function' && typeof bKey !== 'function') { //functions are too verbose
+ console.log(key, 'was', a && toJS(aKey))
+ console.log(key, 'is', b && toJS(bKey))
+ }
+ })
+}
+
+const toJS = o => (Iterable.isIterable(o) ? o.toJS() : o)
diff --git a/src/app/utils/userIllegalContent.js b/src/app/utils/userIllegalContent.js
new file mode 100644
index 0000000..0cceeda
--- /dev/null
+++ b/src/app/utils/userIllegalContent.js
@@ -0,0 +1,5 @@
+const list = `
+aplomb
+`.trim().split('\n');
+
+export default list;
diff --git a/src/db/config/config.json b/src/db/config/config.json
new file mode 100644
index 0000000..601e287
--- /dev/null
+++ b/src/db/config/config.json
@@ -0,0 +1,9 @@
+{
+ "development": {
+ "username": "root",
+ "password": "password",
+ "database": "steemit_dev",
+ "host": "127.0.0.1",
+ "dialect": "mysql"
+ }
+}
diff --git a/src/db/migrations/20160419161331-create-user.js b/src/db/migrations/20160419161331-create-user.js
new file mode 100644
index 0000000..a683d25
--- /dev/null
+++ b/src/db/migrations/20160419161331-create-user.js
@@ -0,0 +1,71 @@
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('users', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ email: {
+ type: Sequelize.STRING,
+ },
+ uid: {
+ type: Sequelize.STRING(64)
+ },
+ first_name: {
+ type: Sequelize.STRING
+ },
+ last_name: {
+ type: Sequelize.STRING
+ },
+ birthday: {
+ type: Sequelize.DATE
+ },
+ gender: {
+ type: Sequelize.STRING(8)
+ },
+ picture_small: {
+ type: Sequelize.STRING
+ },
+ picture_large: {
+ type: Sequelize.STRING
+ },
+ location_id: {
+ type: Sequelize.BIGINT.UNSIGNED
+ },
+ location_name: {
+ type: Sequelize.STRING
+ },
+ locale: {
+ type: Sequelize.STRING(12)
+ },
+ timezone: {
+ type: Sequelize.INTEGER
+ },
+ verified: {
+ type: Sequelize.BOOLEAN
+ },
+ bot: {
+ type: Sequelize.BOOLEAN
+ },
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updated_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('users', ['email']);
+ queryInterface.addIndex('users', ['uid'], {indicesType: 'UNIQUE'});
+ });
+ },
+ down: function (queryInterface, Sequelize) {
+ return queryInterface.dropTable('users');
+ }
+};
diff --git a/src/db/migrations/20160420133848-create-identity.js b/src/db/migrations/20160420133848-create-identity.js
new file mode 100644
index 0000000..e3e6229
--- /dev/null
+++ b/src/db/migrations/20160420133848-create-identity.js
@@ -0,0 +1,60 @@
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('identities', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ user_id: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'users',
+ key: 'id'
+ },
+ onUpdate: 'cascade',
+ onDelete: 'cascade'
+ },
+ provider: {
+ type: Sequelize.STRING
+ },
+ provider_user_id: {
+ type: Sequelize.STRING
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ email: {
+ type: Sequelize.STRING
+ },
+ phone: {
+ type: Sequelize.STRING(32)
+ },
+ confirmation_code: {
+ type: Sequelize.STRING
+ },
+ verified: {
+ type: Sequelize.BOOLEAN
+ },
+ score: {
+ type: Sequelize.INTEGER
+ },
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updated_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('identities', ['email']);
+ queryInterface.addIndex('identities', ['phone']);
+ queryInterface.addIndex('identities', ['confirmation_code']);
+ });
+ },
+ down: function (queryInterface, Sequelize) {
+ return queryInterface.dropTable('identities');
+ }
+};
diff --git a/src/db/migrations/20160420151336-create-account.js b/src/db/migrations/20160420151336-create-account.js
new file mode 100644
index 0000000..bb3d3d4
--- /dev/null
+++ b/src/db/migrations/20160420151336-create-account.js
@@ -0,0 +1,67 @@
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('accounts', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ user_id: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'users',
+ key: 'id'
+ },
+ onUpdate: 'cascade',
+ onDelete: 'cascade'
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ owner_key: {
+ type: Sequelize.STRING
+ },
+ active_key: {
+ type: Sequelize.STRING
+ },
+ posting_key: {
+ type: Sequelize.STRING
+ },
+ memo_key: {
+ type: Sequelize.STRING
+ },
+ referrer: {
+ type: Sequelize.STRING
+ },
+ refcode: {
+ type: Sequelize.STRING
+ },
+ remote_ip: {
+ type: Sequelize.STRING
+ },
+ ignored: {
+ type: Sequelize.BOOLEAN,
+ defaultValue: false,
+ allowNull: false
+ },
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updated_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('accounts', ['name'], {indicesType: 'UNIQUE'});
+ queryInterface.addIndex('accounts', ['owner_key']);
+ queryInterface.addIndex('accounts', ['active_key']);
+ queryInterface.addIndex('accounts', ['posting_key']);
+ queryInterface.addIndex('accounts', ['memo_key']);
+ });
+ },
+ down: function (queryInterface, Sequelize) {
+ return queryInterface.dropTable('accounts');
+ }
+};
diff --git a/src/db/migrations/20160506223257-create-web-events.js b/src/db/migrations/20160506223257-create-web-events.js
new file mode 100644
index 0000000..037618c
--- /dev/null
+++ b/src/db/migrations/20160506223257-create-web-events.js
@@ -0,0 +1,47 @@
+'use strict';
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('web_events', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ event_type: {type: Sequelize.STRING(64)},
+ value: {type: Sequelize.STRING(1024)},
+ user_id: {type: Sequelize.INTEGER},
+ uid: {type: Sequelize.STRING(32)},
+ account_name: {type: Sequelize.STRING(64)},
+ first_visit: {type: Sequelize.BOOLEAN},
+ new_session: {type: Sequelize.BOOLEAN},
+ ip: {type: Sequelize.STRING(48)},
+ refurl: {type: Sequelize.STRING},
+ user_agent: {type: Sequelize.STRING},
+ status: {type: Sequelize.INTEGER},
+ city: {type: Sequelize.STRING(64)},
+ state: {type: Sequelize.STRING(64)},
+ country: {type: Sequelize.STRING(64)},
+ channel: {type: Sequelize.STRING(64)},
+ referrer: {type: Sequelize.STRING(64)},
+ refcode: {type: Sequelize.STRING(64)},
+ campaign: {type: Sequelize.STRING(64)},
+ adgroupid: {type: Sequelize.INTEGER},
+ adid: {type: Sequelize.INTEGER},
+ keywordid: {type: Sequelize.INTEGER},
+ contentid: {type: Sequelize.INTEGER},
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('web_events', ['event_type']);
+ queryInterface.addIndex('web_events', ['user_id']);
+ queryInterface.addIndex('web_events', ['uid']);
+ queryInterface.addIndex('web_events', ['account_name']);
+ });
+ },
+ down: function (queryInterface, Sequelize) {
+ return queryInterface.dropTable('web_events');
+ }
+};
diff --git a/src/db/migrations/20160519211043-users-waiting-list.js b/src/db/migrations/20160519211043-users-waiting-list.js
new file mode 100644
index 0000000..05ef061
--- /dev/null
+++ b/src/db/migrations/20160519211043-users-waiting-list.js
@@ -0,0 +1,13 @@
+'use strict';
+
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ queryInterface.addColumn('users', 'waiting_list', Sequelize.BOOLEAN);
+ queryInterface.addColumn('users', 'remote_ip', Sequelize.STRING);
+ },
+
+ down: function (queryInterface, Sequelize) {
+ queryInterface.removeColumn('users', 'waiting_list');
+ queryInterface.removeColumn('users', 'remote_ip');
+ }
+};
diff --git a/src/db/migrations/20160715233035-account-recovery-request.js b/src/db/migrations/20160715233035-account-recovery-request.js
new file mode 100644
index 0000000..ae2b8f2
--- /dev/null
+++ b/src/db/migrations/20160715233035-account-recovery-request.js
@@ -0,0 +1,54 @@
+'use strict';
+
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('arecs', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ user_id: {type: Sequelize.INTEGER},
+ uid: {type: Sequelize.STRING(32)},
+ contact_email: {type: Sequelize.STRING(256)},
+ account_name: {type: Sequelize.STRING(64)},
+ provider: {type: Sequelize.STRING(64)},
+ email_confirmation_code: {type: Sequelize.STRING(64)},
+ validation_code: {type: Sequelize.STRING(64)},
+ request_submitted_at: {type: Sequelize.DATE},
+ owner_key: {
+ type: Sequelize.STRING
+ },
+ old_owner_key: {
+ type: Sequelize.STRING
+ },
+ new_owner_key: {
+ type: Sequelize.STRING
+ },
+ remote_ip: {
+ type: Sequelize.STRING
+ },
+ status: {
+ type: Sequelize.STRING
+ },
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updated_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('arecs', ['user_id']);
+ queryInterface.addIndex('arecs', ['uid']);
+ queryInterface.addIndex('arecs', ['account_name']);
+ queryInterface.addIndex('arecs', ['contact_email']);
+ });
+ },
+
+ down: function (queryInterface, Sequelize) {
+
+ }
+};
diff --git a/src/db/migrations/20160930210310-create-list.js b/src/db/migrations/20160930210310-create-list.js
new file mode 100644
index 0000000..bfbf4bb
--- /dev/null
+++ b/src/db/migrations/20160930210310-create-list.js
@@ -0,0 +1,32 @@
+'use strict';
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('lists', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ kk: {type: Sequelize.STRING(64)},
+ value: {type: Sequelize.STRING(256)},
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('lists', ['kk']);
+ queryInterface.addIndex(
+ 'lists',
+ ['kk', 'value'],
+ {
+ indexName: 'KeyValue',
+ indicesType: 'UNIQUE'
+ }
+ )
+ });
+ },
+ down: function (queryInterface, Sequelize) {
+ return queryInterface.dropTable('lists');
+ }
+};
diff --git a/src/db/migrations/20161129170500-create-page.js b/src/db/migrations/20161129170500-create-page.js
new file mode 100644
index 0000000..d5fcde1
--- /dev/null
+++ b/src/db/migrations/20161129170500-create-page.js
@@ -0,0 +1,24 @@
+'use strict';
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('pages', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ permlink: {type: Sequelize.STRING(256)},
+ views: {type: Sequelize.INTEGER},
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('pages', ['permlink'], {indicesType: 'UNIQUE'});
+ });
+ },
+ down: function (queryInterface, Sequelize) {
+ return queryInterface.dropTable('pages');
+ }
+};
diff --git a/src/db/migrations/20170426204791-wait-columns.js b/src/db/migrations/20170426204791-wait-columns.js
new file mode 100644
index 0000000..841011a
--- /dev/null
+++ b/src/db/migrations/20170426204791-wait-columns.js
@@ -0,0 +1,26 @@
+'use strict';
+
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ queryInterface.addColumn('users', 'account_status',
+ {
+ type: Sequelize.STRING,
+ defaultValue: "waiting",
+ allowNull: false
+ }
+ );
+ queryInterface.addColumn('users', 'sign_up_meta',
+ {
+ type: Sequelize.TEXT
+ }
+ );
+ queryInterface.addColumn('accounts', 'created',
+ {
+ type: Sequelize.BOOLEAN
+ }
+ );
+ },
+
+ down: function (queryInterface, Sequelize) {
+ }
+};
diff --git a/src/db/migrations/20170511160822-not_unique_account_names.js b/src/db/migrations/20170511160822-not_unique_account_names.js
new file mode 100644
index 0000000..c8537a4
--- /dev/null
+++ b/src/db/migrations/20170511160822-not_unique_account_names.js
@@ -0,0 +1,11 @@
+'use strict';
+
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ queryInterface.removeIndex('accounts', ['name']);
+ queryInterface.addIndex('accounts', ['name']);
+ },
+
+ down: function (queryInterface, Sequelize) {
+ }
+};
diff --git a/src/db/migrations/20170518201152-create-attributes.js b/src/db/migrations/20170518201152-create-attributes.js
new file mode 100644
index 0000000..2ee2c71
--- /dev/null
+++ b/src/db/migrations/20170518201152-create-attributes.js
@@ -0,0 +1,44 @@
+'use strict';
+
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.createTable('user_attributes', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ user_id: {
+ type: Sequelize.INTEGER
+ },
+ type_of: {
+ type: Sequelize.STRING(64)
+ },
+ value: {
+ type: Sequelize.STRING(256)
+ },
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updated_at: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }).then(function () {
+ queryInterface.addIndex('user_attributes', ['user_id']);
+ queryInterface.addIndex('user_attributes', ['type_of']);
+ });
+ },
+
+ down: function (queryInterface, Sequelize) {
+ /*
+ Add reverting commands here.
+ Return a promise to correctly handle asynchronicity.
+
+ Example:
+ return queryInterface.dropTable('users');
+ */
+ }
+};
diff --git a/src/db/models/account.js b/src/db/models/account.js
new file mode 100644
index 0000000..5cbeb80
--- /dev/null
+++ b/src/db/models/account.js
@@ -0,0 +1,39 @@
+module.exports = function (sequelize, DataTypes) {
+ var Account = sequelize.define('Account', {
+ UserId: {
+ type: DataTypes.INTEGER,
+ references: {
+ model: 'users',
+ key: 'id'
+ },
+ field: 'user_id'
+ },
+ name: {type: DataTypes.STRING, unique: true},
+ owner_key: {type: DataTypes.STRING, unique: true},
+ active_key: {type: DataTypes.STRING, unique: true},
+ posting_key: {type: DataTypes.STRING, unique: true},
+ memo_key: {type: DataTypes.STRING, unique: true},
+ referrer: DataTypes.STRING,
+ refcode: DataTypes.STRING,
+ remote_ip: DataTypes.STRING,
+ ignored: {type: DataTypes.BOOLEAN},
+ created: {type: DataTypes.BOOLEAN}
+ }, {
+ tableName: 'accounts',
+ createdAt : 'created_at',
+ updatedAt : 'updated_at',
+ timestamps : true,
+ underscored : true,
+ classMethods: {
+ associate: function (models) {
+ Account.belongsTo(models.User, {
+ onDelete: "CASCADE",
+ foreignKey: {
+ allowNull: false
+ }
+ });
+ }
+ }
+ });
+ return Account;
+};
diff --git a/src/db/models/account_recovery_request.js b/src/db/models/account_recovery_request.js
new file mode 100644
index 0000000..8c44d3c
--- /dev/null
+++ b/src/db/models/account_recovery_request.js
@@ -0,0 +1,42 @@
+module.exports = function (sequelize, DataTypes) {
+ var AccountRecoveryRequest = sequelize.define('AccountRecoveryRequest', {
+ UserId: {
+ type: DataTypes.INTEGER,
+ references: {
+ model: 'users',
+ key: 'id'
+ },
+ field: 'user_id'
+ },
+ uid: {type: DataTypes.STRING, unique: false},
+ contact_email: {type: DataTypes.STRING, unique: false},
+ account_name: {type: DataTypes.STRING, unique: false},
+ email_confirmation_code: {type: DataTypes.STRING(64), unique: false},
+ provider: {type: DataTypes.STRING(64), unique: false},
+ validation_code: {type: DataTypes.STRING(64), unique: false},
+ request_submitted_at: {type: DataTypes.DATE, unique: false},
+ owner_key: {type: DataTypes.STRING, unique: false},
+ old_owner_key: {type: DataTypes.STRING, unique: false},
+ new_owner_key: {type: DataTypes.STRING, unique: false},
+ memo_key: {type: DataTypes.STRING, unique: false},
+ remote_ip: {type: DataTypes.STRING, unique: false},
+ status: {type: DataTypes.STRING, unique: false},
+ }, {
+ tableName: 'arecs',
+ createdAt : 'created_at',
+ updatedAt : 'updated_at',
+ timestamps : true,
+ underscored : true,
+ classMethods: {
+ associate: function (models) {
+ AccountRecoveryRequest.belongsTo(models.User, {
+ onDelete: "SET NULL",
+ foreignKey: {
+ allowNull: true
+ }
+ });
+ }
+ }
+ });
+ return AccountRecoveryRequest;
+};
diff --git a/src/db/models/identity.js b/src/db/models/identity.js
new file mode 100644
index 0000000..a9323d2
--- /dev/null
+++ b/src/db/models/identity.js
@@ -0,0 +1,37 @@
+module.exports = function (sequelize, DataTypes) {
+ var Identity = sequelize.define('Identity', {
+ UserId: {
+ type: DataTypes.INTEGER,
+ references: {
+ model: 'users',
+ key: 'id'
+ },
+ field: 'user_id'
+ },
+ provider: DataTypes.STRING,
+ provider_user_id: {type: DataTypes.STRING},
+ name: DataTypes.STRING,
+ email: {type: DataTypes.STRING},
+ phone: {type: DataTypes.STRING(32)},
+ confirmation_code: {type: DataTypes.STRING, unique: true},
+ verified: DataTypes.BOOLEAN,
+ score: DataTypes.INTEGER
+ }, {
+ tableName: 'identities',
+ createdAt : 'created_at',
+ updatedAt : 'updated_at',
+ timestamps : true,
+ underscored : true,
+ classMethods: {
+ associate: function (models) {
+ Identity.belongsTo(models.User, {
+ onDelete: "CASCADE",
+ foreignKey: {
+ allowNull: false
+ }
+ });
+ }
+ }
+ });
+ return Identity;
+};
diff --git a/src/db/models/index.js b/src/db/models/index.js
new file mode 100644
index 0000000..8b84119
--- /dev/null
+++ b/src/db/models/index.js
@@ -0,0 +1,73 @@
+var fs = require('fs');
+var path = require('path');
+var Sequelize = require('sequelize');
+var basename = path.basename(module.filename);
+var env = process.env.NODE_ENV || 'development';
+var config = require('config');
+var db = {};
+
+var sequelize = new Sequelize(config.get('database_url'));
+
+fs.readdirSync(__dirname)
+ .filter(function (file) {
+ return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
+ })
+ .forEach(function (file) {
+ var model = sequelize['import'](path.join(__dirname, file));
+ db[model.name] = model;
+ });
+
+Object.keys(db).forEach(function (modelName) {
+ if (db[modelName].associate) {
+ db[modelName].associate(db);
+ }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+if(env === 'development') {
+ // in dev, sync all table schema automatically for convenience
+ sequelize.sync();
+}
+
+
+function esc(value, max_length = 256) {
+ if (!value) return '';
+ if (typeof value === 'number') return value;
+ if (typeof value === 'boolean') return value;
+ if (typeof value !== 'string') return '(object)';
+ let res = value.substring(0, max_length - max_length * 0.2).replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
+ switch (char) {
+ case '\0':
+ return '\\0';
+ case '\x08':
+ return '\\b';
+ case '\x09':
+ return '\\t';
+ case '\x1a': return '\\z';
+ case '\n':
+ return '\\n';
+ case '\r':
+ return '\\r';
+ // case '\'':
+ // case "'":
+ // case '"':
+ // case '\\':
+ // case '%':
+ // return '\\' + char; // prepends a backslash to backslash, percent, and double/single quotes
+ }
+ return '-';
+ });
+ return res.length < max_length ? res : '-';
+}
+
+db.esc = esc;
+
+db.escAttrs = function (attrs) {
+ const res = {};
+ Object.keys(attrs).forEach(key => res[key] = esc(attrs[key]));
+ return res;
+};
+
+module.exports = db;
diff --git a/src/db/models/list.js b/src/db/models/list.js
new file mode 100644
index 0000000..1560a50
--- /dev/null
+++ b/src/db/models/list.js
@@ -0,0 +1,13 @@
+module.exports = function (sequelize, DataTypes) {
+ var List = sequelize.define('List', {
+ kk: DataTypes.STRING(64),
+ value: DataTypes.STRING(256),
+ }, {
+ tableName: 'lists',
+ createdAt: 'created_at',
+ updatedAt: false,
+ timestamps : true,
+ underscored : true
+ });
+ return List;
+};
diff --git a/src/db/models/page.js b/src/db/models/page.js
new file mode 100644
index 0000000..dfe6fb3
--- /dev/null
+++ b/src/db/models/page.js
@@ -0,0 +1,13 @@
+module.exports = function (sequelize, DataTypes) {
+ var Page = sequelize.define('Page', {
+ permlink: DataTypes.STRING(256),
+ views: DataTypes.INTEGER,
+ }, {
+ tableName: 'pages',
+ createdAt: 'created_at',
+ updatedAt: false,
+ timestamps : true,
+ underscored : true
+ });
+ return Page;
+};
diff --git a/src/db/models/user.js b/src/db/models/user.js
new file mode 100644
index 0000000..ab47219
--- /dev/null
+++ b/src/db/models/user.js
@@ -0,0 +1,38 @@
+module.exports = function (sequelize, DataTypes) {
+ var User = sequelize.define('User', {
+ name: DataTypes.STRING,
+ email: {type: DataTypes.STRING},
+ uid: {type: DataTypes.STRING(64)},
+ first_name: DataTypes.STRING,
+ last_name: DataTypes.STRING,
+ birthday: DataTypes.DATE,
+ gender: DataTypes.STRING(8),
+ picture_small: DataTypes.STRING,
+ picture_large: DataTypes.STRING,
+ location_id: DataTypes.BIGINT.UNSIGNED,
+ location_name: DataTypes.STRING,
+ locale: DataTypes.STRING(12),
+ timezone: DataTypes.INTEGER,
+ remote_ip: DataTypes.STRING,
+ verified: DataTypes.BOOLEAN,
+ waiting_list: DataTypes.BOOLEAN,
+ bot: DataTypes.BOOLEAN,
+ sign_up_meta: DataTypes.TEXT,
+ account_status: DataTypes.STRING,
+ settings: DataTypes.TEXT
+ }, {
+ tableName: 'users',
+ createdAt : 'created_at',
+ updatedAt : 'updated_at',
+ timestamps : true,
+ underscored : true,
+ classMethods: {
+ associate: function (models) {
+ User.hasMany(models.Identity);
+ User.hasMany(models.Account);
+ User.hasMany(models.UserAttribute);
+ }
+ }
+ });
+ return User;
+};
diff --git a/src/db/models/user_attributes.js b/src/db/models/user_attributes.js
new file mode 100644
index 0000000..44e7351
--- /dev/null
+++ b/src/db/models/user_attributes.js
@@ -0,0 +1,31 @@
+module.exports = function (sequelize, DataTypes) {
+ var UserAttribute = sequelize.define('UserAttribute', {
+ UserId: {
+ type: DataTypes.INTEGER,
+ references: {
+ model: 'users',
+ key: 'id'
+ },
+ field: 'user_id'
+ },
+ type_of: DataTypes.STRING(64),
+ value: DataTypes.STRING(256)
+ }, {
+ tableName: 'user_attributes',
+ createdAt : 'created_at',
+ updatedAt : 'updated_at',
+ timestamps : true,
+ underscored : true,
+ classMethods: {
+ associate: function (models) {
+ UserAttribute.belongsTo(models.User, {
+ onDelete: "CASCADE",
+ foreignKey: {
+ allowNull: false
+ }
+ });
+ }
+ }
+ });
+ return UserAttribute;
+};
diff --git a/src/db/models/web_event.js b/src/db/models/web_event.js
new file mode 100644
index 0000000..84d5aeb
--- /dev/null
+++ b/src/db/models/web_event.js
@@ -0,0 +1,34 @@
+module.exports = function (sequelize, DataTypes) {
+ var WebEvent = sequelize.define('WebEvent', {
+ event_type: DataTypes.STRING(64),
+ value: DataTypes.STRING(1024),
+ user_id: DataTypes.INTEGER,
+ uid: DataTypes.STRING(32),
+ account_name: DataTypes.STRING(64),
+ first_visit: DataTypes.BOOLEAN,
+ new_session: DataTypes.BOOLEAN,
+ ip: DataTypes.STRING(48),
+ page: DataTypes.STRING,
+ refurl: DataTypes.STRING,
+ user_agent: DataTypes.STRING,
+ status: DataTypes.INTEGER,
+ city: DataTypes.STRING(64),
+ state: DataTypes.STRING(64),
+ country: DataTypes.STRING(64),
+ channel: DataTypes.STRING(64),
+ referrer: DataTypes.STRING(64),
+ refcode: DataTypes.STRING(64),
+ campaign: DataTypes.STRING(64),
+ adgroupid: DataTypes.INTEGER,
+ adid: DataTypes.INTEGER,
+ keywordid: DataTypes.INTEGER,
+ messageid: DataTypes.INTEGER,
+ }, {
+ tableName: 'web_events',
+ createdAt: 'created_at',
+ updatedAt: false,
+ timestamps : true,
+ underscored : true
+ });
+ return WebEvent;
+};
diff --git a/src/db/models/web_events.js b/src/db/models/web_events.js
new file mode 100644
index 0000000..4bb251b
--- /dev/null
+++ b/src/db/models/web_events.js
@@ -0,0 +1,14 @@
+'use strict';
+module.exports = function(sequelize, DataTypes) {
+ var web_events = sequelize.define('web_events', {
+ event_type: DataTypes.STRING,
+ value: DataTypes.STRING
+ }, {
+ classMethods: {
+ associate: function(models) {
+ // associations can be defined here
+ }
+ }
+ });
+ return web_events;
+};
\ No newline at end of file
diff --git a/src/db/tarantool.js b/src/db/tarantool.js
new file mode 100644
index 0000000..be397a9
--- /dev/null
+++ b/src/db/tarantool.js
@@ -0,0 +1,70 @@
+import config from 'config';
+import TarantoolDriver from 'tarantool-driver';
+
+let instance = null;
+
+class Tarantool {
+ constructor() {
+ const host = config.get('tarantool.host');
+ const port = config.get('tarantool.port');
+ const username = config.get('tarantool.username');
+ const password = config.get('tarantool.password');
+
+ const connection = this.connection = new TarantoolDriver({host, port});
+ this.ready_promise = new Promise((resolve, reject) => {
+ connection.connect()
+ .then(() => connection.auth(username, password))
+ .then(() => resolve())
+ .catch(() => resolve(false));
+ });
+ }
+
+ makeCall(call_name, args) {
+ return this.ready_promise
+ .then(() => {
+ const call_time = Date.now();
+ return new Promise((resolve, reject) => {
+ this.connection[call_name].apply(this.connection, args).then(res => {
+ resolve(res)
+ }).catch(error => reject(error));
+ });
+ })
+ .catch(error => {
+ if (error.message.indexOf('connect') >= 0)
+ instance = null;
+ return Promise.reject(error);
+ });
+ }
+
+ select() {
+ return this.makeCall('select', arguments);
+ }
+ delete() {
+ return this.makeCall('delete', arguments);
+ }
+ insert() {
+ return this.makeCall('insert', arguments);
+ }
+ replace() {
+ return this.makeCall('replace', arguments);
+ }
+ update() {
+ return this.makeCall('update', arguments);
+ }
+ eval() {
+ return this.makeCall('eval', arguments);
+ }
+ call() {
+ return this.makeCall('call', arguments);
+ }
+ upsert() {
+ return this.makeCall('upsert', arguments);
+ }
+}
+
+Tarantool.instance = function () {
+ if (!instance) instance = new Tarantool();
+ return instance;
+};
+
+export default Tarantool;
diff --git a/src/db/utils/find_user.js b/src/db/utils/find_user.js
new file mode 100644
index 0000000..54dbae3
--- /dev/null
+++ b/src/db/utils/find_user.js
@@ -0,0 +1,45 @@
+import models from '../models';
+
+function findByProvider(provider_user_id, resolve) {
+ if (!provider_user_id) resolve(null);
+ const query = {
+ attributes: ['user_id'],
+ where: {provider_user_id}
+ };
+ models.Identity.findOne(query).then(identity => {
+ if (identity) {
+ models.User.findOne({
+ attributes: ['id'],
+ where: {id: identity.user_id}
+ }).then(u => resolve(u));
+ } else {
+ resolve(null);
+ }
+ });
+}
+
+export default function findUser({user_id, email, uid, provider_user_id}) {
+ console.log('-- findUser -->', user_id, email, uid, provider_user_id);
+ return new Promise(resolve => {
+ let query;
+ const where_or = [];
+ if (user_id) where_or.push({id: user_id});
+ if (email) where_or.push({email});
+ if (uid) where_or.push({uid});
+ if (where_or.length > 0) {
+ query = {
+ attributes: ['id'],
+ where: {$or: where_or}
+ };
+ console.log('-- findUser query -->', query);
+ models.User.findOne(query).then(user => {
+ if (user) resolve(user);
+ else {
+ findByProvider(provider_user_id, resolve);
+ }
+ });
+ } else {
+ findByProvider(provider_user_id, resolve);
+ }
+ });
+}
diff --git a/src/server/api/account_recovery.js b/src/server/api/account_recovery.js
new file mode 100644
index 0000000..cc94eca
--- /dev/null
+++ b/src/server/api/account_recovery.js
@@ -0,0 +1,184 @@
+import koa_router from 'koa-router';
+import koa_body from 'koa-body';
+import models from 'db/models';
+import config from 'config';
+import {esc, escAttrs} from 'db/models';
+import {getRemoteIp, rateLimitReq, checkCSRF} from 'server/utils/misc';
+import {broadcast} from 'steem';
+
+export default function useAccountRecoveryApi(app) {
+ const router = koa_router();
+ app.use(router.routes());
+ const koaBody = koa_body();
+
+ router.post('/initiate_account_recovery', koaBody, function *() {
+ if (rateLimitReq(this, this.req)) return;
+ let params = this.request.body;
+ params = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, params.csrf)) return;
+ console.log('-- /initiate_account_recovery -->', this.session.uid, params);
+ this.session.recover_account = null;
+ if (!params.account_name) {
+ this.status = 500;
+ this.body = 'please provide account name';
+ return;
+ }
+ const attrs = {uid: this.session.uid, status: 'open', ...params};
+ attrs.remote_ip = getRemoteIp(this.req);
+ const request = yield models.AccountRecoveryRequest.create(escAttrs(attrs));
+ console.log('-- /initiate_account_recovery request id -->', this.session.uid, request.id);
+ this.session.arec = request.id;
+ this.redirect('/connect/' + params.provider);
+ });
+
+ router.get('/account_recovery_confirmation/:code', function *() {
+ if (rateLimitReq(this, this.req)) return;
+ const code = this.params.code;
+ if (!code) return this.throw('no confirmation code', 404);
+ const arec = yield models.AccountRecoveryRequest.findOne({
+ attributes: ['id', 'account_name', 'owner_key'],
+ where: {validation_code: esc(code)},
+ order: 'id desc'
+ });
+ if (arec) {
+ this.session.arec = arec.id;
+ console.log('-- /account_recovery_confirmation -->', this.session.uid, arec.id, arec.account_name, arec.owner_key);
+ this.redirect('/recover_account_step_2');
+ } else {
+ console.log('-- /account_recovery_confirmation code not found -->', this.session.uid, code);
+ this.throw('wrong confirmation code', 404);
+ this.session.arec = null;
+ }
+ this.body = code;
+ });
+
+ router.post('/api/v1/request_account_recovery', koaBody, function *() {
+ if (rateLimitReq(this, this.req)) return;
+ let params = this.request.body;
+ params = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, params.csrf)) return;
+ try {
+ if (!this.session.arec) {
+ console.log('-- /request_account_recovery --> this.session.arec is empty', this.session.uid);
+ this.body = JSON.stringify({error: 'Unauthorized'});
+ this.status = 401;
+ return;
+ }
+
+ const account_recovery_record = yield models.AccountRecoveryRequest.findOne({
+ attributes: ['id', 'account_name', 'provider', 'status'],
+ where: {id: this.session.arec}
+ });
+
+ if (!account_recovery_record || account_recovery_record.account_name !== params.name) {
+ console.log('-- /request_account_recovery --> no arec found or wrong name', this.session.uid, params.name);
+ this.body = JSON.stringify({error: 'Unauthorized'});
+ this.status = 401;
+ return;
+ }
+
+ if (account_recovery_record.status !== 'confirmed') {
+ console.log('-- /request_account_recovery --> no arec found or wrong name', this.session.uid, params.name);
+ this.body = JSON.stringify({error: 'Unauthorized'});
+ this.status = 401;
+ return;
+ }
+
+ const recovery_account = config.get('registrar.account');
+ const signing_key = config.get('registrar.signing_key');
+ const {new_owner_authority, old_owner_key, new_owner_key} = params;
+
+ yield requestAccountRecovery({
+ signing_key,
+ account_to_recover: params.name,
+ recovery_account,
+ new_owner_authority
+ });
+ console.log('-- /request_account_recovery completed -->', this.session.uid, this.session.user, params.name, old_owner_key, new_owner_key);
+
+ const attrs = {
+ old_owner_key: esc(old_owner_key),
+ new_owner_key: esc(new_owner_key),
+ request_submitted_at: new Date()
+ };
+ account_recovery_record.update(attrs);
+
+ this.body = JSON.stringify({status: 'ok'});
+ } catch (error) {
+ console.error('Error in /request_account_recovery api call', this.session.uid, this.session.user, error.toString(), error.stack);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ });
+
+ router.post('/api/v1/account_identity_providers', koaBody, function *() {
+ if (rateLimitReq(this, this.req)) return;
+ try {
+ const params = this.request.body;
+ const {csrf, name, owner_key} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ console.log('-- /account_identity_providers -->', this.session.uid, name, owner_key);
+ const existing_account = yield models.Account.findOne({
+ attributes: ['id', 'user_id', 'owner_key'],
+ where: {name: esc(name)},
+ order: 'id DESC'
+ });
+ if (existing_account) {
+ if (existing_account.owner_key === owner_key) {
+ const identity = yield models.Identity.findOne({
+ attributes: ['provider'],
+ where: {user_id: existing_account.user_id},
+ order: 'id DESC'
+ });
+ this.body = JSON.stringify({
+ status: 'found',
+ provider: identity ? (identity.provider === 'phone' ? 'email' : identity.provider) : null});
+ } else {
+ this.body = JSON.stringify({status: 'found', provider: 'email'});
+ }
+ } else {
+ this.body = JSON.stringify({status: 'not found found', provider: 'email'});
+ }
+ } catch (error) {
+ console.error('Error in /account_identity_providers api call', this.session.uid, error);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ });
+
+ router.post('/api/v1/initiate_account_recovery_with_email', koaBody, function *() {
+ const params = this.request.body;
+ const {csrf, contact_email, account_name, owner_key} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ console.log('-- /initiate_account_recovery_with_email -->', this.session.uid, contact_email, account_name, owner_key);
+ if (!account_name || !contact_email || !owner_key) {
+ this.body = JSON.stringify({status: 'error'});
+ return;
+ }
+ const arec = yield models.AccountRecoveryRequest.findOne({
+ attributes: ['id'],
+ where: escAttrs({account_name, contact_email})
+ });
+ if (arec) {
+ this.body = JSON.stringify({status: 'duplicate'});
+ return;
+ }
+ const attrs = {uid: this.session.uid, status: 'open', contact_email, account_name, owner_key, provider: 'email'};
+ attrs.remote_ip = getRemoteIp(this.req);
+ const request = yield models.AccountRecoveryRequest.create(escAttrs(attrs));
+ console.log('-- initiate_account_recovery_with_email -->', this.session.uid, request.id, account_name, owner_key);
+ this.body = JSON.stringify({status: 'ok'});
+ });
+}
+
+function* requestAccountRecovery({
+ recovery_account,
+ account_to_recover,
+ new_owner_authority,
+ signing_key
+}) {
+ const operations = [['request_account_recovery', {
+ recovery_account, account_to_recover, new_owner_authority,
+ }]];
+ yield broadcast.sendAsync({extensions: [], operations}, [signing_key]);
+}
diff --git a/src/server/api/general.js b/src/server/api/general.js
new file mode 100644
index 0000000..b14361a
--- /dev/null
+++ b/src/server/api/general.js
@@ -0,0 +1,482 @@
+/*global $STM_Config */
+import koa_router from 'koa-router';
+import koa_body from 'koa-body';
+import models from 'db/models';
+import findUser from 'db/utils/find_user';
+import config from 'config';
+import recordWebEvent from 'server/record_web_event';
+import {esc, escAttrs} from 'db/models';
+import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from 'server/utils/misc';
+import coBody from 'co-body';
+import Mixpanel from 'mixpanel';
+import Tarantool from 'db/tarantool';
+import {PublicKey, Signature, hash} from 'steem/lib/auth/ecc';
+import {api, broadcast} from 'steem';
+
+const mixpanel = config.get('mixpanel') ? Mixpanel.init(config.get('mixpanel')) : null;
+
+const _stringval = (v) => typeof v === 'string' ? v : JSON.stringify(v)
+function logRequest(path, ctx, extra) {
+ let d = {ip: getRemoteIp(ctx.req)}
+ if (ctx.session) {
+ if (ctx.session.user) {
+ d.user = ctx.session.user
+ }
+ if (ctx.session.uid) {
+ d.uid = ctx.session.uid
+ }
+ if (ctx.session.a) {
+ d.account = ctx.session.a
+ }
+ }
+ if (extra) {
+ Object.keys(extra).forEach((k) => {
+ const nk = d[k] ? '_'+k : k
+ d[nk] = extra[k]
+ })
+ }
+ const info = Object.keys(d).map((k) => `${ k }=${ _stringval(d[k]) }`).join(' ')
+ console.log(`-- /${ path } --> ${ info }`)
+}
+
+export default function useGeneralApi(app) {
+ const router = koa_router({prefix: '/api/v1'});
+ app.use(router.routes());
+ const koaBody = koa_body();
+
+ router.post('/accounts_wait', koaBody, function *() {
+ if (rateLimitReq(this, this.req)) return;
+ const params = this.request.body;
+ const account = typeof(params) === 'string' ? JSON.parse(params) : params;
+ const remote_ip = getRemoteIp(this.req);
+ if (!checkCSRF(this, account.csrf)) return;
+ logRequest('accounts_wait', this, {account});
+ const user_id = this.session.user;
+ try {
+ models.Account.create(escAttrs({
+ user_id,
+ name: account.name,
+ owner_key: account.owner_key,
+ active_key: account.active_key,
+ posting_key: account.posting_key,
+ memo_key: account.memo_key,
+ remote_ip,
+ referrer: this.session.r,
+ created: false
+ })).catch(error => {
+ console.error('!!! Can\'t create account wait model in /accounts api', this.session.uid, error);
+ });
+ if (mixpanel) {
+ mixpanel.track('Signup WaitList', {
+ distinct_id: this.session.uid,
+ ip: remote_ip
+ });
+ mixpanel.people.set(this.session.uid, {ip: remote_ip});
+ }
+ } catch (error) {
+ console.error('Error in /accounts_wait', error);
+ }
+ this.body = JSON.stringify({status: 'ok'});
+ recordWebEvent(this, 'api/accounts_wait', account ? account.name : 'n/a');
+ });
+
+ router.post('/accounts', koaBody, function *() {
+ if (rateLimitReq(this, this.req)) return;
+ const params = this.request.body;
+ const account = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, account.csrf)) return;
+ logRequest('accounts', this, {account})
+ if ($STM_Config.disable_signups) {
+ this.body = JSON.stringify({error: 'New signups are temporary disabled.'});
+ this.status = 401;
+ return;
+ }
+
+ const user_id = this.session.user;
+ if (!user_id) { // require user to sign in with identity provider
+ this.body = JSON.stringify({error: 'Unauthorized'});
+ this.status = 401;
+ return;
+ }
+
+ // acquire global lock so only one account can be created at a time
+ try {
+ const lock_entity_res = yield Tarantool.instance().call('lock_entity', user_id+'');
+ if (!lock_entity_res[0][0]) {
+ console.log('-- /accounts lock_entity -->', user_id, lock_entity_res[0][0]);
+ this.body = JSON.stringify({error: 'Conflict'});
+ this.status = 409;
+ return;
+ }
+ } catch (e) {
+ console.error('-- /accounts tarantool is not available, fallback to another method', e)
+ const rnd_wait_time = Math.random() * 10000;
+ console.log('-- /accounts rnd_wait_time -->', rnd_wait_time);
+ yield new Promise((resolve) =>
+ setTimeout(() => resolve(), rnd_wait_time)
+ )
+ }
+
+ try {
+ const user = yield models.User.findOne(
+ {attributes: ['id'], where: {id: user_id, account_status: 'approved'}}
+ );
+ if (!user) {
+ throw new Error("We can't find your sign up request. You either haven't started your sign up application or weren't approved yet.");
+ }
+
+ // disable session/multi account for now
+
+ // const existing_created_account = yield models.Account.findOne({
+ // attributes: ['id'],
+ // where: {user_id, ignored: false, created: true},
+ // order: 'id DESC'
+ // });
+ // if (existing_created_account) {
+ // throw new Error("Only one Steem account per user is allowed in order to prevent abuse");
+ // }
+
+ const remote_ip = getRemoteIp(this.req);
+ // rate limit account creation to one per IP every 10 minutes
+ const same_ip_account = yield models.Account.findOne(
+ {attributes: ['created_at'], where: {remote_ip: esc(remote_ip), created: true}, order: 'id DESC'}
+ );
+ if (same_ip_account) {
+ const minutes = (Date.now() - same_ip_account.created_at) / 60000;
+ if (minutes < 10) {
+ console.log(`api /accounts: IP rate limit for user ${this.session.uid} #${user_id}, IP ${remote_ip}`);
+ throw new Error('Only one Steem account allowed per IP address every 10 minutes');
+ }
+ }
+
+ yield createAccount({
+ signingKey: config.get('registrar.signing_key'),
+ fee: config.get('registrar.fee'),
+ creator: config.get('registrar.account'),
+ new_account_name: account.name,
+ delegation: config.get('registrar.delegation'),
+ owner: account.owner_key,
+ active: account.active_key,
+ posting: account.posting_key,
+ memo: account.memo_key
+ });
+ console.log('-- create_account_with_keys created -->', this.session.uid, account.name, user.id, account.owner_key);
+
+ this.body = JSON.stringify({status: 'ok'});
+
+ // update user account status
+ yield user.update({account_status: 'created'});
+
+ // update or create account record
+ const account_attrs = escAttrs({
+ user_id,
+ name: account.name,
+ owner_key: account.owner_key,
+ active_key: account.active_key,
+ posting_key: account.posting_key,
+ memo_key: account.memo_key,
+ remote_ip,
+ referrer: this.session.r,
+ created: true
+ });
+
+ const existing_account = yield models.Account.findOne({
+ attributes: ['id'],
+ where: {user_id, name: account.name},
+ order: 'id DESC'
+ });
+ if (existing_account) {
+ yield existing_account.update(account_attrs);
+ } else {
+ yield models.Account.create(account_attrs);
+ }
+ if (mixpanel) {
+ mixpanel.track('Signup', {
+ distinct_id: this.session.uid,
+ ip: remote_ip
+ });
+ mixpanel.people.set(this.session.uid, {ip: remote_ip});
+ }
+ } catch (error) {
+ console.error('Error in /accounts api call', this.session.uid, error.toString());
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ } finally {
+ // console.log('-- /accounts unlock_entity -->', user_id);
+ // release global lock
+ try { yield Tarantool.instance().call('unlock_entity', user_id + ''); } catch(e) {/* ram lock */}
+ }
+ recordWebEvent(this, 'api/accounts', account ? account.name : 'n/a');
+ });
+
+ router.post('/update_email', koaBody, function *() {
+ if (rateLimitReq(this, this.req)) return;
+ const params = this.request.body;
+ const {csrf, email} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ logRequest('update_email', this, {email});
+ try {
+ if (!emailRegex.test(email.toLowerCase())) throw new Error('not valid email: ' + email);
+ // TODO: limit by 1/min/ip
+ let user = yield findUser({user_id: this.session.user, email: esc(email), uid: this.session.uid});
+ if (user) {
+ user = yield models.User.update({email: esc(email), waiting_list: true}, {where: {id: user.id}});
+ } else {
+ user = yield models.User.create({email: esc(email), waiting_list: true});
+ }
+ this.session.user = user.id;
+ this.body = JSON.stringify({status: 'ok'});
+ } catch (error) {
+ console.error('Error in /update_email api call', this.session.uid, error);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ recordWebEvent(this, 'api/update_email', email);
+ });
+
+ router.post('/login_account', koaBody, function *() {
+ // if (rateLimitReq(this, this.req)) return;
+ const params = this.request.body;
+ const {csrf, account, signatures} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ logRequest('login_account', this, {account});
+ try {
+ const db_account = yield models.Account.findOne(
+ {attributes: ['user_id'], where: {name: esc(account)}, logging: false}
+ );
+ if (db_account) this.session.user = db_account.user_id;
+
+ if(signatures) {
+ if(!this.session.login_challenge) {
+ console.error('/login_account missing this.session.login_challenge');
+ } else {
+ const [chainAccount] = yield api.getAccountsAsync([account])
+ if(!chainAccount) {
+ console.error('/login_account missing blockchain account', account);
+ } else {
+ const auth = {posting: false}
+ const bufSha = hash.sha256(JSON.stringify({token: this.session.login_challenge}, null, 0))
+ const verify = (type, sigHex, pubkey, weight, weight_threshold) => {
+ if(!sigHex) return
+ if(weight !== 1 || weight_threshold !== 1) {
+ console.error(`/login_account login_challenge unsupported ${type} auth configuration: ${account}`);
+ } else {
+ const sig = parseSig(sigHex)
+ const public_key = PublicKey.fromString(pubkey)
+ const verified = sig.verifyHash(bufSha, public_key)
+ if (!verified) {
+ console.error('/login_account verification failed', this.session.uid, account, pubkey)
+ }
+ auth[type] = verified
+ }
+ }
+ const {posting: {key_auths: [[posting_pubkey, weight]], weight_threshold}} = chainAccount
+ verify('posting', signatures.posting, posting_pubkey, weight, weight_threshold)
+ if (auth.posting) this.session.a = account;
+ }
+ }
+ }
+
+ this.body = JSON.stringify({status: 'ok'});
+ const remote_ip = getRemoteIp(this.req);
+ if (mixpanel) {
+ mixpanel.people.set(this.session.uid, {ip: remote_ip, $ip: remote_ip});
+ mixpanel.people.increment(this.session.uid, 'Logins', 1);
+ }
+ } catch (error) {
+ console.error('Error in /login_account api call', this.session.uid, error.message);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ recordWebEvent(this, 'api/login_account', account);
+ });
+
+ router.post('/logout_account', koaBody, function *() {
+ // if (rateLimitReq(this, this.req)) return; - logout maybe immediately followed with login_attempt event
+ const params = this.request.body;
+ const {csrf} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ logRequest('logout_account', this);
+ try {
+ this.session.a = null;
+ this.body = JSON.stringify({status: 'ok'});
+ } catch (error) {
+ console.error('Error in /logout_account api call', this.session.uid, error);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ });
+
+ router.post('/record_event', koaBody, function *() {
+ if (rateLimitReq(this, this.req)) return;
+ try {
+ const params = this.request.body;
+ const {csrf, type, value} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ logRequest('record_event', this, {type, value});
+ const str_value = typeof value === 'string' ? value : JSON.stringify(value);
+ if (type.match(/^[A-Z]/)) {
+ if (mixpanel) {
+ mixpanel.track(type, {distinct_id: this.session.uid, Page: str_value});
+ mixpanel.people.increment(this.session.uid, type, 1);
+ }
+ } else {
+ recordWebEvent(this, type, str_value);
+ }
+ this.body = JSON.stringify({status: 'ok'});
+ } catch (error) {
+ console.error('Error in /record_event api call', error.message);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ });
+
+ router.post('/csp_violation', function *() {
+ if (rateLimitReq(this, this.req)) return;
+ let params;
+ try {
+ params = yield coBody(this);
+ } catch (error) {
+ console.log('-- /csp_violation error -->', error);
+ }
+ if (params && params['csp-report']) {
+ const csp_report = params['csp-report'];
+ const value = `${csp_report['document-uri']} : ${csp_report['blocked-uri']}`;
+ console.log('-- /csp_violation -->', value, '--', this.req.headers['user-agent']);
+ recordWebEvent(this, 'csp_violation', value);
+ } else {
+ console.log('-- /csp_violation [no csp-report] -->', params, '--', this.req.headers['user-agent']);
+ }
+ this.body = '';
+ });
+
+ router.post('/page_view', koaBody, function *() {
+ const params = this.request.body;
+ const {csrf, page, ref} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ if (page.match(/\/feed$/)) {
+ this.body = JSON.stringify({views: 0});
+ return;
+ }
+ const remote_ip = getRemoteIp(this.req);
+ logRequest('page_view', this, {page});
+ try {
+ let views = 1, unique = true;
+ if (config.has('tarantool') && config.has('tarantool.host')) {
+ try {
+ const res = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref);
+ unique = res[0][0];
+ } catch (e) {}
+ }
+ const page_model = yield models.Page.findOne(
+ {attributes: ['id', 'views'], where: {permlink: esc(page)}, logging: false}
+ );
+ if (unique) {
+ if (page_model) {
+ views = page_model.views + 1;
+ yield yield models.Page.update({views}, {where: {id: page_model.id}, logging: false});
+ } else {
+ yield models.Page.create(escAttrs({permlink: page, views}), {logging: false});
+ }
+ } else {
+ if (page_model) views = page_model.views;
+ }
+ this.body = JSON.stringify({views});
+ if (mixpanel) {
+ let referring_domain = '';
+ if (ref) {
+ const matches = ref.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
+ referring_domain = matches && matches[1];
+ }
+ const mp_params = {
+ distinct_id: this.session.uid,
+ Page: page,
+ ip: remote_ip,
+ $referrer: ref,
+ $referring_domain: referring_domain
+ };
+ mixpanel.track('PageView', mp_params);
+ if (!this.session.mp) {
+ mixpanel.track('FirstVisit', mp_params);
+ this.session.mp = 1;
+ }
+ if (ref) mixpanel.people.set_once(this.session.uid, '$referrer', ref);
+ mixpanel.people.set_once(this.session.uid, 'FirstPage', page);
+ mixpanel.people.increment(this.session.uid, 'PageView', 1);
+ }
+ } catch (error) {
+ console.error('Error in /page_view api call', this.session.uid, error.message);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ });
+
+ router.post('/save_cords', koaBody, function *() {
+ const params = this.request.body;
+ const {csrf, x, y} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ const user = yield models.User.findOne({
+ where: { id: this.session.user }
+ });
+ if (user) {
+ let data = user.sign_up_meta ? JSON.parse(user.sign_up_meta) : {};
+ data["button_screen_x"] = x;
+ data["button_screen_y"] = y;
+ data["last_step"] = 3;
+ try {
+ user.update({
+ sign_up_meta: JSON.stringify(data)
+ });
+ } catch (error) {
+ console.error('Error in /save_cords api call', this.session.uid, error.message);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ }
+ this.body = JSON.stringify({status: 'ok'});
+ });
+
+ router.post('/setUserPreferences', koaBody, function *() {
+ const params = this.request.body;
+ const {csrf, payload} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ console.log('-- /setUserPreferences -->', this.session.user, this.session.uid, payload);
+ if (!this.session.a) {
+ this.body = 'missing logged in account';
+ this.status = 500;
+ return;
+ }
+ try {
+ const json = JSON.stringify(payload);
+ if (json.length > 1024) throw new Error('the data is too long');
+ this.session.user_prefs = json;
+ this.body = JSON.stringify({status: 'ok'});
+ } catch (error) {
+ console.error('Error in /setUserPreferences api call', this.session.uid, error);
+ this.body = JSON.stringify({error: error.message});
+ this.status = 500;
+ }
+ });
+}
+
+/**
+ @arg signingKey {string|PrivateKey} - WIF or PrivateKey object
+ */
+function* createAccount({
+ signingKey, fee, creator, new_account_name, json_metadata = '', delegation,
+ owner, active, posting, memo
+}) {
+ const operations = [['account_create_with_delegation', {
+ fee, creator, new_account_name, json_metadata, delegation,
+ owner: {weight_threshold: 1, account_auths: [], key_auths: [[owner, 1]]},
+ active: {weight_threshold: 1, account_auths: [], key_auths: [[active, 1]]},
+ posting: {weight_threshold: 1, account_auths: [], key_auths: [[posting, 1]]},
+ memo_key: memo,
+ }]]
+ yield broadcast.sendAsync({
+ extensions: [],
+ operations
+ }, [signingKey])
+}
+
+const parseSig = hexSig => {try {return Signature.fromHex(hexSig)} catch(e) {return null}}
diff --git a/src/server/api/notifications.js b/src/server/api/notifications.js
new file mode 100644
index 0000000..3a99cef
--- /dev/null
+++ b/src/server/api/notifications.js
@@ -0,0 +1,88 @@
+import koa_router from 'koa-router';
+import koa_body from 'koa-body';
+import Tarantool from 'db/tarantool';
+import config from 'config';
+import webPush from 'web-push';
+import {checkCSRF} from 'server/utils/misc';
+import sendEmail from "../sendEmail";
+
+if(config.has('notify.gcm_key')) {
+ webPush.setGCMAPIKey(config.get('notify.gcm_key'));
+}
+
+function toResArray(result) {
+ if (!result || result.length < 1) return [];
+ return result[0].slice(1);
+}
+
+export default function useNotificationsApi(app) {
+ const router = koa_router({prefix: '/api/v1'});
+ app.use(router.routes());
+ const koaBody = koa_body();
+
+ // get all notifications for account
+ router.get('/notifications/:account', function *() {
+ const account = this.params.account;
+ console.log('-- GET /notifications/:account -->', this.session.uid, account, status(this, account));
+
+ if (!account || account !== this.session.a) {
+ this.body = []; return;
+ }
+ try {
+ const res = yield Tarantool.instance().select('notifications', 0, 1, 0, 'eq', account);
+ this.body = toResArray(res);
+ } catch (error) {
+ console.error('-- /notifications/:account error -->', this.session.uid, error.message);
+ this.body = [];
+ }
+ return;
+ });
+
+ // mark account's notification as read
+ router.put('/notifications/:account/:ids', function *() {
+ const {account, ids} = this.params;
+ console.log('-- PUT /notifications/:account/:id -->', this.session.uid, account, status(this, account));
+
+ if (!ids || !account || account !== this.session.a) {
+ this.body = []; return;
+ }
+ const fields = ids.split('-');
+ try {
+ let res;
+ for(const id of fields) {
+ res = yield Tarantool.instance().call('notification_read', account, id);
+ }
+ this.body = toResArray(res);
+ } catch (error) {
+ console.error('-- /notifications/:account/:id error -->', this.session.uid, error.message);
+ this.body = [];
+ }
+ return;
+ });
+
+ router.post('/notifications/register', koaBody, function *() {
+ this.body = '';
+ try {
+ const params = this.request.body;
+ const {csrf, account, webpush_params} = typeof(params) === 'string' ? JSON.parse(params) : params;
+ if (!checkCSRF(this, csrf)) return;
+ console.log('-- POST /notifications/register -->', this.session.uid, account, webpush_params);
+ if (!account || account !== this.session.a) return;
+ if (!webpush_params || !webpush_params.endpoint || !webpush_params.endpoint.match(/^https:\/\/android\.googleapis\.com/)) return;
+ if (!webpush_params.keys || !webpush_params.keys.auth) return;
+ yield Tarantool.instance().call('webpush_subscribe', account, webpush_params);
+ } catch (error) {
+ console.error('-- POST /notifications/register error -->', this.session.uid, error.message);
+ }
+ });
+
+ router.post('/notifications/send_confirm', koaBody, function *(email, id) {
+ console.log("sendign email", email,id);
+ sendEmail("confirm_email", email, { id });
+ });
+}
+
+const status = (ctx, account) =>
+ctx.session.a == null ? 'not logged in' :
+ account !== ctx.session.a ? 'wrong account' + ctx.session.a :
+ '';
diff --git a/src/server/api/oauth.js b/src/server/api/oauth.js
new file mode 100644
index 0000000..df1a2db
--- /dev/null
+++ b/src/server/api/oauth.js
@@ -0,0 +1,266 @@
+import route from 'koa-route';
+import Purest from 'purest';
+import models from 'db/models';
+import findUser from 'db/utils/find_user';
+import {esc, escAttrs} from 'db/models';
+
+const facebook = new Purest({provider: 'facebook'});
+const reddit = new Purest({provider: 'reddit'});
+
+function logErrorAndRedirect(ctx, where, error) {
+ const s = ctx.session;
+ let msg = 'unknown';
+ if (error.toString()) msg = error.toString()
+ else msg = error.error && error.error.message ? error.error.message : (error.msg || JSON.stringify(error));
+ console.error(`oauth error [${where}|${s.user}|${s.uid}]|${ctx.req.headers['user-agent']}: ${msg}`);
+ if (process.env.NODE_ENV === 'development') console.log(error.stack);
+ ctx.flash = {alert: `${where} error: ${msg}`};
+ ctx.redirect('/');
+ return null;
+}
+
+function getRemoteIp(req) {
+ const remote_address = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
+ const ip_match = remote_address ? remote_address.match(/(\d+\.\d+\.\d+\.\d+)/) : null;
+ return ip_match ? ip_match[1] : esc(remote_address);
+}
+
+function retrieveFacebookUserData(access_token) {
+ return new Promise((resolve, reject) => {
+ facebook.query()
+ .get('me?fields=name,email,location,picture{url},verified')
+ .auth(access_token)
+ .request((err, res) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(res.body);
+ }
+ });
+ });
+}
+
+function* handleFacebookCallback() {
+ console.log('-- /handle_facebook_callback -->', this.session.uid, this.query);
+ let email = null;
+ try {
+ if (this.query['error[error][message]']) {
+ return logErrorAndRedirect(this, 'facebook:1', this.query['error[error][message]']);
+ }
+ const u = yield retrieveFacebookUserData(this.query.access_token);
+ email = u.email;
+ const attrs = {
+ uid: this.session.uid,
+ name: u.name,
+ email: u.email,
+ first_name: u.first_name,
+ last_name: u.last_name,
+ birthday: u.birthday ? new Date(u.birthday) : null,
+ gender: u.gender,
+ picture_small: u.picture ? u.picture.data.url : null,
+ location_id: u.location ? u.location.id : null,
+ location_name: u.location ? u.location.name : null,
+ locale: u.locale,
+ timezone: u.timezone,
+ remote_ip: getRemoteIp(this.request.req),
+ verified: u.verified,
+ waiting_list: false,
+ facebook_id: u.id
+ };
+ const i_attrs = {
+ provider: 'facebook',
+ uid: u.id,
+ name: u.name,
+ email: u.email,
+ verified: u.verified,
+ provider_user_id: u.id
+ };
+ // const i_attrs_email = {
+ // provider: 'email',
+ // email: u.email,
+ // verified: false
+ // };
+
+ let user = yield findUser({email: u.email, provider_user_id: u.id});
+ console.log('-- /handle_facebook_callback user id -->', this.session.uid, user ? user.id : 'not found');
+
+ let account_recovery_record = null;
+ const provider = this.session.prv = 'facebook';
+ if (this.session.arec) {
+ const arec = yield models.AccountRecoveryRequest.findOne({
+ attributes: ['id', 'created_at', 'account_name', 'owner_key'],
+ where: {id: this.session.arec}
+ });
+ if (arec) {
+ const seconds_ago = (Date.now() - arec.created_at) / 1000;
+ console.log('-- /handle_facebook_callback arec -->', this.session.uid, seconds_ago, arec.created_at);
+ if (seconds_ago < 600) account_recovery_record = arec;
+ }
+ }
+ if (account_recovery_record) {
+ if (user) {
+ const existing_account = yield models.Account.findOne({
+ attributes: ['id'],
+ where: {user_id: user.id, name: account_recovery_record.account_name},
+ order: 'id DESC'
+ });
+ if (existing_account) {
+ console.log('-- arec: confirmed user for account -->', this.session.uid, provider, account_recovery_record.id, existing_account.name, this.session.uid, account_recovery_record.owner_key);
+ account_recovery_record.update({user_id: user.id, status: 'confirmed'});
+ this.redirect('/recover_account_step_2');
+ } else {
+ console.log('-- arec: failed to confirm user for account (no account) -->', this.session.uid, provider, account_recovery_record.id, user.id, this.session.uid, account_recovery_record.owner_key);
+ account_recovery_record.update({user_id: user.id, status: 'account not found'});
+ this.body = 'We cannot verify the user account. Please contact support@steemit.com';
+ }
+ } else {
+ console.log('-- arec: failed to confirm user for account (no user) -->', this.session.uid, provider, this.session.uid, this.session.email);
+ account_recovery_record.update({status: 'user not found'});
+ this.body = 'We cannot verify the user account. Please contact support@steemit.com';
+ }
+ return null;
+ }
+ // no longer necessary since there is phone verification now
+ // if (!u.email) {
+ // console.log('-- /handle_facebook_callback no email -->', this.session.uid, u);
+ // this.flash = {alert: 'Facebook login didn\'t provide any email addresses. Please make sure your Facebook account has a primary email address and try again.'};
+ // this.redirect('/');
+ // return;
+ // }
+ // if (!u.verified) {
+ // throw new Error('Not verified Facebook account. Please verify your Facebook account and try again to sign up to Steemit.');
+ // }
+
+ if (user) {
+ attrs.id = user.id;
+ yield models.User.update(attrs, {where: {id: user.id}});
+ yield models.Identity.update(i_attrs, {where: {user_id: user.id, provider: 'facebook'}});
+ console.log('-- fb updated user -->', this.session.uid, user.id, u.name, u.email);
+ } else {
+ user = yield models.User.create(attrs);
+ i_attrs.user_id = user.id;
+ console.log('-- fb created user -->', user.id, u.name, u.email);
+ const identity = yield models.Identity.create(i_attrs);
+ console.log('-- fb created identity -->', this.session.uid, identity.id);
+ // if (i_attrs_email.email) {
+ // i_attrs_email.user_id = user.id
+ // const email_identity = yield models.Identity.create(i_attrs_email);
+ // console.log('-- fb created email identity -->', this.session.uid, email_identity.id);
+ // }
+ }
+ this.session.user = user.id;
+ } catch (error) {
+ return logErrorAndRedirect(this, 'facebook:2', error);
+ }
+ this.flash = {success: 'Successfully authenticated with Facebook'};
+ this.redirect('/enter_email' + (email ? `?email=${email}` : ''));
+ return null;
+}
+
+function retrieveRedditUserData(access_token) {
+ return new Promise((resolve, reject) => {
+ reddit.query()
+ .get('https://oauth.reddit.com/api/v1/me.json?raw_json=1')
+ .headers({
+ Authorization: `bearer ${access_token}`,
+ 'User-Agent': 'Steembot/1.0 (+http://steemit.com)',
+ Accept: 'application/json',
+ 'Content-type': 'application/json'
+ })
+ .request((err, res) => {
+ if (err) {
+ reject(err);
+ } else {
+ delete res.body.features;
+ resolve(res.body);
+ }
+ });
+ });
+}
+
+function* handleRedditCallback() {
+ try {
+ const u = yield retrieveRedditUserData(this.query.access_token);
+ console.log('-- /handle_reddit_callback -->', this.session.uid, u);
+ let user = yield findUser({provider_user_id: u.id});
+ console.log('-- /handle_reddit_callback user id -->', this.session.uid, user ? user.id : 'not found');
+
+ let account_recovery_record = null;
+ const provider = this.session.prv = 'reddit';
+ if (this.session.arec) {
+ const arec = yield models.AccountRecoveryRequest.findOne({
+ attributes: ['id', 'created_at', 'account_name', 'owner_key'],
+ where: {id: this.session.arec}
+ });
+ if (arec) {
+ const seconds_ago = (Date.now() - arec.created_at) / 1000;
+ if (seconds_ago < 600) account_recovery_record = arec;
+ }
+ }
+ if (account_recovery_record) {
+ if (user) {
+ const existing_account = yield models.Account.findOne({
+ attributes: ['id'],
+ where: {user_id: user.id, name: account_recovery_record.account_name},
+ order: 'id DESC'
+ });
+ if (existing_account) {
+ console.log('-- arec: confirmed user for account -->', this.session.uid, provider, account_recovery_record.id, existing_account.name, this.session.uid, account_recovery_record.owner_key);
+ account_recovery_record.update({user_id: user.id, status: 'confirmed'});
+ this.redirect('/recover_account_step_2');
+ } else {
+ console.log('-- arec: failed to confirm user for account (no account) -->', this.session.uid, provider, account_recovery_record.id, user.id, this.session.uid, account_recovery_record.owner_key);
+ account_recovery_record.update({user_id: user.id, status: 'account not found'});
+ this.body = 'We cannot verify the user account. Please contact support@steemit.com';
+ }
+ } else {
+ console.log('-- arec: failed to confirm user for account (no user) -->', this.session.uid, provider, this.session.arec, this.session.email);
+ account_recovery_record.update({status: 'user not found'});
+ this.body = 'We cannot verify the user account. Please contact support@steemit.com';
+ }
+ return null;
+ }
+
+ const waiting_list = !u.comment_karma || u.comment_karma < 5;
+ const i_attrs = {
+ provider: 'reddit',
+ provider_user_id: u.id,
+ name: u.name,
+ score: u.comment_karma
+ };
+ const attrs = {
+ id: user ? user.id : null,
+ uid: this.session.uid,
+ name: u.name,
+ remote_ip: getRemoteIp(this.req),
+ verified: false
+ };
+ if (user) {
+ if (!waiting_list) attrs.waiting_list = false;
+ yield models.User.update(attrs, {where: {id: user.id}});
+ yield models.Identity.update(i_attrs, {where: {user_id: user.id, provider: 'reddit'}});
+ console.log('-- reddit updated user -->', this.session.uid, user.id, u.name);
+ } else {
+ attrs.waiting_list = waiting_list;
+ user = yield models.User.create(attrs);
+ console.log('-- reddit created user -->', this.session.uid, user.id, u.name);
+ i_attrs.user_id = user.id;
+ const identity = yield models.Identity.create(i_attrs);
+ console.log('-- reddit created identity -->', this.session.uid, identity.id);
+ }
+ this.session.user = user.id;
+ if (waiting_list) {
+ this.redirect('/waiting_list.html');
+ return null;
+ }
+ } catch (error) {
+ return logErrorAndRedirect(this, 'reddit', error);
+ }
+ this.redirect('/enter_email');
+ return null;
+}
+
+export default function useOauthLogin(app) {
+ app.use(route.get('/handle_facebook_callback', handleFacebookCallback));
+ app.use(route.get('/handle_reddit_callback', handleRedditCallback));
+}
diff --git a/src/server/app_render.jsx b/src/server/app_render.jsx
new file mode 100644
index 0000000..5d6ccf4
--- /dev/null
+++ b/src/server/app_render.jsx
@@ -0,0 +1,145 @@
+import React from 'react';
+import { renderToString } from 'react-dom/server';
+import Tarantool from 'db/tarantool';
+import ServerHTML from './server-html';
+import universalRender from '../shared/UniversalRender';
+import models from 'db/models';
+import secureRandom from 'secure-random';
+import ErrorPage from 'server/server-error';
+import fs from 'fs';
+
+const path = require('path');
+const ROOT = path.join(__dirname, '../..');
+
+const DB_RECONNECT_TIMEOUT = process.env.NODE_ENV === 'development' ? 1000 * 60 * 60 : 1000 * 60 * 10;
+
+function getSupportedLocales() {
+ const locales = [];
+ const files = fs.readdirSync(path.join(ROOT, 'src/app/locales'));
+ for (const filename of files) {
+ const match_res = filename.match(/(\w+)\.json?$/)
+ if (match_res) locales.push(match_res[1]);
+ }
+ return locales;
+}
+
+const supportedLocales = getSupportedLocales();
+
+async function appRender(ctx) {
+ const store = {};
+ try {
+ let userPreferences = {};
+ if (ctx.session.user_prefs) {
+ try {
+ userPreferences = JSON.parse(ctx.session.user_prefs);
+ } catch (err) {
+ console.error('cannot parse user preferences:', ctx.session.uid, err);
+ }
+ }
+ if (!userPreferences.locale) {
+ let locale = ctx.getLocaleFromHeader();
+ if (locale) locale = locale.substring(0, 2);
+ const localeIsSupported = supportedLocales.find(l => l === locale);
+ if (!localeIsSupported) locale = 'en';
+ userPreferences.locale = locale;
+ }
+ let login_challenge = ctx.session.login_challenge;
+ if (!login_challenge) {
+ login_challenge = secureRandom.randomBuffer(16).toString('hex');
+ ctx.session.login_challenge = login_challenge;
+ }
+ const offchain = {
+ csrf: ctx.csrf,
+ flash: ctx.flash,
+ new_visit: ctx.session.new_visit,
+ account: ctx.session.a,
+ config: $STM_Config,
+ uid: ctx.session.uid,
+ login_challenge
+ };
+ const user_id = ctx.session.user;
+ if (user_id) {
+ let user = null;
+ if (appRender.dbStatus.ok || (new Date() - appRender.dbStatus.lastAttempt) > DB_RECONNECT_TIMEOUT) {
+ try {
+ user = await models.User.findOne({
+ attributes: ['name', 'email', 'picture_small', 'account_status'],
+ where: {id: user_id},
+ include: [{model: models.Account, attributes: ['name', 'ignored', 'created', 'owner_key']}],
+ order: 'Accounts.id desc',
+ logging: false
+ });
+ appRender.dbStatus = {ok: true};
+ } catch (e) {
+ appRender.dbStatus = {ok: false, lastAttempt: new Date()};
+ console.error('WARNING! mysql query failed: ', e.toString());
+ offchain.serverBusy = true;
+ }
+ } else {
+ offchain.serverBusy = true;
+ }
+ if (user) {
+ let account = null;
+ let account_has_keys = null;
+ for (const a of user.Accounts) {
+ if (!a.ignored) {
+ account = a.name;
+ if (a.owner_key && !a.created) {
+ account_has_keys = true;
+ }
+ break;
+ }
+ }
+ offchain.user = {
+ id: user_id,
+ name: user.name,
+ email: user.email,
+ picture: user.picture_small,
+ prv: ctx.session.prv,
+ account_status: user.account_status,
+ account,
+ account_has_keys
+ }
+ }
+ }
+ if (ctx.session.arec) {
+ const account_recovery_record = await models.AccountRecoveryRequest.findOne({
+ attributes: ['id', 'account_name', 'status', 'provider'],
+ where: {id: ctx.session.arec, status: 'confirmed'}
+ });
+ if (account_recovery_record) {
+ offchain.recover_account = account_recovery_record.account_name;
+ }
+ }
+
+ const { body, title, statusCode, meta } = await universalRender({location: ctx.request.url, store, offchain, ErrorPage, tarantool: Tarantool.instance(), userPreferences});
+
+ // Assets name are found in `webpack-stats` file
+ const assets_filename = ROOT + (process.env.NODE_ENV === 'production' ? '/tmp/webpack-stats-prod.json' : '/tmp/webpack-stats-dev.json');
+ const assets = require(assets_filename);
+
+ // Don't cache assets name on dev
+ if (process.env.NODE_ENV === 'development') {
+ delete require.cache[require.resolve(assets_filename)];
+ }
+
+ const props = {body, assets, title, meta};
+ ctx.status = statusCode;
+ ctx.body = '' + renderToString( );
+ } catch (err) {
+ // Render 500 error page from server
+ const { error, redirect } = err;
+ if (error) throw error;
+
+ // Handle component `onEnter` transition
+ if (redirect) {
+ const { pathname, search } = redirect;
+ ctx.redirect(pathname + search);
+ }
+
+ throw err;
+ }
+}
+
+appRender.dbStatus = {ok: true};
+module.exports = appRender;
diff --git a/src/server/hardwarestats.js b/src/server/hardwarestats.js
new file mode 100644
index 0000000..b1e81c8
--- /dev/null
+++ b/src/server/hardwarestats.js
@@ -0,0 +1,56 @@
+import cpuStat from 'cpu-stat';
+import memStat from 'mem-stat';
+import diskStat from 'disk-stat';
+
+module.exports = hardwareStats;
+
+let stats = {};
+
+function handleError(err) {
+ // perpetually throws the same error down the chain for promises
+ throw err;
+}
+
+function startPromise() {
+ return new Promise(function(resolve, reject) {
+ resolve();
+ });
+}
+
+function getCpuUsage() {
+ return new Promise(function(resolve, reject) {
+ cpuStat.usagePercent(function(err, percent, seconds) {
+ if (err)
+ return err;
+ stats.cpuPercent = percent;
+ resolve();
+ });
+ });
+}
+
+function getMemoryUsage() {
+ return new Promise(function(resolve, reject) {
+ stats.memoryStatsInGiB = memStat.allStats('GiB');
+ resolve();
+ });
+}
+
+function getDiskUsage() {
+ return new Promise(function(resolve, reject) {
+ stats.diskStats = diskStat.raw();
+ resolve();
+ });
+}
+
+function hardwareStats() {
+ return startPromise()
+ .then(getCpuUsage, handleError)
+ .then(getMemoryUsage, handleError)
+ .then(getDiskUsage, handleError)
+ .then(function () {
+ console.log(JSON.stringify(stats));
+ }, handleError)
+ .then(null, function(err) {
+ console.log('error getting hardware stats: ' + err);
+ });
+}
\ No newline at end of file
diff --git a/src/server/index.js b/src/server/index.js
new file mode 100644
index 0000000..b619f3d
--- /dev/null
+++ b/src/server/index.js
@@ -0,0 +1,58 @@
+import config from 'config';
+
+import * as steem from 'steem';
+
+const path = require('path');
+const ROOT = path.join(__dirname, '../..');
+
+// Tell `require` calls to look into `/app` also
+// it will avoid `../../../../../` require strings
+
+// use Object.assign to bypass transform-inline-environment-variables-babel-plugin (process.env.NODE_PATH= will not work)
+Object.assign(process.env, {NODE_PATH: path.resolve(__dirname, '..')});
+
+require('module').Module._initPaths();
+
+// Load Intl polyfill
+// require('utils/intl-polyfill')(require('./config/init').locales);
+
+global.$STM_Config = {
+ fb_app: config.get('grant.facebook.key'),
+ steemd_connection_client: config.get('steemd_connection_client'),
+ steemd_connection_server: config.get('steemd_connection_server'),
+ chain_id: config.get('chain_id'),
+ address_prefix: config.get('address_prefix'),
+ img_proxy_prefix: config.get('img_proxy_prefix'),
+ ipfs_prefix: config.get('ipfs_prefix'),
+ disable_signups: config.get('disable_signups'),
+ read_only_mode: config.get('read_only_mode'),
+ registrar_fee: config.get('registrar.fee'),
+ upload_image: config.get('upload_image'),
+ site_domain: config.get('site_domain'),
+ facebook_app_id: config.get('facebook_app_id'),
+ google_analytics_id: config.get('google_analytics_id')
+};
+
+const WebpackIsomorphicTools = require('webpack-isomorphic-tools');
+const WebpackIsomorphicToolsConfig = require(
+ '../../webpack/webpack-isotools-config'
+);
+
+global.webpackIsomorphicTools = new WebpackIsomorphicTools(
+ WebpackIsomorphicToolsConfig
+);
+
+global.webpackIsomorphicTools.server(ROOT, () => {
+ steem.api.setOptions({ url: config.steemd_connection_server });
+ steem.config.set('address_prefix', config.get('address_prefix'));
+ steem.config.set('chain_id', config.get('chain_id'));
+
+ // const CliWalletClient = require('shared/api_client/CliWalletClient').default;
+ // if (process.env.NODE_ENV === 'production') connect_promises.push(CliWalletClient.instance().connect_promise());
+ try {
+ require('./server');
+ } catch (error) {
+ console.error(error);
+ process.exit(1);
+ }
+});
diff --git a/src/server/json/post_json.jsx b/src/server/json/post_json.jsx
new file mode 100644
index 0000000..c9cc82e
--- /dev/null
+++ b/src/server/json/post_json.jsx
@@ -0,0 +1,32 @@
+import koa_router from 'koa-router';
+import React from 'react';
+import {routeRegex} from "app/ResolveRoute";
+import {api} from 'steem'
+
+export default function usePostJson(app) {
+ const router = koa_router();
+ app.use(router.routes());
+
+ router.get(routeRegex.PostJson, function *() {
+ // validate and build post details in JSON
+ const author = this.url.match(/(\@[\w\d\.-]+)/)[0].replace('@', '');
+ const permalink = this.url.match(/(\@[\w\d\.-]+)\/?([\w\d-]+)/)[2];
+ let status = "";
+ let post = yield api.getContentAsync(author, permalink);
+
+ if (post.author) {
+ status = "200";
+ // try parse for post metadata
+ try {
+ post.json_metadata = JSON.parse(post.json_metadata);
+ } catch(e) {
+ post.json_metadata = "";
+ }
+ } else {
+ post = "No post found";
+ status = "404";
+ }
+ // return response and status code
+ this.body = {post, status};
+ });
+}
diff --git a/src/server/json/user_json.jsx b/src/server/json/user_json.jsx
new file mode 100644
index 0000000..a296e53
--- /dev/null
+++ b/src/server/json/user_json.jsx
@@ -0,0 +1,34 @@
+import koa_router from 'koa-router';
+import React from 'react';
+import {routeRegex} from "app/ResolveRoute";
+import {api} from 'steem'
+
+export default function useUserJson(app) {
+ const router = koa_router();
+ app.use(router.routes());
+
+ router.get(routeRegex.UserJson, function *() {
+ // validate and build user details in JSON
+ const segments = this.url.split('/');
+ const user_name = segments[1].match(routeRegex.UserNameJson)[0].replace('@', '');
+ let user = "";
+ let status = "";
+
+ const [chainAccount] = yield api.getAccountsAsync([user_name]);
+
+ if (chainAccount) {
+ user = chainAccount;
+ try {
+ user.json_metadata = JSON.parse(user.json_metadata);
+ } catch (e) {
+ user.json_metadata = "";
+ }
+ status = "200";
+ } else {
+ user = "No account found";
+ status = "404";
+ }
+ // return response and status code
+ this.body = {user, status};
+ });
+}
diff --git a/src/server/prod_logger.js b/src/server/prod_logger.js
new file mode 100644
index 0000000..28003d8
--- /dev/null
+++ b/src/server/prod_logger.js
@@ -0,0 +1,57 @@
+var humanize = require('humanize-number');
+var bytes = require('bytes');
+
+module.exports = prod_logger;
+
+function prod_logger() {
+ return function *logger(next) {
+ // request
+ var start = new Date;
+ var asset = this.originalUrl.indexOf('/assets/') === 0
+ || this.originalUrl.indexOf('/images/') === 0
+ || this.originalUrl.indexOf('/favicon.ico') === 0;
+ if (!asset)
+ console.log(' <-- ' + this.method + ' ' + this.originalUrl + ' ' + (this.session.uid || ''));
+ try {
+ yield next;
+ } catch (err) {
+ log(this, start, null, err, false);
+ throw err;
+ }
+ var length = this.response.length;
+ log(this, start, length, null, asset);
+ }
+}
+
+function log(ctx, start, len, err, asset) {
+ var status = err
+ ? (err.status || 500)
+ : (ctx.status || 404);
+
+ var length;
+ if (~[204, 205, 304].indexOf(status)) {
+ length = '';
+ } else if (null == len) {
+ length = '-';
+ } else {
+ length = bytes(len);
+ }
+
+ var upstream = err ? 'xxx' : '-->';
+
+ if (!asset || err || ctx.status > 400) console.log(' ' + upstream + ' %s %s %s %s %s %s',
+ ctx.method,
+ ctx.originalUrl,
+ status,
+ time(start),
+ length,
+ ctx.session.uid || '');
+}
+
+function time(start) {
+ var delta = new Date - start;
+ delta = delta < 10000
+ ? delta + 'ms'
+ : Math.round(delta / 1000) + 's';
+ return humanize(delta);
+}
diff --git a/src/server/record_web_event.js b/src/server/record_web_event.js
new file mode 100644
index 0000000..67a538b
--- /dev/null
+++ b/src/server/record_web_event.js
@@ -0,0 +1,32 @@
+import {WebEvent, esc} from 'db/models';
+
+export default function recordWebEvent(ctx, event_type, value) {
+ if (ctx.state.isBot) return;
+ const s = ctx.session;
+ const r = ctx.req;
+ let new_session = true;
+ if (ctx.last_visit) {
+ new_session = ((new Date()).getTime() / 1000 - ctx.last_visit) > 1800;
+ }
+ const remote_address = r.headers['x-forwarded-for'] || r.connection.remoteAddress;
+ const ip_match = remote_address ? remote_address.match(/(\d+\.\d+\.\d+\.\d+)/) : null;
+ const d = {
+ event_type: esc(event_type, 1000),
+ user_id: s.user,
+ uid: s.uid,
+ account_name: null,
+ first_visit: ctx.first_visit,
+ new_session,
+ ip: ip_match ? ip_match[1] : null,
+ value: value ? esc(value, 1000) : esc(r.originalUrl),
+ refurl: esc(r.headers.referrer || r.headers.referer),
+ user_agent: esc(r.headers['user-agent']),
+ status: ctx.status,
+ channel: esc(s.ch, 64),
+ referrer: esc(s.r, 64),
+ campaign: esc(s.cn, 64)
+ };
+ WebEvent.create(d, {logging: false}).catch(error => {
+ console.error('!!! Can\'t create web event record', error);
+ });
+}
diff --git a/src/server/redirects.js b/src/server/redirects.js
new file mode 100644
index 0000000..dc82535
--- /dev/null
+++ b/src/server/redirects.js
@@ -0,0 +1,21 @@
+import koa_router from 'koa-router';
+
+const redirects = [
+ // example: [/\/about(\d+)-(.+)/, '/about?$0:$1', 302],
+ [/^\/recent\/?$/, '/created']
+];
+
+export default function useRedirects(app) {
+ const router = koa_router();
+
+ app.use(router.routes());
+
+ redirects.forEach(r => {
+ router.get(r[0], function *() {
+ const dest = Object.keys(this.params).reduce((value, key) => value.replace('$' + key, this.params[key]), r[1]);
+ console.log(`server redirect: [${r[0]}] ${this.request.url} -> ${dest}`);
+ this.status = r[2] || 301;
+ this.redirect(dest);
+ });
+ });
+}
diff --git a/src/server/requesttimings.js b/src/server/requesttimings.js
new file mode 100644
index 0000000..671cabf
--- /dev/null
+++ b/src/server/requesttimings.js
@@ -0,0 +1,16 @@
+function requestTime(numProcesses) {
+ let number_of_requests = 0;
+ return function *(next) {
+ number_of_requests += 1;
+ const start = Date.now();
+ yield* next;
+ const delta = Math.ceil(Date.now() - start);
+ // log all requests that take longer than 150ms
+ if (delta > 150)
+ console.log(`Request took too long! ${delta}ms: ${this.request.method} ${this.request.path}. Number of parallel requests: ${number_of_requests}, number of processes: ${numProcesses}`);
+ number_of_requests -= 1;
+ }
+}
+
+module.exports = requestTime;
+
diff --git a/src/server/sendEmail.js b/src/server/sendEmail.js
new file mode 100644
index 0000000..2c83c5d
--- /dev/null
+++ b/src/server/sendEmail.js
@@ -0,0 +1,34 @@
+import sendgrid from 'sendgrid';
+import config from 'config';
+
+const sg = sendgrid(config.get('sendgrid.key'));
+
+export default function sendEmail(template, to, params, from = null) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.log(`mail: to <${to}>, from <${from}>, template ${template} (not sent due to not production env)`);
+ return;
+ }
+ const tmpl_id = config.get('sendgrid.templates')[template];
+ if (!tmpl_id) throw new Error(`can't find template ${template}`);
+
+ const request = sg.emptyRequest({
+ method: 'POST',
+ path: '/v3/mail/send',
+ body: {
+ template_id: tmpl_id,
+ personalizations: [
+ {to: [{email: to}],
+ substitutions: params},
+ ],
+ from: {email: from || config.get('sendgrid.from')}
+ }
+ });
+
+ sg.API(request)
+ .then(response => {
+ console.log(`sent '${template}' email to '${to}'`, response.statusCode);
+ })
+ .catch(error => {
+ console.error(`failed to send '${template}' email to '${to}'`, error);
+ });
+}
diff --git a/src/server/server-error.jsx b/src/server/server-error.jsx
new file mode 100644
index 0000000..5f0eff4
--- /dev/null
+++ b/src/server/server-error.jsx
@@ -0,0 +1,20 @@
+import React, { Component } from 'react';
+
+class ServerError extends Component {
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+
+}
+
+export default ServerError;
diff --git a/src/server/server-html.jsx b/src/server/server-html.jsx
new file mode 100644
index 0000000..1699ca5
--- /dev/null
+++ b/src/server/server-html.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+
+export default function ServerHTML({ body, assets, locale, title, meta }) {
+ let page_title = title;
+ return (
+
+
+
+
+ {
+ meta && meta.map(m => {
+ if (m.title) {
+ page_title = m.title;
+ return null;
+ }
+ if (m.canonical)
+ return ;
+ if (m.name && m.content)
+ return ;
+ if (m.property && m.content)
+ return ;
+ if (m.name && m.content)
+ return ;
+ return null;
+ })
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { assets.style.map((href, idx) =>
+ ) }
+ {page_title}
+
+
+
+ {assets.script.map((href, idx) => ) }
+
+
+ );
+}
diff --git a/src/server/server.js b/src/server/server.js
new file mode 100644
index 0000000..3572915
--- /dev/null
+++ b/src/server/server.js
@@ -0,0 +1,327 @@
+import path from 'path';
+import fs from 'fs';
+import Koa from 'koa';
+import mount from 'koa-mount';
+import helmet from 'koa-helmet';
+import koa_logger from 'koa-logger';
+import requestTime from './requesttimings';
+import hardwareStats from './hardwarestats';
+import cluster from 'cluster';
+import os from 'os';
+import prod_logger from './prod_logger';
+import favicon from 'koa-favicon';
+import staticCache from 'koa-static-cache';
+import useRedirects from './redirects';
+import useOauthLogin from './api/oauth';
+import useGeneralApi from './api/general';
+import useAccountRecoveryApi from './api/account_recovery';
+import useNotificationsApi from './api/notifications';
+import useEnterAndConfirmEmailPages from './sign_up_pages/enter_confirm_email';
+import useEnterAndConfirmMobilePages from './sign_up_pages/enter_confirm_mobile';
+import useUserJson from './json/user_json';
+import usePostJson from './json/post_json';
+import isBot from 'koa-isbot';
+import session from '@steem/crypto-session';
+import csrf from 'koa-csrf';
+import flash from 'koa-flash';
+import minimist from 'minimist';
+import Grant from 'grant-koa';
+import config from 'config';
+import { routeRegex } from 'app/ResolveRoute';
+import secureRandom from 'secure-random';
+import userIllegalContent from 'app/utils/userIllegalContent';
+import koaLocale from 'koa-locale';
+
+if(cluster.isMaster)
+ console.log('application server starting, please wait.');
+
+const grant = new Grant(config.grant);
+// import uploadImage from 'server/upload-image' //medium-editor
+
+const app = new Koa();
+app.name = 'Steemit app';
+const env = process.env.NODE_ENV || 'development';
+// cache of a thousand days
+const cacheOpts = { maxAge: 86400000, gzip: true };
+
+// set number of processes equal to number of cores
+// (unless passed in as an env var)
+const numProcesses = process.env.NUM_PROCESSES || os.cpus().length;
+
+app.use(requestTime(numProcesses));
+
+app.keys = [config.get('session_key')];
+
+const crypto_key = config.get('server_session_secret');
+session(app, {
+ maxAge: 1000 * 3600 * 24 * 60,
+ crypto_key,
+ key: config.get('session_cookie_key')
+});
+csrf(app);
+
+app.use(mount(grant));
+app.use(flash({ key: 'flash' }));
+koaLocale(app);
+
+function convertEntriesToArrays(obj) {
+ return Object.keys(obj).reduce((result, key) => {
+ result[key] = obj[key].split(/\s+/);
+ return result;
+}, {});
+}
+
+const service_worker_js_content = fs
+ .readFileSync(path.join(__dirname, './service-worker.js'))
+ .toString();
+
+// some redirects and health status
+app.use(function* (next) {
+
+ if (this.method === 'GET' && this.url === '/.well-known/healthcheck.json') {
+ this.status = 200;
+ this.body = {status: 'ok'};
+ return;
+ }
+
+ // redirect to home page/feed if known account
+ if (this.method === 'GET' && this.url === '/' && this.session.a) {
+ this.status = 302;
+ this.redirect(`/@${this.session.a}/feed`);
+ return;
+ }
+ // normalize user name url from cased params
+ if (
+ this.method === 'GET' &&
+ (routeRegex.UserProfile1.test(this.url) ||
+ routeRegex.PostNoCategory.test(this.url) || routeRegex.Post.test(this.url)
+ )
+ ) {
+ const p = this.originalUrl.toLowerCase();
+ let userCheck = "";
+ if (routeRegex.Post.test(this.url)) {
+ userCheck = p.split("/")[2].slice(1);
+ } else {
+ userCheck = p.split("/")[1].slice(1);
+ }
+ if (userIllegalContent.includes(userCheck)) {
+ console.log('Illegal content user found blocked', userCheck);
+ this.status = 451;
+ return;
+ }
+ if (p !== this.originalUrl) {
+ this.status = 301;
+ this.redirect(p);
+ return;
+ }
+ }
+ // normalize top category filtering from cased params
+ if (this.method === 'GET' && routeRegex.CategoryFilters.test(this.url)) {
+ const p = this.originalUrl.toLowerCase();
+ if (p !== this.originalUrl) {
+ this.status = 301;
+ this.redirect(p);
+ return;
+ }
+ }
+ // // do not enter unless session uid & verified phone
+ // if (this.url === '/create_account' && !this.session.uid) {
+ // this.status = 302;
+ // this.redirect('/enter_email');
+ // return;
+ // }
+ // remember ch, cn, r url params in the session and remove them from url
+ if (this.method === 'GET' && /\?[^\w]*(ch=|cn=|r=)/.test(this.url)) {
+ let redir = this.url.replace(/((ch|cn|r)=[^&]+)/gi, r => {
+ const p = r.split('=');
+ if (p.length === 2) this.session[p[0]] = p[1];
+ return '';
+ });
+ redir = redir.replace(/&&&?/, '');
+ redir = redir.replace(/\?&?$/, '');
+ console.log(`server redirect ${this.url} -> ${redir}`);
+ this.status = 302;
+ this.redirect(redir);
+ } else {
+ yield next;
+ }
+});
+
+// load production middleware
+if (env === 'production') {
+ app.use(require('koa-conditional-get')());
+ app.use(require('koa-etag')());
+ app.use(require('koa-compressor')());
+}
+
+// Logging
+if (env === 'production') {
+ app.use(prod_logger());
+} else {
+ app.use(koa_logger());
+}
+
+app.use(helmet());
+
+app.use(
+ mount(
+ '/static',
+ staticCache(path.join(__dirname, '../app/assets/static'), cacheOpts)
+ )
+);
+
+app.use(
+ mount('/robots.txt', function*() {
+ this.set('Cache-Control', 'public, max-age=86400000');
+ this.type = 'text/plain';
+ this.body = 'User-agent: *\nAllow: /';
+ })
+);
+
+app.use(
+ mount('/service-worker.js', function*() {
+ this.set('Cache-Control', 'public, max-age=7200000');
+ this.type = 'application/javascript';
+ // TODO: use APP_URL from client_config.js
+ // actually use a config value for it
+ this.body = service_worker_js_content.replace(
+ /\{DEFAULT_URL\}/i,
+ 'https://' + this.request.header.host
+ );
+ })
+);
+
+// set user's uid - used to identify users in logs and some other places
+// FIXME SECURITY PRIVACY cycle this uid after a period of time
+app.use(function*(next) {
+ const last_visit = this.session.last_visit;
+ this.session.last_visit = new Date().getTime() / 1000 | 0;
+ const from_link = this.request.headers.referer;
+ if (!this.session.uid) {
+ this.session.uid = secureRandom.randomBuffer(13).toString('hex');
+ this.session.new_visit = true;
+ if (from_link) this.session.r = from_link;
+ } else {
+ this.session.new_visit = this.session.last_visit - last_visit > 1800;
+ if (!this.session.r && from_link) {
+ this.session.r = from_link;
+ }
+ }
+ yield next;
+});
+
+useRedirects(app);
+useEnterAndConfirmEmailPages(app);
+useEnterAndConfirmMobilePages(app);
+useUserJson(app);
+usePostJson(app);
+
+useAccountRecoveryApi(app);
+useOauthLogin(app);
+useGeneralApi(app);
+useNotificationsApi(app);
+
+// helmet wants some things as bools and some as lists, makes config difficult.
+// our config uses strings, this splits them to lists on whitespace.
+
+if (env === 'production') {
+ const helmetConfig = {
+ directives: convertEntriesToArrays(config.get('helmet.directives')),
+ reportOnly: config.get('helmet.reportOnly'),
+ setAllHeaders: config.get('helmet.setAllHeaders')
+ };
+ helmetConfig.directives.reportUri = helmetConfig.directives.reportUri[0];
+ if (helmetConfig.directives.reportUri === '-') {
+ delete helmetConfig.directives.reportUri;
+ }
+ app.use(helmet.contentSecurityPolicy(helmetConfig));
+}
+
+app.use(
+ favicon(path.join(__dirname, '../app/assets/images/favicons/favicon.ico'))
+);
+app.use(isBot());
+app.use(
+ mount(
+ '/favicons',
+ staticCache(
+ path.join(__dirname, '../app/assets/images/favicons'),
+ cacheOpts
+ )
+ )
+);
+app.use(
+ mount(
+ '/images',
+ staticCache(path.join(__dirname, '../app/assets/images'), cacheOpts)
+ )
+);
+// Proxy asset folder to webpack development server in development mode
+if (env === 'development') {
+ const webpack_dev_port = process.env.PORT
+ ? parseInt(process.env.PORT) + 1
+ : 8081;
+ const proxyhost = 'http://0.0.0.0:' + webpack_dev_port;
+ console.log('proxying to webpack dev server at ' + proxyhost);
+ const proxy = require('koa-proxy')({
+ host: proxyhost,
+ map: filePath => 'assets/' + filePath
+});
+ app.use(mount('/assets', proxy));
+} else {
+ app.use(
+ mount(
+ '/assets',
+ staticCache(path.join(__dirname, '../../dist'), cacheOpts)
+ )
+ );
+}
+
+if (env !== 'test') {
+ const appRender = require('./app_render');
+ app.use(function*() {
+ yield appRender(this);
+ // if (app_router.dbStatus.ok) recordWebEvent(this, 'page_load');
+ const bot = this.state.isBot;
+ if (bot) {
+ console.log(
+ ` --> ${this.method} ${this.originalUrl} ${this.status} (BOT '${bot}')`
+ );
+ }
+ });
+
+ const argv = minimist(process.argv.slice(2));
+
+ const port = process.env.PORT ? parseInt(process.env.PORT) : 8080;
+
+ if(env === 'production') {
+ if(cluster.isMaster) {
+ for(var i = 0; i < numProcesses; i++) {
+ cluster.fork();
+ }
+ // if a worker dies replace it so application keeps running
+ cluster.on('exit', function (worker) {
+ console.log('error: worker %d died, starting a new one', worker.id);
+ cluster.fork();
+ });
+ }
+ else {
+ app.listen(port);
+ if (process.send) process.send('online');
+ console.log(`Worker process started for port ${port}`);
+ }
+ }
+ else {
+ // spawn a single thread if not running in production mode
+ app.listen(port);
+ if (process.send) process.send('online');
+ console.log(`Application started on port ${port}`);
+ }
+}
+
+// set PERFORMANCE_TRACING to the number of seconds desired for
+// logging hardware stats to the console
+if(process.env.PERFORMANCE_TRACING)
+ setInterval(hardwareStats, (1000*process.env.PERFORMANCE_TRACING));
+
+module.exports = app;
diff --git a/src/server/server.test.js b/src/server/server.test.js
new file mode 100644
index 0000000..c9f9ff1
--- /dev/null
+++ b/src/server/server.test.js
@@ -0,0 +1,22 @@
+/*global describe, it, before, beforeEach, after, afterEach */
+
+require('co-mocha');
+import server from './server';
+import {agent} from 'co-supertest';
+import chai from 'chai';
+import dirtyChai from 'dirty-chai';
+const expect = chai.expect;
+const request = agent(server.listen());
+
+chai.use(dirtyChai);
+
+describe('/favicon.ico', function () {
+/* not maintained
+ it('should return image', function *() {
+ const res = yield request.get('/favicon.ico').end();
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.ok();
+ expect(res.headers['content-type']).to.equal('image/x-icon');
+ });
+*/
+});
diff --git a/src/server/service-worker.js b/src/server/service-worker.js
new file mode 100644
index 0000000..77e16d7
--- /dev/null
+++ b/src/server/service-worker.js
@@ -0,0 +1,30 @@
+self.addEventListener('install', function(event) {
+ event.waitUntil(self.skipWaiting());
+});
+self.addEventListener('activate', function(event) {
+ event.waitUntil(self.clients.claim());
+});
+var clickUrl;
+self.addEventListener('push', function(event) {
+ var payload = JSON.parse(event.data.text());
+ clickUrl = payload.url;
+ event.waitUntil(
+ self.registration.showNotification(payload.title, {
+ body: payload.body,
+ icon: payload.icon
+ })
+ );
+});
+self.addEventListener('notificationclick', function(event) {
+ event.waitUntil(
+ self.clients.matchAll().then(function(clientList) {
+ if (clientList.length > 0) {
+ if (clickUrl && 'navigate' in clientList[0]) {
+ clientList[0].navigate(clickUrl);
+ }
+ return clientList[0].focus();
+ }
+ return self.clients.openWindow(clickUrl || '{DEFAULT_URL}');
+ })
+ );
+});
diff --git a/src/server/sign_up_pages/enter_confirm_email.jsx b/src/server/sign_up_pages/enter_confirm_email.jsx
new file mode 100644
index 0000000..ffe2b62
--- /dev/null
+++ b/src/server/sign_up_pages/enter_confirm_email.jsx
@@ -0,0 +1,392 @@
+import koa_router from "koa-router";
+import koa_body from "koa-body";
+import request from "co-request";
+import React from "react";
+import { renderToString } from "react-dom/server";
+import models from "db/models";
+import ServerHTML from "../server-html";
+import sendEmail from "../sendEmail";
+import { getRemoteIp, checkCSRF } from "server/utils/misc";
+import config from "config";
+import MiniHeader from "app/components/modules/MiniHeader";
+import secureRandom from "secure-random";
+import Mixpanel from "mixpanel";
+import Progress from "react-foundation-components/lib/global/progress-bar";
+import {api} from 'steem';
+
+const path = require('path');
+const ROOT = path.join(__dirname, '../../..');
+
+// FIXME copy paste code, refactor mixpanel out
+let mixpanel = null;
+if (config.has("mixpanel") && config.get("mixpanel")) {
+ mixpanel = Mixpanel.init(config.get("mixpanel"));
+}
+
+let assets_file = ROOT + "/tmp/webpack-stats-dev.json";
+if (process.env.NODE_ENV === "production") {
+ assets_file = ROOT + "/tmp/webpack-stats-prod.json";
+}
+
+const assets = Object.assign({}, require(assets_file), { script: [] });
+
+assets.script.push("https://www.google.com/recaptcha/api.js");
+assets.script.push("/enter_email/submit_form.js");
+
+function* confirmEmailHandler() {
+ const confirmation_code = this.params && this.params.code
+ ? this.params.code
+ : this.request.body.code;
+ console.log("-- /confirm_email -->", this.session.uid, this.session.user, confirmation_code);
+ const eid = yield models.Identity.findOne({
+ where: { confirmation_code, provider: "email"}
+ });
+ if (!eid) {
+ console.log("confirmation code not found", this.session.uid, this.session.user, confirmation_code);
+ this.status = 401;
+ this.body = "confirmation code not found";
+ return;
+ }
+ if (eid.email_verified) {
+ this.session.user = eid.user_id; // session recovery (user changed browsers)
+ this.flash = { success: "Email has already been verified" };
+ this.redirect("/approval?confirm_email=true");
+ return;
+ }
+ const hours_ago = (Date.now() - eid.updated_at) / 1000.0 / 3600.0;
+ if (hours_ago > 24.0 * 10) {
+ eid.destroy();
+ this.status = 401;
+ this.body = 'Confirmation code expired. Please re-submit your email for verification.';
+ return;
+ }
+
+ const number_of_created_accounts = yield models.sequelize.query(
+ `select count(*) as result from identities i join accounts a on a.user_id=i.user_id where i.provider='email' and i.email=:email and a.created=1 and a.ignored<>1`,
+ { replacements: { email: eid.email }, type: models.sequelize.QueryTypes.SELECT }
+ );
+ if (number_of_created_accounts && number_of_created_accounts[0].result > 0) {
+ console.log(
+ "-- /confirm_email email has already been used -->",
+ this.session.uid,
+ eid.email
+ );
+ this.flash = {error: 'This email has already been used'};
+ this.redirect('/pick_account');
+ return;
+ }
+
+ this.session.user = eid.user_id;
+ yield eid.update({
+ verified: true
+ });
+ yield models.User.update({ email: eid.email}, {
+ where: { id: eid.user_id }
+ });
+ yield models.User.update({ account_status: 'waiting'}, {
+ where: { id: eid.user_id, account_status: 'onhold' }
+ });
+ if (mixpanel)
+ mixpanel.track("SignupStepConfirmEmail", { distinct_id: this.session.uid });
+
+ const eid_phone = yield models.Identity.findOne({
+ where: { user_id: eid.user_id, provider: "phone", verified: true}
+ });
+
+ if (eid_phone) {
+ // this.flash = { success: "Thanks for confirming your email!" };
+ this.redirect("/approval?confirm_email=true");
+ } else {
+ this.flash = { success: "Thanks for confirming your email. Your phone needs to be confirmed before proceeding." };
+ this.redirect("/enter_mobile");
+ }
+
+ // check if the phone is confirmed then redirect to create account - this is useful when we invite users and send them the link
+ // const mid = yield models.Identity.findOne({
+ // attributes: ["verified"],
+ // where: { user_id: eid.user_id, provider: "phone" },
+ // order: "id DESC"
+ // });
+ // if (mid && mid.verified) {
+ // this.redirect("/create_account");
+ // } else {
+ // this.redirect("/enter_mobile");
+ // }
+}
+
+export default function useEnterAndConfirmEmailPages(app) {
+ const router = koa_router();
+ app.use(router.routes());
+ const koaBody = koa_body();
+ const rc_site_key = config.get("recaptcha.site_key");
+
+ router.get("/start/:code", function*() {
+ const code = this.params.code;
+ const eid = yield models.Identity.findOne({ attributes: ["id", "user_id", "verified"], where: { provider: "email", confirmation_code: code }});
+ const user = eid ? yield models.User.findOne({
+ attributes: ["id", "account_status"],
+ where: { id: eid.user_id },
+ include: [{model: models.Account, attributes: ['id', 'name', 'ignored', 'created']}],
+ }) : null;
+ // validate there is email identity and user record
+ if (eid && user) {
+ // set session based on confirmation code(user from diff device, etc)
+ this.session.user = user.id;
+ if (user.uid) this.session.uid = user.uid;
+ console.log('-- checking incoming start request -->', this.session.uid, this.session.user);
+ if (!eid.verified) {
+ yield eid.update({ verified: true });
+ }
+ if (user.account_status === "approved") {
+ console.log("-- approved account for -->", this.session.uid, this.session.user);
+ this.redirect("/create_account");
+ } else if (user.account_status === "created") {
+ // check if account is really created onchain
+ let there_is_created_account = false;
+ for (const a of user.Accounts) {
+ const check_account_res = yield api.getAccountsAsync([a.name]);
+ const account_created = check_account_res && check_account_res.length > 0;
+ if (account_created && !a.ignored) there_is_created_account = true;
+ if (!account_created && a.created) {
+ console.log("-- found ghost account -->", this.session.uid, this.session.user, a.name);
+ a.update({created: false});
+ }
+ }
+ if (there_is_created_account) {
+ // user clicked expired link - already created account
+ this.flash = {alert: "Your account has already been created."};
+ this.redirect("/login.html");
+ } else {
+ user.update({account_status: 'approved'});
+ console.log("-- approved account (ghost) for -->", this.session.uid, this.session.user);
+ this.redirect("/create_account");
+ }
+ } else if (user.account_status === "waiting") {
+ this.flash = { error: "Your account has not been approved yet." };
+ this.redirect("/");
+ } else {
+ this.flash = { error: "Issue with your sign up status." };
+ this.redirect("/");
+ }
+ } else {
+ // no matching identity found redirect
+ this.flash = { error: "This is not a valid sign up code. Please click the link in your welcome email." };
+ this.redirect("/");
+ }
+ });
+
+ router.get("/enter_email", function*() {
+ console.log("-- /enter_email -->", this.session.uid, this.session.user, this.request.query.account);
+ const picked_account_name = this.session.picked_account_name = this.request.query.account;
+ if (!picked_account_name) {
+ this.flash = { error: "Please select your account name" };
+ this.redirect('/pick_account');
+ return;
+ }
+ // check for existing account
+ const check_account_res = yield api.getAccountsAsync([picked_account_name]);
+ if (check_account_res && check_account_res.length > 0) {
+ this.flash = { error: `${picked_account_name} is already taken, please try another name` };
+ this.redirect('/pick_account');
+ return;
+ }
+ let default_email = "";
+ if (this.request.query && this.request.query.email)
+ default_email = this.request.query.email;
+ const body = renderToString(
+
+
+
+
+
+
+
+
+
+
+ );
+ const props = { body, title: "Email Address", assets, meta: [] };
+ this.body = "" +
+ renderToString( );
+ if (mixpanel)
+ mixpanel.track("SignupStepEmail", { distinct_id: this.session.uid });
+ });
+
+ router.post("/submit_email", koaBody, function*() {
+ if (!checkCSRF(this, this.request.body.csrf)) return;
+
+ let {email, account} = this.request.body;
+ console.log('-- /submit_email -->', this.session.uid, email, account);
+ if (!email) {
+ this.flash = { error: "Please provide an email address" };
+ this.redirect(`/enter_email?account=${account}`);
+ return;
+ }
+ email = email.trim().toLowerCase();
+ account = account.trim().toLowerCase();
+
+ //recaptcha
+ if (config.get('recaptcha.site_key')) {
+ if (!(yield checkRecaptcha(this))) {
+ console.log(
+ "-- /submit_email captcha verification failed -->",
+ this.session.uid,
+ email,
+ this.req.connection.remoteAddress
+ );
+ this.flash = {
+ error: "Failed captcha verification, please try again"
+ };
+ this.redirect(`/enter_email?email=${email}&account=${account}`);
+ return;
+ }
+ }
+
+ const parsed_email = email.match(/^.+\@.*?([\w\d-]+\.\w+)$/);
+
+ if (!parsed_email || parsed_email.length < 2) {
+ console.log(
+ "-- /submit_email not valid email -->",
+ this.session.uid,
+ email
+ );
+ this.flash = { error: "Not valid email address" };
+ this.redirect(`/enter_email?email=${email}&account=${account}`);
+ return;
+ }
+
+ try {
+ // create user, use new uid
+ const old_uid = this.session.uid;
+ this.session.uid = secureRandom.randomBuffer(13).toString('hex');
+ const user = yield models.User.create({
+ uid: this.session.uid,
+ remote_ip: getRemoteIp(this.request.req),
+ sign_up_meta: JSON.stringify({last_step: 2}),
+ account_status: 'waiting'
+ });
+ this.session.user = user.id;
+ console.log('-- /submit_email created new user -->', old_uid, this.session.uid, user.id);
+
+ yield models.UserAttribute.create({
+ user_id: user.id,
+ value: this.session.r,
+ type_of: 'referer'
+ });
+
+ const confirmation_code = secureRandom.randomBuffer(13).toString("hex");
+ // create identity
+ yield models.Identity.create({
+ user_id: user.id,
+ provider: 'email',
+ verified: false,
+ email,
+ confirmation_code
+ });
+
+ console.log(
+ "-- /submit_email ->",
+ this.session.uid,
+ this.session.user,
+ email,
+ confirmation_code
+ );
+
+ sendEmail("confirm_email", email, { confirmation_code });
+
+ if (account) {
+ const existing_account = yield models.Account.findOne({
+ attributes: ['id'],
+ where: {user_id: user.id, name: account},
+ order: 'id DESC'
+ });
+ if (!existing_account) {
+ yield models.Account.create({
+ user_id: user.id,
+ name: account,
+ remote_ip: getRemoteIp(this.request.req)
+ });
+ }
+ }
+ } catch (error) {
+ this.flash = {error: 'Internal Server Error'};
+ this.redirect(`/enter_email?email=${email}&account=${account}`);
+ console.error('Error in /submit_email :', this.session.uid, error.toString());
+ }
+
+ // redirect to phone verification
+ this.redirect("/enter_mobile");
+ });
+
+ router.get("/confirm_email/:code", confirmEmailHandler);
+ router.post("/confirm_email", koaBody, confirmEmailHandler);
+ router.get("/enter_email/submit_form.js", function*() {
+ this.type = 'application/javascript';
+ this.body = "function submit_email_form(){document.getElementById('submit_email').submit()}";
+ });
+}
+
+function* checkRecaptcha(ctx) {
+ if (process.env.NODE_ENV !== "production") return true;
+ const recaptcha = ctx.request.body["g-recaptcha-response"];
+ const verificationUrl = "https://www.google.com/recaptcha/api/siteverify?secret=" +
+ config.get("recaptcha.secret_key") +
+ "&response=" +
+ recaptcha +
+ "&remoteip=" +
+ ctx.req.connection.remoteAddress;
+ let captcha_failed;
+ try {
+ const recaptcha_res = yield request(verificationUrl);
+ const body = JSON.parse(recaptcha_res.body);
+ captcha_failed = !body.success;
+ } catch (e) {
+ captcha_failed = true;
+ console.error(
+ "-- /submit_email recaptcha request failed -->",
+ verificationUrl,
+ e
+ );
+ }
+ return !captcha_failed;
+}
diff --git a/src/server/sign_up_pages/enter_confirm_mobile.jsx b/src/server/sign_up_pages/enter_confirm_mobile.jsx
new file mode 100644
index 0000000..d52aea6
--- /dev/null
+++ b/src/server/sign_up_pages/enter_confirm_mobile.jsx
@@ -0,0 +1,362 @@
+import koa_router from "koa-router";
+import koa_body from "koa-body";
+import React from "react";
+import { renderToString } from "react-dom/server";
+import models from "db/models";
+import ServerHTML from "server/server-html";
+// import twilioVerify from "server/utils/twilio";
+import teleSignVerify from "server/utils/teleSign";
+import CountryCode from "app/components/elements/CountryCode";
+import { getRemoteIp, checkCSRF } from "server/utils/misc";
+import MiniHeader from "app/components/modules/MiniHeader";
+import secureRandom from "secure-random";
+import config from "config";
+import Mixpanel from "mixpanel";
+import Progress from 'react-foundation-components/lib/global/progress-bar';
+
+const path = require('path');
+const ROOT = path.join(__dirname, '../../..');
+
+// FIXME copy paste code, refactor mixpanel out
+var mixpanel = null;
+if (config.has("mixpanel") && config.get("mixpanel")) {
+ mixpanel = Mixpanel.init(config.get("mixpanel"));
+}
+
+var assets_file = ROOT + "/tmp/webpack-stats-dev.json";
+if (process.env.NODE_ENV === "production") {
+ assets_file = ROOT + "/tmp/webpack-stats-prod.json";
+}
+
+const assets = Object.assign({}, require(assets_file), { script: [] });
+
+// function mousePosition(e) {
+// // log x/y cords
+// console.log("hereI am man", e);
+// if(e.type === 'mouseenter') {
+// console.log(e.screenX, e.screenY);
+// }
+// }
+
+function* confirmMobileHandler(e) {
+ if (!checkCSRF(this, this.request.body.csrf)) return;
+ const confirmation_code = this.params && this.params.code
+ ? this.params.code
+ : this.request.body.code;
+ console.log(
+ "-- /confirm_mobile -->",
+ this.session.uid,
+ this.session.user,
+ confirmation_code
+ );
+
+ const user = yield models.User.findOne({
+ attributes: ['id', 'account_status'],
+ where: { id: this.session.user }
+ });
+ if (!user) {
+ this.flash = { error: "User session not found, please make sure you have cookies enabled in your browser for this website" };
+ this.redirect("/enter_mobile");
+ return;
+ }
+ const mid = yield models.Identity.findOne({
+ where: { user_id: user.id, provider: 'phone', confirmation_code }
+ });
+
+ if (!mid) {
+ this.flash = { error: "Wrong confirmation code" };
+ this.redirect("/enter_mobile");
+ return;
+ }
+
+ const hours_ago = (Date.now() - mid.updated_at) / 1000.0 / 3600.0;
+ if (hours_ago > 24.0) {
+ this.status = 401;
+ this.flash = { error: "Confirmation code has been expired" };
+ this.redirect("/enter_mobile");
+ return;
+ }
+
+ const number_of_created_accounts = yield models.sequelize.query(
+ `select count(*) as result from identities i join accounts a on a.user_id=i.user_id where i.provider='phone' and i.phone=:phone and a.created=1 and a.ignored<>1`,
+ { replacements: { phone: mid.phone }, type: models.sequelize.QueryTypes.SELECT }
+ );
+ if (number_of_created_accounts && number_of_created_accounts[0].result > 0) {
+ console.log(
+ "-- /confirm_mobile there are created accounts -->",
+ user.id,
+ mid.phone
+ );
+ this.flash = { error: "This phone number has already been used" };
+ this.redirect('/enter_mobile');
+ return;
+ }
+
+ // successful new verified phone number
+ yield mid.update({ provider: 'phone', verified: true });
+ if (user.account_status === 'onhold') yield user.update({account_status: 'waiting'});
+ if (mixpanel)
+ mixpanel.track("SignupStepPhone", { distinct_id: this.session.uid });
+
+ console.log("--/Success phone redirecting user", this.session.user);
+ this.redirect("/approval");
+}
+
+export default function useEnterAndConfirmMobilePages(app) {
+ const router = koa_router();
+ app.use(router.routes());
+ const koaBody = koa_body();
+
+ router.get("/enter_mobile", function*() {
+ console.log(
+ "-- /enter_mobile -->",
+ this.session.uid,
+ this.session.user
+ );
+
+ const phone = this.query.phone;
+ const country = this.query.country;
+
+ const body = renderToString(
+
+
+
+
+
+
+
+
+
+
+ );
+ const props = { body, title: "Phone Number", assets, meta: [] };
+ this.body = "" +
+ renderToString( );
+ if (mixpanel)
+ mixpanel.track("SignupStep2", { distinct_id: this.session.uid });
+ });
+
+ router.post("/submit_mobile", koaBody, function*() {
+ if (!checkCSRF(this, this.request.body.csrf)) return;
+ const user_id = this.session.user;
+ if (!user_id) {
+ this.flash = { error: "Your session has been interrupted, please start over" };
+ this.redirect('/pick_account');
+ return;
+ }
+
+ const country = this.request.body.country;
+ const localPhone = this.request.body.phone;
+ const enterMobileUrl = `/enter_mobile?phone=${localPhone}&country=${country}`;
+
+ if (!country || country === "") {
+ this.flash = { error: "Please select a country code" };
+ this.redirect(enterMobileUrl);
+ return;
+ }
+
+ if (!localPhone || digits(localPhone).length === 0) {
+ this.flash = { error: "Please provide a phone number" };
+ this.redirect(enterMobileUrl);
+ return;
+ }
+
+ let phone = digits(parseInt(country) + localPhone);
+
+ // const blocked_prefixes = yield models.List.findAll({
+ // attributes: ["id", "value"],
+ // where: { kk: "block-phone-prefix" }
+ // });
+ // for (const bp of blocked_prefixes) {
+ // if (phone.match(new RegExp("^" + bp.value))) {
+ // this.flash = {
+ // error: "Unfortunately, we don't yet have support to send SMS to your carrier, please try again later."
+ // };
+ // this.redirect("/enter_mobile");
+ // return;
+ // }
+ // }
+
+ const confirmation_code = parseInt(
+ secureRandom.randomBuffer(8).toString("hex"),
+ 16
+ )
+ .toString(10)
+ .substring(0, 5); // 4 digit code
+
+ let mid = yield models.Identity.findOne({
+ where: { user_id, provider: "phone" }
+ });
+
+ if (mid) {
+ if (mid.verified) {
+ if (mid.phone === phone) {
+ this.flash = { success: "Phone number has been verified" };
+ if (mixpanel)
+ mixpanel.track("SignupStep3", {
+ distinct_id: this.session.uid
+ });
+ this.redirect("/approval");
+ return;
+ }
+ yield mid.update({ verified: false, phone });
+ }
+ const seconds_ago = (Date.now() - mid.updated_at) / 1000.0;
+ if (seconds_ago < 60) {
+ this.flash = {
+ error: "Confirmation was attempted a moment ago. You can attempt verification again in one minute."
+ };
+ this.redirect(enterMobileUrl);
+ return;
+ }
+ }
+
+ // const twilioResult = yield twilioVerify(phone);
+ // console.log('-- /submit_mobile twilioResult -->', twilioResult);
+ //
+ // if (twilioResult === 'block') {
+ // mid.update({score: 111111});
+ // this.flash = { error: 'Unable to verify your phone number. Please try a different phone number.' };
+ // this.redirect(enterMobileUrl);
+ // return;
+ // }
+
+ const verifyResult = yield teleSignVerify({
+ mobile: phone,
+ confirmation_code,
+ ip: getRemoteIp(this.req),
+ ignore_score: true //twilioResult === 'pass'
+ });
+
+ if (verifyResult.error) {
+ this.flash = { error: verifyResult.error };
+ this.redirect(enterMobileUrl);
+ return;
+ }
+
+ phone = verifyResult.phone;
+
+ if (mid) {
+ yield mid.update({confirmation_code, phone, score: verifyResult.score});
+ } else {
+ mid = yield models.Identity.create({
+ provider: "phone",
+ user_id,
+ uid: this.session.uid,
+ phone,
+ verified: false,
+ confirmation_code,
+ score: verifyResult.score
+ });
+ }
+
+ console.log(
+ '-- /submit_mobile -->',
+ this.session.uid,
+ this.session.user,
+ phone,
+ mid.id
+ );
+
+ const body = renderToString(
+
+
+
+
+
+
+ Thank you for providing your phone number (
+ {phone}
+ ).
+
+
+ To continue please enter the SMS code we've sent you.
+
+
+
+
+
+
+
+ );
+ const props = { body, title: "Phone Confirmation", assets, meta: [] };
+ this.body = "" +
+ renderToString( );
+ });
+
+ router.get("/confirm_mobile/:code", confirmMobileHandler);
+ router.post("/confirm_mobile", koaBody, confirmMobileHandler);
+}
+
+function digits(text) {
+ const digitArray = text.match(/\d+/g);
+ return digitArray ? digitArray.join("") : "";
+}
diff --git a/src/server/utils/misc.js b/src/server/utils/misc.js
new file mode 100644
index 0000000..b3bdd73
--- /dev/null
+++ b/src/server/utils/misc.js
@@ -0,0 +1,55 @@
+import {esc} from 'db/models';
+
+const emailRegex = /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/;
+
+function getRemoteIp(req) {
+ const remote_address = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
+ const ip_match = remote_address ? remote_address.match(/(\d+\.\d+\.\d+\.\d+)/) : null;
+ return ip_match ? ip_match[1] : esc(remote_address);
+}
+
+var ip_last_hit = new Map();
+function rateLimitReq(ctx, req) {
+ const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
+ const now = Date.now()
+
+ // purge hits older than minutes_max
+ ip_last_hit.forEach((v, k) => {
+ const seconds = (now - v) / 1000;
+ if (seconds > 1) {
+ ip_last_hit.delete(ip)
+ }
+ })
+
+ let result = false;
+ // if ip is still in the map, abort
+ if (ip_last_hit.has(ip)) {
+ // console.log(`api rate limited for ${ip}: ${req}`);
+ // throw new Error(`Rate limit reached: one call per ${minutes_max} minutes allowed.`);
+ console.error(`Rate limit reached: one call per 1 second allowed.`);
+ ctx.status = 429;
+ ctx.body = 'Too Many Requests';
+ result = true;
+ }
+
+ // record api hit
+ ip_last_hit.set(ip, now);
+ return result;
+}
+
+function checkCSRF(ctx, csrf) {
+ try { ctx.assertCSRF(csrf); } catch (e) {
+ ctx.status = 403;
+ ctx.body = 'invalid csrf token';
+ console.log('-- invalid csrf token -->', ctx.request.method, ctx.request.url, ctx.session.uid);
+ return false;
+ }
+ return true;
+}
+
+module.exports = {
+ emailRegex,
+ getRemoteIp,
+ rateLimitReq,
+ checkCSRF
+};
diff --git a/src/server/utils/teleSign.js b/src/server/utils/teleSign.js
new file mode 100644
index 0000000..ed1131c
--- /dev/null
+++ b/src/server/utils/teleSign.js
@@ -0,0 +1,181 @@
+import fetch from 'node-fetch';
+import config from 'config';
+import crypto from 'crypto';
+import secureRandom from 'secure-random';
+
+const customer_id = config.get('telesign.customer_id');
+
+let api_key = '';
+
+if (config.get('telesign.rest_api_key')) {
+ api_key = new Buffer(config.get('telesign.rest_api_key'), 'base64');
+}
+
+const use_case_code = 'BACS'; // Use Case: avoid bulk attack and spammers
+
+// Testing, always blocked: 1-310-555-0100
+
+/** @return {object} - {reference_id} or {error} */
+export default function* verify({ mobile, confirmation_code, ip, ignore_score }) {
+ try {
+ const result = yield getScore(mobile);
+ const { recommendation, score } = result.risk;
+ let phone = mobile;
+ // if (!ignore_score && recommendation !== 'allow') {
+ if (!ignore_score && (!score || score > 600)) {
+ console.log(
+ `TeleSign did not allow phone ${mobile} ip ${ip}. TeleSign responded: ${recommendation}`
+ );
+ return {
+ error: 'Unable to verify your phone number. Please try a different phone number.',
+ score
+ };
+ }
+ if (result.numbering && result.numbering.cleansing && result.numbering.cleansing.sms) {
+ const sms = result.numbering.cleansing.sms;
+ phone = sms.country_code + sms.phone_number;
+ }
+ const { reference_id } = yield verifySms({
+ mobile,
+ confirmation_code,
+ ip
+ });
+ return { reference_id, score, phone };
+ } catch (error) {
+ console.log('-- verify score error -->', error);
+ return { error: 'Unable to verify phone, please try again later.' };
+ }
+}
+
+function getScore(mobile) {
+ const fields = urlencode({
+ ucid: use_case_code
+ });
+ const resource = '/v1/phoneid/score/' + mobile.match(/\d+/g).join('');
+ const method = 'GET';
+ return fetch(`https://rest-ww.telesign.com${resource}?${fields}`, {
+ method,
+ headers: authHeaders({ resource, method })
+ })
+ .then(r => r.json())
+ .catch(error => {
+ console.error(
+ `ERROR: Phone ${mobile} score exception`,
+ JSON.stringify(error, null, 0)
+ );
+ return Promise.reject(error);
+ })
+ .then(response => {
+ const { status } = response;
+ if (status.code === 300) {
+ // Transaction successfully completed
+ console.log(
+ `Phone ${mobile} score`,
+ JSON.stringify(response, null, 0)
+ );
+ return Promise.resolve(response);
+ }
+ console.error(
+ `ERROR: Phone ${mobile} score`,
+ JSON.stringify(response, null, 0)
+ );
+ return Promise.reject(response);
+ });
+}
+
+function verifySms({ mobile, confirmation_code, ip }) {
+ // https://developer.telesign.com/v2.0/docs/rest_api-verify-sms
+ const f = {
+ phone_number: mobile,
+ language: 'en-US',
+ ucid: use_case_code,
+ verify_code: confirmation_code,
+ template: '$$CODE$$ is your Steemit confirmation code'
+ };
+ if (ip) f.originating_ip = ip;
+ const fields = urlencode(f);
+ // console.log('fields', fields) // logspam
+
+ const resource = '/v1/verify/sms';
+ const method = 'POST';
+ return fetch('https://rest.telesign.com' + resource, {
+ method,
+ body: fields,
+ headers: authHeaders({ resource, method, fields })
+ })
+ .then(r => r.json())
+ .catch(error => {
+ console.error(
+ `ERROR: SMS failed to ${mobile} code ${confirmation_code} req ip ${ip} exception`,
+ JSON.stringify(error, null, 0)
+ );
+ return Promise.reject(error);
+ })
+ .then(response => {
+ const { status } = response;
+ if (status.code === 290) {
+ // Message in progress
+ console.log(
+ `Sent SMS to ${mobile} code ${confirmation_code}`,
+ JSON.stringify(response, null, 0)
+ );
+ return Promise.resolve(response);
+ }
+ console.error(
+ `ERROR: SMS failed to ${mobile} code ${confirmation_code}:`,
+ JSON.stringify(response, null, 0)
+ );
+ return Promise.reject(response);
+ });
+}
+
+/**
+ @arg {string} resource `/v1/verify/AEBC93B5898342F790E4E19FED41A7DA`
+ @arg {string} method [GET|POST|PUT]
+ @arg {string} fields url query string
+*/
+function authHeaders(
+ {
+ resource,
+ fields,
+ method = 'GET'
+ }
+) {
+ const auth_method = 'HMAC-SHA256';
+ const currDate = new Date().toUTCString();
+ const nonce = parseInt(
+ secureRandom.randomBuffer(8).toString('hex'),
+ 16
+ ).toString(36);
+
+ let content_type = '';
+ if (/POST|PUT/.test(method))
+ content_type = 'application/x-www-form-urlencoded';
+
+ let strToSign = `${method}\n${content_type}\n\nx-ts-auth-method:${auth_method}\nx-ts-date:${currDate}\nx-ts-nonce:${nonce}`;
+
+ if (fields) {
+ strToSign += '\n' + fields;
+ }
+ strToSign += '\n' + resource;
+
+ // console.log('strToSign', strToSign) // logspam
+ const sig = crypto
+ .createHmac('sha256', api_key)
+ .update(strToSign, 'utf8')
+ .digest('base64');
+
+ const headers = {
+ Authorization: `TSA ${customer_id}:${sig}`,
+ 'Content-Type': content_type,
+ 'x-ts-date': currDate,
+ 'x-ts-auth-method': auth_method,
+ 'x-ts-nonce': nonce
+ };
+ return headers;
+}
+
+const urlencode = json =>
+ Object.keys(json)
+ .map(key => encodeURI(key) + '=' + encodeURI(json[key]))
+ .join('&');
diff --git a/src/server/utils/twilio.js b/src/server/utils/twilio.js
new file mode 100644
index 0000000..0a9777a
--- /dev/null
+++ b/src/server/utils/twilio.js
@@ -0,0 +1,69 @@
+import twilio from 'twilio';
+import config from 'config';
+
+const accountSid = config.get('twilio.account_sid');
+const authToken = config.get('twilio.auth_token');
+let client;
+
+function checkEligibility(phone) {
+ // US, Canada +1
+ // France +33
+ // Spain +34
+ // Italy +39
+ // UK +44
+ // Sweden +46
+ // Germany +49
+ // Mexico +52
+ // Australia +61
+ // Phillipines +63
+ // Singapore +65
+ // Turkey +90
+ // Hong Kong +852
+ // Israel +972
+
+ for(const prefix of ['1', '33', '34', '39', '44', '46', '49', '52', '61', '63', '65', '90', '852', '972']) {
+ if (phone.startsWith(prefix)) return true;
+ }
+ return false;
+}
+
+export default function verify(phone) {
+ if (!client) client = new twilio.LookupsClient(accountSid, authToken);
+ return new Promise(resolve => {
+ if (!checkEligibility(phone)) {
+ resolve('na');
+ return;
+ }
+ client.phoneNumbers(phone).get({
+ type: 'carrier',
+ addOns: 'whitepages_pro_phone_rep',
+ }, (error, result) => {
+ if (error) {
+ if (error.code === 20404) {
+ console.log('Twilio phone not found ', phone);
+ resolve('block');
+ } else {
+ console.error('Twilio error', JSON.stringify(error, null, 2));
+ resolve('error');
+ }
+ } else {
+ if (result.addOns &&
+ result.addOns.results &&
+ result.addOns.results.whitepages_pro_phone_rep &&
+ result.addOns.results.whitepages_pro_phone_rep.result &&
+ result.addOns.results.whitepages_pro_phone_rep.result.results &&
+ result.addOns.results.whitepages_pro_phone_rep.result.results[0] &&
+ result.addOns.results.whitepages_pro_phone_rep.result.results[0].reputation &&
+ result.addOns.results.whitepages_pro_phone_rep.result.results[0].reputation.level
+ ) {
+ const reputation_level = result.addOns.results.whitepages_pro_phone_rep.result.results[0].reputation.level;
+ console.log('Twilio reputation level ', phone, reputation_level);
+ resolve(reputation_level < 3 ? 'pass' : 'block');
+ } else {
+ console.error('Twilio result does not contain reputation level:', JSON.stringify(result, null, 2));
+ resolve('error');
+ }
+ }
+ });
+ });
+}
diff --git a/src/shared/HtmlReady.js b/src/shared/HtmlReady.js
new file mode 100644
index 0000000..8156ffd
--- /dev/null
+++ b/src/shared/HtmlReady.js
@@ -0,0 +1,322 @@
+import xmldom from 'xmldom'
+import tt from 'counterpart'
+import linksRe, { any as linksAny } from 'app/utils/Links'
+import {validate_account_name} from 'app/utils/ChainValidation'
+import proxifyImageUrl from 'app/utils/ProxifyUrl'
+
+export const getPhishingWarningMessage = () => tt('g.phishy_message');
+
+const noop = () => {}
+const DOMParser = new xmldom.DOMParser({
+ errorHandler: {warning: noop, error: noop}
+})
+const XMLSerializer = new xmldom.XMLSerializer()
+
+/**
+ * Functions performed by HTMLReady
+ *
+ * State reporting
+ * - hashtags: collect all #tags in content
+ * - usertags: collect all @mentions in content
+ * - htmltags: collect all html used (for validation)
+ * - images: collect all image URLs in content
+ * - links: collect all href URLs in content
+ *
+ * Mutations
+ * - link()
+ * - ensure all href's begin with a protocol. prepend https:// otherwise.
+ * - iframe()
+ * - wrap all